Skip to content

Commit 6c204dd

Browse files
committed
feat(desktop): start/stop recording from the app menu bar
Add a recording item to the Wisp application menu (and a Cmd+R shortcut) that toggles the session via the same state machine as the in-window Record button. The label flips between "Start Recording" and "Stop Recording" with the session state; the native menu is only rebuilt when that label actually changes.
1 parent 89c914e commit 6c204dd

2 files changed

Lines changed: 65 additions & 14 deletions

File tree

apps/wisp-desktop/src/app_menu.rs

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
//! macOS menu bar: application menu (About, Quit) and Cmd+Q.
1+
//! macOS menu bar: application menu (About, Start/Stop Recording, Quit)
2+
//! plus the Cmd+Q and Cmd+R shortcuts.
23
4+
use std::path::PathBuf;
35
use std::sync::Arc;
46
use std::time::Duration;
57

@@ -11,14 +13,15 @@ use crate::library::SharedStorage;
1113
use crate::session_runner::SessionRunner;
1214
use crate::session_updates::apply_update;
1315

14-
actions!(wisp_desktop, [Quit, About]);
16+
actions!(wisp_desktop, [Quit, About, ToggleRecording]);
1517

1618
/// Wire up the menu bar, keyboard shortcuts, and quit handlers.
1719
pub fn configure(
1820
cx: &mut App,
1921
runner: Arc<SessionRunner>,
2022
storage: SharedStorage,
2123
model: Entity<AppModel>,
24+
recordings_dir: PathBuf,
2225
) {
2326
let runner_for_quit = runner.clone();
2427
let model_for_quit = model.clone();
@@ -33,16 +36,33 @@ pub fn configure(
3336
about_view::open(cx);
3437
});
3538

36-
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
39+
// Start/stop recording straight from the menu bar (and Cmd+R), so the
40+
// user doesn't have to reach for the in-window Record button. Reuses the
41+
// same state machine as that button via `toggle_recording`.
42+
let runner_for_toggle = runner.clone();
43+
let model_for_toggle = model.clone();
44+
cx.on_action(move |_: &ToggleRecording, cx| {
45+
crate::toggle_recording(&runner_for_toggle, &model_for_toggle, &recordings_dir, cx);
46+
});
3747

38-
cx.set_menus(vec![Menu {
39-
name: "Wisp".into(),
40-
items: vec![
41-
MenuItem::action("About Wisp", About),
42-
MenuItem::separator(),
43-
MenuItem::action("Quit Wisp", Quit),
44-
],
45-
}]);
48+
cx.bind_keys([
49+
KeyBinding::new("cmd-q", Quit, None),
50+
KeyBinding::new("cmd-r", ToggleRecording, None),
51+
]);
52+
53+
// The recording item's label flips between "Start" and "Stop" with the
54+
// session state. `set_menus` rebuilds the whole native menu, so we only
55+
// call it when the label actually changes (not on every transcript tick).
56+
let mut last_label = recording_menu_label(model.read(cx).state);
57+
cx.set_menus(build_menus(last_label));
58+
cx.observe(&model, move |model, cx| {
59+
let label = recording_menu_label(model.read(cx).state);
60+
if label != last_label {
61+
last_label = label;
62+
cx.set_menus(build_menus(label));
63+
}
64+
})
65+
.detach();
4666

4767
let runner_for_shutdown = runner;
4868
let model_for_shutdown = model;
@@ -58,6 +78,31 @@ pub fn configure(
5878
});
5979
}
6080

81+
/// The recording menu item's label for the given session state: "Stop" while
82+
/// a session is live (or transitioning), "Start" otherwise.
83+
fn recording_menu_label(state: SessionState) -> &'static str {
84+
match state {
85+
SessionState::Recording { .. } | SessionState::Starting | SessionState::Stopping => {
86+
"Stop Recording"
87+
},
88+
SessionState::Idle | SessionState::Failed => "Start Recording",
89+
}
90+
}
91+
92+
/// Build the application menu with the recording item carrying `record_label`.
93+
fn build_menus(record_label: &'static str) -> Vec<Menu> {
94+
vec![Menu {
95+
name: "Wisp".into(),
96+
items: vec![
97+
MenuItem::action("About Wisp", About),
98+
MenuItem::separator(),
99+
MenuItem::action(record_label, ToggleRecording),
100+
MenuItem::separator(),
101+
MenuItem::action("Quit Wisp", Quit),
102+
],
103+
}]
104+
}
105+
61106
/// If a recording is active (or stopping), request stop and wait for the
62107
/// worker to finish so segments can be persisted before exit.
63108
fn graceful_stop_session(

apps/wisp-desktop/src/main.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,16 @@ fn main() {
8989
runner.clone(),
9090
storage.clone(),
9191
model.clone(),
92-
recordings_dir,
92+
recordings_dir.clone(),
9393
);
9494

95-
configure_app_menu(cx, runner.clone(), storage.clone(), model.clone());
95+
configure_app_menu(
96+
cx,
97+
runner.clone(),
98+
storage.clone(),
99+
model.clone(),
100+
recordings_dir,
101+
);
96102

97103
spawn_session_update_pump(cx, runner, storage, model.clone());
98104
spawn_cursor_blink(cx, window);
@@ -245,7 +251,7 @@ fn spawn_permission_refresh(
245251
.detach();
246252
}
247253

248-
fn toggle_recording(
254+
pub(crate) fn toggle_recording(
249255
runner: &SessionRunner,
250256
model: &gpui::Entity<AppModel>,
251257
recordings_dir: &std::path::Path,

0 commit comments

Comments
 (0)