feat: add direct pane resize keybindings (#2558)
This commit is contained in:
parent
ca1af383a8
commit
4e31084495
|
|
@ -3,6 +3,7 @@
|
|||
## Unreleased
|
||||
|
||||
### Added
|
||||
- Optional `keys.resize_pane_left`, `keys.resize_pane_down`, `keys.resize_pane_up`, and `keys.resize_pane_right` bindings now resize the focused pane in one keystroke without entering resize mode.
|
||||
- Devin CLI, Cursor Agent CLI, MastraCode, Hermes Agent, and Grok CLI integrations now install and run natively on Windows.
|
||||
- Panes can now route normal right-click gestures to mouse-reporting applications through the pane menu, `herdr pane input`, `pane.input.set`, or the `pane split --right-click pane` launch option.
|
||||
- `theme.custom.sidebar_bg` can now give the desktop sidebar its own background without changing built-in theme defaults.
|
||||
|
|
|
|||
|
|
@ -137,7 +137,15 @@ A binding may also be an array when one action needs multiple shortcuts:
|
|||
next_tab = ["prefix+n", "ctrl+alt+]"]
|
||||
```
|
||||
|
||||
Optional actions are unset by default. Bind them with `prefix+` for prefix-mode behavior, or use an explicit modified chord when you intentionally want a direct shortcut.
|
||||
Optional actions are unset by default. Bind them with `prefix+` for prefix-mode behavior, or use an explicit modified chord when you intentionally want a direct shortcut. For example, tmux-style one-keystroke pane resizing without entering resize mode:
|
||||
|
||||
```toml
|
||||
[keys]
|
||||
resize_pane_left = "ctrl+shift+alt+left"
|
||||
resize_pane_down = "ctrl+shift+alt+down"
|
||||
resize_pane_up = "ctrl+shift+alt+up"
|
||||
resize_pane_right = "ctrl+shift+alt+right"
|
||||
```
|
||||
|
||||
Key strings accept plain keys, modifier combinations such as `ctrl+a`, `shift+n`, `alt+1`, `cmd+k`, and special keys such as `enter`, `tab`, `esc`, `left`, `right`, `up`, and `down`. Named punctuation such as `minus`, `comma`, `ampersand`, `plus`, and `backtick` is also accepted. Plain direct printable keys such as `n` are unsafe because they intercept typing; use `prefix+n` unless you intentionally want a direct binding. The `navigate_workspace_*` and `navigate_pane_*` fields are navigate-mode-only and may use plain keys such as `j` or `k`; they must not use `prefix+`, `esc`, `enter`, `tab`, `shift+tab`, `left`, `right`, or unmodified `1` through `9`. Left and right arrows are permanent aliases for pane-left and pane-right navigation. These navigate-mode shortcuts are independent from general action bindings such as `focus_pane_down = "prefix+j"`; when both use the same key, the navigate-mode shortcut wins while navigate mode is open. Alt, Cmd/Super, and punctuation with modifiers depend on your terminal and tmux settings.
|
||||
|
||||
|
|
|
|||
|
|
@ -523,6 +523,30 @@
|
|||
"default": "\"prefix+r\"",
|
||||
"description": "Enter resize mode."
|
||||
},
|
||||
{
|
||||
"key": "keys.resize_pane_left",
|
||||
"type": "keybinding",
|
||||
"default": "unset",
|
||||
"description": "Resize the focused pane toward the left. Unset by default."
|
||||
},
|
||||
{
|
||||
"key": "keys.resize_pane_down",
|
||||
"type": "keybinding",
|
||||
"default": "unset",
|
||||
"description": "Resize the focused pane downward. Unset by default."
|
||||
},
|
||||
{
|
||||
"key": "keys.resize_pane_up",
|
||||
"type": "keybinding",
|
||||
"default": "unset",
|
||||
"description": "Resize the focused pane upward. Unset by default."
|
||||
},
|
||||
{
|
||||
"key": "keys.resize_pane_right",
|
||||
"type": "keybinding",
|
||||
"default": "unset",
|
||||
"description": "Resize the focused pane toward the right. Unset by default."
|
||||
},
|
||||
{
|
||||
"key": "keys.toggle_sidebar",
|
||||
"type": "keybinding",
|
||||
|
|
|
|||
|
|
@ -383,6 +383,22 @@ impl App {
|
|||
leave_navigate_mode(&mut self.state);
|
||||
}
|
||||
NavigateAction::EnterResizeMode => self.state.mode = Mode::Resize,
|
||||
NavigateAction::ResizePaneLeft => {
|
||||
self.resize_pane_direction_via_api(NavDirection::Left);
|
||||
leave_navigate_mode(&mut self.state);
|
||||
}
|
||||
NavigateAction::ResizePaneDown => {
|
||||
self.resize_pane_direction_via_api(NavDirection::Down);
|
||||
leave_navigate_mode(&mut self.state);
|
||||
}
|
||||
NavigateAction::ResizePaneUp => {
|
||||
self.resize_pane_direction_via_api(NavDirection::Up);
|
||||
leave_navigate_mode(&mut self.state);
|
||||
}
|
||||
NavigateAction::ResizePaneRight => {
|
||||
self.resize_pane_direction_via_api(NavDirection::Right);
|
||||
leave_navigate_mode(&mut self.state);
|
||||
}
|
||||
NavigateAction::ToggleSidebar => {
|
||||
self.state.sidebar_collapsed = !self.state.sidebar_collapsed;
|
||||
leave_navigate_mode(&mut self.state);
|
||||
|
|
@ -537,6 +553,17 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resize_pane_direction_via_api(&mut self, direction: NavDirection) {
|
||||
self.runtime_pane_resize(
|
||||
"tui.pane.resize",
|
||||
crate::api::schema::PaneResizeParams {
|
||||
pane_id: None,
|
||||
direction: api_pane_direction(direction),
|
||||
amount: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn swap_pane_direction_via_api(&mut self, direction: NavDirection) {
|
||||
if let Some((ws_idx, source, target)) = self.directional_pane_swap_from_view(direction) {
|
||||
let source_pane_id = self.public_pane_id(ws_idx, source);
|
||||
|
|
@ -1367,6 +1394,10 @@ pub(crate) enum NavigateAction {
|
|||
CopyMode,
|
||||
Zoom,
|
||||
EnterResizeMode,
|
||||
ResizePaneLeft,
|
||||
ResizePaneDown,
|
||||
ResizePaneUp,
|
||||
ResizePaneRight,
|
||||
ToggleSidebar,
|
||||
CyclePaneNext,
|
||||
CyclePanePrevious,
|
||||
|
|
@ -1511,6 +1542,10 @@ fn non_indexed_action_for_key(
|
|||
(&kb.close_pane, NavigateAction::ClosePane),
|
||||
(&kb.zoom, NavigateAction::Zoom),
|
||||
(&kb.resize_mode, NavigateAction::EnterResizeMode),
|
||||
(&kb.resize_pane_left, NavigateAction::ResizePaneLeft),
|
||||
(&kb.resize_pane_down, NavigateAction::ResizePaneDown),
|
||||
(&kb.resize_pane_up, NavigateAction::ResizePaneUp),
|
||||
(&kb.resize_pane_right, NavigateAction::ResizePaneRight),
|
||||
(&kb.toggle_sidebar, NavigateAction::ToggleSidebar),
|
||||
(&kb.reload_config, NavigateAction::ReloadConfig),
|
||||
(
|
||||
|
|
@ -1742,6 +1777,22 @@ pub(super) fn execute_navigate_action_in_context(
|
|||
leave_navigate_mode(state);
|
||||
}
|
||||
NavigateAction::EnterResizeMode => state.mode = Mode::Resize,
|
||||
NavigateAction::ResizePaneLeft => {
|
||||
state.resize_pane(NavDirection::Left);
|
||||
leave_navigate_mode(state);
|
||||
}
|
||||
NavigateAction::ResizePaneDown => {
|
||||
state.resize_pane(NavDirection::Down);
|
||||
leave_navigate_mode(state);
|
||||
}
|
||||
NavigateAction::ResizePaneUp => {
|
||||
state.resize_pane(NavDirection::Up);
|
||||
leave_navigate_mode(state);
|
||||
}
|
||||
NavigateAction::ResizePaneRight => {
|
||||
state.resize_pane(NavDirection::Right);
|
||||
leave_navigate_mode(state);
|
||||
}
|
||||
NavigateAction::ToggleSidebar => {
|
||||
state.sidebar_collapsed = !state.sidebar_collapsed;
|
||||
leave_navigate_mode(state);
|
||||
|
|
@ -2569,6 +2620,44 @@ navigate_pane_right = "ctrl+l"
|
|||
assert_eq!(action, Some(NavigateAction::SwapPaneRight));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_direct_resize_pane_shortcut_maps_to_navigation_action() {
|
||||
let mut state = state_with_workspaces(&["test"]);
|
||||
state.keybinds.resize_pane_right =
|
||||
crate::config::ActionKeybinds::direct("ctrl+shift+alt+right");
|
||||
|
||||
let action = terminal_direct_navigation_action(
|
||||
&state,
|
||||
TerminalKey::new(
|
||||
KeyCode::Right,
|
||||
KeyModifiers::CONTROL | KeyModifiers::SHIFT | KeyModifiers::ALT,
|
||||
),
|
||||
);
|
||||
|
||||
assert_eq!(action, Some(NavigateAction::ResizePaneRight));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix_resize_pane_binding_maps_to_navigation_action() {
|
||||
let config: Config = toml::from_str(
|
||||
r#"
|
||||
[keys]
|
||||
resize_pane_left = "prefix+shift+left"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let mut state = state_with_workspaces(&["test"]);
|
||||
state.keybinds = config.keybinds();
|
||||
|
||||
let action = action_for_key(
|
||||
&state,
|
||||
TerminalKey::new(KeyCode::Left, KeyModifiers::SHIFT),
|
||||
BindingDispatch::Prefix,
|
||||
);
|
||||
|
||||
assert_eq!(action, Some(NavigateAction::ResizePaneLeft));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_direct_last_pane_shortcut_maps_to_navigation_action() {
|
||||
let mut state = state_with_workspaces(&["test"]);
|
||||
|
|
|
|||
|
|
@ -350,6 +350,10 @@ pub struct Keybinds {
|
|||
pub close_pane: ActionKeybinds,
|
||||
pub zoom: ActionKeybinds,
|
||||
pub resize_mode: ActionKeybinds,
|
||||
pub resize_pane_left: ActionKeybinds,
|
||||
pub resize_pane_down: ActionKeybinds,
|
||||
pub resize_pane_up: ActionKeybinds,
|
||||
pub resize_pane_right: ActionKeybinds,
|
||||
pub toggle_sidebar: ActionKeybinds,
|
||||
pub custom_commands: Vec<CustomCommandKeybind>,
|
||||
}
|
||||
|
|
@ -512,6 +516,10 @@ impl Config {
|
|||
close_pane: empty_action!(),
|
||||
zoom: empty_action!(),
|
||||
resize_mode: empty_action!(),
|
||||
resize_pane_left: empty_action!(),
|
||||
resize_pane_down: empty_action!(),
|
||||
resize_pane_up: empty_action!(),
|
||||
resize_pane_right: empty_action!(),
|
||||
toggle_sidebar: empty_action!(),
|
||||
custom_commands: Vec::new(),
|
||||
};
|
||||
|
|
@ -653,6 +661,10 @@ impl Config {
|
|||
apply_action!(keybinds.close_pane, close_pane, source);
|
||||
apply_action!(keybinds.zoom, zoom, source);
|
||||
apply_action!(keybinds.resize_mode, resize_mode, source);
|
||||
apply_action!(keybinds.resize_pane_left, resize_pane_left, source);
|
||||
apply_action!(keybinds.resize_pane_down, resize_pane_down, source);
|
||||
apply_action!(keybinds.resize_pane_up, resize_pane_up, source);
|
||||
apply_action!(keybinds.resize_pane_right, resize_pane_right, source);
|
||||
apply_action!(keybinds.toggle_sidebar, toggle_sidebar, source);
|
||||
|
||||
if source == field_source!(indexed) {
|
||||
|
|
|
|||
|
|
@ -438,6 +438,14 @@ pub struct KeysConfig {
|
|||
pub zoom: BindingConfig,
|
||||
/// Enter resize mode. Default: "prefix+r"
|
||||
pub resize_mode: BindingConfig,
|
||||
/// Resize the focused pane toward the left. Unset by default.
|
||||
pub resize_pane_left: BindingConfig,
|
||||
/// Resize the focused pane downward. Unset by default.
|
||||
pub resize_pane_down: BindingConfig,
|
||||
/// Resize the focused pane upward. Unset by default.
|
||||
pub resize_pane_up: BindingConfig,
|
||||
/// Resize the focused pane toward the right. Unset by default.
|
||||
pub resize_pane_right: BindingConfig,
|
||||
/// Toggle sidebar collapse. Default: "prefix+b"
|
||||
pub toggle_sidebar: BindingConfig,
|
||||
/// Optional indexed shortcuts expanded over number keys 1-9.
|
||||
|
|
@ -557,6 +565,14 @@ pub(crate) struct KeysConfigOverlay {
|
|||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
resize_mode: Option<BindingConfig>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
resize_pane_left: Option<BindingConfig>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
resize_pane_down: Option<BindingConfig>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
resize_pane_up: Option<BindingConfig>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
resize_pane_right: Option<BindingConfig>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
toggle_sidebar: Option<BindingConfig>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
indexed: Option<IndexedKeysConfig>,
|
||||
|
|
@ -633,6 +649,10 @@ impl<'de> Deserialize<'de> for KeysConfig {
|
|||
apply_field!(close_pane);
|
||||
apply_field!(zoom);
|
||||
apply_field!(resize_mode);
|
||||
apply_field!(resize_pane_left);
|
||||
apply_field!(resize_pane_down);
|
||||
apply_field!(resize_pane_up);
|
||||
apply_field!(resize_pane_right);
|
||||
apply_field!(toggle_sidebar);
|
||||
apply_field!(indexed);
|
||||
apply_field!(command);
|
||||
|
|
@ -731,6 +751,10 @@ impl KeysConfig {
|
|||
copy_effective_action_field!(close_pane, keybinds.close_pane);
|
||||
copy_effective_action_field!(zoom, keybinds.zoom);
|
||||
copy_effective_action_field!(resize_mode, keybinds.resize_mode);
|
||||
copy_effective_action_field!(resize_pane_left, keybinds.resize_pane_left);
|
||||
copy_effective_action_field!(resize_pane_down, keybinds.resize_pane_down);
|
||||
copy_effective_action_field!(resize_pane_up, keybinds.resize_pane_up);
|
||||
copy_effective_action_field!(resize_pane_right, keybinds.resize_pane_right);
|
||||
copy_effective_action_field!(toggle_sidebar, keybinds.toggle_sidebar);
|
||||
copy_user_field!(indexed);
|
||||
|
||||
|
|
@ -1019,6 +1043,10 @@ impl Default for KeysConfig {
|
|||
close_pane: BindingConfig::one("prefix+x"),
|
||||
zoom: BindingConfig::one("prefix+z"),
|
||||
resize_mode: BindingConfig::one("prefix+r"),
|
||||
resize_pane_left: BindingConfig::empty(),
|
||||
resize_pane_down: BindingConfig::empty(),
|
||||
resize_pane_up: BindingConfig::empty(),
|
||||
resize_pane_right: BindingConfig::empty(),
|
||||
toggle_sidebar: BindingConfig::one("prefix+b"),
|
||||
indexed: IndexedKeysConfig::default(),
|
||||
command: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -213,6 +213,10 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration
|
|||
# close_pane = "prefix+x"
|
||||
# zoom = "prefix+z" # legacy alias: fullscreen
|
||||
# resize_mode = "prefix+r"
|
||||
# resize_pane_left = "" # optional, e.g. "ctrl+shift+alt+left" resizes without entering resize mode
|
||||
# resize_pane_down = "" # optional, e.g. "ctrl+shift+alt+down"
|
||||
# resize_pane_up = "" # optional, e.g. "ctrl+shift+alt+up"
|
||||
# resize_pane_right = "" # optional, e.g. "ctrl+shift+alt+right"
|
||||
# toggle_sidebar = "prefix+b"
|
||||
|
||||
# Navigate-mode movement. These local shortcuts win while navigate mode is open.
|
||||
|
|
|
|||
|
|
@ -145,6 +145,10 @@ pub(super) fn keybind_help_groups(app: &AppState) -> Vec<HelpGroup> {
|
|||
help_entry(keybind_label(&kb.copy_mode), "copy mode"),
|
||||
help_entry(keybind_label(&kb.zoom), "zoom pane"),
|
||||
help_entry(keybind_label(&kb.resize_mode), "resize mode"),
|
||||
help_entry(keybind_label(&kb.resize_pane_left), "resize pane left"),
|
||||
help_entry(keybind_label(&kb.resize_pane_down), "resize pane down"),
|
||||
help_entry(keybind_label(&kb.resize_pane_up), "resize pane up"),
|
||||
help_entry(keybind_label(&kb.resize_pane_right), "resize pane right"),
|
||||
help_entry(keybind_label(&kb.toggle_sidebar), "toggle sidebar"),
|
||||
help_entry(keybind_label(&kb.focus_pane_left), "focus pane left"),
|
||||
help_entry(keybind_label(&kb.focus_pane_down), "focus pane down"),
|
||||
|
|
|
|||
Loading…
Reference in New Issue