Revert "chore(release): prepare v1.3.5"

This reverts commit ade146f9f3.
This commit is contained in:
Yuri Karamian
2026-08-17 17:03:01 +02:00
parent ade146f9f3
commit 43f15962f7
35 changed files with 3179 additions and 3978 deletions
+184 -359
View File
@@ -6,23 +6,19 @@ 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> {
@@ -193,7 +189,9 @@ 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);
}
@@ -355,10 +353,7 @@ 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;
@@ -396,12 +391,10 @@ mod custom_theme_reference_tests {
#[test]
fn deleting_custom_theme_resets_system_pair_references() {
let mut config = AppConfig {
theme: "custom-work".to_string(),
system_light_theme: "custom-work".to_string(),
system_dark_theme: "custom-work".to_string(),
..Default::default()
};
let mut config = AppConfig::default();
config.theme = "custom-work".to_string();
config.system_light_theme = "custom-work".to_string();
config.system_dark_theme = "custom-work".to_string();
clear_custom_theme_references(&mut config, "custom-work");
@@ -412,16 +405,9 @@ 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())?;
@@ -430,15 +416,11 @@ pub fn export_custom_theme(
}
#[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) {
@@ -480,7 +462,11 @@ 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)?;
@@ -527,14 +513,8 @@ pub fn create_notebook(
}
#[tauri::command]
pub fn rename_notebook(
state: State<'_, AppState>,
path: String,
new_name: String,
) -> Result<String, String> {
let config = state.config.lock().map_err(|error| error.to_string())?;
let vault_path = config.active_vault.as_ref().ok_or("No active vault")?;
operations::rename_notebook(vault_path, &path, &new_name)
pub fn rename_notebook(path: String, new_name: String) -> Result<String, String> {
operations::rename_notebook(&path, &new_name)
}
#[tauri::command]
@@ -551,7 +531,7 @@ pub fn move_notebook(
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
let new_full_path = operations::move_notebook(vault_path, &notebook_path, &dest_parent)?;
let new_full_path = operations::move_notebook(&notebook_path, &dest_parent)?;
let new_relative = Path::new(&new_full_path)
.strip_prefix(vault_path.as_str())
@@ -614,11 +594,7 @@ 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)
}
@@ -636,10 +612,8 @@ pub fn get_notes(
}
#[tauri::command]
pub fn read_note(state: State<'_, AppState>, path: String) -> Result<NoteContent, String> {
let config = state.config.lock().map_err(|error| error.to_string())?;
let vault_path = config.active_vault.as_ref().ok_or("No active vault")?;
operations::read_note(vault_path, &path)
pub fn read_note(path: String) -> Result<NoteContent, String> {
operations::read_note(&path)
}
#[tauri::command]
@@ -649,26 +623,27 @@ pub fn save_note(
meta: NoteMeta,
body: String,
) -> Result<(), String> {
let config = state.config.lock().map_err(|error| error.to_string())?;
let vault_path = config
.active_vault
.as_ref()
.ok_or("No active vault")?
.clone();
let max_versions = config.max_versions_per_note;
let old_raw = operations::read_vault_note(&vault_path, &path)?.raw;
// Snapshot current content before overwriting (if file exists)
let config = state.config.lock().map_err(|e| e.to_string())?;
if let Some(vault_path) = &config.active_vault {
if std::path::Path::new(&path).exists() {
if let Ok(old_raw) = std::fs::read_to_string(&path) {
let max_versions = config.max_versions_per_note;
let note_id = meta.id.clone();
let vp = vault_path.clone();
// Snapshot in background so save isn't slowed down
std::thread::spawn(move || {
crate::history::maybe_snapshot(&vp, &note_id, &old_raw, max_versions);
});
}
}
}
drop(config);
let note_id = meta.id.clone();
let snapshot_vault = vault_path.clone();
std::thread::spawn(move || {
crate::history::maybe_snapshot(&snapshot_vault, &note_id, &old_raw, max_versions);
});
operations::save_note(&path, &meta, &body)?;
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(())
}
@@ -679,11 +654,7 @@ 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)?;
@@ -697,43 +668,31 @@ 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_note(vault_path, notebook_relative.as_deref(), &title)?;
let entry = operations::create_daily_note(vault_path, date.as_deref(), &config.daily_title_format)?;
// Index new note (background to avoid blocking on FUSE fsync)
index_note_bg(&state, &entry.path);
index_note_bg(&state, &entry.path);
Ok(entry)
}
#[tauri::command]
pub fn create_daily_note(
state: State<'_, AppState>,
date: Option<String>,
) -> Result<NoteEntry, 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 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();
let vault_path = config.active_vault.as_ref().ok_or("No active vault")?.clone();
drop(config);
operations::rename_note(&path, &new_title, &vault_path)
}
@@ -744,8 +703,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(())
}
@@ -765,7 +724,7 @@ pub fn move_note(
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
let new_full_path = operations::move_note(vault_path, &note_path, &dest_notebook)?;
let new_full_path = operations::move_note(&note_path, &dest_notebook)?;
// Update quick access if the moved note was in it
if !old_relative.is_empty() {
@@ -841,7 +800,10 @@ 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)
@@ -897,34 +859,23 @@ 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();
@@ -936,10 +887,7 @@ 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);
}
@@ -955,27 +903,16 @@ 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 });
}
};
@@ -1024,32 +961,25 @@ 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---")?;
let frontmatter = &after_open[..end];
for line in frontmatter.lines() {
let line = line.trim();
if let Some(title) = line.strip_prefix("title:") {
let val = title.trim();
if line.starts_with("title:") {
let val = line[6..].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());
@@ -1092,11 +1022,7 @@ 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() {
@@ -1109,11 +1035,7 @@ 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();
@@ -1135,21 +1057,6 @@ pub fn get_tasks(state: State<'_, AppState>) -> Result<Vec<crate::types::TaskIte
Ok(tasks)
}
fn read_task_note(
state: &State<'_, AppState>,
note_path: &str,
) -> Result<(String, NoteMeta, String), String> {
let config = state.config.lock().map_err(|error| error.to_string())?;
let vault_path = config
.active_vault
.as_ref()
.ok_or("No active vault")?
.clone();
drop(config);
let note = operations::read_vault_note(&vault_path, note_path)?;
Ok((vault_path, note.meta, note.content))
}
fn toggle_checkbox_line(line: &str, done: bool) -> String {
let mut s = line.to_string();
if done {
@@ -1175,8 +1082,12 @@ pub fn set_task_done(
raw_line: String,
done: bool,
) -> Result<(), String> {
let (vault_path, meta, body) = read_task_note(&state, &note_path)?;
let mut lines: Vec<String> = body.lines().map(|line| line.to_string()).collect();
let p = std::path::Path::new(&note_path);
let raw = std::fs::read_to_string(p).map_err(|e| e.to_string())?;
let filename = p.file_name().unwrap_or_default().to_string_lossy().to_string();
let (meta, body) = crate::vault::frontmatter::parse_note(&raw, &filename);
let mut lines: Vec<String> = body.lines().map(|l| l.to_string()).collect();
// Verify the expected line; if the note drifted, fall back to the first exact match.
let idx = if lines.get(line).map(|l| *l == raw_line).unwrap_or(false) {
line
@@ -1196,9 +1107,9 @@ pub fn set_task_done(
if body.ends_with('\n') && !new_body.ends_with('\n') {
new_body.push('\n');
}
operations::save_note(&vault_path, &note_path, &meta, &new_body)?;
operations::save_note(&note_path, &meta, &new_body)?;
index_note_bg(&state, &note_path);
index_note_bg(&state, &note_path);
Ok(())
}
@@ -1229,18 +1140,18 @@ pub fn set_task_priority(
Some(_) => return Err("Invalid priority".to_string()),
};
let (vault_path, meta, body) = read_task_note(&state, &note_path)?;
let mut lines: Vec<String> = body.lines().map(|line| line.to_string()).collect();
let idx = if lines
.get(line)
.map(|item| *item == raw_line)
.unwrap_or(false)
{
let p = std::path::Path::new(&note_path);
let raw = std::fs::read_to_string(p).map_err(|e| e.to_string())?;
let filename = p.file_name().unwrap_or_default().to_string_lossy().to_string();
let (meta, body) = crate::vault::frontmatter::parse_note(&raw, &filename);
let mut lines: Vec<String> = body.lines().map(|l| l.to_string()).collect();
let idx = if lines.get(line).map(|l| *l == raw_line).unwrap_or(false) {
line
} else {
lines
.iter()
.position(|item| *item == raw_line)
.position(|l| *l == raw_line)
.ok_or("Task line not found (note changed)")?
};
@@ -1253,7 +1164,7 @@ pub fn set_task_priority(
if body.ends_with('\n') && !new_body.ends_with('\n') {
new_body.push('\n');
}
operations::save_note(&vault_path, &note_path, &meta, &new_body)?;
operations::save_note(&note_path, &meta, &new_body)?;
index_note_bg(&state, &note_path);
Ok(())
@@ -1285,18 +1196,18 @@ pub fn set_task_due(
Some(_) => return Err("Invalid due date".to_string()),
};
let (vault_path, meta, body) = read_task_note(&state, &note_path)?;
let mut lines: Vec<String> = body.lines().map(|line| line.to_string()).collect();
let idx = if lines
.get(line)
.map(|item| *item == raw_line)
.unwrap_or(false)
{
let p = std::path::Path::new(&note_path);
let raw = std::fs::read_to_string(p).map_err(|e| e.to_string())?;
let filename = p.file_name().unwrap_or_default().to_string_lossy().to_string();
let (meta, body) = crate::vault::frontmatter::parse_note(&raw, &filename);
let mut lines: Vec<String> = body.lines().map(|l| l.to_string()).collect();
let idx = if lines.get(line).map(|l| *l == raw_line).unwrap_or(false) {
line
} else {
lines
.iter()
.position(|item| *item == raw_line)
.position(|l| *l == raw_line)
.ok_or("Task line not found (note changed)")?
};
@@ -1309,7 +1220,7 @@ pub fn set_task_due(
if body.ends_with('\n') && !new_body.ends_with('\n') {
new_body.push('\n');
}
operations::save_note(&vault_path, &note_path, &meta, &new_body)?;
operations::save_note(&note_path, &meta, &new_body)?;
index_note_bg(&state, &note_path);
Ok(())
@@ -1358,11 +1269,7 @@ 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())
}
@@ -1371,11 +1278,7 @@ 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)
}
@@ -1384,11 +1287,7 @@ 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)
}
@@ -1448,11 +1347,8 @@ 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
@@ -1476,8 +1372,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 {
@@ -1485,10 +1381,9 @@ 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(())
}
@@ -1503,8 +1398,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 {
@@ -1512,10 +1407,9 @@ 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(())
}
@@ -1575,7 +1469,6 @@ pub fn set_notebook_icon(
// ── General Settings ──
#[tauri::command]
#[allow(clippy::too_many_arguments)]
pub fn set_general_settings(
state: State<'_, AppState>,
compact_notes: bool,
@@ -1800,11 +1693,7 @@ 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));
}
@@ -1813,10 +1702,7 @@ 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) {
@@ -1988,6 +1874,7 @@ pub fn write_bytes_to(destination: String, data: Vec<u8>) -> Result<(), String>
Ok(())
}
// ── Backup ──
#[tauri::command]
@@ -2048,18 +1935,15 @@ pub fn list_backups(state: State<'_, AppState>) -> Result<Vec<BackupEntry>, Stri
#[tauri::command]
pub fn restore_backup(app: AppHandle, backup_path: String) -> Result<(), String> {
let (vault_path, backup_dir) = {
let vault_path = {
let state = app.state::<AppState>();
let config = state.config.lock().map_err(|e| e.to_string())?;
(
config.active_vault.clone().ok_or("No active vault")?,
crate::backup::get_backup_dir(&config.backup_location)?,
)
config.active_vault.clone().ok_or("No active vault")?
};
std::thread::spawn(move || {
use tauri::Emitter;
match crate::backup::restore_backup(&vault_path, &backup_dir, &backup_path) {
match crate::backup::restore_backup(&vault_path, &backup_path) {
Ok(()) => {
let _ = app.emit(
"restore-done",
@@ -2083,10 +1967,8 @@ pub fn restore_backup(app: AppHandle, backup_path: String) -> Result<(), String>
}
#[tauri::command]
pub fn delete_backup(state: State<'_, AppState>, backup_path: String) -> Result<(), String> {
let config = state.config.lock().map_err(|error| error.to_string())?;
let backup_dir = crate::backup::get_backup_dir(&config.backup_location)?;
crate::backup::delete_backup(&backup_dir, &backup_path)
pub fn delete_backup(backup_path: String) -> Result<(), String> {
crate::backup::delete_backup(&backup_path)
}
#[tauri::command]
@@ -2129,8 +2011,9 @@ pub fn create_version(
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 max_versions = config.max_versions_per_note;
let raw = operations::read_vault_note(vault_path, &path)?.raw;
crate::history::force_snapshot(vault_path, &note_id, &raw, max_versions)
let raw = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
crate::history::force_snapshot(vault_path, &note_id, &raw, max_versions);
Ok(())
}
#[tauri::command]
@@ -2147,7 +2030,6 @@ pub fn get_note_version_content(
// ── AI ──
#[tauri::command]
#[allow(clippy::too_many_arguments)]
pub fn set_ai_settings(
state: State<'_, AppState>,
provider: Option<String>,
@@ -2168,8 +2050,7 @@ 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,
@@ -2192,9 +2073,7 @@ 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(),
}
@@ -2236,7 +2115,11 @@ 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 {
@@ -2265,25 +2148,23 @@ mod vault_identity_tests {
#[test]
fn bookmark_identity_disambiguates_vaults_with_the_same_path() {
let mut config = AppConfig {
active_vault: Some("/same/path".to_string()),
vaults: vec![
VaultConfig {
path: "/same/path".to_string(),
name: "Local".to_string(),
..Default::default()
},
VaultConfig {
path: "/same/path".to_string(),
name: "Files".to_string(),
bookmark_id: Some("bookmark".to_string()),
..Default::default()
},
],
active_bookmark_id: Some("bookmark".to_string()),
..Default::default()
};
let mut config = AppConfig::default();
config.active_vault = Some("/same/path".to_string());
config.vaults = vec![
VaultConfig {
path: "/same/path".to_string(),
name: "Local".to_string(),
..Default::default()
},
VaultConfig {
path: "/same/path".to_string(),
name: "Files".to_string(),
bookmark_id: Some("bookmark".to_string()),
..Default::default()
},
];
config.active_bookmark_id = Some("bookmark".to_string());
assert_eq!(active_vault_config(&config).unwrap().name, "Files");
config.active_bookmark_id = None;
@@ -2309,7 +2190,6 @@ fn sync_config_from(config: &AppConfig) -> Result<crate::sync::WebdavConfig, Str
}
#[tauri::command]
#[allow(clippy::too_many_arguments)]
pub fn set_sync_settings(
state: State<'_, AppState>,
provider: Option<String>,
@@ -2382,7 +2262,9 @@ 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,
@@ -2396,9 +2278,7 @@ 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();
@@ -2416,10 +2296,7 @@ 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 }));
}
}
});
@@ -2443,9 +2320,7 @@ 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(),
}
@@ -2550,9 +2425,7 @@ 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();
@@ -2576,56 +2449,10 @@ fn migrate_global_sync_to_vault(config: &mut AppConfig) -> bool {
true
}
fn write_private_file(path: &std::path::Path, data: &[u8]) -> Result<(), String> {
use std::io::Write;
let mut options = std::fs::OpenOptions::new();
options.create(true).truncate(true).write(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options.open(path).map_err(|error| error.to_string())?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
file.set_permissions(std::fs::Permissions::from_mode(0o600))
.map_err(|error| error.to_string())?;
}
file.write_all(data).map_err(|error| error.to_string())
}
#[cfg(all(test, unix))]
mod config_permission_tests {
use super::write_private_file;
use std::os::unix::fs::PermissionsExt;
#[test]
fn config_files_are_owner_only() {
let path = std::env::temp_dir().join(format!(
"helixnotes-config-permissions-{}.json",
uuid::Uuid::new_v4()
));
std::fs::write(&path, "old").unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
write_private_file(&path, b"secret").unwrap();
assert_eq!(
std::fs::metadata(&path).unwrap().permissions().mode() & 0o777,
0o600
);
assert_eq!(std::fs::read(&path).unwrap(), b"secret");
std::fs::remove_file(path).unwrap();
}
}
fn save_app_config(config: &AppConfig) -> Result<(), String> {
let path = app_config_path()?;
let data = serde_json::to_string_pretty(config).map_err(|e| e.to_string())?;
write_private_file(&path, data.as_bytes())?;
std::fs::write(path, data).map_err(|e| e.to_string())?;
Ok(())
}
@@ -2662,15 +2489,13 @@ 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() {