Files
buzz/desktop/src-tauri/src/app_menu.rs
Matt Kursmark e5d1dfef7b feat(desktop): close Buzz window with Cmd+W (#6314)
## Summary

- close the current Buzz window with `Cmd-W` on macOS
- keep Buzz Term's existing `Cmd-W` behavior when the terminal owns
input, where it closes the active terminal tab
- cover the close-window chord and its modifier/composition guards with
unit tests

## Why

Buzz removes macOS's native Close Window menu accelerator so the webview
can conditionally route `Cmd-W` to Buzz Term. That also left normal Buzz
mode without the standard close-window shortcut. The new app-level
bubble-phase handler restores window closing while allowing the
terminal's capture-phase handler to retain priority.

For the main window, the existing Rust close-request handler keeps the
webview alive and hides it to the tray, matching the titlebar close
button.

## Validation

- desktop unit tests: 5,093 passed
- focused close-window and terminal shortcut tests: 28 passed at
`71a2cce42013440e297c7b237d45b20e9638078d`
- desktop TypeScript: passed
- desktop checks: passed (pre-existing warnings only)
- workspace Rust clippy: passed
- Tauri Rust clippy/check: passed
- desktop build: passed
- mobile format/analyze: passed
- Tauri tests: 2,592 desktop tests and 26/27 terminal tests passed; the
existing `default_prog_child_observes_the_login_argv0` PTY fixture
repeatedly timed out waiting for `/bin/sh`, including when run alone,
and is unrelated to this TypeScript shortcut change

---------

Signed-off-by: Matt Kursmark <kursmark@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
2026-08-20 16:31:39 +01:00

112 lines
4.5 KiB
Rust

//! The macOS application menu.
//!
//! Buzz never called `Builder::menu()`, so Tauri installed `Menu::default()`
//! for us (`tauri::app::Builder::build`, macOS arm). That default puts a
//! `close_window` item in both the File and Window submenus, and muda gives
//! that item a Cmd+W key equivalent bound to `performClose:`.
//!
//! That default cannot express Buzz's context-dependent behavior:
//!
//! 1. `CloseRequested` on the main window is intercepted in `lib.rs` and turned
//! into hide-to-tray. Cmd+W should take that path in normal Buzz mode.
//! 2. macOS resolves a menu key equivalent before the webview receives any key
//! event, so Buzz Term could never bind Cmd+W to "close this terminal tab"
//! while the accelerator was claimed here.
//!
//! So this module builds the standard menu minus both `close_window` items.
//! Everything else matches `Menu::default()` deliberately. The webview routes
//! Cmd+W conditionally instead: Buzz Term consumes it in capture phase while
//! it owns input, and `useCloseWindowShortcut` closes the current window in
//! normal Buzz mode.
#[cfg(target_os = "macos")]
use tauri::menu::{
AboutMetadata, Menu, PredefinedMenuItem, Submenu, HELP_SUBMENU_ID, WINDOW_SUBMENU_ID,
};
#[cfg(target_os = "macos")]
use tauri::AppHandle;
use tauri::{Builder, Runtime};
/// Installs Buzz's menu, replacing the `Menu::default()` Tauri would otherwise
/// auto-install. A no-op off macOS, where that default is never created and
/// the Cmd+W accelerator does not exist.
pub fn install<R: Runtime>(builder: Builder<R>) -> Builder<R> {
#[cfg(target_os = "macos")]
let builder = builder.menu(build);
builder
}
/// Mirrors `Menu::default()` with every `close_window` item omitted.
///
/// The Window and Help submenus keep Tauri's well-known ids: `init_app_menu`
/// looks them up by id to call `set_as_windows_menu_for_nsapp` and
/// `set_as_help_menu_for_nsapp`, and a plain `with_items` submenu would skip
/// both silently -- no error, just a Window menu AppKit no longer manages.
#[cfg(target_os = "macos")]
pub fn build<R: Runtime>(app: &AppHandle<R>) -> tauri::Result<Menu<R>> {
let pkg_info = app.package_info();
let config = app.config();
let about_metadata = AboutMetadata {
name: Some(pkg_info.name.clone()),
version: Some(pkg_info.version.to_string()),
copyright: config.bundle.copyright.clone(),
authors: config.bundle.publisher.clone().map(|p| vec![p]),
..Default::default()
};
Menu::with_items(
app,
&[
&Submenu::with_items(
app,
pkg_info.name.clone(),
true,
&[
&PredefinedMenuItem::about(app, None, Some(about_metadata))?,
&PredefinedMenuItem::separator(app)?,
&PredefinedMenuItem::services(app, None)?,
&PredefinedMenuItem::separator(app)?,
&PredefinedMenuItem::hide(app, None)?,
&PredefinedMenuItem::hide_others(app, None)?,
&PredefinedMenuItem::separator(app)?,
&PredefinedMenuItem::quit(app, None)?,
],
)?,
// `Menu::default()`'s File submenu holds exactly one item on macOS
// -- close_window -- so dropping that item drops the submenu too.
&Submenu::with_items(
app,
"Edit",
true,
&[
&PredefinedMenuItem::undo(app, None)?,
&PredefinedMenuItem::redo(app, None)?,
&PredefinedMenuItem::separator(app)?,
&PredefinedMenuItem::cut(app, None)?,
&PredefinedMenuItem::copy(app, None)?,
&PredefinedMenuItem::paste(app, None)?,
&PredefinedMenuItem::select_all(app, None)?,
],
)?,
&Submenu::with_items(
app,
"View",
true,
&[&PredefinedMenuItem::fullscreen(app, None)?],
)?,
&Submenu::with_id_and_items(
app,
WINDOW_SUBMENU_ID,
"Window",
true,
&[
&PredefinedMenuItem::minimize(app, None)?,
&PredefinedMenuItem::maximize(app, None)?,
],
)?,
// Empty upstream too on macOS: About lives in the app submenu.
&Submenu::with_id_and_items(app, HELP_SUBMENU_ID, "Help", true, &[])?,
],
)
}