v1.1.6 - Multi-window, single-instance file handling, line height setting

This commit is contained in:
Yuri Karamian
2026-02-23 11:16:16 +01:00
parent 73a53dcb85
commit 3453d406fc
19 changed files with 634 additions and 32 deletions
+1 -1
View File
@@ -1750,7 +1750,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "helixnotes"
version = "1.1.5"
version = "1.1.6"
dependencies = [
"chrono",
"dirs",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "helixnotes"
version = "1.1.5"
version = "1.1.6"
description = "Local markdown note-taking app"
authors = ["HelixNotes"]
license = "AGPL-3.0-or-later"
+3 -1
View File
@@ -2,7 +2,7 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "HelixNotes default capabilities",
"windows": ["main"],
"windows": ["main", "note-*"],
"permissions": [
"core:default",
"core:window:default",
@@ -12,6 +12,8 @@
"core:window:allow-close",
"core:window:allow-is-maximized",
"core:window:allow-set-decorations",
"core:window:allow-set-title",
"core:webview:allow-create-webview-window",
"dialog:default",
"dialog:allow-open",
"dialog:allow-save",
+14
View File
@@ -45,6 +45,12 @@ pub fn get_app_config(state: State<'_, AppState>) -> Result<AppConfig, String> {
Ok(config.clone())
}
#[tauri::command]
pub fn get_pending_open_file(state: State<'_, AppState>) -> Result<Option<String>, String> {
let mut pending = state.pending_open_file.lock().map_err(|e| e.to_string())?;
Ok(pending.take())
}
#[tauri::command]
pub fn set_theme(state: State<'_, AppState>, theme: String) -> Result<(), String> {
let mut config = state.config.lock().map_err(|e| e.to_string())?;
@@ -77,6 +83,14 @@ pub fn set_font_family(state: State<'_, AppState>, family: String) -> Result<(),
Ok(())
}
#[tauri::command]
pub fn set_line_height(state: State<'_, AppState>, height: f64) -> Result<(), String> {
let mut config = state.config.lock().map_err(|e| e.to_string())?;
config.line_height = Some(height);
save_app_config(&config)?;
Ok(())
}
// ── Notebooks ──
#[tauri::command]
+66 -9
View File
@@ -9,7 +9,7 @@ mod vault;
use state::AppState;
#[allow(unused_imports)]
use tauri::Manager;
use tauri::{Emitter, Manager};
#[cfg(not(target_os = "android"))]
use tauri::{
@@ -59,6 +59,28 @@ pub fn run() {
setup_tray(app)?;
}
// Check CLI args for a .md file path on initial launch
#[cfg(not(target_os = "android"))]
{
let file_arg = std::env::args().skip(1).find(|a| {
let a = a.trim();
!a.starts_with('-') && a.ends_with(".md")
});
if let Some(path) = file_arg {
let resolved = if std::path::Path::new(&path).is_absolute() {
std::path::PathBuf::from(&path)
} else {
std::env::current_dir().unwrap_or_default().join(&path)
};
if let Some(resolved_str) = resolved.to_str() {
let app_state = app.state::<AppState>();
let _ = app_state.pending_open_file.lock().map(|mut p| {
*p = Some(resolved_str.to_string());
});
}
}
}
Ok(())
})
.manage(app_state)
@@ -69,6 +91,7 @@ pub fn run() {
commands::set_accent_color,
commands::set_font_size,
commands::set_font_family,
commands::set_line_height,
commands::get_notebooks,
commands::create_notebook,
commands::rename_notebook,
@@ -117,28 +140,62 @@ pub fn run() {
commands::test_ai_connection,
commands::ai_ask,
commands::get_install_type,
commands::get_pending_open_file,
]);
#[cfg(not(target_os = "android"))]
{
builder = builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
builder = builder.plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
// Always show/focus the main window
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.unminimize();
let _ = window.set_focus();
}
// Extract .md file path from args (args[0] is the binary)
let file_path = args.iter().skip(1).find(|arg| {
let a = arg.trim();
!a.starts_with('-') && a.ends_with(".md")
});
if let Some(path) = file_path {
let resolved = if std::path::Path::new(path.as_str()).is_absolute() {
std::path::PathBuf::from(path)
} else {
std::path::Path::new(&cwd).join(path)
};
if let Some(resolved_str) = resolved.to_str() {
let _ = app.emit("open-file", resolved_str.to_string());
}
}
}));
builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
if close_to_tray {
builder = builder.on_window_event(|window, event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close();
let _ = window.hide();
builder = builder.on_window_event(move |window, event| {
match event {
tauri::WindowEvent::CloseRequested { api, .. } => {
// Only hide to tray for the main window
if close_to_tray && window.label() == "main" {
api.prevent_close();
let _ = window.hide();
}
}
});
}
tauri::WindowEvent::Destroyed => {
// When main window is destroyed, close all note windows
if window.label() == "main" {
let app = window.app_handle();
for (label, win) in app.webview_windows() {
if label.starts_with("note-") {
let _ = win.close();
}
}
}
}
_ => {}
}
});
}
builder
+2
View File
@@ -9,6 +9,7 @@ pub struct AppState {
pub search_index: Mutex<Option<SearchIndex>>,
pub watcher: Mutex<Option<RecommendedWatcher>>,
pub importing: AtomicBool,
pub pending_open_file: Mutex<Option<String>>,
}
impl AppState {
@@ -18,6 +19,7 @@ impl AppState {
search_index: Mutex::new(None),
watcher: Mutex::new(None),
importing: AtomicBool::new(false),
pending_open_file: Mutex::new(None),
}
}
}
+3
View File
@@ -54,6 +54,8 @@ pub struct AppConfig {
#[serde(default)]
pub font_family: Option<String>,
#[serde(default)]
pub line_height: Option<f64>,
#[serde(default)]
pub compact_notes: bool,
#[serde(default)]
pub time_format: String,
@@ -142,6 +144,7 @@ impl Default for AppConfig {
accent_color: None,
font_size: None,
font_family: None,
line_height: None,
compact_notes: false,
time_format: "relative".to_string(),
gpu_acceleration: true,
+27 -2
View File
@@ -77,6 +77,29 @@ pub fn parse_note(raw: &str, filename: &str) -> (NoteMeta, String) {
(meta, content)
}
/// Ensure the body starts with `# Title` heading for external app compatibility.
/// If the body already starts with `# Title`, leave it as-is.
fn ensure_title_heading(body: &str, title: &str) -> String {
let trimmed = body.trim_start();
// Check if body already starts with `# Title`
if let Some(rest) = trimmed.strip_prefix("# ") {
let first_line_end = rest.find('\n').unwrap_or(rest.len());
let heading_text = rest[..first_line_end].trim();
if heading_text.eq_ignore_ascii_case(title.trim()) {
return body.to_string();
}
}
// Don't add heading to empty notes (new/blank notes)
if trimmed.is_empty() {
return body.to_string();
}
// Prepend `# Title` heading
format!("# {}\n\n{}", title, body)
}
pub fn serialize_frontmatter(meta: &NoteMeta) -> String {
let tags_str = if meta.tags.is_empty() {
"[]".to_string()
@@ -104,7 +127,8 @@ pub fn serialize_frontmatter(meta: &NoteMeta) -> String {
pub fn update_note_raw(meta: &NoteMeta, body: &str) -> String {
let fm = serialize_frontmatter(meta);
format!("{}{}", fm, body)
let body_with_heading = ensure_title_heading(body, &meta.title);
format!("{}{}", fm, body_with_heading)
}
/// Merge NoteMeta fields into existing frontmatter, preserving unknown YAML keys.
@@ -163,7 +187,8 @@ pub fn merge_frontmatter(original_raw: &str, meta: &NoteMeta, body: &str) -> Str
Err(_) => return update_note_raw(meta, body),
};
format!("---\n{}---\n{}", yaml_str, body)
let body_with_heading = ensure_title_heading(body, &meta.title);
format!("---\n{}---\n{}", yaml_str, body_with_heading)
}
fn filename_to_title(filename: &str) -> String {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "HelixNotes",
"version": "1.1.5",
"version": "1.1.6",
"identifier": "com.helixnotes.app",
"build": {
"frontendDist": "../build",