mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-09-23 03:17:29 +02:00
Revert "chore(release): prepare v1.3.5"
This reverts commit ade146f9f3.
This commit is contained in:
+184
-359
@@ -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, ¬ebook_path, &dest_parent)?;
|
||||
let new_full_path = operations::move_notebook(¬ebook_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, ¬e_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, ¬e_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, ¬e_path, &dest_notebook)?;
|
||||
let new_full_path = operations::move_note(¬e_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, ¬e_path)?;
|
||||
let mut lines: Vec<String> = body.lines().map(|line| line.to_string()).collect();
|
||||
let p = std::path::Path::new(¬e_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, ¬e_path, &meta, &new_body)?;
|
||||
operations::save_note(¬e_path, &meta, &new_body)?;
|
||||
|
||||
index_note_bg(&state, ¬e_path);
|
||||
index_note_bg(&state, ¬e_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, ¬e_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(¬e_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, ¬e_path, &meta, &new_body)?;
|
||||
operations::save_note(¬e_path, &meta, &new_body)?;
|
||||
|
||||
index_note_bg(&state, ¬e_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, ¬e_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(¬e_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, ¬e_path, &meta, &new_body)?;
|
||||
operations::save_note(¬e_path, &meta, &new_body)?;
|
||||
|
||||
index_note_bg(&state, ¬e_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, ¬e_id, &raw, max_versions)
|
||||
let raw = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
||||
crate::history::force_snapshot(vault_path, ¬e_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() {
|
||||
|
||||
Reference in New Issue
Block a user