style: format Rust code

This commit is contained in:
Yuri Karamian
2026-08-17 19:27:05 +02:00
parent ac7c79af04
commit d0fb87cf3a
9 changed files with 368 additions and 190 deletions
+21 -10
View File
@@ -23,7 +23,11 @@ pub fn ai_request(
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async { rt.block_on(async {
// Handle all API keys as optional; ollama and v1 completions doesnt always require it. // Handle all API keys as optional; ollama and v1 completions doesnt always require it.
let key_opt = if api_key.is_empty() { None } else { Some(api_key.as_str()) }; let key_opt = if api_key.is_empty() {
None
} else {
Some(api_key.as_str())
};
let result = match provider.as_str() { let result = match provider.as_str() {
"openai" => { "openai" => {
stream_openai( stream_openai(
@@ -99,7 +103,10 @@ pub fn ai_request(
/// so both `https://host` and `https://host/v1` work (we append `/v1/chat/completions`). /// so both `https://host` and `https://host/v1` work (we append `/v1/chat/completions`).
fn normalize_openai_base(base: &str) -> String { fn normalize_openai_base(base: &str) -> String {
let b = base.trim().trim_end_matches('/'); let b = base.trim().trim_end_matches('/');
b.strip_suffix("/v1").unwrap_or(b).trim_end_matches('/').to_string() b.strip_suffix("/v1")
.unwrap_or(b)
.trim_end_matches('/')
.to_string()
} }
async fn stream_anthropic( async fn stream_anthropic(
@@ -269,9 +276,7 @@ async fn stream_openai(
body["temperature"] = json!(0.7); body["temperature"] = json!(0.7);
} }
let mut req = client let mut req = client.post(url).header("content-type", "application/json");
.post(url)
.header("content-type", "application/json");
if let Some(key) = api_key { if let Some(key) = api_key {
req = req.header("Authorization", format!("Bearer {}", key)); req = req.header("Authorization", format!("Bearer {}", key));
@@ -381,7 +386,11 @@ pub async fn test_connection(
model: &str, model: &str,
base_url: Option<&str>, base_url: Option<&str>,
) -> Result<String, String> { ) -> Result<String, String> {
let key_opt = if api_key.is_empty() { None } else { Some(api_key) }; let key_opt = if api_key.is_empty() {
None
} else {
Some(api_key)
};
match provider { match provider {
"openai" => test_openai(OPENAI_API_URL, Some(api_key), model).await, "openai" => test_openai(OPENAI_API_URL, Some(api_key), model).await,
"ollama" => { "ollama" => {
@@ -437,7 +446,11 @@ async fn test_anthropic(api_key: &str, model: &str) -> Result<String, String> {
async fn test_openai(url: &str, api_key: Option<&str>, model: &str) -> Result<String, String> { async fn test_openai(url: &str, api_key: Option<&str>, model: &str) -> Result<String, String> {
let client = Client::new(); let client = Client::new();
let is_gpt5 = model.starts_with("gpt-5"); let is_gpt5 = model.starts_with("gpt-5");
let token_key = if is_gpt5 { "max_completion_tokens" } else { "max_tokens" }; let token_key = if is_gpt5 {
"max_completion_tokens"
} else {
"max_tokens"
};
let body = json!({ let body = json!({
"model": model, "model": model,
@@ -450,9 +463,7 @@ async fn test_openai(url: &str, api_key: Option<&str>, model: &str) -> Result<St
] ]
}); });
let mut req = client let mut req = client.post(url).header("content-type", "application/json");
.post(url)
.header("content-type", "application/json");
if let Some(key) = api_key { if let Some(key) = api_key {
req = req.header("Authorization", format!("Bearer {}", key)); req = req.header("Authorization", format!("Bearer {}", key));
+173 -73
View File
@@ -9,7 +9,9 @@ fn index_note_bg(state: &State<'_, AppState>, path: &str) {
let search = state.search_index.lock().ok().and_then(|g| g.clone()); let search = state.search_index.lock().ok().and_then(|g| g.clone());
if let Some(search) = search { if let Some(search) = search {
let p = path.to_string(); let p = path.to_string();
std::thread::spawn(move || { let _ = search.index_note(&p); }); std::thread::spawn(move || {
let _ = search.index_note(&p);
});
} }
} }
@@ -17,7 +19,9 @@ fn remove_note_bg(state: &State<'_, AppState>, path: &str) {
let search = state.search_index.lock().ok().and_then(|g| g.clone()); let search = state.search_index.lock().ok().and_then(|g| g.clone());
if let Some(search) = search { if let Some(search) = search {
let p = path.to_string(); let p = path.to_string();
std::thread::spawn(move || { let _ = search.remove_note(&p); }); std::thread::spawn(move || {
let _ = search.remove_note(&p);
});
} }
} }
@@ -189,9 +193,7 @@ pub async fn choose_external_vault(
if bookmark_was_registered { if bookmark_was_registered {
let _ = app.ios_vault_access().rollback_staged(); let _ = app.ios_vault_access().rollback_staged();
} else { } else {
let _ = app let _ = app.ios_vault_access().forget_bookmark(&result.bookmark_id);
.ios_vault_access()
.forget_bookmark(&result.bookmark_id);
} }
return Err(error); return Err(error);
} }
@@ -353,7 +355,10 @@ pub fn set_accent_color(state: State<'_, AppState>, color: String) -> Result<(),
} }
#[tauri::command] #[tauri::command]
pub fn save_custom_theme(state: State<'_, AppState>, theme: crate::types::CustomTheme) -> Result<(), String> { pub fn save_custom_theme(
state: State<'_, AppState>,
theme: crate::types::CustomTheme,
) -> Result<(), String> {
let mut config = state.config.lock().map_err(|e| e.to_string())?; let mut config = state.config.lock().map_err(|e| e.to_string())?;
if let Some(pos) = config.custom_themes.iter().position(|t| t.id == theme.id) { if let Some(pos) = config.custom_themes.iter().position(|t| t.id == theme.id) {
config.custom_themes[pos] = theme; config.custom_themes[pos] = theme;
@@ -407,9 +412,16 @@ mod custom_theme_reference_tests {
} }
#[tauri::command] #[tauri::command]
pub fn export_custom_theme(state: State<'_, AppState>, id: String, path: String) -> Result<(), String> { pub fn export_custom_theme(
state: State<'_, AppState>,
id: String,
path: String,
) -> Result<(), String> {
let config = state.config.lock().map_err(|e| e.to_string())?; let config = state.config.lock().map_err(|e| e.to_string())?;
let theme = config.custom_themes.iter().find(|t| t.id == id) let theme = config
.custom_themes
.iter()
.find(|t| t.id == id)
.ok_or_else(|| "Theme not found".to_string())?; .ok_or_else(|| "Theme not found".to_string())?;
let export = serde_json::json!({ "version": 1, "themes": [theme] }); let export = serde_json::json!({ "version": 1, "themes": [theme] });
let data = serde_json::to_string_pretty(&export).map_err(|e| e.to_string())?; let data = serde_json::to_string_pretty(&export).map_err(|e| e.to_string())?;
@@ -418,10 +430,14 @@ pub fn export_custom_theme(state: State<'_, AppState>, id: String, path: String)
} }
#[tauri::command] #[tauri::command]
pub fn import_custom_themes(state: State<'_, AppState>, path: String) -> Result<Vec<crate::types::CustomTheme>, String> { pub fn import_custom_themes(
state: State<'_, AppState>,
path: String,
) -> Result<Vec<crate::types::CustomTheme>, String> {
let data = std::fs::read_to_string(&path).map_err(|e| e.to_string())?; let data = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
let parsed: serde_json::Value = serde_json::from_str(&data).map_err(|e| e.to_string())?; let parsed: serde_json::Value = serde_json::from_str(&data).map_err(|e| e.to_string())?;
let themes: Vec<crate::types::CustomTheme> = serde_json::from_value(parsed["themes"].clone()) let themes: Vec<crate::types::CustomTheme> =
serde_json::from_value(parsed["themes"].clone())
.map_err(|e| format!("Invalid theme file: {}", e))?; .map_err(|e| format!("Invalid theme file: {}", e))?;
let mut config = state.config.lock().map_err(|e| e.to_string())?; let mut config = state.config.lock().map_err(|e| e.to_string())?;
for theme in &themes { for theme in &themes {
@@ -464,11 +480,7 @@ pub fn set_line_height(state: State<'_, AppState>, height: f64) -> Result<(), St
} }
#[tauri::command] #[tauri::command]
pub fn set_ui_scale( pub fn set_ui_scale(app: AppHandle, state: State<'_, AppState>, scale: f64) -> Result<(), String> {
app: AppHandle,
state: State<'_, AppState>,
scale: f64,
) -> Result<(), String> {
let mut config = state.config.lock().map_err(|e| e.to_string())?; let mut config = state.config.lock().map_err(|e| e.to_string())?;
config.ui_scale = Some(scale); config.ui_scale = Some(scale);
save_app_config(&config)?; save_app_config(&config)?;
@@ -602,7 +614,11 @@ pub fn move_notebook(
pub fn delete_notebook(state: State<'_, AppState>, path: String) -> Result<(), String> { pub fn delete_notebook(state: State<'_, AppState>, path: String) -> Result<(), String> {
let vault_path = { let vault_path = {
let config = state.config.lock().map_err(|e| e.to_string())?; let config = state.config.lock().map_err(|e| e.to_string())?;
config.active_vault.as_ref().ok_or("No active vault")?.clone() config
.active_vault
.as_ref()
.ok_or("No active vault")?
.clone()
}; };
operations::delete_notebook(&vault_path, &path) operations::delete_notebook(&vault_path, &path)
} }
@@ -663,7 +679,11 @@ pub fn duplicate_note(
path: String, path: String,
) -> Result<crate::types::NoteEntry, String> { ) -> Result<crate::types::NoteEntry, String> {
let config = state.config.lock().map_err(|e| e.to_string())?; let config = state.config.lock().map_err(|e| e.to_string())?;
let vault = config.active_vault.as_ref().ok_or("No active vault")?.clone(); let vault = config
.active_vault
.as_ref()
.ok_or("No active vault")?
.clone();
drop(config); drop(config);
let entry = operations::duplicate_note(&path, &vault)?; let entry = operations::duplicate_note(&path, &vault)?;
@@ -688,10 +708,14 @@ pub fn create_note(
} }
#[tauri::command] #[tauri::command]
pub fn create_daily_note(state: State<'_, AppState>, date: Option<String>) -> Result<NoteEntry, String> { pub fn create_daily_note(
state: State<'_, AppState>,
date: Option<String>,
) -> Result<NoteEntry, String> {
let config = state.config.lock().map_err(|e| e.to_string())?; let config = state.config.lock().map_err(|e| e.to_string())?;
let vault_path = config.active_vault.as_ref().ok_or("No active vault")?; let vault_path = config.active_vault.as_ref().ok_or("No active vault")?;
let entry = operations::create_daily_note(vault_path, date.as_deref(), &config.daily_title_format)?; let entry =
operations::create_daily_note(vault_path, date.as_deref(), &config.daily_title_format)?;
index_note_bg(&state, &entry.path); index_note_bg(&state, &entry.path);
@@ -699,9 +723,17 @@ pub fn create_daily_note(state: State<'_, AppState>, date: Option<String>) -> Re
} }
#[tauri::command] #[tauri::command]
pub fn rename_note(state: State<'_, AppState>, path: String, new_title: String) -> Result<String, String> { pub fn rename_note(
state: State<'_, AppState>,
path: String,
new_title: String,
) -> Result<String, String> {
let config = state.config.lock().map_err(|e| e.to_string())?; let config = state.config.lock().map_err(|e| e.to_string())?;
let vault_path = config.active_vault.as_ref().ok_or("No active vault")?.clone(); let vault_path = config
.active_vault
.as_ref()
.ok_or("No active vault")?
.clone();
drop(config); drop(config);
operations::rename_note(&path, &new_title, &vault_path) operations::rename_note(&path, &new_title, &vault_path)
} }
@@ -809,10 +841,7 @@ pub fn get_all_note_titles(state: State<'_, AppState>) -> Result<Vec<NoteTitleEn
.map(|r| r.to_string_lossy().replace('\\', "/").to_string()) .map(|r| r.to_string_lossy().replace('\\', "/").to_string())
.unwrap_or_else(|_| path.to_string_lossy().to_string()); .unwrap_or_else(|_| path.to_string_lossy().to_string());
entries.push(NoteTitleEntry { entries.push(NoteTitleEntry { title, path: rel });
title,
path: rel,
});
} }
Ok(entries) Ok(entries)
@@ -868,23 +897,34 @@ pub fn get_graph_data(state: State<'_, AppState>) -> Result<crate::types::GraphD
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
{ {
let path = entry.path(); let path = entry.path();
if !path.is_file() { continue; } if !path.is_file() {
continue;
}
let path_str = path.to_string_lossy().to_string(); let path_str = path.to_string_lossy().to_string();
if path.extension().and_then(|e| e.to_str()) != Some("md") { continue; } if path.extension().and_then(|e| e.to_str()) != Some("md") {
continue;
}
// Skip Syncthing conflict files // Skip Syncthing conflict files
if let Some(name) = path.file_name().and_then(|n| n.to_str()) { if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if name.contains(".sync-conflict-") { continue; } if name.contains(".sync-conflict-") {
continue;
}
} }
// Deduplicate by canonical path (handles symlinks) // Deduplicate by canonical path (handles symlinks)
let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
let canonical_str = canonical.to_string_lossy().to_string(); let canonical_str = canonical.to_string_lossy().to_string();
if !seen_paths.insert(canonical_str) { continue; } if !seen_paths.insert(canonical_str) {
continue;
}
let raw = std::fs::read_to_string(path).unwrap_or_default(); let raw = std::fs::read_to_string(path).unwrap_or_default();
// Fast title extraction: scan for "title: " line in frontmatter without full YAML parse // Fast title extraction: scan for "title: " line in frontmatter without full YAML parse
let title = extract_title_fast(&raw).unwrap_or_else(|| { let title = extract_title_fast(&raw).unwrap_or_else(|| {
path.file_stem().unwrap_or_default().to_string_lossy().to_string() path.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string()
}); });
let idx = graph_nodes.len(); let idx = graph_nodes.len();
@@ -896,7 +936,10 @@ pub fn get_graph_data(state: State<'_, AppState>) -> Result<crate::types::GraphD
if let Ok(rel) = path.strip_prefix(vault) { if let Ok(rel) = path.strip_prefix(vault) {
let rel_no_ext = rel.with_extension(""); let rel_no_ext = rel.with_extension("");
// Normalize Windows backslashes so [[folder/note]] links resolve cross-platform. // Normalize Windows backslashes so [[folder/note]] links resolve cross-platform.
let rel_lower = rel_no_ext.to_string_lossy().replace('\\', "/").to_lowercase(); let rel_lower = rel_no_ext
.to_string_lossy()
.replace('\\', "/")
.to_lowercase();
relpath_to_idx.entry(rel_lower).or_insert(idx); relpath_to_idx.entry(rel_lower).or_insert(idx);
} }
@@ -912,16 +955,27 @@ pub fn get_graph_data(state: State<'_, AppState>) -> Result<crate::types::GraphD
let mut edges: Vec<crate::types::GraphEdge> = Vec::new(); let mut edges: Vec<crate::types::GraphEdge> = Vec::new();
let mut edge_map: HashMap<(usize, usize), usize> = HashMap::new(); let mut edge_map: HashMap<(usize, usize), usize> = HashMap::new();
let add_edge = |edges: &mut Vec<crate::types::GraphEdge>, edge_map: &mut HashMap<(usize, usize), usize>, src: usize, tgt: usize| { let add_edge = |edges: &mut Vec<crate::types::GraphEdge>,
if src == tgt { return; } edge_map: &mut HashMap<(usize, usize), usize>,
if edge_map.contains_key(&(src, tgt)) { return; } // exact duplicate src: usize,
tgt: usize| {
if src == tgt {
return;
}
if edge_map.contains_key(&(src, tgt)) {
return;
} // exact duplicate
if let Some(&rev_idx) = edge_map.get(&(tgt, src)) { if let Some(&rev_idx) = edge_map.get(&(tgt, src)) {
// Reverse direction already exists - mark it as bidirectional // Reverse direction already exists - mark it as bidirectional
edges[rev_idx].bidirectional = true; edges[rev_idx].bidirectional = true;
} else { } else {
let idx = edges.len(); let idx = edges.len();
edge_map.insert((src, tgt), idx); edge_map.insert((src, tgt), idx);
edges.push(crate::types::GraphEdge { source: src, target: tgt, bidirectional: false }); edges.push(crate::types::GraphEdge {
source: src,
target: tgt,
bidirectional: false,
});
} }
}; };
@@ -970,14 +1024,19 @@ pub fn get_graph_data(state: State<'_, AppState>) -> Result<crate::types::GraphD
} }
} }
Ok(crate::types::GraphData { nodes: graph_nodes, edges }) Ok(crate::types::GraphData {
nodes: graph_nodes,
edges,
})
} }
/// Fast title extraction from frontmatter without full YAML parsing. /// Fast title extraction from frontmatter without full YAML parsing.
/// Scans for `title: ...` line within `---` fences. /// Scans for `title: ...` line within `---` fences.
fn extract_title_fast(raw: &str) -> Option<String> { fn extract_title_fast(raw: &str) -> Option<String> {
let trimmed = raw.trim_start(); let trimmed = raw.trim_start();
if !trimmed.starts_with("---") { return None; } if !trimmed.starts_with("---") {
return None;
}
// Find the closing --- // Find the closing ---
let after_open = &trimmed[3..]; let after_open = &trimmed[3..];
let end = after_open.find("\n---")?; let end = after_open.find("\n---")?;
@@ -987,7 +1046,9 @@ fn extract_title_fast(raw: &str) -> Option<String> {
if let Some(title) = line.strip_prefix("title:") { if let Some(title) = line.strip_prefix("title:") {
let val = title.trim(); let val = title.trim();
// Strip surrounding quotes // Strip surrounding quotes
if (val.starts_with('"') && val.ends_with('"')) || (val.starts_with('\'') && val.ends_with('\'')) { if (val.starts_with('"') && val.ends_with('"'))
|| (val.starts_with('\'') && val.ends_with('\''))
{
return Some(val[1..val.len() - 1].to_string()); return Some(val[1..val.len() - 1].to_string());
} }
if !val.is_empty() { if !val.is_empty() {
@@ -1031,7 +1092,11 @@ pub fn get_tasks(state: State<'_, AppState>) -> Result<Vec<crate::types::TaskIte
Ok(r) => r, Ok(r) => r,
Err(_) => return out, Err(_) => return out,
}; };
let filename = path.file_name().unwrap_or_default().to_string_lossy().to_string(); let filename = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let (meta, body) = crate::vault::frontmatter::parse_note(&raw, &filename); let (meta, body) = crate::vault::frontmatter::parse_note(&raw, &filename);
let note_path = path.to_string_lossy().to_string(); let note_path = path.to_string_lossy().to_string();
for (i, line) in body.lines().enumerate() { for (i, line) in body.lines().enumerate() {
@@ -1044,7 +1109,11 @@ pub fn get_tasks(state: State<'_, AppState>) -> Result<Vec<crate::types::TaskIte
let due = due_re.captures(&content).map(|c| c[1].to_string()); let due = due_re.captures(&content).map(|c| c[1].to_string());
let priority = prio_re.captures(&content).map(|c| { let priority = prio_re.captures(&content).map(|c| {
let p = c[1].to_lowercase(); let p = c[1].to_lowercase();
if p == "medium" { "med".to_string() } else { p } if p == "medium" {
"med".to_string()
} else {
p
}
}); });
let mut text = due_re.replace_all(&content, "").to_string(); let mut text = due_re.replace_all(&content, "").to_string();
text = prio_re.replace_all(&text, " ").to_string(); text = prio_re.replace_all(&text, " ").to_string();
@@ -1281,7 +1350,11 @@ pub fn restore_note(
) -> Result<String, String> { ) -> Result<String, String> {
let vault_path = { let vault_path = {
let config = state.config.lock().map_err(|e| e.to_string())?; let config = state.config.lock().map_err(|e| e.to_string())?;
config.active_vault.as_ref().ok_or("No active vault")?.clone() config
.active_vault
.as_ref()
.ok_or("No active vault")?
.clone()
}; };
operations::restore_note(&vault_path, &trash_path, dest_notebook.as_deref()) operations::restore_note(&vault_path, &trash_path, dest_notebook.as_deref())
} }
@@ -1290,7 +1363,11 @@ pub fn restore_note(
pub fn restore_notebook(state: State<'_, AppState>, trash_path: String) -> Result<String, String> { pub fn restore_notebook(state: State<'_, AppState>, trash_path: String) -> Result<String, String> {
let vault_path = { let vault_path = {
let config = state.config.lock().map_err(|e| e.to_string())?; let config = state.config.lock().map_err(|e| e.to_string())?;
config.active_vault.as_ref().ok_or("No active vault")?.clone() config
.active_vault
.as_ref()
.ok_or("No active vault")?
.clone()
}; };
operations::restore_notebook(&vault_path, &trash_path) operations::restore_notebook(&vault_path, &trash_path)
} }
@@ -1299,7 +1376,11 @@ pub fn restore_notebook(state: State<'_, AppState>, trash_path: String) -> Resul
pub fn permanent_delete(state: State<'_, AppState>, path: String) -> Result<(), String> { pub fn permanent_delete(state: State<'_, AppState>, path: String) -> Result<(), String> {
let vault_path = { let vault_path = {
let config = state.config.lock().map_err(|e| e.to_string())?; let config = state.config.lock().map_err(|e| e.to_string())?;
config.active_vault.as_ref().ok_or("No active vault")?.clone() config
.active_vault
.as_ref()
.ok_or("No active vault")?
.clone()
}; };
operations::permanent_delete(&vault_path, &path) operations::permanent_delete(&vault_path, &path)
} }
@@ -1359,8 +1440,11 @@ pub fn read_clipboard_image() -> Result<Vec<u8>, String> {
// Encode RGBA data to PNG // Encode RGBA data to PNG
let mut buf: Vec<u8> = Vec::new(); let mut buf: Vec<u8> = Vec::new();
{ {
let mut encoder = let mut encoder = png::Encoder::new(
png::Encoder::new(std::io::Cursor::new(&mut buf), img.width as u32, img.height as u32); std::io::Cursor::new(&mut buf),
img.width as u32,
img.height as u32,
);
encoder.set_color(png::ColorType::Rgba); encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight); encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder let mut writer = encoder
@@ -1384,8 +1468,8 @@ pub fn read_clipboard_image() -> Result<Vec<u8>, String> {
#[tauri::command] #[tauri::command]
pub fn copy_image_to_clipboard(path: String) -> Result<(), String> { pub fn copy_image_to_clipboard(path: String) -> Result<(), String> {
let data = std::fs::read(&path).map_err(|e| format!("Failed to read image: {}", e))?; let data = std::fs::read(&path).map_err(|e| format!("Failed to read image: {}", e))?;
let img = image::load_from_memory(&data) let img =
.map_err(|e| format!("Failed to decode image: {}", e))?; image::load_from_memory(&data).map_err(|e| format!("Failed to decode image: {}", e))?;
let rgba = img.to_rgba8(); let rgba = img.to_rgba8();
let (w, h) = rgba.dimensions(); let (w, h) = rgba.dimensions();
let img_data = arboard::ImageData { let img_data = arboard::ImageData {
@@ -1393,9 +1477,10 @@ pub fn copy_image_to_clipboard(path: String) -> Result<(), String> {
height: h as usize, height: h as usize,
bytes: std::borrow::Cow::Owned(rgba.into_raw()), bytes: std::borrow::Cow::Owned(rgba.into_raw()),
}; };
let mut clipboard = arboard::Clipboard::new() let mut clipboard =
.map_err(|e| format!("Clipboard init failed: {}", e))?; arboard::Clipboard::new().map_err(|e| format!("Clipboard init failed: {}", e))?;
clipboard.set_image(img_data) clipboard
.set_image(img_data)
.map_err(|e| format!("Failed to set clipboard image: {}", e))?; .map_err(|e| format!("Failed to set clipboard image: {}", e))?;
Ok(()) Ok(())
} }
@@ -1410,8 +1495,8 @@ pub fn copy_image_to_clipboard(_path: String) -> Result<(), String> {
#[cfg(desktop)] #[cfg(desktop)]
#[tauri::command] #[tauri::command]
pub fn copy_png_to_clipboard(data: Vec<u8>) -> Result<(), String> { pub fn copy_png_to_clipboard(data: Vec<u8>) -> Result<(), String> {
let img = image::load_from_memory(&data) let img =
.map_err(|e| format!("Failed to decode image: {}", e))?; image::load_from_memory(&data).map_err(|e| format!("Failed to decode image: {}", e))?;
let rgba = img.to_rgba8(); let rgba = img.to_rgba8();
let (w, h) = rgba.dimensions(); let (w, h) = rgba.dimensions();
let img_data = arboard::ImageData { let img_data = arboard::ImageData {
@@ -1419,9 +1504,10 @@ pub fn copy_png_to_clipboard(data: Vec<u8>) -> Result<(), String> {
height: h as usize, height: h as usize,
bytes: std::borrow::Cow::Owned(rgba.into_raw()), bytes: std::borrow::Cow::Owned(rgba.into_raw()),
}; };
let mut clipboard = arboard::Clipboard::new() let mut clipboard =
.map_err(|e| format!("Clipboard init failed: {}", e))?; arboard::Clipboard::new().map_err(|e| format!("Clipboard init failed: {}", e))?;
clipboard.set_image(img_data) clipboard
.set_image(img_data)
.map_err(|e| format!("Failed to set clipboard image: {}", e))?; .map_err(|e| format!("Failed to set clipboard image: {}", e))?;
Ok(()) Ok(())
} }
@@ -1706,7 +1792,11 @@ fn scan_orphaned_attachments(vault: &str) -> Result<Vec<(String, u64)>, String>
let entry = entry.map_err(|e| e.to_string())?; let entry = entry.map_err(|e| e.to_string())?;
let p = entry.path(); let p = entry.path();
if p.is_file() { if p.is_file() {
let name = p.file_name().unwrap_or_default().to_string_lossy().to_string(); let name = p
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let size = entry.metadata().map(|m| m.len()).unwrap_or(0); let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
files.push((name, size)); files.push((name, size));
} }
@@ -1715,7 +1805,10 @@ fn scan_orphaned_attachments(vault: &str) -> Result<Vec<(String, u64)>, String>
return Ok(Vec::new()); return Ok(Vec::new());
} }
let mut haystack = String::new(); let mut haystack = String::new();
for entry in walkdir::WalkDir::new(vault).into_iter().filter_map(|e| e.ok()) { for entry in walkdir::WalkDir::new(vault)
.into_iter()
.filter_map(|e| e.ok())
{
let p = entry.path(); let p = entry.path();
if p.is_file() && p.extension().and_then(|x| x.to_str()) == Some("md") { if p.is_file() && p.extension().and_then(|x| x.to_str()) == Some("md") {
if let Ok(content) = std::fs::read_to_string(p) { if let Ok(content) = std::fs::read_to_string(p) {
@@ -1887,7 +1980,6 @@ pub fn write_bytes_to(destination: String, data: Vec<u8>) -> Result<(), String>
Ok(()) Ok(())
} }
// ── Backup ── // ── Backup ──
#[tauri::command] #[tauri::command]
@@ -2068,7 +2160,8 @@ pub fn set_ai_settings(
config.ollama_api_key = ollama_api_key.filter(|k| !k.is_empty()); config.ollama_api_key = ollama_api_key.filter(|k| !k.is_empty());
} }
Some("openai_compatible") => { Some("openai_compatible") => {
config.openai_compatible_base_url = openai_compatible_base_url.filter(|u| !u.trim().is_empty()); config.openai_compatible_base_url =
openai_compatible_base_url.filter(|u| !u.trim().is_empty());
config.openai_compatible_api_key = openai_compatible_api_key.filter(|k| !k.is_empty()); config.openai_compatible_api_key = openai_compatible_api_key.filter(|k| !k.is_empty());
} }
_ => config.ai_api_key = key, _ => config.ai_api_key = key,
@@ -2091,7 +2184,9 @@ pub fn test_ai_connection(app: AppHandle) -> Result<(), String> {
.unwrap_or_else(|| "anthropic".to_string()); .unwrap_or_else(|| "anthropic".to_string());
let key = match provider.as_str() { let key = match provider.as_str() {
"ollama" => Some(config.ollama_api_key.clone().unwrap_or_default()), "ollama" => Some(config.ollama_api_key.clone().unwrap_or_default()),
"openai_compatible" => Some(config.openai_compatible_api_key.clone().unwrap_or_default()), "openai_compatible" => {
Some(config.openai_compatible_api_key.clone().unwrap_or_default())
}
"openai" => config.openai_api_key.clone(), "openai" => config.openai_api_key.clone(),
_ => config.ai_api_key.clone(), _ => config.ai_api_key.clone(),
} }
@@ -2133,11 +2228,7 @@ pub fn test_ai_connection(app: AppHandle) -> Result<(), String> {
// ── Sync (WebDAV) ── // ── Sync (WebDAV) ──
fn vault_matches_identity( fn vault_matches_identity(vault: &VaultConfig, path: &str, bookmark_id: Option<&str>) -> bool {
vault: &VaultConfig,
path: &str,
bookmark_id: Option<&str>,
) -> bool {
if let Some(bookmark_id) = bookmark_id { if let Some(bookmark_id) = bookmark_id {
vault.bookmark_id.as_deref() == Some(bookmark_id) vault.bookmark_id.as_deref() == Some(bookmark_id)
} else { } else {
@@ -2282,9 +2373,7 @@ pub fn sync_now(app: AppHandle) -> Result<(), String> {
.clone() .clone()
.ok_or_else(|| "No active vault".to_string()) .ok_or_else(|| "No active vault".to_string())
.and_then(|vault| { .and_then(|vault| {
sync_config_from(&config).map(|cfg| { sync_config_from(&config).map(|cfg| (vault, config.active_bookmark_id.clone(), cfg))
(vault, config.active_bookmark_id.clone(), cfg)
})
}); });
match gathered { match gathered {
Ok(vault_config) => vault_config, Ok(vault_config) => vault_config,
@@ -2298,7 +2387,9 @@ pub fn sync_now(app: AppHandle) -> Result<(), String> {
std::thread::spawn(move || { std::thread::spawn(move || {
use tauri::Emitter; use tauri::Emitter;
let result = crate::sync::run_sync(app.clone(), vault.clone(), cfg); let result = crate::sync::run_sync(app.clone(), vault.clone(), cfg);
app.state::<AppState>().syncing.store(false, Ordering::SeqCst); app.state::<AppState>()
.syncing
.store(false, Ordering::SeqCst);
match result { match result {
Ok(summary) => { Ok(summary) => {
let ts = chrono::Utc::now().to_rfc3339(); let ts = chrono::Utc::now().to_rfc3339();
@@ -2316,7 +2407,10 @@ pub fn sync_now(app: AppHandle) -> Result<(), String> {
); );
} }
Err(e) => { Err(e) => {
let _ = app.emit("sync-error", serde_json::json!({ "success": false, "error": e })); let _ = app.emit(
"sync-error",
serde_json::json!({ "success": false, "error": e }),
);
} }
} }
}); });
@@ -2340,7 +2434,9 @@ pub fn ai_ask(
.unwrap_or_else(|| "anthropic".to_string()); .unwrap_or_else(|| "anthropic".to_string());
let key = match provider.as_str() { let key = match provider.as_str() {
"ollama" => Some(config.ollama_api_key.clone().unwrap_or_default()), "ollama" => Some(config.ollama_api_key.clone().unwrap_or_default()),
"openai_compatible" => Some(config.openai_compatible_api_key.clone().unwrap_or_default()), "openai_compatible" => {
Some(config.openai_compatible_api_key.clone().unwrap_or_default())
}
"openai" => config.openai_api_key.clone(), "openai" => config.openai_api_key.clone(),
_ => config.ai_api_key.clone(), _ => config.ai_api_key.clone(),
} }
@@ -2445,7 +2541,9 @@ fn migrate_global_sync_to_vault(config: &mut AppConfig) -> bool {
if config.sync_provider.is_none() && config.webdav_url.is_none() { if config.sync_provider.is_none() && config.webdav_url.is_none() {
return false; return false;
} }
let Ok(active_index) = active_vault_index(config) else { return false; }; let Ok(active_index) = active_vault_index(config) else {
return false;
};
let g_provider = config.sync_provider.clone(); let g_provider = config.sync_provider.clone();
let g_url = config.webdav_url.clone(); let g_url = config.webdav_url.clone();
let g_user = config.webdav_username.clone(); let g_user = config.webdav_username.clone();
@@ -2555,7 +2653,9 @@ pub fn get_install_type() -> String {
} else if std::path::Path::new("/var/lib/dpkg/info/helix-notes.list").exists() { } else if std::path::Path::new("/var/lib/dpkg/info/helix-notes.list").exists() {
"deb".to_string() "deb".to_string()
} else if std::path::Path::new("/var/lib/pacman/local").exists() } else if std::path::Path::new("/var/lib/pacman/local").exists()
&& ["helixnotes", "helixnotes-bin", "helixnotes-appimage-bin"].iter().any(|pkg| { && ["helixnotes", "helixnotes-bin", "helixnotes-appimage-bin"]
.iter()
.any(|pkg| {
std::process::Command::new("pacman") std::process::Command::new("pacman")
.args(["-Q", pkg]) .args(["-Q", pkg])
.output() .output()
+11 -5
View File
@@ -26,7 +26,10 @@ fn vault_index_base(vault_path: &str) -> Option<std::path::PathBuf> {
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(vault_path.as_bytes()); hasher.update(vault_path.as_bytes());
let key: String = hasher.finalize()[..8].iter().map(|b| format!("{:02x}", b)).collect(); let key: String = hasher.finalize()[..8]
.iter()
.map(|b| format!("{:02x}", b))
.collect();
dirs::data_local_dir().map(|d| d.join("helixnotes").join("search").join(key)) dirs::data_local_dir().map(|d| d.join("helixnotes").join("search").join(key))
} }
@@ -235,7 +238,9 @@ impl SearchIndex {
// before the writer is created, so both indexing and querying use it). // before the writer is created, so both indexing and querying use it).
index.tokenizers().register( index.tokenizers().register(
"cjk", "cjk",
TextAnalyzer::builder(CjkTokenizer).filter(LowerCaser).build(), TextAnalyzer::builder(CjkTokenizer)
.filter(LowerCaser)
.build(),
); );
#[cfg(mobile)] #[cfg(mobile)]
@@ -376,9 +381,10 @@ impl SearchIndex {
)); ));
vec![(Occur::Should, exact)] vec![(Occur::Should, exact)]
} else { } else {
let prefix: Box<dyn Query> = Box::new(PhrasePrefixQuery::new( let prefix: Box<dyn Query> =
vec![Term::from_field_text(field, term)], Box::new(PhrasePrefixQuery::new(vec![Term::from_field_text(
)); field, term,
)]));
let fuzzy: Box<dyn Query> = Box::new(FuzzyTermQuery::new( let fuzzy: Box<dyn Query> = Box::new(FuzzyTermQuery::new(
Term::from_field_text(field, term), Term::from_field_text(field, term),
1, 1,
+19 -4
View File
@@ -81,7 +81,10 @@ fn sha256_hex(bytes: &[u8]) -> String {
} }
fn normalize_etag(s: &str) -> String { fn normalize_etag(s: &str) -> String {
s.trim().trim_start_matches("W/").trim_matches('"').to_string() s.trim()
.trim_start_matches("W/")
.trim_matches('"')
.to_string()
} }
/// Percent-encode each path segment, keeping the `/` separators. /// Percent-encode each path segment, keeping the `/` separators.
@@ -265,7 +268,11 @@ impl WebdavClient {
.send() .send()
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
if !resp.status().is_success() { if !resp.status().is_success() {
return Err(format!("GET {} failed: HTTP {}", relpath, resp.status().as_u16())); return Err(format!(
"GET {} failed: HTTP {}",
relpath,
resp.status().as_u16()
));
} }
Ok(resp.bytes().map_err(|e| e.to_string())?.to_vec()) Ok(resp.bytes().map_err(|e| e.to_string())?.to_vec())
} }
@@ -279,7 +286,11 @@ impl WebdavClient {
.send() .send()
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
if !resp.status().is_success() { if !resp.status().is_success() {
return Err(format!("PUT {} failed: HTTP {}", relpath, resp.status().as_u16())); return Err(format!(
"PUT {} failed: HTTP {}",
relpath,
resp.status().as_u16()
));
} }
Ok(resp Ok(resp
.headers() .headers()
@@ -671,7 +682,11 @@ pub fn test_connection(cfg: WebdavConfig) -> Result<String, String> {
/// Run a full sync. Mutes the file watcher while applying local writes, then /// Run a full sync. Mutes the file watcher while applying local writes, then
/// rebuilds the search index. Returns a summary of what changed. /// rebuilds the search index. Returns a summary of what changed.
pub fn run_sync(app: tauri::AppHandle, vault: String, cfg: WebdavConfig) -> Result<SyncSummary, String> { pub fn run_sync(
app: tauri::AppHandle,
vault: String,
cfg: WebdavConfig,
) -> Result<SyncSummary, String> {
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
let state = app.state::<AppState>(); let state = app.state::<AppState>();
+1 -4
View File
@@ -549,10 +549,7 @@ mod startup_view_tests {
assert!(!config.show_note_switcher); assert!(!config.show_note_switcher);
let mut value = serde_json::to_value(config).unwrap(); let mut value = serde_json::to_value(config).unwrap();
value value.as_object_mut().unwrap().remove("show_note_switcher");
.as_object_mut()
.unwrap()
.remove("show_note_switcher");
let config: AppConfig = serde_json::from_value(value).unwrap(); let config: AppConfig = serde_json::from_value(value).unwrap();
assert!(!config.show_note_switcher); assert!(!config.show_note_switcher);
+40 -22
View File
@@ -112,7 +112,11 @@ pub fn import(vault_path: &str) -> Result<ImportResult, String> {
"png" | "jpg" | "jpeg" | "gif" | "svg" | "webp" | "bmp" | "ico" | "pdf" "png" | "jpg" | "jpeg" | "gif" | "svg" | "webp" | "bmp" | "ico" | "pdf"
); );
if is_embeddable { if is_embeddable {
let alt = if is_dimension_spec(alt_param) { "" } else { alt_param }; let alt = if is_dimension_spec(alt_param) {
""
} else {
alt_param
};
result.links_converted += 1; result.links_converted += 1;
format!("![{}]({})", alt, link_target) format!("![{}]({})", alt, link_target)
} else { } else {
@@ -135,7 +139,11 @@ pub fn import(vault_path: &str) -> Result<ImportResult, String> {
let (file_part, anchor) = split_anchor(note_ref); let (file_part, anchor) = split_anchor(note_ref);
if file_part.is_empty() { if file_part.is_empty() {
if let Some(a) = anchor { if let Some(a) = anchor {
let display = if display_param.is_empty() { a } else { display_param }; let display = if display_param.is_empty() {
a
} else {
display_param
};
result.links_converted += 1; result.links_converted += 1;
return format!("[{}](#{})", display, a); return format!("[{}](#{})", display, a);
} }
@@ -163,8 +171,22 @@ pub fn import(vault_path: &str) -> Result<ImportResult, String> {
.to_string(); .to_string();
content = after_links; content = after_links;
content = fix_md_image_refs(&content, &md_img_re, vault, note_dir, &file_index, &mut result.links_converted); content = fix_md_image_refs(
content = fix_md_link_refs(&content, &md_link_re, vault, note_dir, &file_index, &mut result.links_converted); &content,
&md_img_re,
vault,
note_dir,
&file_index,
&mut result.links_converted,
);
content = fix_md_link_refs(
&content,
&md_link_re,
vault,
note_dir,
&file_index,
&mut result.links_converted,
);
if result.links_converted > links_before { if result.links_converted > links_before {
changed = true; changed = true;
@@ -387,9 +409,7 @@ fn file_created(path: &Path) -> Option<chrono::DateTime<Utc>> {
.ok() .ok()
.and_then(|m| m.created().ok()) .and_then(|m| m.created().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.and_then(|d| { .and_then(|d| chrono::DateTime::from_timestamp(d.as_secs() as i64, d.subsec_nanos()))
chrono::DateTime::from_timestamp(d.as_secs() as i64, d.subsec_nanos())
})
} }
fn file_modified(path: &Path) -> Option<chrono::DateTime<Utc>> { fn file_modified(path: &Path) -> Option<chrono::DateTime<Utc>> {
@@ -397,9 +417,7 @@ fn file_modified(path: &Path) -> Option<chrono::DateTime<Utc>> {
.ok() .ok()
.and_then(|m| m.modified().ok()) .and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.and_then(|d| { .and_then(|d| chrono::DateTime::from_timestamp(d.as_secs() as i64, d.subsec_nanos()))
chrono::DateTime::from_timestamp(d.as_secs() as i64, d.subsec_nanos())
})
} }
fn convert_syntax(content: &str, highlight_re: &Regex, comment_re: &Regex) -> String { fn convert_syntax(content: &str, highlight_re: &Regex, comment_re: &Regex) -> String {
@@ -511,7 +529,9 @@ fn fix_md_link_refs(
let display = &caps[1]; let display = &caps[1];
let href = &caps[2]; let href = &caps[2];
let decoded = percent_decode(href); let decoded = percent_decode(href);
if decoded.starts_with("http") || decoded.starts_with('/') || decoded.starts_with("data:") if decoded.starts_with("http")
|| decoded.starts_with('/')
|| decoded.starts_with("data:")
|| decoded.starts_with('#') || decoded.starts_with('#')
{ {
return format!("[{}]({})", display, href); return format!("[{}]({})", display, href);
@@ -547,9 +567,7 @@ fn fix_md_link_refs(
.to_string() .to_string()
} }
fn move_attachments( fn move_attachments(vault: &Path) -> Result<HashMap<String, String>, String> {
vault: &Path,
) -> Result<HashMap<String, String>, String> {
let attachments_dir = vault.join(".helixnotes").join("attachments"); let attachments_dir = vault.join(".helixnotes").join("attachments");
let _ = std::fs::create_dir_all(&attachments_dir); let _ = std::fs::create_dir_all(&attachments_dir);
@@ -599,10 +617,7 @@ fn move_attachments(
Ok(moved) Ok(moved)
} }
fn rewrite_attachment_refs( fn rewrite_attachment_refs(vault: &Path, moved: &HashMap<String, String>) -> Result<(), String> {
vault: &Path,
moved: &HashMap<String, String>,
) -> Result<(), String> {
let md_ref = Regex::new(r"(!?\[[^\]]*\])\(([^)]+)\)").map_err(|e| e.to_string())?; let md_ref = Regex::new(r"(!?\[[^\]]*\])\(([^)]+)\)").map_err(|e| e.to_string())?;
let md_files: Vec<_> = walkdir::WalkDir::new(vault) let md_files: Vec<_> = walkdir::WalkDir::new(vault)
@@ -717,7 +732,8 @@ fn is_dimension_spec(s: &str) -> bool {
if s.is_empty() { if s.is_empty() {
return false; return false;
} }
s.chars().all(|c| c.is_ascii_digit() || c == 'x' || c == 'X') s.chars()
.all(|c| c.is_ascii_digit() || c == 'x' || c == 'X')
} }
fn resolve_wiki_ref(file_index: &HashMap<String, String>, reference: &str) -> String { fn resolve_wiki_ref(file_index: &HashMap<String, String>, reference: &str) -> String {
@@ -905,7 +921,10 @@ mod tests {
extract_heading_title("# My Title\n\nBody"), extract_heading_title("# My Title\n\nBody"),
Some("My Title".to_string()) Some("My Title".to_string())
); );
assert_eq!(extract_heading_title("\n\n# Spaced Title"), Some("Spaced Title".to_string())); assert_eq!(
extract_heading_title("\n\n# Spaced Title"),
Some("Spaced Title".to_string())
);
assert_eq!(extract_heading_title("Body without heading"), None); assert_eq!(extract_heading_title("Body without heading"), None);
assert_eq!(extract_heading_title("## Subheading"), None); assert_eq!(extract_heading_title("## Subheading"), None);
assert_eq!(extract_heading_title(""), None); assert_eq!(extract_heading_title(""), None);
@@ -945,8 +964,7 @@ mod tests {
#[test] #[test]
fn test_normalize_tags_strips_hash() { fn test_normalize_tags_strips_hash() {
let mapping: serde_yaml::Mapping = let mapping: serde_yaml::Mapping = serde_yaml::from_str("tags:\n - \"#hashed\"").unwrap();
serde_yaml::from_str("tags:\n - \"#hashed\"").unwrap();
let tags = normalize_tags(&mapping); let tags = normalize_tags(&mapping);
assert_eq!(tags, vec!["hashed"]); assert_eq!(tags, vec!["hashed"]);
} }
+61 -30
View File
@@ -51,7 +51,10 @@ fn ensure_vault_content_dir(vault_path: &str, requested_path: &Path) -> Result<P
fn ensure_note_path(vault_path: &str, requested_path: &Path) -> Result<PathBuf, String> { fn ensure_note_path(vault_path: &str, requested_path: &Path) -> Result<PathBuf, String> {
let requested = ensure_vault_content_path(vault_path, requested_path, false)?; let requested = ensure_vault_content_path(vault_path, requested_path, false)?;
if !requested.is_file() if !requested.is_file()
|| requested.extension().and_then(|extension| extension.to_str()) != Some("md") || requested
.extension()
.and_then(|extension| extension.to_str())
!= Some("md")
{ {
return Err("Note path must point to a Markdown file".to_string()); return Err("Note path must point to a Markdown file".to_string());
} }
@@ -65,7 +68,10 @@ fn ensure_readable_note_path(vault_path: &str, requested_path: &Path) -> Result<
let trashed_note = ensure_trash_entry(vault_path, requested_path)?; let trashed_note = ensure_trash_entry(vault_path, requested_path)?;
if !trashed_note.is_file() if !trashed_note.is_file()
|| trashed_note.extension().and_then(|extension| extension.to_str()) != Some("md") || trashed_note
.extension()
.and_then(|extension| extension.to_str())
!= Some("md")
{ {
return Err("Note path must point to a Markdown file".to_string()); return Err("Note path must point to a Markdown file".to_string());
} }
@@ -235,7 +241,11 @@ fn scan_dir_recursive(dir: &Path, vault_root: &str) -> Vec<NotebookEntry> {
paths paths
.par_iter() .par_iter()
.map(|path| { .map(|path| {
let name = path.file_name().unwrap_or_default().to_string_lossy().to_string(); let name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let relative = path let relative = path
.strip_prefix(root) .strip_prefix(root)
.unwrap_or(path) .unwrap_or(path)
@@ -286,7 +296,11 @@ fn scan_dir_with_count(dir: &Path, vault_root: &str) -> (Vec<NotebookEntry>, usi
let entries: Vec<NotebookEntry> = paths let entries: Vec<NotebookEntry> = paths
.par_iter() .par_iter()
.map(|path| { .map(|path| {
let name = path.file_name().unwrap_or_default().to_string_lossy().to_string(); let name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let relative = path let relative = path
.strip_prefix(root) .strip_prefix(root)
.unwrap_or(path) .unwrap_or(path)
@@ -341,7 +355,12 @@ pub fn scan_notes(vault_path: &str, notebook_path: Option<&str>) -> Result<Vec<N
ensure_vault_content_dir(vault_path, root)?; ensure_vault_content_dir(vault_path, root)?;
let vault_root = Path::new(vault_path); let vault_root = Path::new(vault_path);
log::info!("scan_notes: vault={}, scan={}, exists={}", vault_path, scan_path, root.exists()); log::info!(
"scan_notes: vault={}, scan={}, exists={}",
vault_path,
scan_path,
root.exists()
);
if !root.exists() { if !root.exists() {
return Err("Path does not exist".to_string()); return Err("Path does not exist".to_string());
@@ -397,7 +416,9 @@ pub fn scan_notes(vault_path: &str, notebook_path: Option<&str>) -> Result<Vec<N
} else { } else {
WalkDir::new(root) WalkDir::new(root)
.into_iter() .into_iter()
.filter_entry(|e| !is_hidden(e.path()) && !e.path().starts_with(helixnotes_dir(vault_path))) .filter_entry(|e| {
!is_hidden(e.path()) && !e.path().starts_with(helixnotes_dir(vault_path))
})
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
.map(|e| e.path().to_path_buf()) .map(|e| e.path().to_path_buf())
.filter(|p| p.is_file() && p.extension().and_then(|x| x.to_str()) == Some("md")) .filter(|p| p.is_file() && p.extension().and_then(|x| x.to_str()) == Some("md"))
@@ -774,7 +795,9 @@ pub fn create_daily_note(
"eu" => target_date.format("%d/%m/%Y").to_string(), "eu" => target_date.format("%d/%m/%Y").to_string(),
_ => { _ => {
let locale = get_system_locale(); let locale = get_system_locale();
target_date.format_localized("%B %d, %Y", locale).to_string() target_date
.format_localized("%B %d, %Y", locale)
.to_string()
} }
}; };
@@ -935,7 +958,13 @@ pub fn rename_note(path: &str, new_title: &str, vault_path: &str) -> Result<Stri
let new_path_str = new_path.to_string_lossy().to_string(); let new_path_str = new_path.to_string_lossy().to_string();
// Update wikilinks in other notes that reference this note // Update wikilinks in other notes that reference this note
update_wikilinks_after_rename(vault_path, &old_path_str, &new_path_str, &old_title, new_title); update_wikilinks_after_rename(
vault_path,
&old_path_str,
&new_path_str,
&old_title,
new_title,
);
Ok(new_path_str) Ok(new_path_str)
} }
@@ -994,11 +1023,17 @@ fn update_wikilinks_after_rename(
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
{ {
let path = entry.path(); let path = entry.path();
if !path.is_file() { continue; } if !path.is_file() {
continue;
}
let path_str = path.to_string_lossy(); let path_str = path.to_string_lossy();
if path.extension().and_then(|e| e.to_str()) != Some("md") { continue; } if path.extension().and_then(|e| e.to_str()) != Some("md") {
continue;
}
// Skip the renamed note itself // Skip the renamed note itself
if *path_str == *new_path { continue; } if *path_str == *new_path {
continue;
}
let content = match fs::read_to_string(path) { let content = match fs::read_to_string(path) {
Ok(c) => c, Ok(c) => c,
@@ -1014,22 +1049,13 @@ fn update_wikilinks_after_rename(
// If another note shares the same title, these would be ambiguous. // If another note shares the same title, these would be ambiguous.
if old_title != new_title && title_is_unique { if old_title != new_title && title_is_unique {
// 1. Short title ref: [[Old Title]] → [[New Title]] // 1. Short title ref: [[Old Title]] → [[New Title]]
result = result.replace( result = result.replace(&format!("[[{}]]", old_title), &format!("[[{}]]", new_title));
&format!("[[{}]]", old_title),
&format!("[[{}]]", new_title),
);
// 2. Short title with alias: [[Old Title|display]] → [[New Title|display]] // 2. Short title with alias: [[Old Title|display]] → [[New Title|display]]
result = result.replace( result = result.replace(&format!("[[{}|", old_title), &format!("[[{}|", new_title));
&format!("[[{}|", old_title),
&format!("[[{}|", new_title),
);
// 3. Short title as alias display: [[ref|Old Title]] → [[ref|New Title]] // 3. Short title as alias display: [[ref|Old Title]] → [[ref|New Title]]
result = result.replace( result = result.replace(&format!("|{}]]", old_title), &format!("|{}]]", new_title));
&format!("|{}]]", old_title),
&format!("|{}]]", new_title),
);
} }
// Path-based rules are always safe (paths are unique). // Path-based rules are always safe (paths are unique).
@@ -1144,7 +1170,10 @@ fn cleanup_empty_trash_dir(vault_path: &str, dir: Option<&Path>) {
pub fn get_trash_contents(vault_path: &str) -> Result<TrashContents, String> { pub fn get_trash_contents(vault_path: &str) -> Result<TrashContents, String> {
let trash_dir = helixnotes_dir(vault_path).join("trash"); let trash_dir = helixnotes_dir(vault_path).join("trash");
if !trash_dir.exists() { if !trash_dir.exists() {
return Ok(TrashContents { notes: Vec::new(), notebooks: Vec::new() }); return Ok(TrashContents {
notes: Vec::new(),
notebooks: Vec::new(),
});
} }
let vault_root = Path::new(vault_path); let vault_root = Path::new(vault_path);
@@ -1163,7 +1192,10 @@ pub fn get_trash_contents(vault_path: &str) -> Result<TrashContents, String> {
.min_depth(1) .min_depth(1)
.into_iter() .into_iter()
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
.filter(|e| e.path().is_file() && e.path().extension().and_then(|x| x.to_str()) == Some("md")) .filter(|e| {
e.path().is_file()
&& e.path().extension().and_then(|x| x.to_str()) == Some("md")
})
.count(); .count();
let dirname = path.file_name().unwrap_or_default().to_string_lossy(); let dirname = path.file_name().unwrap_or_default().to_string_lossy();
// Strip timestamp prefix to get original notebook name // Strip timestamp prefix to get original notebook name
@@ -1239,7 +1271,9 @@ pub fn restore_notebook(vault_path: &str, trash_path: &str) -> Result<String, St
let dirname = src.file_name().unwrap_or_default().to_string_lossy(); let dirname = src.file_name().unwrap_or_default().to_string_lossy();
// Try to read original path from sidecar .meta file // Try to read original path from sidecar .meta file
let meta_path = src.with_extension("").with_file_name(format!("{}.meta", dirname)); let meta_path = src
.with_extension("")
.with_file_name(format!("{}.meta", dirname));
let relative = if let Ok(original) = fs::read_to_string(&meta_path) { let relative = if let Ok(original) = fs::read_to_string(&meta_path) {
original original
} else { } else {
@@ -1542,10 +1576,7 @@ mod tests {
fn compares_numeric_segments_anywhere_in_names() { fn compares_numeric_segments_anywhere_in_names() {
let mut names = ["Class 10b", "Class 2b", "Class 10a", "Class 2a"]; let mut names = ["Class 10b", "Class 2b", "Class 10a", "Class 2a"];
names.sort_by(|left, right| compare_natural_names(left, right)); names.sort_by(|left, right| compare_natural_names(left, right));
assert_eq!( assert_eq!(names, ["Class 2a", "Class 2b", "Class 10a", "Class 10b"]);
names,
["Class 2a", "Class 2b", "Class 10a", "Class 10b"]
);
assert_eq!( assert_eq!(
compare_natural_names("Class 02", "Class 2b"), compare_natural_names("Class 02", "Class 2b"),
std::cmp::Ordering::Less std::cmp::Ordering::Less