aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-08-04 05:19:40 +0300
committerGitHub <noreply@github.com>2026-08-04 05:19:40 +0300
commitadb50cba9b15583db019bb655119915869e8c44e (patch)
tree910180a20322a581436b8d8e88b1279f40e1500e /src
parentcffc6c4b50178d404fbb939346dab7a33e5ed8d4 (diff)
fix(input): preserve shift-tab in pane automation (#2259)
refs #1561
Diffstat (limited to 'src')
-rw-r--r--src/app/api/panes.rs18
-rw-r--r--src/pane/input.rs7
-rw-r--r--src/pane/terminal.rs39
3 files changed, 63 insertions, 1 deletions
diff --git a/src/app/api/panes.rs b/src/app/api/panes.rs
index 72798a1b..a5f1cbcb 100644
--- a/src/app/api/panes.rs
+++ b/src/app/api/panes.rs
@@ -1979,6 +1979,24 @@ mod tests {
}
#[tokio::test]
+ async fn api_pane_send_keys_encodes_shift_tab_as_backtab() {
+ let (mut app, pane_id, mut rx) = app_with_send_key_runtime(1);
+
+ let response = app.handle_api_request(crate::api::schema::Request {
+ id: "req".into(),
+ method: crate::api::schema::Method::PaneSendKeys(PaneSendKeysParams {
+ pane_id,
+ keys: vec!["shift+tab".into()],
+ }),
+ });
+
+ let success: SuccessResponse = serde_json::from_str(&response).unwrap();
+ assert_eq!(success.result, ResponseResult::Ok {});
+ assert_eq!(rx.try_recv().unwrap(), bytes::Bytes::from_static(b"\x1b[Z"));
+ assert!(rx.try_recv().is_err());
+ }
+
+ #[tokio::test]
async fn api_pane_get_exposes_scroll_metrics() {
let (mut app, public_pane_id, pane_id) = app_with_scrollback_runtime();
let runtime = app
diff --git a/src/pane/input.rs b/src/pane/input.rs
index 38e8138b..2e60bfc8 100644
--- a/src/pane/input.rs
+++ b/src/pane/input.rs
@@ -13,7 +13,12 @@ pub(super) fn ghostty_key_event_from_terminal_key(
crate::ghostty::ffi::GhosttyKeyAction_GHOSTTY_KEY_ACTION_REPEAT
}
});
- event.set_mods(ghostty_mods_from_key_modifiers(key.modifiers));
+ let mut mods = ghostty_mods_from_key_modifiers(key.modifiers);
+ if matches!(key.code, crossterm::event::KeyCode::BackTab) {
+ // Ghostty represents backtab as Tab with Shift rather than a distinct key.
+ mods |= crate::ghostty::MOD_SHIFT;
+ }
+ event.set_mods(mods);
event.set_key(ghostty_key_from_crossterm_key_code(
key.code,
key.shifted_codepoint,
diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs
index b90c63f1..d4f167a9 100644
--- a/src/pane/terminal.rs
+++ b/src/pane/terminal.rs
@@ -3857,6 +3857,45 @@ mod tests {
}
#[test]
+ fn ghostty_backtab_preserves_shift_across_keyboard_protocols() {
+ for (kitty_flags, expected) in [
+ (None, b"\x1b[Z".as_slice()),
+ (Some(1), b"\x1b[9;2u".as_slice()),
+ ] {
+ let (tx, _rx) = mpsc::channel(4);
+ let mut terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap();
+ if let Some(flags) = kitty_flags {
+ terminal.write(format!("\x1b[>{flags}u").as_bytes());
+ }
+ let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap();
+ let protocol = pane.keyboard_protocol().unwrap();
+
+ for modifiers in [
+ crossterm::event::KeyModifiers::empty(),
+ crossterm::event::KeyModifiers::SHIFT,
+ ] {
+ let encoded = pane.encode_terminal_key(
+ crate::input::TerminalKey::new(crossterm::event::KeyCode::BackTab, modifiers),
+ protocol,
+ );
+ assert_eq!(encoded, expected, "backtab with modifiers {modifiers:?}");
+ }
+ }
+
+ let (tx, _rx) = mpsc::channel(4);
+ let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap();
+ let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap();
+ let encoded = pane.encode_terminal_key(
+ crate::input::TerminalKey::new(
+ crossterm::event::KeyCode::Tab,
+ crossterm::event::KeyModifiers::empty(),
+ ),
+ crate::input::KeyboardProtocol::Legacy,
+ );
+ assert_eq!(encoded, b"\t");
+ }
+
+ #[test]
fn ghostty_enter_backspace_release_in_legacy_pane_emits_nothing() {
let (tx, _rx) = mpsc::channel(4);
let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap();