From 77098c720639a421f81ee14e47250d167d7af7bd Mon Sep 17 00:00:00 2001 From: _ov4 Date: Thu, 22 Oct 2026 02:46:30 +0000 Subject: feat(bundle): add a native menu bar listing the shortcuts One Tauri menu definition serves the macOS menu bar and the top of each window on Windows. Menu clicks run the same frontend commands as the keyboard shortcuts in the focused window. Co-Authored-By: Claude Opus 5.5 --- bundle/AGENTS/DEVELOPMENT.md | 2 +- bundle/README.md | 2 +- bundle/src-tauri/src/main.rs | 16 ++++++++++++- bundle/src-tauri/src/menu.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++ bundle/src/main.js | 28 +++++++++++++++------- 5 files changed, 92 insertions(+), 11 deletions(-) create mode 100644 bundle/src-tauri/src/menu.rs (limited to 'bundle') diff --git a/bundle/AGENTS/DEVELOPMENT.md b/bundle/AGENTS/DEVELOPMENT.md index 52d13ba..a77e1e8 100644 --- a/bundle/AGENTS/DEVELOPMENT.md +++ b/bundle/AGENTS/DEVELOPMENT.md @@ -30,6 +30,6 @@ The Rust runner launches the packaged compiler directly, using the source direct After building the compiler, run `cargo test --release --manifest-path src-tauri/Cargo.toml`. The two native tests exercise real compilation, prefilled input, EOF, Unicode, diagnostics, output before process exit, stopping, single-run exclusion and restart, including source paths with spaces. CI runs them on all three native platforms. -Keyboard shortcuts are Monaco `KeyMod.CtrlCmd` keybindings: the editor registers Find and Toggle Line Comment, and the document handler matches New, New Window (Shift), Open, Save and Find with `StandardKeyboardEvent.equals`. Never test `ctrlKey`/`metaKey` directly; Monaco resolves CtrlCmd to Cmd on macOS and Ctrl elsewhere. +The native menu is defined once in `src-tauri/src/menu.rs` with Tauri's menu API; macOS shows it in the system menu bar, Windows and Linux at the top of each window. Only the macOS application menu is platform-specific. Edit keeps the predefined clipboard and undo items, which macOS needs in the webview. Other platforms have no Quit item because it exits without unsaved-changes prompts. Custom item ids name commands in `src/main.js`; a click emits a `menu` event with the id to the focused window. Keyboard shortcuts are Monaco `KeyMod.CtrlCmd` keybindings: the editor registers Find and Toggle Line Comment, and the document handler matches the menu's commands with `StandardKeyboardEvent.equals`, so a shortcut runs once whether the webview or the native menu receives the key first. Keep menu accelerators and handler bindings in sync. Never test `ctrlKey`/`metaKey` directly; Monaco resolves CtrlCmd to Cmd on macOS and Ctrl elsewhere. Use brief English UI labels and messages. Keep one document per window and prefilled stdin. More documents use native OS windows created by the `new_window` command from the configured window; do not add tabs or in-page switching. Before feature commits, run affected plugin tests, build the frontend and check native compilation as appropriate. Do not add a separate UI test framework for routine changes. diff --git a/bundle/README.md b/bundle/README.md index bf696f7..93b619e 100644 --- a/bundle/README.md +++ b/bundle/README.md @@ -2,7 +2,7 @@ Open a `.cps` file, enter its input in **Input**, and click **Run**. The file is saved before running. Program output and errors appear in **Output**, with compiler errors colored. Click **Stop** to end a running program. -Shortcuts use Cmd on macOS and Ctrl on Windows: **N** new, **Shift+N** new window, **O** open, **S** save, **F** find, **/** toggle line comment. Each window has its own file, Input and Run. +Shortcuts use Cmd on macOS and Ctrl on Windows: **N** new, **Shift+N** new window, **O** open, **S** save, **F** find, **/** toggle line comment. The menu bar lists these commands with their shortcuts: at the top of the screen on macOS, at the top of each window on Windows. Each window has its own file, Input and Run. Nightly provides a standalone Windows x64 `.exe` and a macOS Apple Silicon `.tar.xz` containing the `.app`. Windows extracts the included `cpsc.exe` into the current working directory on startup; macOS keeps it inside the `.app`. Windows uses the system WebView2 runtime. diff --git a/bundle/src-tauri/src/main.rs b/bundle/src-tauri/src/main.rs index 80b28c5..9f63fc5 100644 --- a/bundle/src-tauri/src/main.rs +++ b/bundle/src-tauri/src/main.rs @@ -1,6 +1,7 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] mod compiler; +mod menu; mod runner; use compiler::Compiler; use runner::{OutputEvent, Runner}; @@ -11,7 +12,9 @@ use std::{ Mutex, }, }; -use tauri::{ipc::Channel, AppHandle, Manager, State, WebviewWindow, WebviewWindowBuilder}; +use tauri::{ + ipc::Channel, AppHandle, Emitter, Manager, State, WebviewWindow, WebviewWindowBuilder, +}; // Each window runs its own program, keyed by window label. #[derive(Default)] @@ -87,6 +90,17 @@ fn main() { let compiler = Compiler::load(); tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) + .menu(menu::build) + // Menu items act on the focused window, like its keyboard shortcuts. + .on_menu_event(|app, event| { + let focused = app + .webview_windows() + .into_values() + .find(|window| window.is_focused().unwrap_or(false)); + if let Some(window) = focused { + let _ = window.emit_to(window.label(), "menu", event.id().as_ref()); + } + }) .manage(Runners::default()) .manage(compiler) .invoke_handler(tauri::generate_handler![ diff --git a/bundle/src-tauri/src/menu.rs b/bundle/src-tauri/src/menu.rs new file mode 100644 index 0000000..83ba7e3 --- /dev/null +++ b/bundle/src-tauri/src/menu.rs @@ -0,0 +1,55 @@ +use tauri::{ + menu::{Menu, MenuBuilder, MenuItem, SubmenuBuilder}, + AppHandle, Runtime, +}; + +// One menu for all platforms: the macOS menu bar, or the top of each window elsewhere. +// Custom item ids name the frontend commands that the same shortcuts run. +pub fn build(app: &AppHandle) -> tauri::Result> { + let item = |id: &str, text: &str, accelerator: &str| { + MenuItem::with_id(app, id, text, true, Some(accelerator)) + }; + let file = SubmenuBuilder::new(app, "File") + .item(&item("new", "New", "CmdOrCtrl+N")?) + .item(&item("new_window", "New Window", "CmdOrCtrl+Shift+N")?) + .item(&item("open", "Open…", "CmdOrCtrl+O")?) + .item(&item("save", "Save", "CmdOrCtrl+S")?) + .separator() + .close_window() + .build()?; + // macOS needs the predefined edit items for clipboard and undo keys in the webview. + let edit = SubmenuBuilder::new(app, "Edit") + .undo() + .redo() + .separator() + .cut() + .copy() + .paste() + .select_all() + .separator() + .item(&item("find", "Find", "CmdOrCtrl+F")?) + .item(&item("comment", "Toggle Line Comment", "CmdOrCtrl+/")?) + .build()?; + let window = SubmenuBuilder::new(app, "Window") + .minimize() + .maximize() + .build()?; + let menu = MenuBuilder::new(app); + // macOS shows the first submenu as the application menu. Quit skips unsaved-changes + // prompts, so other platforms close windows instead. + #[cfg(target_os = "macos")] + let menu = menu.item( + &SubmenuBuilder::new(app, "CPSC Bundle") + .about(None) + .separator() + .services() + .separator() + .hide() + .hide_others() + .show_all() + .separator() + .quit() + .build()?, + ); + menu.items(&[&file, &edit, &window]).build() +} diff --git a/bundle/src/main.js b/bundle/src/main.js index 4d06998..9a78ae7 100644 --- a/bundle/src/main.js +++ b/bundle/src/main.js @@ -136,14 +136,25 @@ async function action(callback) { busy = true; try { await callback(); } catch (error) { report(error); } finally { busy = false; } } -// CtrlCmd is Cmd on macOS and Ctrl elsewhere, matching Monaco's own keybindings. -const shortcuts = [ - [KeyMod.CtrlCmd | KeyCode.KeyN, () => action(newFile)], +function editorAction(id) { editor.focus(); editor.getAction(id).run(); } +// Keyed by the menu item ids in src-tauri/src/menu.rs. +const commands = { + new: () => action(newFile), // Each window is an independent native OS window, even while this one runs. - [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyN, () => invoke('new_window').catch(report)], - [KeyMod.CtrlCmd | KeyCode.KeyO, () => action(openFile)], - [KeyMod.CtrlCmd | KeyCode.KeyS, () => action(saveFile)], - [KeyMod.CtrlCmd | KeyCode.KeyF, () => { editor.focus(); editor.getAction('actions.find').run(); }] + new_window: () => invoke('new_window').catch(report), + open: () => action(openFile), + save: () => action(saveFile), + find: () => editorAction('actions.find'), + comment: () => editorAction('editor.action.commentLine') +}; +// CtrlCmd is Cmd on macOS and Ctrl elsewhere, matching Monaco's own keybindings and the menu. +const shortcuts = [ + [KeyMod.CtrlCmd | KeyCode.KeyN, 'new'], + [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyN, 'new_window'], + [KeyMod.CtrlCmd | KeyCode.KeyO, 'open'], + [KeyMod.CtrlCmd | KeyCode.KeyS, 'save'], + [KeyMod.CtrlCmd | KeyCode.KeyF, 'find'], + [KeyMod.CtrlCmd | KeyCode.Slash, 'comment'] ]; try { @@ -166,8 +177,9 @@ try { const shortcut = shortcuts.find(([binding]) => key.equals(binding)); if (!shortcut) return; event.preventDefault(); - shortcut[1](); + commands[shortcut[1]](); }); + await window.listen('menu', event => commands[event.payload]()); await window.onCloseRequested(async event => { if (closing) return; event.preventDefault(); -- cgit v1.3.1-4-g156e