mirror of
https://github.com/herdrdev/herdr.git
synced 2026-11-06 00:06:18 +00:00
fix: restore keyboard protocol flags after a terminal reconnect (#5088)
* fix: restore keyboard protocol flags after a terminal reconnect refs #4985 * test: cover keyboard protocol refresh with an outer kitty mode refs #4985
This commit is contained in:
@@ -85,6 +85,20 @@ fn refresh_host_mouse_capture(enabled: bool, sgr_pixels: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_host_keyboard_protocol(
|
||||
terminal_guard: &TerminalGuard,
|
||||
report_all_keys: bool,
|
||||
focus_reporting: bool,
|
||||
) {
|
||||
if let Err(err) = terminal_guard.refresh_host_keyboard_protocol(
|
||||
&mut io::stdout(),
|
||||
report_all_keys,
|
||||
focus_reporting,
|
||||
) {
|
||||
warn!(err = %err, "failed to re-assert host keyboard protocol");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
use terminal_setup::is_ssh_session;
|
||||
#[cfg(test)]
|
||||
@@ -861,6 +875,11 @@ async fn run_client_loop(
|
||||
state.mouse_capture_active,
|
||||
host_sgr_pixels_active.load(Ordering::Acquire),
|
||||
);
|
||||
refresh_host_keyboard_protocol(
|
||||
_terminal_guard,
|
||||
state.keyboard_report_all_active,
|
||||
false,
|
||||
);
|
||||
}
|
||||
let (outcome, frame) = {
|
||||
let shell = state.shell.as_mut().expect("checked shell mode");
|
||||
@@ -1122,6 +1141,11 @@ async fn run_client_loop(
|
||||
state.mouse_capture_active,
|
||||
host_sgr_pixels_active.load(Ordering::Acquire),
|
||||
);
|
||||
refresh_host_keyboard_protocol(
|
||||
_terminal_guard,
|
||||
state.keyboard_report_all_active,
|
||||
false,
|
||||
);
|
||||
}
|
||||
let image_target = state
|
||||
.shell
|
||||
@@ -1237,6 +1261,14 @@ async fn run_client_loop(
|
||||
host_sgr_pixels_active.load(Ordering::Acquire),
|
||||
);
|
||||
}
|
||||
// A host that reset its modes (a reconnect or window reload)
|
||||
// stops sending focus reports, so a resize is the one
|
||||
// dependable trigger to restore them.
|
||||
refresh_host_keyboard_protocol(
|
||||
_terminal_guard,
|
||||
state.keyboard_report_all_active,
|
||||
true,
|
||||
);
|
||||
state.reported_size = (new_cols, new_rows);
|
||||
state.reported_cell_size = (cell_width_px, cell_height_px);
|
||||
state.pixel_geometry_exact = pixel_geometry_exact;
|
||||
|
||||
@@ -52,6 +52,7 @@ pub(super) fn setup_terminal_with_capabilities(
|
||||
buffered_host_input: Vec::new(),
|
||||
reset_keyboard_enhancements: false,
|
||||
reset_modify_other_keys: false,
|
||||
modify_other_keys_sequence: None,
|
||||
reset_host_color_scheme_reports: false,
|
||||
restore_claimed: Arc::new(AtomicBool::new(false)),
|
||||
restored: false,
|
||||
@@ -114,6 +115,7 @@ pub(super) fn setup_terminal_with_capabilities(
|
||||
.flatten();
|
||||
if let Some(mode) = modify_other_keys_mode {
|
||||
terminal_guard.reset_modify_other_keys = true;
|
||||
terminal_guard.modify_other_keys_sequence = Some(mode.set_sequence());
|
||||
io::stdout().write_all(mode.set_sequence())?;
|
||||
io::stdout().flush()?;
|
||||
}
|
||||
@@ -137,6 +139,8 @@ pub(super) struct TerminalGuard {
|
||||
buffered_host_input: Vec<u8>,
|
||||
reset_keyboard_enhancements: bool,
|
||||
reset_modify_other_keys: bool,
|
||||
/// The modifyOtherKeys sequence written at setup, re-sent on refresh.
|
||||
modify_other_keys_sequence: Option<&'static [u8]>,
|
||||
reset_host_color_scheme_reports: bool,
|
||||
restore_claimed: Arc<AtomicBool>,
|
||||
restored: bool,
|
||||
@@ -749,6 +753,39 @@ fn disable_windows_win32_input_mode(writer: &mut impl std::io::Write) -> io::Res
|
||||
writer.flush()
|
||||
}
|
||||
|
||||
/// Host input modes to re-assert after the host may have reset them.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub(super) struct HostKeyboardProtocolRefresh {
|
||||
/// Some when the host confirmed the kitty keyboard protocol at startup;
|
||||
/// the value says whether all keys are currently reported as escapes.
|
||||
pub(super) kitty_report_all_keys: Option<bool>,
|
||||
pub(super) modify_other_keys_sequence: Option<&'static [u8]>,
|
||||
/// Re-enable focus reporting (1004). Callers reacting to a focus-in skip
|
||||
/// it: that host still reports focus, and re-enabling from a focus report
|
||||
/// could loop on a host that answers the enable with another report.
|
||||
pub(super) focus_reporting: bool,
|
||||
}
|
||||
|
||||
pub(super) fn write_host_keyboard_protocol_refresh(
|
||||
writer: &mut impl io::Write,
|
||||
refresh: HostKeyboardProtocolRefresh,
|
||||
) -> io::Result<()> {
|
||||
if let Some(report_all_keys) = refresh.kitty_report_all_keys {
|
||||
// Replace Herdr's stack entry rather than pushing, so repeated
|
||||
// refreshes never grow the host's keyboard stack. On a host that
|
||||
// reset its stack, the pop is a no-op and the push restores the flags.
|
||||
crate::terminal_modes::set_host_kitty_keyboard_report_all(writer, report_all_keys)?;
|
||||
}
|
||||
if let Some(sequence) = refresh.modify_other_keys_sequence {
|
||||
writer.write_all(sequence)?;
|
||||
}
|
||||
writer.write_all(b"\x1b[?2004h")?;
|
||||
if refresh.focus_reporting {
|
||||
writer.write_all(b"\x1b[?1004h")?;
|
||||
}
|
||||
writer.flush()
|
||||
}
|
||||
|
||||
impl TerminalGuard {
|
||||
pub(super) fn host_escape_disambiguation_active(&self) -> bool {
|
||||
self.host_escape_disambiguation_active
|
||||
@@ -763,6 +800,31 @@ impl TerminalGuard {
|
||||
std::mem::take(&mut self.buffered_host_input)
|
||||
}
|
||||
|
||||
/// Re-sends the keyboard and input modes enabled at setup. A host that
|
||||
/// reset its terminal state (a reconnect or window reload) forgets them,
|
||||
/// which turns Shift+Enter and Ctrl+Enter into plain Enter.
|
||||
pub(super) fn refresh_host_keyboard_protocol(
|
||||
&self,
|
||||
writer: &mut impl io::Write,
|
||||
report_all_keys: bool,
|
||||
focus_reporting: bool,
|
||||
) -> io::Result<()> {
|
||||
// Direct attach leaves these modes to the attached pane.
|
||||
if !self.reset_keyboard_enhancements || self.restore_claimed.load(Ordering::Acquire) {
|
||||
return Ok(());
|
||||
}
|
||||
write_host_keyboard_protocol_refresh(
|
||||
writer,
|
||||
HostKeyboardProtocolRefresh {
|
||||
kitty_report_all_keys: self
|
||||
.host_escape_disambiguation_active
|
||||
.then_some(report_all_keys),
|
||||
modify_other_keys_sequence: self.modify_other_keys_sequence,
|
||||
focus_reporting,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub(super) fn recover_windows_virtual_terminal_input(&self) -> io::Result<()> {
|
||||
let active = enable_windows_virtual_terminal_input(
|
||||
@@ -887,6 +949,147 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn refresh_test_guard(
|
||||
kitty_confirmed: bool,
|
||||
client_protocols: bool,
|
||||
modify_other_keys_sequence: Option<&'static [u8]>,
|
||||
) -> TerminalGuard {
|
||||
TerminalGuard {
|
||||
host_escape_disambiguation_active: kitty_confirmed,
|
||||
host_sgr_pixel_mouse: None,
|
||||
buffered_host_input: Vec::new(),
|
||||
reset_keyboard_enhancements: client_protocols,
|
||||
reset_modify_other_keys: modify_other_keys_sequence.is_some(),
|
||||
modify_other_keys_sequence,
|
||||
reset_host_color_scheme_reports: false,
|
||||
restore_claimed: Arc::new(AtomicBool::new(false)),
|
||||
// Keep Drop from writing restore sequences to the test's stdout.
|
||||
restored: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn refresh_bytes(guard: &TerminalGuard, report_all_keys: bool, focus: bool) -> Vec<u8> {
|
||||
let mut output = Vec::new();
|
||||
guard
|
||||
.refresh_host_keyboard_protocol(&mut output, report_all_keys, focus)
|
||||
.unwrap();
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn host_keyboard_protocol_refresh_resends_the_setup_modes() {
|
||||
// Focus-in on a kitty host: replace Herdr's stack entry, keep paste on.
|
||||
let kitty = refresh_test_guard(true, true, None);
|
||||
assert_eq!(
|
||||
refresh_bytes(&kitty, false, false),
|
||||
b"\x1b[<1u\x1b[>7u\x1b[?2004h"
|
||||
);
|
||||
// Resize while a pane wants all keys, on a modifyOtherKeys host too.
|
||||
let both = refresh_test_guard(true, true, Some(b"\x1b[>4;1m"));
|
||||
assert_eq!(
|
||||
refresh_bytes(&both, true, true),
|
||||
b"\x1b[<1u\x1b[>31u\x1b[>4;1m\x1b[?2004h\x1b[?1004h"
|
||||
);
|
||||
// A host that never confirmed the kitty protocol gets no kitty flags.
|
||||
let legacy = refresh_test_guard(false, true, None);
|
||||
assert_eq!(
|
||||
refresh_bytes(&legacy, false, true),
|
||||
b"\x1b[?2004h\x1b[?1004h"
|
||||
);
|
||||
// Direct attach leaves keyboard modes to the attached pane.
|
||||
let direct = refresh_test_guard(true, false, None);
|
||||
assert!(refresh_bytes(&direct, false, true).is_empty());
|
||||
// Nothing is re-enabled once the terminal is being restored.
|
||||
kitty.restore_claimed.store(true, Ordering::Release);
|
||||
assert!(refresh_bytes(&kitty, false, true).is_empty());
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn host_keyboard_protocol_refresh_restores_shift_enter_after_host_reset() {
|
||||
use crate::input::{KeyboardProtocol, TerminalKey};
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
|
||||
let guard = refresh_test_guard(true, true, None);
|
||||
let shift_enter = TerminalKey::new(KeyCode::Enter, KeyModifiers::SHIFT);
|
||||
// libghostty stands in for the host terminal. `reset` models a host
|
||||
// that lost its modes, like a reloaded VS Code window (#4985).
|
||||
for reset in [false, true] {
|
||||
let (host, _rx) =
|
||||
crate::terminal::TerminalRuntime::test_with_channel_and_scrollback_bytes(
|
||||
80, 24, 0, b"", 4096,
|
||||
);
|
||||
// Herdr's setup push.
|
||||
host.test_process_pty_bytes(b"\x1b[>7u");
|
||||
if reset {
|
||||
host.test_process_pty_bytes(b"\x1bc");
|
||||
assert_eq!(host.keyboard_protocol(), KeyboardProtocol::Legacy);
|
||||
assert_eq!(host.encode_terminal_key(shift_enter.clone()), b"\r");
|
||||
}
|
||||
for _ in 0..3 {
|
||||
host.test_process_pty_bytes(&refresh_bytes(&guard, false, true));
|
||||
}
|
||||
assert_eq!(
|
||||
host.keyboard_protocol(),
|
||||
KeyboardProtocol::Kitty { flags: 7 },
|
||||
"reset={reset}"
|
||||
);
|
||||
assert_eq!(
|
||||
host.encode_terminal_key(shift_enter.clone()),
|
||||
b"\x1b[13;2u",
|
||||
"reset={reset}"
|
||||
);
|
||||
// Refreshes never grow the stack: Herdr's one exit pop restores
|
||||
// the host's own keyboard mode.
|
||||
host.test_process_pty_bytes(b"\x1b[<1u");
|
||||
assert_eq!(
|
||||
host.keyboard_protocol(),
|
||||
KeyboardProtocol::Legacy,
|
||||
"reset={reset}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn host_keyboard_protocol_refresh_keeps_the_outer_mode_across_report_all_toggles() {
|
||||
use crate::input::KeyboardProtocol;
|
||||
|
||||
let guard = refresh_test_guard(true, true, None);
|
||||
let (host, _rx) = crate::terminal::TerminalRuntime::test_with_channel_and_scrollback_bytes(
|
||||
80, 24, 0, b"", 4096,
|
||||
);
|
||||
// Something outside Herdr (a shell or wrapper) already set its own mode.
|
||||
host.test_process_pty_bytes(b"\x1b[>1u");
|
||||
// Herdr's setup push.
|
||||
host.test_process_pty_bytes(b"\x1b[>7u");
|
||||
for round in 0..6 {
|
||||
let report_all = round % 2 == 0;
|
||||
let mut toggle = Vec::new();
|
||||
crate::terminal_modes::set_host_kitty_keyboard_report_all(&mut toggle, report_all)
|
||||
.unwrap();
|
||||
host.test_process_pty_bytes(&toggle);
|
||||
for focus in [false, true, false] {
|
||||
host.test_process_pty_bytes(&refresh_bytes(&guard, report_all, focus));
|
||||
}
|
||||
let flags = if report_all { 31 } else { 7 };
|
||||
assert_eq!(
|
||||
host.keyboard_protocol(),
|
||||
KeyboardProtocol::Kitty { flags },
|
||||
"round {round}"
|
||||
);
|
||||
}
|
||||
// Herdr's one exit pop restores exactly the outer mode.
|
||||
host.test_process_pty_bytes(b"\x1b[<1u");
|
||||
assert_eq!(
|
||||
host.keyboard_protocol(),
|
||||
KeyboardProtocol::Kitty { flags: 1 }
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn host_keyboard_probe_consumes_fragmented_responses_and_preserves_input() {
|
||||
|
||||
Reference in New Issue
Block a user