From acf78dfe2986ca266e705c1f08b9229b5c98cd2a Mon Sep 17 00:00:00 2001 From: PJ Date: Wed, 15 Jul 2026 09:30:18 +0530 Subject: [PATCH] feat(menu): reorganize native menu bar with discoverable actions Move Check for Updates into the macOS app menu and add Settings (Cmd+,), Save (Cmd+S), Find (Cmd+F), chapter navigation, and a Help > Report an Issue link. Route new menu actions through menu-action events and trim duplicate editor keybindings. --- src-tauri/Cargo.lock | 2 +- src-tauri/src/lib.rs | 108 +++++++++++++++++++++++++++++----- src/App.tsx | 3 + src/components/EditorView.tsx | 33 ++++++++--- src/export/typst.ts | 2 +- 5 files changed, 122 insertions(+), 26 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4e46e71..5343bc9 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4713,7 +4713,7 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "margin-app" -version = "0.1.11" +version = "0.1.12" dependencies = [ "base64 0.22.1", "fontdb", diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7ddb7aa..33b181f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -16,50 +16,116 @@ use tauri::{Emitter, Runtime}; #[cfg(desktop)] fn build_menu(handle: &tauri::AppHandle) -> tauri::Result> { let menu = Menu::default(handle)?; + let new_book = MenuItemBuilder::with_id("new-book", "New Book") .accelerator("CmdOrCtrl+N") .build(handle)?; - let export_pdf = MenuItemBuilder::with_id("export-pdf", "Export as PDF…").build(handle)?; - let export_epub = MenuItemBuilder::with_id("export-epub", "Export as EPUB…").build(handle)?; + let save = MenuItemBuilder::with_id("save", "Save") + .accelerator("CmdOrCtrl+S") + .build(handle)?; + let export_pdf = MenuItemBuilder::with_id("export-pdf", "Export as PDF…") + .accelerator("CmdOrCtrl+Shift+P") + .build(handle)?; + let export_epub = MenuItemBuilder::with_id("export-epub", "Export as EPUB…") + .accelerator("CmdOrCtrl+Shift+E") + .build(handle)?; let check_updates = MenuItemBuilder::with_id("check-updates", "Check for Updates…").build(handle)?; + let settings = MenuItemBuilder::with_id("settings", "Settings…") + .accelerator("CmdOrCtrl+,") + .build(handle)?; + let find = MenuItemBuilder::with_id("find", "Find…") + .accelerator("CmdOrCtrl+F") + .build(handle)?; + let report_issue = + MenuItemBuilder::with_id("report-issue", "Report an Issue…").build(handle)?; - let file_submenu = menu + let submenus: Vec<_> = menu .items()? .into_iter() - .find_map(|item| match item { - MenuItemKind::Submenu(submenu) - if submenu.text().map(|t| t == "File").unwrap_or(false) => - { - Some(submenu) - } + .filter_map(|item| match item { + MenuItemKind::Submenu(submenu) => Some(submenu), _ => None, - }); + }) + .collect(); - match file_submenu { + let find_submenu = |name: &str| { + submenus + .iter() + .find(|submenu| submenu.text().map(|t| t == name).unwrap_or(false)) + .cloned() + }; + + match find_submenu("File") { Some(submenu) => { submenu.prepend_items(&[ &new_book, &PredefinedMenuItem::separator(handle)?, + &save, + &PredefinedMenuItem::separator(handle)?, &export_pdf, &export_epub, &PredefinedMenuItem::separator(handle)?, - &check_updates, - &PredefinedMenuItem::separator(handle)?, ])?; } None => { let submenu = SubmenuBuilder::new(handle, "File") .item(&new_book) .item(&PredefinedMenuItem::separator(handle)?) + .item(&save) + .item(&PredefinedMenuItem::separator(handle)?) .item(&export_pdf) .item(&export_epub) - .item(&PredefinedMenuItem::separator(handle)?) - .item(&check_updates) .build()?; menu.insert(&submenu, 1)?; } } + + if let Some(edit) = find_submenu("Edit") { + edit.append_items(&[&PredefinedMenuItem::separator(handle)?, &find])?; + } + + if let Some(help) = find_submenu("Help") { + help.append_items(&[&report_issue])?; + } + + #[cfg(target_os = "macos")] + { + if let Some(app_submenu) = submenus.first() { + app_submenu.insert(&check_updates, 1)?; + app_submenu.insert(&settings, 3)?; + app_submenu.insert(&PredefinedMenuItem::separator(handle)?, 4)?; + } + if let Some(view) = find_submenu("View") { + let toggle_chapters = MenuItemBuilder::with_id("toggle-chapters", "Toggle Chapters") + .accelerator("CmdOrCtrl+\\") + .build(handle)?; + let next_chapter = MenuItemBuilder::with_id("next-chapter", "Next Chapter") + .accelerator("CmdOrCtrl+Alt+Down") + .build(handle)?; + let prev_chapter = MenuItemBuilder::with_id("prev-chapter", "Previous Chapter") + .accelerator("CmdOrCtrl+Alt+Up") + .build(handle)?; + view.prepend_items(&[ + &toggle_chapters, + &PredefinedMenuItem::separator(handle)?, + &next_chapter, + &prev_chapter, + &PredefinedMenuItem::separator(handle)?, + ])?; + } + } + + #[cfg(not(target_os = "macos"))] + { + if let Some(file) = find_submenu("File") { + file.append_items(&[&PredefinedMenuItem::separator(handle)?, &check_updates])?; + } + if let Some(edit) = find_submenu("Edit") { + edit.append_items(&[&settings])?; + } + } + Ok(menu) } @@ -95,7 +161,17 @@ pub fn run() { .on_menu_event(|app, event| { if matches!( event.id().0.as_str(), - "new-book" | "export-pdf" | "export-epub" | "check-updates" + "new-book" + | "save" + | "export-pdf" + | "export-epub" + | "check-updates" + | "settings" + | "find" + | "toggle-chapters" + | "next-chapter" + | "prev-chapter" + | "report-issue" ) { app.emit("menu-action", event.id().0.as_str()).ok(); } diff --git a/src/App.tsx b/src/App.tsx index 5b1e4c5..b767b9d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import { useEffect } from "react"; import { listen } from "@tauri-apps/api/event"; +import { openUrl } from "@tauri-apps/plugin-opener"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { Library } from "./components/Library"; import { EditorView } from "./components/EditorView"; @@ -29,6 +30,8 @@ function App() { } else if (event.payload === "export-epub") runExport("epub"); else if (event.payload === "check-updates") checkForUpdates(false); + else if (event.payload === "report-issue") + openUrl("https://github.com/priyanshujain/margin/issues").catch(() => {}); }); const unlistenWarn = listen("pdf-warnings", (event) => { useBook.getState().setNotice(`PDF exported with warnings:\n${event.payload}`); diff --git a/src/components/EditorView.tsx b/src/components/EditorView.tsx index edf0589..b5f5af0 100644 --- a/src/components/EditorView.tsx +++ b/src/components/EditorView.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; +import { listen } from "@tauri-apps/api/event"; import type { Editor as TiptapEditor } from "@tiptap/react"; import { Sidebar } from "./Sidebar"; import { Dock } from "./Dock"; @@ -129,17 +130,14 @@ export function EditorView() { useEffect(() => { const onKey = (e: KeyboardEvent) => { - if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "s") { + if (e.metaKey && e.altKey && e.key.toLowerCase() === "f") { e.preventDefault(); - saveNow(); - } else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "f") { - e.preventDefault(); - openFind(e.altKey); - } else if (e.metaKey && e.altKey && (e.key === "ArrowDown" || e.key === "ArrowRight")) { + openFind(true); + } else if (e.metaKey && e.altKey && e.key === "ArrowRight") { e.preventDefault(); e.stopPropagation(); switchChapter(1); - } else if (e.metaKey && e.altKey && (e.key === "ArrowUp" || e.key === "ArrowLeft")) { + } else if (e.metaKey && e.altKey && e.key === "ArrowLeft") { e.preventDefault(); e.stopPropagation(); switchChapter(-1); @@ -151,7 +149,26 @@ export function EditorView() { }; window.addEventListener("keydown", onKey, true); return () => window.removeEventListener("keydown", onKey, true); - }, [saveNow, openFind, switchChapter]); + }, [openFind, switchChapter]); + + const menuCommands = useRef({ saveNow, openFind, setSettingsOpen, toggleSidebar, switchChapter }); + menuCommands.current = { saveNow, openFind, setSettingsOpen, toggleSidebar, switchChapter }; + + useEffect(() => { + if (!isDesktop) return; + const unlisten = listen("menu-action", (e) => { + const cmd = menuCommands.current; + if (e.payload === "save") cmd.saveNow(); + else if (e.payload === "find") cmd.openFind(false); + else if (e.payload === "settings") cmd.setSettingsOpen(true); + else if (e.payload === "toggle-chapters") cmd.toggleSidebar(); + else if (e.payload === "next-chapter") cmd.switchChapter(1); + else if (e.payload === "prev-chapter") cmd.switchChapter(-1); + }); + return () => { + unlisten.then((stop) => stop()); + }; + }, []); useEffect(() => { if (!widthOpen && !exportOpen && !moreOpen) return; diff --git a/src/export/typst.ts b/src/export/typst.ts index f40829b..d296d40 100644 --- a/src/export/typst.ts +++ b/src/export/typst.ts @@ -20,7 +20,7 @@ const TRIM: Record = { a5: { w: "148mm", h: "210mm" }, }; -const INLINE_SPECIAL = /[\\#$*_`<>@~[\]]/g; +const INLINE_SPECIAL = /[\\#$*_`<>@~()[\]]/g; const LINE_SEPARATORS = /[\n\r\v\f\u2028\u2029]+/g; function esc(text: string): string {