mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-07-24 07:45:57 +02:00
v1.1.0 - View mode, daily notes, tag management, math support, and more
This commit is contained in:
@@ -245,6 +245,21 @@ pub fn create_note(
|
||||
Ok(entry)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_daily_note(state: State<'_, AppState>) -> Result<NoteEntry, String> {
|
||||
let config = state.config.lock().map_err(|e| e.to_string())?;
|
||||
let vault_path = config.active_vault.as_ref().ok_or("No active vault")?;
|
||||
let entry = operations::create_daily_note(vault_path)?;
|
||||
|
||||
if let Ok(search_guard) = state.search_index.lock() {
|
||||
if let Some(ref search) = *search_guard {
|
||||
let _ = search.index_note(&entry.path);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(entry)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn rename_note(path: String, new_title: String) -> Result<String, String> {
|
||||
operations::rename_note(&path, &new_title)
|
||||
@@ -1082,6 +1097,9 @@ fn resolve_wiki_ref(
|
||||
reference: &str,
|
||||
) -> String {
|
||||
let reference = reference.trim();
|
||||
// Strip Obsidian heading anchors (#) and block references (^) before lookup
|
||||
let reference = reference.split('#').next().unwrap_or(reference).trim();
|
||||
let reference = reference.split('^').next().unwrap_or(reference).trim();
|
||||
// Direct match by filename
|
||||
if let Some(rel) = file_index.get(reference) {
|
||||
return rel.clone();
|
||||
|
||||
@@ -66,6 +66,7 @@ pub fn run() {
|
||||
commands::read_note,
|
||||
commands::save_note,
|
||||
commands::create_note,
|
||||
commands::create_daily_note,
|
||||
commands::rename_note,
|
||||
commands::delete_note,
|
||||
commands::move_note,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::types::NoteMeta;
|
||||
use chrono::Utc;
|
||||
use chrono::{NaiveDate, NaiveDateTime, Utc};
|
||||
use gray_matter::engine::YAML;
|
||||
use gray_matter::Matter;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
struct RawFrontmatter {
|
||||
@@ -15,6 +14,32 @@ struct RawFrontmatter {
|
||||
modified: Option<String>,
|
||||
}
|
||||
|
||||
/// Try to parse a date string in multiple common formats.
|
||||
fn parse_date_flexible(s: &str) -> Option<chrono::DateTime<Utc>> {
|
||||
let s = s.trim();
|
||||
// RFC 3339 / ISO 8601 with timezone (e.g. 2024-01-15T10:30:00+00:00)
|
||||
if let Ok(dt) = s.parse::<chrono::DateTime<Utc>>() {
|
||||
return Some(dt);
|
||||
}
|
||||
// Try parsing as DateTime with fixed offset then convert
|
||||
if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
|
||||
return Some(dt.with_timezone(&Utc));
|
||||
}
|
||||
// YYYY-MM-DD HH:MM:SS (no timezone)
|
||||
if let Ok(ndt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
|
||||
return Some(ndt.and_utc());
|
||||
}
|
||||
// YYYY-MM-DDTHH:MM:SS (no timezone, with T separator)
|
||||
if let Ok(ndt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
|
||||
return Some(ndt.and_utc());
|
||||
}
|
||||
// YYYY-MM-DD (date only)
|
||||
if let Ok(nd) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
|
||||
return nd.and_hms_opt(0, 0, 0).map(|ndt| ndt.and_utc());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn parse_note(raw: &str, filename: &str) -> (NoteMeta, String) {
|
||||
let matter = Matter::<YAML>::new();
|
||||
let result = matter.parse(raw);
|
||||
@@ -28,15 +53,16 @@ pub fn parse_note(raw: &str, filename: &str) -> (NoteMeta, String) {
|
||||
|
||||
let created = fm
|
||||
.created
|
||||
.and_then(|s| s.parse().ok())
|
||||
.and_then(|s| parse_date_flexible(&s))
|
||||
.unwrap_or_else(Utc::now);
|
||||
|
||||
let modified = fm
|
||||
.modified
|
||||
.and_then(|s| s.parse().ok())
|
||||
.and_then(|s| parse_date_flexible(&s))
|
||||
.unwrap_or_else(Utc::now);
|
||||
|
||||
let id = fm.id.unwrap_or_else(|| Uuid::new_v4().to_string());
|
||||
// Don't generate UUID on read — empty string signals "no ID yet"
|
||||
let id = fm.id.unwrap_or_default();
|
||||
|
||||
let meta = NoteMeta {
|
||||
id,
|
||||
@@ -81,6 +107,65 @@ pub fn update_note_raw(meta: &NoteMeta, body: &str) -> String {
|
||||
format!("{}{}", fm, body)
|
||||
}
|
||||
|
||||
/// Merge NoteMeta fields into existing frontmatter, preserving unknown YAML keys.
|
||||
/// Falls back to serialize_frontmatter() if the original has no frontmatter.
|
||||
pub fn merge_frontmatter(original_raw: &str, meta: &NoteMeta, body: &str) -> String {
|
||||
let matter = Matter::<YAML>::new();
|
||||
let parsed = matter.parse(original_raw);
|
||||
|
||||
// If original had no frontmatter, use the simple serializer
|
||||
if parsed.matter.trim().is_empty() {
|
||||
return update_note_raw(meta, body);
|
||||
}
|
||||
|
||||
// Parse existing YAML into a Mapping to preserve all keys
|
||||
let mut mapping: serde_yaml::Mapping = match serde_yaml::from_str(&parsed.matter) {
|
||||
Ok(m) => m,
|
||||
Err(_) => return update_note_raw(meta, body), // unparseable YAML, fall back
|
||||
};
|
||||
|
||||
// Update only our known fields
|
||||
if !meta.id.is_empty() {
|
||||
mapping.insert(
|
||||
serde_yaml::Value::String("id".into()),
|
||||
serde_yaml::Value::String(meta.id.clone()),
|
||||
);
|
||||
}
|
||||
mapping.insert(
|
||||
serde_yaml::Value::String("title".into()),
|
||||
serde_yaml::Value::String(meta.title.clone()),
|
||||
);
|
||||
mapping.insert(
|
||||
serde_yaml::Value::String("tags".into()),
|
||||
serde_yaml::Value::Sequence(
|
||||
meta.tags
|
||||
.iter()
|
||||
.map(|t| serde_yaml::Value::String(t.clone()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
mapping.insert(
|
||||
serde_yaml::Value::String("pinned".into()),
|
||||
serde_yaml::Value::Bool(meta.pinned),
|
||||
);
|
||||
mapping.insert(
|
||||
serde_yaml::Value::String("created".into()),
|
||||
serde_yaml::Value::String(meta.created.to_rfc3339()),
|
||||
);
|
||||
mapping.insert(
|
||||
serde_yaml::Value::String("modified".into()),
|
||||
serde_yaml::Value::String(meta.modified.to_rfc3339()),
|
||||
);
|
||||
|
||||
// Serialize the mapping back to YAML
|
||||
let yaml_str = match serde_yaml::to_string(&serde_yaml::Value::Mapping(mapping)) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return update_note_raw(meta, body),
|
||||
};
|
||||
|
||||
format!("---\n{}---\n{}", yaml_str, body)
|
||||
}
|
||||
|
||||
fn filename_to_title(filename: &str) -> String {
|
||||
let stem = filename.trim_end_matches(".md");
|
||||
// Only replace dashes/underscores acting as word separators (between non-space chars).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::types::{NoteContent, NoteEntry, NoteMeta, NotebookEntry, VaultState};
|
||||
use crate::vault::frontmatter;
|
||||
use chrono::Utc;
|
||||
use chrono::{Local, Utc};
|
||||
use rayon::prelude::*;
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
@@ -184,13 +184,19 @@ fn read_note_entry_from_str(
|
||||
|
||||
let (mut meta, content) = frontmatter::parse_note(raw, &filename);
|
||||
|
||||
// Use filesystem times if frontmatter didn't have them
|
||||
// Use filesystem times only if frontmatter didn't have created/modified fields
|
||||
let has_created = raw.contains("\ncreated:") || raw.starts_with("created:");
|
||||
let has_modified = raw.contains("\nmodified:") || raw.starts_with("modified:");
|
||||
if let Ok(fs_meta) = fs::metadata(path) {
|
||||
if let Ok(modified) = fs_meta.modified() {
|
||||
meta.modified = modified.into();
|
||||
if !has_modified {
|
||||
if let Ok(modified) = fs_meta.modified() {
|
||||
meta.modified = modified.into();
|
||||
}
|
||||
}
|
||||
if let Ok(created) = fs_meta.created() {
|
||||
meta.created = created.into();
|
||||
if !has_created {
|
||||
if let Ok(created) = fs_meta.created() {
|
||||
meta.created = created.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +239,19 @@ pub fn save_note(path: &str, meta: &NoteMeta, body: &str) -> Result<(), String>
|
||||
let mut updated_meta = meta.clone();
|
||||
updated_meta.modified = Utc::now();
|
||||
|
||||
let raw = frontmatter::update_note_raw(&updated_meta, body);
|
||||
// Generate UUID on first save if note didn't have one
|
||||
if updated_meta.id.is_empty() {
|
||||
updated_meta.id = Uuid::new_v4().to_string();
|
||||
}
|
||||
|
||||
// Read existing file to preserve unknown frontmatter fields
|
||||
let existing = fs::read_to_string(path).unwrap_or_default();
|
||||
let raw = if existing.is_empty() {
|
||||
frontmatter::update_note_raw(&updated_meta, body)
|
||||
} else {
|
||||
frontmatter::merge_frontmatter(&existing, &updated_meta, body)
|
||||
};
|
||||
|
||||
fs::write(path, raw).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -290,6 +308,49 @@ pub fn create_note(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_daily_note(vault_path: &str) -> Result<NoteEntry, String> {
|
||||
let today = Local::now();
|
||||
let date_str = today.format("%Y-%m-%d").to_string();
|
||||
let title = today.format("%B %d, %Y").to_string();
|
||||
|
||||
let dir = Path::new(vault_path).join("Daily");
|
||||
fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
|
||||
let file_path = dir.join(format!("{}.md", date_str));
|
||||
let vault_root = Path::new(vault_path);
|
||||
|
||||
// If today's note already exists, return it
|
||||
if file_path.exists() {
|
||||
return read_note_entry(&file_path, vault_root);
|
||||
}
|
||||
|
||||
let now = Utc::now();
|
||||
let meta = NoteMeta {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
title,
|
||||
tags: vec!["daily".to_string()],
|
||||
pinned: false,
|
||||
created: now,
|
||||
modified: now,
|
||||
};
|
||||
|
||||
let raw = frontmatter::update_note_raw(&meta, "\n");
|
||||
fs::write(&file_path, raw).map_err(|e| e.to_string())?;
|
||||
|
||||
let relative = file_path
|
||||
.strip_prefix(vault_root)
|
||||
.unwrap_or(&file_path)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
Ok(NoteEntry {
|
||||
path: file_path.to_string_lossy().to_string(),
|
||||
relative_path: relative,
|
||||
meta,
|
||||
preview: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_notebook(
|
||||
vault_path: &str,
|
||||
parent_relative: Option<&str>,
|
||||
@@ -385,7 +446,10 @@ pub fn rename_note(path: &str, new_title: &str) -> Result<String, String> {
|
||||
let (mut meta, content) = frontmatter::parse_note(&raw, &filename);
|
||||
meta.title = new_title.to_string();
|
||||
meta.modified = Utc::now();
|
||||
let updated = frontmatter::update_note_raw(&meta, &content);
|
||||
if meta.id.is_empty() {
|
||||
meta.id = Uuid::new_v4().to_string();
|
||||
}
|
||||
let updated = frontmatter::merge_frontmatter(&raw, &meta, &content);
|
||||
|
||||
// Rename file
|
||||
let new_filename = sanitize_filename(new_title);
|
||||
|
||||
Reference in New Issue
Block a user