diff --git a/CONFIGURATION.md b/CONFIGURATION.md index d92e0b83..187ba04c 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -94,6 +94,7 @@ new_workspace = "n" rename_workspace = "shift+n" close_workspace = "shift+d" reload_config = "" # optional, unset by default +open_notification_target = "" # optional, unset by default new_tab = "c" split_vertical = "v" split_horizontal = "-" @@ -122,6 +123,7 @@ focus_pane_right = "alt+l" | `close_workspace` | `shift+d` | close selected workspace | | `detach` | unset | optional explicit detach shortcut in the persistent session | | `reload_config` | unset | reload `config.toml` in the running app/server | +| `open_notification_target` | unset | jump to the currently visible notification target | | `previous_workspace` | unset | switch to the previous workspace directly from terminal mode | | `next_workspace` | unset | switch to the next workspace directly from terminal mode | | `new_tab` | `c` | create a new tab | diff --git a/src/app/input/mouse.rs b/src/app/input/mouse.rs index f7997807..99157d0d 100644 --- a/src/app/input/mouse.rs +++ b/src/app/input/mouse.rs @@ -1009,7 +1009,7 @@ impl AppState { && rect_contains(self.view.toast_hit_area, col, row) } - fn focus_toast_target(&mut self) { + pub(crate) fn focus_toast_target(&mut self) { let Some(target) = self.toast.as_ref().and_then(|toast| toast.target.clone()) else { return; }; diff --git a/src/app/input/navigate.rs b/src/app/input/navigate.rs index bb88c46b..ce3d9cf4 100644 --- a/src/app/input/navigate.rs +++ b/src/app/input/navigate.rs @@ -380,6 +380,7 @@ pub(crate) enum NavigateAction { EnterResizeMode, ToggleSidebar, ReloadConfig, + OpenNotificationTarget, Detach, } @@ -463,6 +464,12 @@ fn navigate_action_for_key(state: &AppState, key: &KeyEvent) -> Option { + state.focus_toast_target(); + if state.mode == Mode::Navigate { + leave_navigate_mode(state); + } + } NavigateAction::Detach => { state.detach_requested = true; leave_navigate_mode(state); @@ -652,6 +665,38 @@ mod tests { assert_eq!(state.mode, Mode::Terminal); } + #[test] + fn custom_open_notification_key_focuses_current_toast_target() { + let mut state = state_with_workspaces(&["one", "two"]); + state.active = Some(0); + state.selected = 0; + state.mode = Mode::Navigate; + state.keybinds.open_notification_target = Some((KeyCode::Char('g'), KeyModifiers::empty())); + state.keybinds.open_notification_target_label = Some("g".into()); + let target_workspace_id = state.workspaces[1].id.clone(); + let target_pane = state.workspaces[1].tabs[0].root_pane; + state.toast = Some(crate::app::state::ToastNotification { + kind: crate::app::state::ToastKind::NeedsAttention, + title: "pi needs attention".into(), + context: "two".into(), + target: Some(crate::app::state::ToastTarget { + workspace_id: target_workspace_id, + pane_id: target_pane, + }), + }); + + handle_navigate_key( + &mut state, + KeyEvent::new(KeyCode::Char('g'), KeyModifiers::empty()), + ); + + assert_eq!(state.active, Some(1)); + assert_eq!(state.selected, 1); + assert_eq!(state.workspaces[1].focused_pane_id(), Some(target_pane)); + assert!(state.toast.is_none()); + assert_eq!(state.mode, Mode::Terminal); + } + #[test] fn movement_action_stays_in_navigate_mode() { let mut state = state_with_workspaces(&["a", "b"]); diff --git a/src/app/state.rs b/src/app/state.rs index dd081298..3791f674 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -897,6 +897,8 @@ impl AppState { detach_label: None, reload_config: None, reload_config_label: None, + open_notification_target: None, + open_notification_target_label: None, previous_workspace: None, previous_workspace_label: None, next_workspace: None, diff --git a/src/config/keybinds.rs b/src/config/keybinds.rs index 4a638ec0..90dc2283 100644 --- a/src/config/keybinds.rs +++ b/src/config/keybinds.rs @@ -67,6 +67,8 @@ pub struct Keybinds { pub detach_label: Option, pub reload_config: Option<(KeyCode, KeyModifiers)>, pub reload_config_label: Option, + pub open_notification_target: Option<(KeyCode, KeyModifiers)>, + pub open_notification_target_label: Option, pub previous_workspace: Option<(KeyCode, KeyModifiers)>, pub previous_workspace_label: Option, pub next_workspace: Option<(KeyCode, KeyModifiers)>, @@ -345,6 +347,12 @@ impl Config { &self.keys.reload_config, &mut diagnostics, ), + optional_binding( + BindingScope::Navigate, + "keys.open_notification_target", + &self.keys.open_notification_target, + &mut diagnostics, + ), optional_binding( BindingScope::Navigate, "keys.previous_workspace", @@ -596,30 +604,32 @@ impl Config { detach_label: optional_bindings[0].label.clone(), reload_config: optional_bindings[1].value, reload_config_label: optional_bindings[1].label.clone(), - previous_workspace: optional_bindings[2].value, - previous_workspace_label: optional_bindings[2].label.clone(), - next_workspace: optional_bindings[3].value, - next_workspace_label: optional_bindings[3].label.clone(), + open_notification_target: optional_bindings[2].value, + open_notification_target_label: optional_bindings[2].label.clone(), + previous_workspace: optional_bindings[3].value, + previous_workspace_label: optional_bindings[3].label.clone(), + next_workspace: optional_bindings[4].value, + next_workspace_label: optional_bindings[4].label.clone(), new_tab: bindings[3].value, new_tab_label: bindings[3].label.clone(), - rename_tab: optional_bindings[4].value, - rename_tab_label: optional_bindings[4].label.clone(), - previous_tab: optional_bindings[5].value, - previous_tab_label: optional_bindings[5].label.clone(), - next_tab: optional_bindings[6].value, - next_tab_label: optional_bindings[6].label.clone(), - close_tab: optional_bindings[7].value, - close_tab_label: optional_bindings[7].label.clone(), - rename_pane: optional_bindings[8].value, - rename_pane_label: optional_bindings[8].label.clone(), - focus_pane_left: optional_bindings[9].value, - focus_pane_left_label: optional_bindings[9].label.clone(), - focus_pane_down: optional_bindings[10].value, - focus_pane_down_label: optional_bindings[10].label.clone(), - focus_pane_up: optional_bindings[11].value, - focus_pane_up_label: optional_bindings[11].label.clone(), - focus_pane_right: optional_bindings[12].value, - focus_pane_right_label: optional_bindings[12].label.clone(), + rename_tab: optional_bindings[5].value, + rename_tab_label: optional_bindings[5].label.clone(), + previous_tab: optional_bindings[6].value, + previous_tab_label: optional_bindings[6].label.clone(), + next_tab: optional_bindings[7].value, + next_tab_label: optional_bindings[7].label.clone(), + close_tab: optional_bindings[8].value, + close_tab_label: optional_bindings[8].label.clone(), + rename_pane: optional_bindings[9].value, + rename_pane_label: optional_bindings[9].label.clone(), + focus_pane_left: optional_bindings[10].value, + focus_pane_left_label: optional_bindings[10].label.clone(), + focus_pane_down: optional_bindings[11].value, + focus_pane_down_label: optional_bindings[11].label.clone(), + focus_pane_up: optional_bindings[12].value, + focus_pane_up_label: optional_bindings[12].label.clone(), + focus_pane_right: optional_bindings[13].value, + focus_pane_right_label: optional_bindings[13].label.clone(), split_vertical: bindings[4].value, split_vertical_label: bindings[4].label.clone(), split_horizontal: bindings[5].value, diff --git a/src/config/model.rs b/src/config/model.rs index 4b0638be..166e3ffa 100644 --- a/src/config/model.rs +++ b/src/config/model.rs @@ -79,6 +79,8 @@ pub struct KeysConfig { pub detach: String, /// Reload config.toml in the running app/server. Unset by default. pub reload_config: String, + /// Focus the currently visible notification target. Unset by default. + pub open_notification_target: String, /// Select the previous workspace. Unset by default. pub previous_workspace: String, /// Select the next workspace. Unset by default. @@ -157,6 +159,7 @@ impl Default for KeysConfig { close_workspace: "shift+d".into(), detach: "".into(), reload_config: "".into(), + open_notification_target: "".into(), previous_workspace: "".into(), next_workspace: "".into(), new_tab: "c".into(), diff --git a/src/main.rs b/src/main.rs index 4b0c71dd..84a1870b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -87,6 +87,7 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration # next_workspace = "" # optional, unset by default # detach = "" # optional explicit detach shortcut in server/client mode # reload_config = "" # optional shortcut to reload config.toml without restarting +# open_notification_target = "" # optional shortcut to jump to the visible notification target # new_tab = "c" # rename_tab = "" # optional, unset by default # previous_tab = "" # optional, unset by default diff --git a/src/ui/keybind_help.rs b/src/ui/keybind_help.rs index fdc600c4..0d555d3d 100644 --- a/src/ui/keybind_help.rs +++ b/src/ui/keybind_help.rs @@ -56,6 +56,10 @@ pub(super) fn keybind_help_groups( (kb.new_workspace_label.clone(), "new workspace"), (kb.rename_workspace_label.clone(), "rename workspace"), (kb.close_workspace_label.clone(), "close workspace"), + ( + optional_keybind_label(&kb.open_notification_target_label), + "open notification target", + ), ( optional_keybind_label(&kb.previous_workspace_label), "previous workspace",