feat: add notification target keybind

This commit is contained in:
Ogulcan Celik 2026-05-11 23:47:51 +03:00
parent fe693f613a
commit 8d5dca8cfe
8 changed files with 90 additions and 23 deletions

View File

@ -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 |

View File

@ -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;
};

View File

@ -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<NavigateA
{
return Some(NavigateAction::ReloadConfig);
}
if kb
.open_notification_target
.is_some_and(|(code, mods)| key_matches(key, code, mods))
{
return Some(NavigateAction::OpenNotificationTarget);
}
if kb
.detach
.is_some_and(|(code, mods)| key_matches(key, code, mods))
@ -553,6 +560,12 @@ pub(super) fn execute_navigate_action(state: &mut AppState, action: NavigateActi
state.request_reload_config = true;
leave_navigate_mode(state);
}
NavigateAction::OpenNotificationTarget => {
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"]);

View File

@ -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,

View File

@ -67,6 +67,8 @@ pub struct Keybinds {
pub detach_label: Option<String>,
pub reload_config: Option<(KeyCode, KeyModifiers)>,
pub reload_config_label: Option<String>,
pub open_notification_target: Option<(KeyCode, KeyModifiers)>,
pub open_notification_target_label: Option<String>,
pub previous_workspace: Option<(KeyCode, KeyModifiers)>,
pub previous_workspace_label: Option<String>,
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,

View File

@ -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(),

View File

@ -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

View File

@ -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",