From 4771dedee61c4eb3bcbd612de288a004d1c1385a Mon Sep 17 00:00:00 2001 From: erictli Date: Mon, 4 May 2026 21:54:37 -0400 Subject: [PATCH 1/2] feat: enable macOS autocorrect on the editor (#142) Adds autocorrect="on", autocapitalize="sentences", and spellcheck="true" to the ProseMirror contenteditable so WKWebView applies the user's system-level spelling correction and auto-capitalization, matching the behavior of Apple Notes. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/editor/Editor.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/editor/Editor.tsx b/src/components/editor/Editor.tsx index dcf0716..0c5ff65 100644 --- a/src/components/editor/Editor.tsx +++ b/src/components/editor/Editor.tsx @@ -1132,6 +1132,9 @@ export function Editor({ attributes: { class: "prose prose-lg dark:prose-invert max-w-3xl mx-auto focus:outline-none min-h-full px-6 pt-8 pb-24", + spellcheck: "true", + autocorrect: "on", + autocapitalize: "sentences", }, // Serialize copied text as markdown instead of plain text clipboardTextSerializer: (slice) => { From 447b7e3116ff9e5ebe06ce91bd72dbeb8223c61d Mon Sep 17 00:00:00 2001 From: erictli Date: Mon, 4 May 2026 22:04:39 -0400 Subject: [PATCH 2/2] fix: register WebKit spellcheck/autocorrect defaults at startup WKWebView reads per-app NSUserDefaults to decide whether to render the spelling underline and apply auto-correct / smart-substitution inside contenteditable. These keys default to off for new bundle IDs, so the HTML spellcheck/autocorrect attributes alone produced no visible behavior. Register the WebKit defaults from Rust before the Tauri builder runs (setting them inside .setup() is too late). Uses registerDefaults so user toggles via the WebKit context menu still win. Co-Authored-By: Claude Opus 4.7 (1M context) --- src-tauri/Cargo.lock | 2 ++ src-tauri/Cargo.toml | 4 ++++ src-tauri/src/lib.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d8b8c6c..d009cc8 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -10,6 +10,8 @@ dependencies = [ "base64 0.22.1", "chrono", "notify", + "objc2", + "objc2-foundation", "open", "regex", "serde", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 1afac9d..b301a70 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -33,3 +33,7 @@ regex = "1" walkdir = "2" tauri-plugin-single-instance = "2" chrono = "0.4" + +[target.'cfg(target_os = "macos")'.dependencies] +objc2 = "0.6" +objc2-foundation = { version = "0.3", features = ["NSUserDefaults", "NSString", "NSDictionary", "NSValue"] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f5c0154..fbb2993 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3632,8 +3632,54 @@ fn handle_cli_args(app: &AppHandle, args: &[String], cwd: &str) -> bool { opened_preview } +// On macOS, WKWebView reads per-app preferences from NSUserDefaults to decide +// whether to show the spelling underline and apply auto-correct / smart-substitution +// in contenteditable regions. These keys default to off for new bundle IDs, which +// is why a fresh Tauri app gets neither the red underline nor auto-replace even +// when the HTML `spellcheck`/`autocorrect` attributes are set. Register the +// WebKit defaults so users get Apple-Notes-like behavior on first launch; this +// must happen before the webview is constructed (setting them inside .setup() is +// too late). registerDefaults is used so user-toggled values via the WebKit +// right-click menu still take precedence. +#[cfg(target_os = "macos")] +fn register_webview_defaults() { + use objc2::runtime::AnyObject; + use objc2_foundation::{NSDictionary, NSNumber, NSString, NSUserDefaults}; + + let keys = [ + "WebContinuousSpellCheckingEnabled", + "WebGrammarCheckingEnabled", + "WebAutomaticSpellingCorrectionEnabled", + "WebAutomaticQuoteSubstitutionEnabled", + "WebAutomaticDashSubstitutionEnabled", + "WebAutomaticTextReplacementEnabled", + "WebAutomaticLinkDetectionEnabled", + ]; + + let ns_keys: Vec<_> = keys.iter().map(|k| NSString::from_str(k)).collect(); + let key_refs: Vec<&NSString> = ns_keys.iter().map(|k| &**k).collect(); + + let yes_values: Vec<_> = (0..keys.len()).map(|_| NSNumber::new_bool(true)).collect(); + let value_refs: Vec<&AnyObject> = yes_values + .iter() + .map(|v| { + let any: &AnyObject = v; + any + }) + .collect(); + + let dict: objc2::rc::Retained> = + NSDictionary::from_slices(&key_refs, &value_refs); + unsafe { + NSUserDefaults::standardUserDefaults().registerDefaults(&dict); + } +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + #[cfg(target_os = "macos")] + register_webview_defaults(); + let app = tauri::Builder::default() // Single-instance: forward CLI args from subsequent launches to the running instance .plugin(tauri_plugin_single_instance::init(|app, args, cwd| {