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
+1 -1
View File
@@ -1,3 +1,3 @@
fn main() {
tauri_build::build()
tauri_build::build()
}
+27 -16
View File
@@ -23,7 +23,11 @@ pub fn ai_request(
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
// 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() {
"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`).
fn normalize_openai_base(base: &str) -> String {
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(
@@ -269,9 +276,7 @@ async fn stream_openai(
body["temperature"] = json!(0.7);
}
let mut req = client
.post(url)
.header("content-type", "application/json");
let mut req = client.post(url).header("content-type", "application/json");
if let Some(key) = api_key {
req = req.header("Authorization", format!("Bearer {}", key));
@@ -381,7 +386,11 @@ pub async fn test_connection(
model: &str,
base_url: Option<&str>,
) -> 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 {
"openai" => test_openai(OPENAI_API_URL, Some(api_key), model).await,
"ollama" => {
@@ -435,14 +444,18 @@ 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> {
let client = Client::new();
let is_gpt5 = model.starts_with("gpt-5");
let token_key = if is_gpt5 { "max_completion_tokens" } else { "max_tokens" };
let client = Client::new();
let is_gpt5 = model.starts_with("gpt-5");
let token_key = if is_gpt5 {
"max_completion_tokens"
} else {
"max_tokens"
};
let body = json!({
"model": model,
token_key: 20,
"messages": [
let body = json!({
"model": model,
token_key: 20,
"messages": [
{
"role": "user",
"content": "Hi"
@@ -450,9 +463,7 @@ async fn test_openai(url: &str, api_key: Option<&str>, model: &str) -> Result<St
]
});
let mut req = client
.post(url)
.header("content-type", "application/json");
let mut req = client.post(url).header("content-type", "application/json");
if let Some(key) = api_key {
req = req.header("Authorization", format!("Bearer {}", key));
+206 -106
View File
@@ -6,19 +6,23 @@ use std::path::Path;
use tauri::{AppHandle, Manager, State};
fn index_note_bg(state: &State<'_, AppState>, path: &str) {
let search = state.search_index.lock().ok().and_then(|g| g.clone());
if let Some(search) = search {
let p = path.to_string();
std::thread::spawn(move || { let _ = search.index_note(&p); });
}
let search = state.search_index.lock().ok().and_then(|g| g.clone());
if let Some(search) = search {
let p = path.to_string();
std::thread::spawn(move || {
let _ = search.index_note(&p);
});
}
}
fn remove_note_bg(state: &State<'_, AppState>, path: &str) {
let search = state.search_index.lock().ok().and_then(|g| g.clone());
if let Some(search) = search {
let p = path.to_string();
std::thread::spawn(move || { let _ = search.remove_note(&p); });
}
let search = state.search_index.lock().ok().and_then(|g| g.clone());
if let Some(search) = search {
let p = path.to_string();
std::thread::spawn(move || {
let _ = search.remove_note(&p);
});
}
}
fn clear_vault_runtime(state: &State<'_, AppState>) -> Result<(), String> {
@@ -189,9 +193,7 @@ pub async fn choose_external_vault(
if bookmark_was_registered {
let _ = app.ios_vault_access().rollback_staged();
} else {
let _ = app
.ios_vault_access()
.forget_bookmark(&result.bookmark_id);
let _ = app.ios_vault_access().forget_bookmark(&result.bookmark_id);
}
return Err(error);
}
@@ -353,7 +355,10 @@ pub fn set_accent_color(state: State<'_, AppState>, color: String) -> Result<(),
}
#[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())?;
if let Some(pos) = config.custom_themes.iter().position(|t| t.id == theme.id) {
config.custom_themes[pos] = theme;
@@ -407,9 +412,16 @@ mod custom_theme_reference_tests {
}
#[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 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())?;
let export = serde_json::json!({ "version": 1, "themes": [theme] });
let data = serde_json::to_string_pretty(&export).map_err(|e| e.to_string())?;
@@ -418,11 +430,15 @@ pub fn export_custom_theme(state: State<'_, AppState>, id: String, path: String)
}
#[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 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())
.map_err(|e| format!("Invalid theme file: {}", e))?;
let themes: Vec<crate::types::CustomTheme> =
serde_json::from_value(parsed["themes"].clone())
.map_err(|e| format!("Invalid theme file: {}", e))?;
let mut config = state.config.lock().map_err(|e| e.to_string())?;
for theme in &themes {
if let Some(pos) = config.custom_themes.iter().position(|t| t.id == theme.id) {
@@ -464,11 +480,7 @@ pub fn set_line_height(state: State<'_, AppState>, height: f64) -> Result<(), St
}
#[tauri::command]
pub fn set_ui_scale(
app: AppHandle,
state: State<'_, AppState>,
scale: f64,
) -> Result<(), String> {
pub fn set_ui_scale(app: AppHandle, state: State<'_, AppState>, scale: f64) -> Result<(), String> {
let mut config = state.config.lock().map_err(|e| e.to_string())?;
config.ui_scale = Some(scale);
save_app_config(&config)?;
@@ -602,7 +614,11 @@ pub fn move_notebook(
pub fn delete_notebook(state: State<'_, AppState>, path: String) -> Result<(), String> {
let vault_path = {
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)
}
@@ -651,8 +667,8 @@ pub fn save_note(
operations::save_note(&vault_path, &path, &meta, &body)?;
// Re-index note so search picks up changes (background to avoid blocking on FUSE fsync)
index_note_bg(&state, &path);
// Re-index note so search picks up changes (background to avoid blocking on FUSE fsync)
index_note_bg(&state, &path);
Ok(())
}
@@ -663,7 +679,11 @@ pub fn duplicate_note(
path: String,
) -> Result<crate::types::NoteEntry, 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);
let entry = operations::duplicate_note(&path, &vault)?;
@@ -677,31 +697,43 @@ pub fn create_note(
notebook_relative: Option<String>,
title: String,
) -> Result<NoteEntry, 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 entry = operations::create_note(vault_path, notebook_relative.as_deref(), &title)?;
// Index new note (background to avoid blocking on FUSE fsync)
index_note_bg(&state, &entry.path);
Ok(entry)
}
#[tauri::command]
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 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_note(vault_path, notebook_relative.as_deref(), &title)?;
index_note_bg(&state, &entry.path);
// Index new note (background to avoid blocking on FUSE fsync)
index_note_bg(&state, &entry.path);
Ok(entry)
}
#[tauri::command]
pub fn rename_note(state: State<'_, AppState>, path: String, new_title: String) -> Result<String, 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 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")?;
let entry =
operations::create_daily_note(vault_path, date.as_deref(), &config.daily_title_format)?;
index_note_bg(&state, &entry.path);
Ok(entry)
}
#[tauri::command]
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 vault_path = config
.active_vault
.as_ref()
.ok_or("No active vault")?
.clone();
drop(config);
operations::rename_note(&path, &new_title, &vault_path)
}
@@ -712,8 +744,8 @@ pub fn delete_note(state: State<'_, AppState>, path: String) -> Result<(), Strin
let vault_path = config.active_vault.as_ref().ok_or("No active vault")?;
operations::delete_note(vault_path, &path)?;
// Remove from index (background to avoid blocking on FUSE fsync)
remove_note_bg(&state, &path);
// Remove from index (background to avoid blocking on FUSE fsync)
remove_note_bg(&state, &path);
Ok(())
}
@@ -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())
.unwrap_or_else(|_| path.to_string_lossy().to_string());
entries.push(NoteTitleEntry {
title,
path: rel,
});
entries.push(NoteTitleEntry { title, path: rel });
}
Ok(entries)
@@ -868,23 +897,34 @@ pub fn get_graph_data(state: State<'_, AppState>) -> Result<crate::types::GraphD
.filter_map(|e| e.ok())
{
let path = entry.path();
if !path.is_file() { continue; }
if !path.is_file() {
continue;
}
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
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)
let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
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();
// Fast title extraction: scan for "title: " line in frontmatter without full YAML parse
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();
@@ -896,7 +936,10 @@ pub fn get_graph_data(state: State<'_, AppState>) -> Result<crate::types::GraphD
if let Ok(rel) = path.strip_prefix(vault) {
let rel_no_ext = rel.with_extension("");
// 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);
}
@@ -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 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| {
if src == tgt { return; }
if edge_map.contains_key(&(src, tgt)) { return; } // exact duplicate
let add_edge = |edges: &mut Vec<crate::types::GraphEdge>,
edge_map: &mut HashMap<(usize, usize), usize>,
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)) {
// Reverse direction already exists - mark it as bidirectional
edges[rev_idx].bidirectional = true;
} else {
let idx = edges.len();
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.
/// Scans for `title: ...` line within `---` fences.
fn extract_title_fast(raw: &str) -> Option<String> {
let trimmed = raw.trim_start();
if !trimmed.starts_with("---") { return None; }
if !trimmed.starts_with("---") {
return None;
}
// Find the closing ---
let after_open = &trimmed[3..];
let end = after_open.find("\n---")?;
@@ -987,8 +1046,10 @@ fn extract_title_fast(raw: &str) -> Option<String> {
if let Some(title) = line.strip_prefix("title:") {
let val = title.trim();
// Strip surrounding quotes
if (val.starts_with('"') && val.ends_with('"')) || (val.starts_with('\'') && val.ends_with('\'')) {
return Some(val[1..val.len()-1].to_string());
if (val.starts_with('"') && val.ends_with('"'))
|| (val.starts_with('\'') && val.ends_with('\''))
{
return Some(val[1..val.len() - 1].to_string());
}
if !val.is_empty() {
return Some(val.to_string());
@@ -1031,7 +1092,11 @@ pub fn get_tasks(state: State<'_, AppState>) -> Result<Vec<crate::types::TaskIte
Ok(r) => r,
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 note_path = path.to_string_lossy().to_string();
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 priority = prio_re.captures(&content).map(|c| {
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();
text = prio_re.replace_all(&text, " ").to_string();
@@ -1129,7 +1198,7 @@ pub fn set_task_done(
}
operations::save_note(&vault_path, &note_path, &meta, &new_body)?;
index_note_bg(&state, &note_path);
index_note_bg(&state, &note_path);
Ok(())
}
@@ -1281,7 +1350,11 @@ pub fn restore_note(
) -> Result<String, String> {
let vault_path = {
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())
}
@@ -1290,7 +1363,11 @@ pub fn restore_note(
pub fn restore_notebook(state: State<'_, AppState>, trash_path: String) -> Result<String, String> {
let vault_path = {
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)
}
@@ -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> {
let vault_path = {
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)
}
@@ -1359,8 +1440,11 @@ pub fn read_clipboard_image() -> Result<Vec<u8>, String> {
// Encode RGBA data to PNG
let mut buf: Vec<u8> = Vec::new();
{
let mut encoder =
png::Encoder::new(std::io::Cursor::new(&mut buf), img.width as u32, img.height as u32);
let mut encoder = png::Encoder::new(
std::io::Cursor::new(&mut buf),
img.width as u32,
img.height as u32,
);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder
@@ -1384,8 +1468,8 @@ pub fn read_clipboard_image() -> Result<Vec<u8>, String> {
#[tauri::command]
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 img = image::load_from_memory(&data)
.map_err(|e| format!("Failed to decode image: {}", e))?;
let img =
image::load_from_memory(&data).map_err(|e| format!("Failed to decode image: {}", e))?;
let rgba = img.to_rgba8();
let (w, h) = rgba.dimensions();
let img_data = arboard::ImageData {
@@ -1393,9 +1477,10 @@ pub fn copy_image_to_clipboard(path: String) -> Result<(), String> {
height: h as usize,
bytes: std::borrow::Cow::Owned(rgba.into_raw()),
};
let mut clipboard = arboard::Clipboard::new()
.map_err(|e| format!("Clipboard init failed: {}", e))?;
clipboard.set_image(img_data)
let mut clipboard =
arboard::Clipboard::new().map_err(|e| format!("Clipboard init failed: {}", e))?;
clipboard
.set_image(img_data)
.map_err(|e| format!("Failed to set clipboard image: {}", e))?;
Ok(())
}
@@ -1410,8 +1495,8 @@ pub fn copy_image_to_clipboard(_path: String) -> Result<(), String> {
#[cfg(desktop)]
#[tauri::command]
pub fn copy_png_to_clipboard(data: Vec<u8>) -> Result<(), String> {
let img = image::load_from_memory(&data)
.map_err(|e| format!("Failed to decode image: {}", e))?;
let img =
image::load_from_memory(&data).map_err(|e| format!("Failed to decode image: {}", e))?;
let rgba = img.to_rgba8();
let (w, h) = rgba.dimensions();
let img_data = arboard::ImageData {
@@ -1419,9 +1504,10 @@ pub fn copy_png_to_clipboard(data: Vec<u8>) -> Result<(), String> {
height: h as usize,
bytes: std::borrow::Cow::Owned(rgba.into_raw()),
};
let mut clipboard = arboard::Clipboard::new()
.map_err(|e| format!("Clipboard init failed: {}", e))?;
clipboard.set_image(img_data)
let mut clipboard =
arboard::Clipboard::new().map_err(|e| format!("Clipboard init failed: {}", e))?;
clipboard
.set_image(img_data)
.map_err(|e| format!("Failed to set clipboard image: {}", e))?;
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 p = entry.path();
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);
files.push((name, size));
}
@@ -1715,7 +1805,10 @@ fn scan_orphaned_attachments(vault: &str) -> Result<Vec<(String, u64)>, String>
return Ok(Vec::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();
if p.is_file() && p.extension().and_then(|x| x.to_str()) == Some("md") {
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(())
}
// ── Backup ──
#[tauri::command]
@@ -2068,7 +2160,8 @@ pub fn set_ai_settings(
config.ollama_api_key = ollama_api_key.filter(|k| !k.is_empty());
}
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.ai_api_key = key,
@@ -2091,7 +2184,9 @@ pub fn test_ai_connection(app: AppHandle) -> Result<(), String> {
.unwrap_or_else(|| "anthropic".to_string());
let key = match provider.as_str() {
"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(),
_ => config.ai_api_key.clone(),
}
@@ -2133,11 +2228,7 @@ pub fn test_ai_connection(app: AppHandle) -> Result<(), String> {
// ── Sync (WebDAV) ──
fn vault_matches_identity(
vault: &VaultConfig,
path: &str,
bookmark_id: Option<&str>,
) -> bool {
fn vault_matches_identity(vault: &VaultConfig, path: &str, bookmark_id: Option<&str>) -> bool {
if let Some(bookmark_id) = bookmark_id {
vault.bookmark_id.as_deref() == Some(bookmark_id)
} else {
@@ -2282,9 +2373,7 @@ pub fn sync_now(app: AppHandle) -> Result<(), String> {
.clone()
.ok_or_else(|| "No active vault".to_string())
.and_then(|vault| {
sync_config_from(&config).map(|cfg| {
(vault, config.active_bookmark_id.clone(), cfg)
})
sync_config_from(&config).map(|cfg| (vault, config.active_bookmark_id.clone(), cfg))
});
match gathered {
Ok(vault_config) => vault_config,
@@ -2298,7 +2387,9 @@ pub fn sync_now(app: AppHandle) -> Result<(), String> {
std::thread::spawn(move || {
use tauri::Emitter;
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 {
Ok(summary) => {
let ts = chrono::Utc::now().to_rfc3339();
@@ -2316,7 +2407,10 @@ pub fn sync_now(app: AppHandle) -> Result<(), String> {
);
}
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());
let key = match provider.as_str() {
"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(),
_ => 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() {
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_url = config.webdav_url.clone();
let g_user = config.webdav_username.clone();
@@ -2555,13 +2653,15 @@ pub fn get_install_type() -> String {
} else if std::path::Path::new("/var/lib/dpkg/info/helix-notes.list").exists() {
"deb".to_string()
} else if std::path::Path::new("/var/lib/pacman/local").exists()
&& ["helixnotes", "helixnotes-bin", "helixnotes-appimage-bin"].iter().any(|pkg| {
std::process::Command::new("pacman")
.args(["-Q", pkg])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
})
&& ["helixnotes", "helixnotes-bin", "helixnotes-appimage-bin"]
.iter()
.any(|pkg| {
std::process::Command::new("pacman")
.args(["-Q", pkg])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
})
{
"aur".to_string()
} else if std::env::var("APPIMAGE").is_ok() {
+1 -1
View File
@@ -2,5 +2,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
app_lib::run();
app_lib::run();
}
+11 -5
View File
@@ -26,7 +26,10 @@ fn vault_index_base(vault_path: &str) -> Option<std::path::PathBuf> {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
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))
}
@@ -235,7 +238,9 @@ impl SearchIndex {
// before the writer is created, so both indexing and querying use it).
index.tokenizers().register(
"cjk",
TextAnalyzer::builder(CjkTokenizer).filter(LowerCaser).build(),
TextAnalyzer::builder(CjkTokenizer)
.filter(LowerCaser)
.build(),
);
#[cfg(mobile)]
@@ -376,9 +381,10 @@ impl SearchIndex {
));
vec![(Occur::Should, exact)]
} else {
let prefix: Box<dyn Query> = Box::new(PhrasePrefixQuery::new(
vec![Term::from_field_text(field, term)],
));
let prefix: Box<dyn Query> =
Box::new(PhrasePrefixQuery::new(vec![Term::from_field_text(
field, term,
)]));
let fuzzy: Box<dyn Query> = Box::new(FuzzyTermQuery::new(
Term::from_field_text(field, term),
1,
+19 -4
View File
@@ -81,7 +81,10 @@ fn sha256_hex(bytes: &[u8]) -> 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.
@@ -265,7 +268,11 @@ impl WebdavClient {
.send()
.map_err(|e| e.to_string())?;
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())
}
@@ -279,7 +286,11 @@ impl WebdavClient {
.send()
.map_err(|e| e.to_string())?;
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
.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
/// 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;
let state = app.state::<AppState>();
+1 -4
View File
@@ -549,10 +549,7 @@ mod startup_view_tests {
assert!(!config.show_note_switcher);
let mut value = serde_json::to_value(config).unwrap();
value
.as_object_mut()
.unwrap()
.remove("show_note_switcher");
value.as_object_mut().unwrap().remove("show_note_switcher");
let config: AppConfig = serde_json::from_value(value).unwrap();
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"
);
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;
format!("![{}]({})", alt, link_target)
} else {
@@ -135,7 +139,11 @@ pub fn import(vault_path: &str) -> Result<ImportResult, String> {
let (file_part, anchor) = split_anchor(note_ref);
if file_part.is_empty() {
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;
return format!("[{}](#{})", display, a);
}
@@ -163,8 +171,22 @@ pub fn import(vault_path: &str) -> Result<ImportResult, String> {
.to_string();
content = after_links;
content = fix_md_image_refs(&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);
content = fix_md_image_refs(
&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 {
changed = true;
@@ -387,9 +409,7 @@ fn file_created(path: &Path) -> Option<chrono::DateTime<Utc>> {
.ok()
.and_then(|m| m.created().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.and_then(|d| {
chrono::DateTime::from_timestamp(d.as_secs() as i64, d.subsec_nanos())
})
.and_then(|d| chrono::DateTime::from_timestamp(d.as_secs() as i64, d.subsec_nanos()))
}
fn file_modified(path: &Path) -> Option<chrono::DateTime<Utc>> {
@@ -397,9 +417,7 @@ fn file_modified(path: &Path) -> Option<chrono::DateTime<Utc>> {
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.and_then(|d| {
chrono::DateTime::from_timestamp(d.as_secs() as i64, d.subsec_nanos())
})
.and_then(|d| chrono::DateTime::from_timestamp(d.as_secs() as i64, d.subsec_nanos()))
}
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 href = &caps[2];
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('#')
{
return format!("[{}]({})", display, href);
@@ -547,9 +567,7 @@ fn fix_md_link_refs(
.to_string()
}
fn move_attachments(
vault: &Path,
) -> Result<HashMap<String, String>, String> {
fn move_attachments(vault: &Path) -> Result<HashMap<String, String>, String> {
let attachments_dir = vault.join(".helixnotes").join("attachments");
let _ = std::fs::create_dir_all(&attachments_dir);
@@ -599,10 +617,7 @@ fn move_attachments(
Ok(moved)
}
fn rewrite_attachment_refs(
vault: &Path,
moved: &HashMap<String, String>,
) -> Result<(), String> {
fn rewrite_attachment_refs(vault: &Path, moved: &HashMap<String, String>) -> Result<(), String> {
let md_ref = Regex::new(r"(!?\[[^\]]*\])\(([^)]+)\)").map_err(|e| e.to_string())?;
let md_files: Vec<_> = walkdir::WalkDir::new(vault)
@@ -717,7 +732,8 @@ fn is_dimension_spec(s: &str) -> bool {
if s.is_empty() {
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 {
@@ -905,7 +921,10 @@ mod tests {
extract_heading_title("# My Title\n\nBody"),
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("## Subheading"), None);
assert_eq!(extract_heading_title(""), None);
@@ -945,8 +964,7 @@ mod tests {
#[test]
fn test_normalize_tags_strips_hash() {
let mapping: serde_yaml::Mapping =
serde_yaml::from_str("tags:\n - \"#hashed\"").unwrap();
let mapping: serde_yaml::Mapping = serde_yaml::from_str("tags:\n - \"#hashed\"").unwrap();
let tags = normalize_tags(&mapping);
assert_eq!(tags, vec!["hashed"]);
}
+62 -31
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> {
let requested = ensure_vault_content_path(vault_path, requested_path, false)?;
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());
}
@@ -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)?;
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());
}
@@ -235,7 +241,11 @@ fn scan_dir_recursive(dir: &Path, vault_root: &str) -> Vec<NotebookEntry> {
paths
.par_iter()
.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
.strip_prefix(root)
.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
.par_iter()
.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
.strip_prefix(root)
.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)?;
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() {
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 {
WalkDir::new(root)
.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())
.map(|e| e.path().to_path_buf())
.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(),
_ => {
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();
// 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)
}
@@ -994,11 +1023,17 @@ fn update_wikilinks_after_rename(
.filter_map(|e| e.ok())
{
let path = entry.path();
if !path.is_file() { continue; }
if !path.is_file() {
continue;
}
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
if *path_str == *new_path { continue; }
if *path_str == *new_path {
continue;
}
let content = match fs::read_to_string(path) {
Ok(c) => c,
@@ -1014,22 +1049,13 @@ fn update_wikilinks_after_rename(
// If another note shares the same title, these would be ambiguous.
if old_title != new_title && title_is_unique {
// 1. Short title ref: [[Old Title]] → [[New Title]]
result = result.replace(
&format!("[[{}]]", old_title),
&format!("[[{}]]", new_title),
);
result = result.replace(&format!("[[{}]]", old_title), &format!("[[{}]]", new_title));
// 2. Short title with alias: [[Old Title|display]] → [[New Title|display]]
result = result.replace(
&format!("[[{}|", old_title),
&format!("[[{}|", new_title),
);
result = result.replace(&format!("[[{}|", old_title), &format!("[[{}|", new_title));
// 3. Short title as alias display: [[ref|Old Title]] → [[ref|New Title]]
result = result.replace(
&format!("|{}]]", old_title),
&format!("|{}]]", new_title),
);
result = result.replace(&format!("|{}]]", old_title), &format!("|{}]]", new_title));
}
// 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> {
let trash_dir = helixnotes_dir(vault_path).join("trash");
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);
@@ -1163,7 +1192,10 @@ pub fn get_trash_contents(vault_path: &str) -> Result<TrashContents, String> {
.min_depth(1)
.into_iter()
.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();
let dirname = path.file_name().unwrap_or_default().to_string_lossy();
// 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();
// 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) {
original
} else {
@@ -1542,10 +1576,7 @@ mod tests {
fn compares_numeric_segments_anywhere_in_names() {
let mut names = ["Class 10b", "Class 2b", "Class 10a", "Class 2a"];
names.sort_by(|left, right| compare_natural_names(left, right));
assert_eq!(
names,
["Class 2a", "Class 2b", "Class 10a", "Class 10b"]
);
assert_eq!(names, ["Class 2a", "Class 2b", "Class 10a", "Class 10b"]);
assert_eq!(
compare_natural_names("Class 02", "Class 2b"),
std::cmp::Ordering::Less
@@ -1793,4 +1824,4 @@ mod tests {
fs::remove_dir_all(vault).unwrap();
}
}
}