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 ;
35use std:: sync:: Arc ;
46use std:: time:: Duration ;
57
@@ -11,14 +13,15 @@ use crate::library::SharedStorage;
1113use crate :: session_runner:: SessionRunner ;
1214use 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.
1719pub 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.
63108fn graceful_stop_session (
0 commit comments