diff --git a/.gitignore b/.gitignore index 095c31a..2eaca73 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ dist-ssr target AGENTS.md coverage +.sandbox \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 8e207f0..b146fd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1543,6 +1543,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -1675,6 +1685,24 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "global-hotkey" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7" +dependencies = [ + "crossbeam-channel", + "keyboard-types", + "objc2", + "objc2-app-kit", + "once_cell", + "serde", + "thiserror 2.0.18", + "windows-sys 0.59.0", + "x11rb", + "xkeysym", +] + [[package]] name = "gobject-sys" version = "0.18.0" @@ -4416,6 +4444,21 @@ dependencies = [ "url", ] +[[package]] +name = "tauri-plugin-global-shortcut" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "424af23c7e88d05e4a1a6fc2c7be077912f8c76bd7900fd50aa2b7cbf5a2c405" +dependencies = [ + "global-hotkey", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + [[package]] name = "tauri-plugin-log" version = "2.8.0" @@ -5977,6 +6020,7 @@ dependencies = [ "tauri-build", "tauri-plugin-dialog", "tauri-plugin-fs", + "tauri-plugin-global-shortcut", "tauri-plugin-log", "tauri-plugin-opener", "tauri-plugin-persisted-scope", @@ -6097,12 +6141,35 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + [[package]] name = "xdg" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index 8c61db7..2163a9b 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -18,9 +18,11 @@ mod text_utils; pub use settings::StyleCheckSettings; pub use settings::UiLayoutSettings; +pub use settings::{CaptureDocRef, CaptureMode, GlobalCaptureSettings}; const UI_LAYOUT_SETTINGS_KEY: &str = "ui_layout"; const STYLE_CHECK_SETTINGS_KEY: &str = "style_check"; +const GLOBAL_CAPTURE_SETTINGS_KEY: &str = "global_capture"; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct StyleCheckPattern { @@ -257,6 +259,59 @@ impl Store { Ok(()) } + pub fn global_capture_get(&self) -> Result { + let conn = self + .conn + .lock() + .map_err(|_| AppError::new(ErrorCode::Io, "Failed to lock database connection"))?; + + let maybe_value = conn + .query_row( + "SELECT value FROM app_settings WHERE key = ?1", + params![GLOBAL_CAPTURE_SETTINGS_KEY], + |row| row.get::<_, String>(0), + ) + .optional() + .map_err(|e| AppError::io(format!("Failed to query global capture settings: {}", e)))?; + + match maybe_value { + Some(value) => serde_json::from_str::(&value).map_err(|e| { + AppError::new( + ErrorCode::Parse, + format!("Failed to parse persisted global capture settings: {}", e), + ) + }), + None => Ok(GlobalCaptureSettings::default()), + } + } + + pub fn global_capture_set(&self, settings: &GlobalCaptureSettings) -> Result<(), AppError> { + let conn = self + .conn + .lock() + .map_err(|_| AppError::new(ErrorCode::Io, "Failed to lock database connection"))?; + + let settings_json = serde_json::to_string(settings).map_err(|e| { + AppError::new( + ErrorCode::Parse, + format!("Failed to serialize global capture settings: {}", e), + ) + })?; + let updated_at = Utc::now().to_rfc3339(); + + conn.execute( + "INSERT INTO app_settings (key, value, updated_at) + VALUES (?1, ?2, ?3) + ON CONFLICT(key) DO UPDATE SET + value = excluded.value, + updated_at = excluded.updated_at", + params![GLOBAL_CAPTURE_SETTINGS_KEY, settings_json, updated_at], + ) + .map_err(|e| AppError::io(format!("Failed to persist global capture settings: {}", e)))?; + + Ok(()) + } + /// Adds a new location pub fn location_add(&self, name: String, root_path: PathBuf) -> Result { let path_str = root_path.to_string_lossy().to_string(); @@ -1474,4 +1529,72 @@ mod tests { assert_eq!(loaded.editor_font_size, 16); assert_eq!(loaded.editor_font_family, "IBM Plex Mono"); } + + #[test] + fn test_global_capture_settings_defaults() { + let (store, _temp) = create_test_store(); + let settings = store.global_capture_get().unwrap(); + + assert_eq!(settings, GlobalCaptureSettings::default()); + assert!(!settings.enabled); + assert_eq!(settings.shortcut, "CommandOrControl+Shift+Space"); + assert!(!settings.paused); + assert_eq!(settings.default_mode, CaptureMode::QuickNote); + assert!(settings.target_location_id.is_none()); + assert_eq!(settings.inbox_relative_dir, "inbox"); + assert!(settings.append_target.is_none()); + assert!(settings.close_after_save); + assert!(settings.show_tray_icon); + assert!(settings.last_capture_target.is_none()); + } + + #[test] + fn test_global_capture_settings_round_trip() { + let (store, _temp) = create_test_store(); + let settings = GlobalCaptureSettings { + enabled: true, + shortcut: "CommandOrControl+Shift+N".to_string(), + paused: false, + default_mode: CaptureMode::Append, + target_location_id: Some(42), + inbox_relative_dir: "captures".to_string(), + append_target: Some(CaptureDocRef { location_id: 42, rel_path: "notes/daily.md".to_string() }), + close_after_save: false, + show_tray_icon: false, + last_capture_target: Some("Inbox/Daily".to_string()), + }; + + store.global_capture_set(&settings).unwrap(); + let loaded = store.global_capture_get().unwrap(); + + assert_eq!(loaded, settings); + } + + #[test] + fn test_global_capture_settings_backfills_defaults() { + let (store, _temp) = create_test_store(); + let conn = store + .conn + .lock() + .expect("expected to lock database connection for test"); + + conn.execute( + "INSERT INTO app_settings (key, value, updated_at) VALUES (?1, ?2, ?3)", + params![ + GLOBAL_CAPTURE_SETTINGS_KEY, + "{\"enabled\":true}", + Utc::now().to_rfc3339(), + ], + ) + .unwrap(); + drop(conn); + + let loaded = store.global_capture_get().unwrap(); + assert!(loaded.enabled); + assert_eq!(loaded.shortcut, "CommandOrControl+Shift+Space"); + assert!(!loaded.paused); + assert_eq!(loaded.inbox_relative_dir, "inbox"); + assert!(loaded.close_after_save); + assert!(loaded.show_tray_icon); + } } diff --git a/crates/store/src/settings.rs b/crates/store/src/settings.rs index 66bcbc7..7bc75b8 100644 --- a/crates/store/src/settings.rs +++ b/crates/store/src/settings.rs @@ -1,6 +1,5 @@ -use serde::{Deserialize, Serialize}; - use super::StyleCheckPattern; +use serde::{Deserialize, Serialize}; fn default_true() -> bool { true @@ -14,6 +13,14 @@ fn default_editor_font_family() -> String { "IBM Plex Mono".to_string() } +fn default_global_capture_shortcut() -> String { + "CommandOrControl+Shift+Space".to_string() +} + +fn default_inbox_dir() -> String { + "inbox".to_string() +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct StyleCheckCategorySettings { pub filler: bool, @@ -68,3 +75,58 @@ impl Default for UiLayoutSettings { } } } + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] +pub enum CaptureMode { + #[default] + QuickNote, + WritingSession, + Append, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct CaptureDocRef { + pub location_id: i64, + pub rel_path: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct GlobalCaptureSettings { + #[serde(default)] + pub enabled: bool, + #[serde(default = "default_global_capture_shortcut")] + pub shortcut: String, + #[serde(default)] + pub paused: bool, + #[serde(default)] + pub default_mode: CaptureMode, + #[serde(default)] + pub target_location_id: Option, + #[serde(default = "default_inbox_dir")] + pub inbox_relative_dir: String, + #[serde(default)] + pub append_target: Option, + #[serde(default = "default_true")] + pub close_after_save: bool, + #[serde(default = "default_true")] + pub show_tray_icon: bool, + #[serde(default)] + pub last_capture_target: Option, +} + +impl Default for GlobalCaptureSettings { + fn default() -> Self { + Self { + enabled: false, + shortcut: default_global_capture_shortcut(), + paused: false, + default_mode: CaptureMode::default(), + target_location_id: None, + inbox_relative_dir: default_inbox_dir(), + append_target: None, + close_after_save: true, + show_tray_icon: true, + last_capture_target: None, + } + } +} diff --git a/docs/roadmap.md b/docs/roadmap.md index 91e7224..2a47120 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -3,10 +3,9 @@ title: "Roadmap" last_updated: 2026-02-23 --- - ## Focus mode enhancements (typewriter scrolling + sentence dimming) -Evolve the existing Focus toggle into a writing environment. This should be the default mode. +Evolve the existing Focus toggle into a stronger writing environment. ### Tasks @@ -70,6 +69,51 @@ Real-time prose polish that flags weak patterns without altering the source text - Allow adding/removing patterns from each dictionary - Persist custom dictionaries in settings/store +## Global capture (shortcut + menu bar/tray quick add) + +Add a global capture flow so notes can be added from anywhere in macOS without opening the full app. + +### Tasks + +1. **Global shortcut registration** + - Register a user-configurable app-wide shortcut in Tauri (default: `Cmd+Shift+Space`) + - Persist shortcut choice in settings and validate conflicts +2. **Quick-add floating window** + - Add a lightweight always-on-top capture window for fast note entry + - Support modes: quick note, longer writing session, and append-to-existing note + - Keyboard-first flow: open, write, save, close without mouse interaction +3. **macOS menu bar + tray integration** + - Add a menu bar/tray icon with actions: New Quick Note, Open Writer, Pause Shortcut, Quit + - Show shortcut hint and last capture target in the menu +4. **Capture routing + storage** + - Route quick captures to a default inbox note/location + - Optional prompt to choose destination when modifier key is held +5. **Reliability + permissions** + - Handle app lifecycle edge cases (relaunch, sleep/wake, multiple windows) + - Add onboarding/help text for accessibility and shortcut permissions on macOS + +## Calm UI defaults (focus-by-default + less intrusive chrome) + +Make writing-first, low-distraction behavior the default experience while preserving power-user controls. + +### Tasks + +1. **Default focus mode** + - Enable Focus mode by default for new installs and when creating/opening a document + - Add migration logic so existing users keep current preference unless they opt in +2. **Reduce interface noise** + - Auto-hide non-essential panels/toolbars while typing + - Replace persistent controls with contextual/hover/focus-triggered controls where appropriate +3. **Density + spacing pass** + - Tune spacing, contrast, and visual hierarchy to keep the editor dominant + - Reduce competing highlights and non-critical badges in writing view +4. **Settings + escape hatches** + - Add a simple `Calm UI` preset toggle with granular overrides + - One-command shortcut to temporarily reveal all UI chrome +5. **Validation** + - Usability pass for keyboard-only and screen-reader flows + - Add regression tests for panel visibility, focus defaults, and persisted preferences + ## Content blocks (transclusion) Allow embedding external Markdown files, images, and CSV data into a master document using `/filename` syntax. diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 30d8afa..7e569f3 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -18,13 +18,14 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2", features = [] } +tauri = { version = "2", features = ["tray-icon"] } tauri-plugin-log = "2" tauri-plugin-opener = "2" tauri-plugin-dialog = "2" tauri-plugin-fs = "2" tauri-plugin-persisted-scope = "2" tauri-plugin-store = "2" +tauri-plugin-global-shortcut = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" tracing = "0.1" diff --git a/src-tauri/icons/tray.png b/src-tauri/icons/tray.png new file mode 100644 index 0000000000000000000000000000000000000000..bba636dda9cf05a05c8805c6ad8e196162acce94 GIT binary patch literal 114230 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 z Result { + if let Some(window) = app.get_webview_window(QUICK_CAPTURE_WINDOW_LABEL) { + return Ok(window); + } + + let window = WebviewWindowBuilder::new(app, QUICK_CAPTURE_WINDOW_LABEL, WebviewUrl::App("/".into())) + .title("Quick Capture") + .inner_size(480.0, 320.0) + .resizable(false) + .always_on_top(true) + .visible(false) + .center() + .focused(true) + .build() + .map_err(|e| AppError::io(format!("Failed to create quick capture window: {}", e)))?; + + Ok(window) +} + +/// Shows and focuses the quick capture window. +pub fn show_quick_capture_window(app: &AppHandle) -> Result<(), AppError> { + let window = ensure_quick_capture_window(app)?; + + window + .show() + .map_err(|e| AppError::io(format!("Failed to show window: {}", e)))?; + window + .set_focus() + .map_err(|e| AppError::io(format!("Failed to focus window: {}", e)))?; + + Ok(()) +} + +/// Shows and focuses the main window. +pub fn show_main_window(app: &AppHandle) -> Result<(), AppError> { + if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) { + window + .show() + .map_err(|e| AppError::io(format!("Failed to show main window: {}", e)))?; + window + .set_focus() + .map_err(|e| AppError::io(format!("Failed to focus main window: {}", e)))?; + } + Ok(()) +} + +/// Registers a global shortcut. +pub fn register_global_shortcut(app: &AppHandle, shortcut_str: &str) -> Result<(), AppError> { + let shortcut = parse_shortcut(shortcut_str)?; + + app.global_shortcut() + .on_shortcut(shortcut, move |app, shortcut, event| { + if event.state == ShortcutState::Pressed { + tracing::info!("Global shortcut triggered: {:?}", shortcut); + if let Err(e) = show_quick_capture_window(app) { + tracing::error!("Failed to show quick capture window: {}", e); + } + } + }) + .map_err(|e| { + AppError::new( + ErrorCode::PermissionDenied, + format!( + "Failed to register global shortcut: {}. This may be due to a conflict with another application.", + e + ), + ) + })?; + + tracing::info!("Global shortcut registered: {}", shortcut_str); + Ok(()) +} + +/// Reconciles shortcut registration based on current settings. +pub fn reconcile_shortcut_registration(app: &AppHandle, settings: &GlobalCaptureSettings) -> Result<(), AppError> { + let gs = app.global_shortcut(); + if let Err(e) = gs.unregister_all() { + tracing::warn!("Failed to unregister existing shortcuts: {}", e); + } + + if settings.enabled && !settings.paused { + register_global_shortcut(app, &settings.shortcut)?; + } + + Ok(()) +} + +fn tray_pause_label(paused: bool) -> &'static str { + if paused { "Resume Shortcut" } else { "Pause Shortcut" } +} + +fn tray_shortcut_status(settings: &GlobalCaptureSettings) -> String { + if !settings.enabled { + return "Shortcut: Disabled".to_string(); + } + if settings.paused { + return format!("Shortcut: Paused ({})", settings.shortcut); + } + format!("Shortcut: {}", settings.shortcut) +} + +fn tray_target_status(settings: &GlobalCaptureSettings) -> String { + match settings.last_capture_target.as_deref() { + Some(target) => format!("Last target: {}", target), + None => "Last target: none".to_string(), + } +} + +fn toggle_shortcut_pause_from_tray(app: &AppHandle) -> Result<(), AppError> { + let state = app.state::(); + let mut settings = state.store.global_capture_get()?; + settings.paused = !settings.paused; + state.store.global_capture_set(&settings)?; + reconcile_shortcut_registration(app, &settings)?; + build_or_update_tray_menu(app, &settings)?; + Ok(()) +} + +fn handle_tray_menu_action(app: &AppHandle, item_id: &str) -> Result<(), AppError> { + match item_id { + MENU_ITEM_NEW_QUICK_NOTE => show_quick_capture_window(app), + MENU_ITEM_OPEN_WRITER => show_main_window(app), + MENU_ITEM_TOGGLE_SHORTCUT => toggle_shortcut_pause_from_tray(app), + MENU_ITEM_QUIT => { + app.exit(0); + Ok(()) + } + _ => Ok(()), + } +} + +/// Builds or updates tray menu/icon according to current global capture settings. +pub fn build_or_update_tray_menu(app: &AppHandle, settings: &GlobalCaptureSettings) -> Result<(), AppError> { + if !settings.show_tray_icon { + if let Some(tray) = app.tray_by_id(TRAY_ICON_ID) { + tray.set_visible(false) + .map_err(|e| AppError::io(format!("Failed to hide tray icon: {}", e)))?; + } + return Ok(()); + } + + let new_quick_note = MenuItem::with_id(app, MENU_ITEM_NEW_QUICK_NOTE, "New Quick Note", true, None::<&str>) + .map_err(|e| AppError::io(format!("Failed to create tray menu item: {}", e)))?; + let open_writer = MenuItem::with_id(app, MENU_ITEM_OPEN_WRITER, "Open Writer", true, None::<&str>) + .map_err(|e| AppError::io(format!("Failed to create tray menu item: {}", e)))?; + let toggle_shortcut = MenuItem::with_id( + app, + MENU_ITEM_TOGGLE_SHORTCUT, + tray_pause_label(settings.paused), + true, + None::<&str>, + ) + .map_err(|e| AppError::io(format!("Failed to create tray menu item: {}", e)))?; + let shortcut_status = MenuItem::with_id( + app, + MENU_ITEM_SHORTCUT_STATUS, + tray_shortcut_status(settings), + false, + None::<&str>, + ) + .map_err(|e| AppError::io(format!("Failed to create tray menu item: {}", e)))?; + let target_status = MenuItem::with_id( + app, + MENU_ITEM_TARGET_STATUS, + tray_target_status(settings), + false, + None::<&str>, + ) + .map_err(|e| AppError::io(format!("Failed to create tray menu item: {}", e)))?; + let separator_a = PredefinedMenuItem::separator(app) + .map_err(|e| AppError::io(format!("Failed to create tray menu separator: {}", e)))?; + let separator_b = PredefinedMenuItem::separator(app) + .map_err(|e| AppError::io(format!("Failed to create tray menu separator: {}", e)))?; + let quit = MenuItem::with_id(app, MENU_ITEM_QUIT, "Quit", true, None::<&str>) + .map_err(|e| AppError::io(format!("Failed to create tray menu item: {}", e)))?; + + let menu = Menu::with_items( + app, + &[ + &new_quick_note, + &open_writer, + &toggle_shortcut, + &separator_a, + &shortcut_status, + &target_status, + &separator_b, + &quit, + ], + ) + .map_err(|e| AppError::io(format!("Failed to build tray menu: {}", e)))?; + + if let Some(tray) = app.tray_by_id(TRAY_ICON_ID) { + tray.set_menu(Some(menu)) + .map_err(|e| AppError::io(format!("Failed to update tray menu: {}", e)))?; + tray.set_visible(true) + .map_err(|e| AppError::io(format!("Failed to show tray icon: {}", e)))?; + return Ok(()); + } + + let mut builder = TrayIconBuilder::with_id(TRAY_ICON_ID) + .menu(&menu) + .tooltip("Writer") + .show_menu_on_left_click(true) + .on_menu_event(|app, event| { + let menu_id = event.id().as_ref(); + if let Err(e) = handle_tray_menu_action(app, menu_id) { + tracing::error!("Tray menu action failed ({}): {}", menu_id, e); + } + }); + + if let Some(icon) = app.default_window_icon().cloned() { + builder = builder.icon(icon); + } + + builder + .build(app) + .map_err(|e| AppError::io(format!("Failed to create tray icon: {}", e)))?; + + Ok(()) +} + +/// Reconciles all runtime capture integrations (shortcut + tray). +pub fn reconcile_capture_runtime(app: &AppHandle, settings: &GlobalCaptureSettings) -> Result<(), AppError> { + reconcile_shortcut_registration(app, settings)?; + build_or_update_tray_menu(app, settings)?; + Ok(()) +} + +/// Parses a shortcut string into a Shortcut object. +fn parse_shortcut(shortcut_str: &str) -> Result { + shortcut_str.parse().map_err(|e| { + AppError::new( + ErrorCode::InvalidPath, + format!("Invalid shortcut format '{}': {}", shortcut_str, e), + ) + }) +} + +/// Generates a filename for a quick note capture. +pub fn generate_quick_note_path(inbox_dir: &str) -> PathBuf { + let now = chrono::Local::now(); + let year = now.format("%Y").to_string(); + let filename = now.format("%Y-%m-%d_%H-%M-%S%.3f.md").to_string(); + + PathBuf::from(inbox_dir).join(year).join(filename) +} + +/// Validates a shortcut string format. +pub fn validate_shortcut_format(shortcut: &str) -> Result<(), AppError> { + if shortcut.trim().is_empty() { + return Err(AppError::new(ErrorCode::InvalidPath, "Shortcut cannot be empty")); + } + + let _: Shortcut = shortcut + .parse() + .map_err(|e| AppError::new(ErrorCode::InvalidPath, format!("Invalid shortcut format: {}", e)))?; + + Ok(()) +} + +/// Handles capture submission based on mode. +pub async fn handle_capture_submit( + app: &AppHandle, mode: CaptureMode, text: String, target_location_id: Option, inbox_dir: &str, + append_target: &Option, close_after_save: bool, +) -> Result { + let state = app.state::(); + + if text.trim().is_empty() { + return Err(AppError::new(ErrorCode::InvalidPath, "Capture text cannot be empty")); + } + + let location_id = match target_location_id { + Some(id) => LocationId(id), + None => { + let locations = state.store.location_list()?; + match locations.first() { + Some(loc) => loc.id, + None => { + return Err(AppError::new( + ErrorCode::NotFound, + "No location configured. Please add a location first.", + )); + } + } + } + }; + + match mode { + CaptureMode::QuickNote => { + let rel_path = generate_quick_note_path(inbox_dir); + let doc_id = DocId::new(location_id, rel_path.clone())?; + let result = state.store.doc_save(&doc_id, &text, None)?; + let target_str = format!("{}/{}", location_id.0, rel_path.to_string_lossy()); + + Ok(CaptureSubmitResult { + success: result.success, + saved_to: Some(rel_path.to_string_lossy().to_string()), + location_id: location_id.0, + should_close: close_after_save, + last_capture_target: Some(target_str), + }) + } + CaptureMode::Append => { + let target = match append_target { + Some(t) => t, + None => { + return Err(AppError::new(ErrorCode::InvalidPath, "No append target configured")); + } + }; + + let target_location = LocationId(target.location_id); + let rel_path = PathBuf::from(&target.rel_path); + let doc_id = DocId::new(target_location, rel_path)?; + let existing_text = state.store.doc_open(&doc_id).map(|c| c.text).unwrap_or_default(); + let new_text = if existing_text.is_empty() { text } else { format!("{}\n\n{}", existing_text, text) }; + let result = state.store.doc_save(&doc_id, &new_text, None)?; + let target_str = format!("{}/{}", target.location_id, target.rel_path); + + Ok(CaptureSubmitResult { + success: result.success, + saved_to: Some(target.rel_path.clone()), + location_id: target.location_id, + should_close: close_after_save, + last_capture_target: Some(target_str), + }) + } + CaptureMode::WritingSession => { + let rel_path = generate_quick_note_path(inbox_dir); + let doc_id = DocId::new(location_id, rel_path.clone())?; + let result = state.store.doc_save(&doc_id, &text, None)?; + let target_str = format!("{}/{}", location_id.0, rel_path.to_string_lossy()); + + Ok(CaptureSubmitResult { + success: result.success, + saved_to: Some(rel_path.to_string_lossy().to_string()), + location_id: location_id.0, + should_close: false, + last_capture_target: Some(target_str), + }) + } + } +} + +/// Result of a capture submission. +#[derive(Debug, Clone, serde::Serialize)] +pub struct CaptureSubmitResult { + pub success: bool, + pub saved_to: Option, + pub location_id: i64, + pub should_close: bool, + pub last_capture_target: Option, +} + +/// Updates the last capture target in settings and refreshes tray status text. +pub fn update_last_capture_target(app: &AppHandle, target: Option) -> Result<(), AppError> { + let state = app.state::(); + let mut settings = state.store.global_capture_get()?; + settings.last_capture_target = target; + state.store.global_capture_set(&settings)?; + build_or_update_tray_menu(app, &settings)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::{generate_quick_note_path, validate_shortcut_format}; + + #[test] + fn validate_shortcut_accepts_known_valid_shortcuts() { + assert!(validate_shortcut_format("CommandOrControl+Shift+Space").is_ok()); + assert!(validate_shortcut_format("Alt+KeyN").is_ok()); + } + + #[test] + fn validate_shortcut_rejects_empty_shortcut() { + assert!(validate_shortcut_format("").is_err()); + assert!(validate_shortcut_format(" ").is_err()); + } + + #[test] + fn generate_quick_note_path_uses_inbox_year_and_markdown_extension() { + let path = generate_quick_note_path("inbox"); + let path_str = path.to_string_lossy(); + let parts: Vec<&str> = path_str.split('/').collect(); + + assert!(parts.len() >= 3); + assert_eq!(parts[0], "inbox"); + assert_eq!(parts[1].len(), 4); + assert!(parts[2].ends_with(".md")); + } +} diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index dc3d66b..a40f85f 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,3 +1,4 @@ +use super::capture; use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; use std::collections::HashMap; use std::path::PathBuf; @@ -631,3 +632,151 @@ pub fn style_check_set(state: State<'_, AppState>, settings: StyleCheckSettings) } } } + +/// Gets global capture settings +#[tauri::command] +pub fn global_capture_get( + state: State<'_, AppState>, +) -> Result, ()> { + tracing::debug!("Loading global capture settings"); + + match state.store.global_capture_get() { + Ok(settings) => Ok(CommandResult::ok(settings)), + Err(e) => { + tracing::error!("Failed to load global capture settings: {}", e); + Ok(CommandResult::err(e)) + } + } +} + +/// Sets global capture settings and reconciles runtime state +#[tauri::command] +pub fn global_capture_set( + app: AppHandle, state: State<'_, AppState>, settings: writer_store::GlobalCaptureSettings, +) -> Result, ()> { + tracing::debug!("Persisting global capture settings"); + + if let Err(e) = capture::validate_shortcut_format(&settings.shortcut) { + return Ok(CommandResult::err(e)); + } + + match state.store.global_capture_set(&settings) { + Ok(()) => match capture::reconcile_capture_runtime(&app, &settings) { + Ok(_) => Ok(CommandResult::ok(true)), + Err(e) => { + tracing::error!("Failed to reconcile capture runtime: {}", e); + Ok(CommandResult::err(e)) + } + }, + Err(e) => { + tracing::error!("Failed to persist global capture settings: {}", e); + Ok(CommandResult::err(e)) + } + } +} + +/// Opens the quick capture window +#[tauri::command] +pub fn global_capture_open(app: AppHandle) -> Result, ()> { + tracing::debug!("Opening quick capture window"); + + match capture::show_quick_capture_window(&app) { + Ok(()) => Ok(CommandResult::ok(true)), + Err(e) => { + tracing::error!("Failed to open quick capture window: {}", e); + Ok(CommandResult::err(e)) + } + } +} + +/// Submits a capture +#[tauri::command] +pub async fn global_capture_submit( + app: AppHandle, state: State<'_, AppState>, mode: writer_store::CaptureMode, text: String, + destination: Option, open_main_after_save: Option, +) -> Result, ()> { + tracing::debug!("Submitting capture: mode={:?}, text_len={}", mode, text.len()); + + let settings = match state.store.global_capture_get() { + Ok(s) => s, + Err(e) => return Ok(CommandResult::err(e)), + }; + + let target_location = destination + .as_ref() + .map(|d| d.location_id) + .or(settings.target_location_id); + + let append_target = if let Some(ref dest) = destination { + Some(writer_store::CaptureDocRef { location_id: dest.location_id, rel_path: dest.rel_path.clone() }) + } else { + settings.append_target.clone() + }; + + match crate::capture::handle_capture_submit( + &app, + mode, + text, + target_location, + &settings.inbox_relative_dir, + &append_target, + settings.close_after_save, + ) + .await + { + Ok(result) => { + if let Err(e) = capture::update_last_capture_target(&app, result.last_capture_target.clone()) { + tracing::warn!("Failed to update last capture target: {}", e); + } + + if open_main_after_save.unwrap_or(false) && result.success { + if let Err(e) = capture::show_main_window(&app) { + tracing::warn!("Failed to show main window: {}", e); + } + } + + Ok(CommandResult::ok(result)) + } + Err(e) => Ok(CommandResult::err(e)), + } +} + +/// Pauses or resumes the global shortcut +#[tauri::command] +pub fn global_capture_pause( + app: AppHandle, state: State<'_, AppState>, paused: bool, +) -> Result, ()> { + tracing::debug!("Setting global capture pause state: {}", paused); + + let mut settings = match state.store.global_capture_get() { + Ok(s) => s, + Err(e) => return Ok(CommandResult::err(e)), + }; + + settings.paused = paused; + + match state.store.global_capture_set(&settings) { + Ok(()) => match capture::reconcile_capture_runtime(&app, &settings) { + Ok(_) => Ok(CommandResult::ok(true)), + Err(e) => { + tracing::error!("Failed to reconcile capture runtime: {}", e); + Ok(CommandResult::err(e)) + } + }, + Err(e) => { + tracing::error!("Failed to persist global capture settings: {}", e); + Ok(CommandResult::err(e)) + } + } +} + +/// Validates a shortcut format +#[tauri::command] +pub fn global_capture_validate_shortcut(shortcut: String) -> Result, ()> { + tracing::debug!("Validating shortcut: {}", shortcut); + + match capture::validate_shortcut_format(&shortcut) { + Ok(()) => Ok(CommandResult::ok(true)), + Err(e) => Ok(CommandResult::err(e)), + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0e8a72e..d459052 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,11 +1,9 @@ use tauri::Manager; +mod capture; mod commands; -use commands::{ - doc_exists, doc_list, doc_open, doc_save, location_add_via_dialog, location_list, location_remove, - location_validate, markdown_render, markdown_render_for_pdf, search, style_check_get, style_check_set, - ui_layout_get, ui_layout_set, watch_disable, watch_enable, -}; + +use commands as cmd; pub use commands::AppState; @@ -22,6 +20,7 @@ pub fn run() { .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_persisted_scope::init()) .plugin(tauri_plugin_store::Builder::default().build()) + .plugin(tauri_plugin_global_shortcut::Builder::new().build()) .setup(|app| { tracing::info!("Initializing application"); @@ -43,27 +42,45 @@ pub fn run() { tracing::error!("Location reconciliation failed: {}", e); } + let state = app.state::(); + match state.store.global_capture_get() { + Ok(settings) => { + if let Err(e) = capture::reconcile_capture_runtime(app.handle(), &settings) { + tracing::warn!("Failed to initialize global capture runtime: {}", e); + } + } + Err(e) => { + tracing::warn!("Failed to load global capture settings: {}", e); + } + } + tracing::info!("Application setup complete"); Ok(()) }) .invoke_handler(tauri::generate_handler![ - location_add_via_dialog, - location_list, - location_remove, - location_validate, - doc_list, - doc_open, - doc_save, - doc_exists, - watch_enable, - watch_disable, - search, - markdown_render, - markdown_render_for_pdf, - ui_layout_get, - ui_layout_set, - style_check_get, - style_check_set + cmd::location_add_via_dialog, + cmd::location_list, + cmd::location_remove, + cmd::location_validate, + cmd::doc_list, + cmd::doc_open, + cmd::doc_save, + cmd::doc_exists, + cmd::watch_enable, + cmd::watch_disable, + cmd::search, + cmd::markdown_render, + cmd::markdown_render_for_pdf, + cmd::ui_layout_get, + cmd::ui_layout_set, + cmd::style_check_get, + cmd::style_check_set, + cmd::global_capture_get, + cmd::global_capture_set, + cmd::global_capture_open, + cmd::global_capture_submit, + cmd::global_capture_pause, + cmd::global_capture_validate_shortcut, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6b882fc..826dc0b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "$schema": "https://schema.tauri.app/config/2", - "productName": "writer", + "productName": "Writer", "version": "0.1.0", "identifier": "org.stormlightlabs.writer", "build": { @@ -9,7 +9,19 @@ "beforeBuildCommand": "pnpm build", "frontendDist": "../dist" }, - "app": { "windows": [{ "title": "writer", "width": 800, "height": 600 }], "security": { "csp": null } }, + "app": { + "windows": [ + { "label": "main", "title": "Writer", "width": 1200, "height": 800, "minWidth": 600, "minHeight": 400 } + ], + "trayIcon": { + "id": "global_capture_tray", + "iconPath": "icons/tray.png", + "iconAsTemplate": true, + "showMenuOnLeftClick": true, + "tooltip": "Writer" + }, + "security": { "csp": null } + }, "bundle": { "active": true, "targets": "all", diff --git a/src/App.tsx b/src/App.tsx index 641850b..50d1ac8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,8 +29,6 @@ import { useWriterToolsActions, useWriterToolsState, } from "./state/appStore"; -import "@fontsource-variable/ibm-plex-sans"; -import "./App.css"; // TODO: make shared utils module function formatDraftDate(date: Date): string { diff --git a/src/__tests__/QuickCaptureForm.test.tsx b/src/__tests__/QuickCaptureForm.test.tsx new file mode 100644 index 0000000..ee68e49 --- /dev/null +++ b/src/__tests__/QuickCaptureForm.test.tsx @@ -0,0 +1,69 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { QuickCaptureForm } from "../components/capture/QuickCaptureForm"; + +describe("QuickCaptureForm", () => { + it("submits on Enter in quick note mode", () => { + const onSubmit = vi.fn(); + const onClose = vi.fn(); + + render(); + + const textarea = screen.getByPlaceholderText("Type your note here..."); + fireEvent.change(textarea, { target: { value: "Capture this" } }); + fireEvent.keyDown(textarea, { key: "Enter" }); + + expect(onSubmit).toHaveBeenCalledWith("Capture this", "QuickNote"); + expect(onClose).not.toHaveBeenCalled(); + }); + + it("does not submit on Enter in writing session mode unless modifier is pressed", () => { + const onSubmit = vi.fn(); + const onClose = vi.fn(); + + render( + , + ); + + const textarea = screen.getByPlaceholderText("Type your note here..."); + fireEvent.change(textarea, { target: { value: "Longer draft" } }); + + fireEvent.keyDown(textarea, { key: "Enter" }); + expect(onSubmit).not.toHaveBeenCalled(); + + fireEvent.keyDown(textarea, { key: "Enter", metaKey: true }); + expect(onSubmit).toHaveBeenCalledWith("Longer draft", "WritingSession"); + }); + + it("does not submit on Shift+Enter in quick note mode", () => { + const onSubmit = vi.fn(); + const onClose = vi.fn(); + + render(); + + const textarea = screen.getByPlaceholderText("Type your note here..."); + fireEvent.change(textarea, { target: { value: "Keep typing" } }); + fireEvent.keyDown(textarea, { key: "Enter", shiftKey: true }); + + expect(onSubmit).not.toHaveBeenCalled(); + expect(onClose).not.toHaveBeenCalled(); + }); + + it("closes on Escape", () => { + const onSubmit = vi.fn(); + const onClose = vi.fn(); + + render(); + + const textarea = screen.getByPlaceholderText("Type your note here..."); + fireEvent.keyDown(textarea, { key: "Escape" }); + + expect(onClose).toHaveBeenCalledTimes(1); + expect(onSubmit).not.toHaveBeenCalled(); + }); +}); diff --git a/src/__tests__/ports.test.ts b/src/__tests__/ports.test.ts index 938f1a4..a1516e3 100644 --- a/src/__tests__/ports.test.ts +++ b/src/__tests__/ports.test.ts @@ -6,6 +6,12 @@ import { docOpen, docSave, err, + globalCaptureGet, + globalCaptureOpen, + globalCapturePause, + globalCaptureSet, + globalCaptureSubmit, + globalCaptureValidateShortcut, invokeCmd, isErr, isOk, @@ -368,6 +374,70 @@ describe(runCmd, () => { expect(onOk).toHaveBeenCalledWith([{ id: 1 }]); expect(onErr).not.toHaveBeenCalled(); }); + + it("normalizes global capture settings from backend snake_case", async () => { + const onOk = vi.fn(); + const onErr = vi.fn(); + + vi.mocked(invoke).mockResolvedValueOnce({ + type: "ok", + value: { + enabled: true, + shortcut: "CommandOrControl+Shift+N", + paused: false, + default_mode: "Append", + target_location_id: 9, + inbox_relative_dir: "captures", + append_target: { location_id: 9, rel_path: "notes/daily.md" }, + close_after_save: false, + show_tray_icon: true, + last_capture_target: "9/notes/daily.md", + }, + }); + + await runCmd(globalCaptureGet(onOk, onErr)); + + expect(onOk).toHaveBeenCalledWith({ + enabled: true, + shortcut: "CommandOrControl+Shift+N", + paused: false, + defaultMode: "Append", + targetLocationId: 9, + inboxRelativeDir: "captures", + appendTarget: { locationId: 9, relPath: "notes/daily.md" }, + closeAfterSave: false, + showTrayIcon: true, + lastCaptureTarget: "9/notes/daily.md", + }); + expect(onErr).not.toHaveBeenCalled(); + }); + + it("normalizes global capture submit results from backend snake_case", async () => { + const onOk = vi.fn(); + const onErr = vi.fn(); + + vi.mocked(invoke).mockResolvedValueOnce({ + type: "ok", + value: { + success: true, + saved_to: "inbox/2026/2026-02-23_12-00-00.md", + location_id: 3, + should_close: true, + last_capture_target: "3/inbox/2026/2026-02-23_12-00-00.md", + }, + }); + + await runCmd(globalCaptureSubmit({ mode: "QuickNote", text: "hello" }, onOk, onErr)); + + expect(onOk).toHaveBeenCalledWith({ + success: true, + savedTo: "inbox/2026/2026-02-23_12-00-00.md", + locationId: 3, + shouldClose: true, + lastCaptureTarget: "3/inbox/2026/2026-02-23_12-00-00.md", + }); + expect(onErr).not.toHaveBeenCalled(); + }); }); describe("batch commands", () => { @@ -690,3 +760,113 @@ describe("ui layout Commands", () => { }); }); }); + +describe("global capture Commands", () => { + describe(globalCaptureGet, () => { + it("should create command with empty payload", () => { + const onOk = vi.fn(); + const onErr = vi.fn(); + const cmd = globalCaptureGet(onOk, onErr) as InvokeCmd; + + expect(cmd.type).toBe("Invoke"); + expect(cmd.command).toBe("global_capture_get"); + expect(cmd.payload).toStrictEqual({}); + }); + }); + + describe(globalCaptureSet, () => { + it("should create command with settings payload", () => { + const onOk = vi.fn(); + const onErr = vi.fn(); + const settings = { + enabled: true, + shortcut: "CommandOrControl+Shift+Space", + paused: false, + defaultMode: "QuickNote" as const, + targetLocationId: 1, + inboxRelativeDir: "inbox", + appendTarget: null, + closeAfterSave: true, + showTrayIcon: true, + lastCaptureTarget: null, + }; + const cmd = globalCaptureSet(settings, onOk, onErr) as InvokeCmd; + + expect(cmd.type).toBe("Invoke"); + expect(cmd.command).toBe("global_capture_set"); + expect(cmd.payload).toStrictEqual({ + settings: { + enabled: true, + shortcut: "CommandOrControl+Shift+Space", + paused: false, + default_mode: "QuickNote", + target_location_id: 1, + inbox_relative_dir: "inbox", + append_target: null, + close_after_save: true, + show_tray_icon: true, + last_capture_target: null, + }, + }); + }); + }); + + describe(globalCaptureOpen, () => { + it("should create command with empty payload", () => { + const onOk = vi.fn(); + const onErr = vi.fn(); + const cmd = globalCaptureOpen(onOk, onErr) as InvokeCmd; + + expect(cmd.type).toBe("Invoke"); + expect(cmd.command).toBe("global_capture_open"); + expect(cmd.payload).toStrictEqual({}); + }); + }); + + describe(globalCaptureSubmit, () => { + it("should create command with input payload", () => { + const onOk = vi.fn(); + const onErr = vi.fn(); + const input = { + mode: "QuickNote" as const, + text: "Test capture", + destination: { locationId: 1, relPath: "notes/capture.md" }, + openMainAfterSave: false, + }; + const cmd = globalCaptureSubmit(input, onOk, onErr) as InvokeCmd; + + expect(cmd.type).toBe("Invoke"); + expect(cmd.command).toBe("global_capture_submit"); + expect(cmd.payload).toStrictEqual({ + mode: "QuickNote", + text: "Test capture", + destination: { location_id: 1, rel_path: "notes/capture.md" }, + open_main_after_save: false, + }); + }); + }); + + describe(globalCapturePause, () => { + it("should create command with paused flag", () => { + const onOk = vi.fn(); + const onErr = vi.fn(); + const cmd = globalCapturePause(true, onOk, onErr) as InvokeCmd; + + expect(cmd.type).toBe("Invoke"); + expect(cmd.command).toBe("global_capture_pause"); + expect(cmd.payload).toStrictEqual({ paused: true }); + }); + }); + + describe(globalCaptureValidateShortcut, () => { + it("should create command with shortcut string", () => { + const onOk = vi.fn(); + const onErr = vi.fn(); + const cmd = globalCaptureValidateShortcut("CommandOrControl+Shift+N", onOk, onErr) as InvokeCmd; + + expect(cmd.type).toBe("Invoke"); + expect(cmd.command).toBe("global_capture_validate_shortcut"); + expect(cmd.payload).toStrictEqual({ shortcut: "CommandOrControl+Shift+N" }); + }); + }); +}); diff --git a/src/components/capture/QuickCaptureApp.tsx b/src/components/capture/QuickCaptureApp.tsx new file mode 100644 index 0000000..cdf106a --- /dev/null +++ b/src/components/capture/QuickCaptureApp.tsx @@ -0,0 +1,85 @@ +import { logger } from "$logger"; +import { globalCaptureGet, globalCaptureSubmit, runCmd } from "$ports"; +import type { CaptureMode, CaptureSubmitResult, GlobalCaptureSettings } from "$types"; +import type { AppError } from "$types"; +import { getCurrentWindow } from "@tauri-apps/api/window"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { QuickCaptureForm } from "./QuickCaptureForm"; + +export function QuickCaptureApp() { + const [settings, setSettings] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + const windowRef = useRef(getCurrentWindow()); + + useEffect(() => { + const loadSettings = async () => { + await runCmd(globalCaptureGet((loadedSettings) => { + setSettings(loadedSettings); + setIsLoading(false); + }, (err) => { + logger.error("Failed to load global capture settings", err); + setError("Failed to load settings"); + setIsLoading(false); + })); + }; + + loadSettings(); + }, []); + + const handleSubmit = useCallback( + async (text: string, mode: CaptureMode, destination?: { locationId: number; relPath: string }) => { + if (!settings || isSubmitting) return; + + setIsSubmitting(true); + setError(null); + + await runCmd( + globalCaptureSubmit({ mode, text, destination, openMainAfterSave: false }, (result: CaptureSubmitResult) => { + setIsSubmitting(false); + + if (result.success) { + logger.info("Capture submitted successfully", { savedTo: result.savedTo, locationId: result.locationId }); + + if (result.shouldClose) { + windowRef.current.close().catch((err) => { + logger.error("Failed to close window", err); + }); + } + } else { + setError("Failed to save capture"); + } + }, (err: AppError) => { + setIsSubmitting(false); + logger.error("Capture submission failed", err); + setError(err.message || "Failed to save capture"); + }), + ); + }, + [settings, isSubmitting], + ); + + const handleClose = useCallback(() => { + windowRef.current.close().catch((err) => { + logger.error("Failed to close window", err); + }); + }, []); + + if (isLoading) { + return ( +
+
+
+ ); + } + + return ( + + ); +} diff --git a/src/components/capture/QuickCaptureForm.tsx b/src/components/capture/QuickCaptureForm.tsx new file mode 100644 index 0000000..2f04e43 --- /dev/null +++ b/src/components/capture/QuickCaptureForm.tsx @@ -0,0 +1,169 @@ +import { XIcon } from "$icons"; +import type { CaptureMode } from "$types"; +import { useCallback, useEffect, useRef, useState } from "react"; + +type QuickCaptureFormProps = { + defaultMode?: CaptureMode; + onSubmit: (text: string, mode: CaptureMode, destination?: { locationId: number; relPath: string }) => void; + onClose: () => void; + isSubmitting: boolean; + error: string | null; +}; + +const getModeLabel = (m: CaptureMode): string => { + switch (m) { + case "QuickNote": + return "Quick Note"; + case "WritingSession": + return "Writing Session"; + case "Append": + return "Append"; + default: + return m; + } +}; + +type ModeButtonsProps = { currentMode: CaptureMode; setMode: (mode: CaptureMode) => void; isSubmitting: boolean }; + +type ModeButtonProps = ModeButtonsProps & { mode: CaptureMode }; + +const ModeButton = ({ currentMode, setMode, isSubmitting, mode }: ModeButtonProps) => { + const handleClick = useCallback(() => { + setMode(mode); + }, [mode, setMode]); + + return ( + + ); +}; + +const ModeButtons = ({ currentMode, setMode, isSubmitting }: ModeButtonsProps) => ( +
+ {(["QuickNote", "WritingSession", "Append"] as CaptureMode[]).map((m) => ( + + ))} +
+); + +const ErrorMessage = ({ error }: { error: string | null }) => { + return
{error && {error}}
; +}; + +const FooterActions = ( + { mode, isSubmitting, handleSubmit, text }: { + mode: CaptureMode; + isSubmitting: boolean; + handleSubmit: () => void; + text: string; + }, +) => ( +
+ + {mode === "WritingSession" + ? "Cmd+Enter to save, Esc to close" + : "Enter to save, Shift+Enter for newline, Esc to close"} + + + +
+); + +const QuickCaptureFormHeader = ( + { onClose, currentMode, setMode, isSubmitting }: ModeButtonsProps & { onClose: () => void }, +) => ( +
+ + + +
+); + +export function QuickCaptureForm( + { defaultMode = "QuickNote", onSubmit, onClose, isSubmitting, error }: QuickCaptureFormProps, +) { + const [text, setText] = useState(""); + const [mode, setMode] = useState(defaultMode); + const textareaRef = useRef(null); + + useEffect(() => { + textareaRef.current?.focus(); + }, []); + + const handleSubmit = useCallback(() => { + if (isSubmitting || !text.trim()) return; + onSubmit(text, mode); + }, [text, mode, isSubmitting, onSubmit]); + + const handleTextChange = useCallback((e: React.ChangeEvent) => { + setText(e.target.value); + }, []); + + const handleTextareaKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault(); + onClose(); + return; + } + + const hasMod = e.metaKey || e.ctrlKey; + if (hasMod && e.key === "Enter") { + e.preventDefault(); + handleSubmit(); + return; + } + + if ( + mode !== "WritingSession" + && e.key === "Enter" + && !hasMod + && !e.shiftKey + && !e.altKey + ) { + e.preventDefault(); + handleSubmit(); + } + }, [handleSubmit, mode, onClose]); + + return ( +
+ + +
+