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