diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 59e75eb..b0e932f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -45,7 +45,15 @@ url = "2" # in the graph via Tauri, so this adds nothing new to the build. [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.6" -objc2-app-kit = { version = "0.3", features = ["NSSpellChecker"] } +objc2-app-kit = { version = "0.3", features = [ + "NSSpellChecker", + "NSApplication", + "NSEvent", + "NSMenu", + "NSMenuItem", + "NSResponder", + "NSWritingToolsCoordinator", +] } objc2-foundation = { version = "0.3", features = ["NSString", "NSArray", "NSRange", "NSTextCheckingResult"] } # Other platforms fall back to the bundled Hunspell dictionary via spellbook. diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 33b181f..fd3d0ea 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -7,6 +7,7 @@ mod macspell; mod pdf; mod project; mod proofing; +mod writingtools; #[cfg(desktop)] use tauri::menu::{Menu, MenuItemBuilder, MenuItemKind, PredefinedMenuItem, SubmenuBuilder}; @@ -151,6 +152,7 @@ pub fn run() { .manage(gdrive::GDriveState::default()) .setup(|app| { gdrive::init_session(app.handle()); + writingtools::label_writing_tool_shortcuts(); Ok(()) }); @@ -193,6 +195,7 @@ pub fn run() { library::delete_book, proofing::proof_text, proofing::remember_word, + writingtools::run_writing_tool, gdrive::gdrive_connect, gdrive::gdrive_disconnect, gdrive::gdrive_status, diff --git a/src-tauri/src/writingtools.rs b/src-tauri/src/writingtools.rs new file mode 100644 index 0000000..23e4aec --- /dev/null +++ b/src-tauri/src/writingtools.rs @@ -0,0 +1,81 @@ +#[cfg(target_os = "macos")] +mod mac { + use objc2::rc::Retained; + use objc2::MainThreadMarker; + use objc2_app_kit::{NSApplication, NSEventModifierFlags, NSMenu, NSMenuItem}; + use objc2_foundation::{NSArray, NSString}; + + const SHORTCUTS: [(&str, &str); 2] = [("Proofread", "f"), ("Rewrite", "r")]; + + fn submenu_named(items: &NSArray, title: &str) -> Option> { + for i in 0..items.count() { + let item = items.objectAtIndex(i); + if item.title().to_string() == title { + return item.submenu(); + } + } + None + } + + fn edit_menu(mtm: MainThreadMarker) -> Option> { + let main = NSApplication::sharedApplication(mtm).mainMenu()?; + for i in 0..main.numberOfItems() { + let Some(item) = main.itemAtIndex(i) else { continue }; + let Some(submenu) = item.submenu() else { continue }; + if submenu.title().to_string() == "Edit" || item.title().to_string() == "Edit" { + return Some(submenu); + } + } + None + } + + fn writing_tools_menu(mtm: MainThreadMarker) -> Option> { + submenu_named(&edit_menu(mtm)?.itemArray(), "Writing Tools") + } + + pub fn label_shortcuts() { + let Some(mtm) = MainThreadMarker::new() else { return }; + let Some(menu) = writing_tools_menu(mtm) else { return }; + let items = menu.itemArray(); + for i in 0..items.count() { + let item = items.objectAtIndex(i); + let title = item.title().to_string(); + let Some((_, key)) = SHORTCUTS.iter().find(|(name, _)| *name == title) else { continue }; + item.setKeyEquivalent(&NSString::from_str(key)); + item.setKeyEquivalentModifierMask( + NSEventModifierFlags::Shift | NSEventModifierFlags::Option, + ); + } + } + + pub fn perform(tool: &str) { + let Some(mtm) = MainThreadMarker::new() else { return }; + let Some(menu) = writing_tools_menu(mtm) else { return }; + let items = menu.itemArray(); + for i in 0..items.count() { + if items.objectAtIndex(i).title().to_string() == tool { + menu.performActionForItemAtIndex(i as isize); + return; + } + } + } +} + +pub fn label_writing_tool_shortcuts() { + #[cfg(target_os = "macos")] + mac::label_shortcuts(); +} + +#[tauri::command] +pub fn run_writing_tool(app: tauri::AppHandle, tool: String) -> Result<(), String> { + #[cfg(target_os = "macos")] + { + app.run_on_main_thread(move || mac::perform(&tool)) + .map_err(|e| e.to_string()) + } + #[cfg(not(target_os = "macos"))] + { + let _ = (app, tool); + Ok(()) + } +} diff --git a/src/components/EditorView.tsx b/src/components/EditorView.tsx index fa9c33a..1e5e2af 100644 --- a/src/components/EditorView.tsx +++ b/src/components/EditorView.tsx @@ -23,11 +23,16 @@ import { WIDTH_OPTIONS } from "../width"; import { bodyNumber, chapterKind, partNumber, partRoman } from "../model/book"; import { saveBook } from "../library"; import { useEscapeLayer } from "../escape"; -import { isDesktop } from "../ipc"; +import { isDesktop, runWritingTool } from "../ipc"; import { useCompact } from "../useMedia"; import { issueSignature, rememberWord, runProof } from "../proofing"; import { runExport } from "../export/run"; +const WRITING_TOOLS: Record = { + KeyF: "Proofread", + KeyR: "Rewrite", +}; + export function EditorView() { const book = useBook((s) => s.book); const activeChapterId = useBook((s) => s.activeChapterId); @@ -172,6 +177,10 @@ export function EditorView() { e.preventDefault(); e.stopPropagation(); switchChapter(e.shiftKey ? -1 : 1); + } else if (e.shiftKey && e.altKey && !e.metaKey && !e.ctrlKey && WRITING_TOOLS[e.code]) { + e.preventDefault(); + e.stopPropagation(); + runWritingTool(WRITING_TOOLS[e.code]).catch(() => {}); } }; window.addEventListener("keydown", onKey, true); diff --git a/src/ipc.ts b/src/ipc.ts index 1adeb0e..4d8dcae 100644 --- a/src/ipc.ts +++ b/src/ipc.ts @@ -2,6 +2,11 @@ import { invoke } from "@tauri-apps/api/core"; export const isDesktop = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window; +export async function runWritingTool(tool: "Proofread" | "Rewrite"): Promise { + if (!isDesktop) return; + await invoke("run_writing_tool", { tool }); +} + export interface ImageInput { path: string; data: string;