refactor(tui): unify terminal-mode recovery in recover_terminal_modes()
Extract the common "re-arm keyboard enhancement, mouse capture, bracketed paste, and focus events" sequence used by app startup, FocusGained recovery, and resume_terminal into a single best-effort helper. The FocusGained handler previously only re-pushed keyboard enhancement flags + (after v0.8.24) mouse capture, missing bracketed paste and focus events. Each new mode added at startup was a fresh gap waiting to be discovered when a user Cmd+Tabbed away. Co-locating the four flags in one canonical helper means future mode additions are one edit, not three. Adds a unit test that pins the gating (mouse + bracketed paste honor their booleans; keyboard + focus are unconditional) by writing into an in-memory buffer and asserting on the CSI sequences crossterm emits. Test: cargo test -p deepseek-tui --bin deepseek-tui --locked
This commit is contained in:
+58
-44
@@ -208,30 +208,24 @@ pub async fn run_tui(config: &Config, options: TuiOptions) -> Result<()> {
|
||||
if use_alt_screen {
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
}
|
||||
if use_mouse_capture {
|
||||
execute!(stdout, EnableMouseCapture)?;
|
||||
}
|
||||
if use_bracketed_paste {
|
||||
execute!(stdout, EnableBracketedPaste)?;
|
||||
}
|
||||
// Enable focus events so the terminal reports FocusGained/FocusLost.
|
||||
// Necessary for IME compositor re-activation on macOS when the user
|
||||
// switches away (Cmd+Tab) and returns.
|
||||
execute!(stdout, EnableFocusChange)?;
|
||||
// #442: opt into the Kitty keyboard protocol's escape-code
|
||||
// disambiguation so terminals that support it (Kitty, Ghostty,
|
||||
// Mouse capture, bracketed paste, focus events, and the Kitty
|
||||
// keyboard-protocol escape-disambiguation flag (#442). Single source
|
||||
// of truth shared with the FocusGained recovery path and
|
||||
// resume_terminal — see recover_terminal_modes.
|
||||
//
|
||||
// Focus events are necessary for IME compositor re-activation on
|
||||
// macOS when the user switches away (Cmd+Tab) and returns. The Kitty
|
||||
// keyboard protocol opt-in is best-effort: terminals that don't
|
||||
// support it (iTerm2, Terminal.app, Windows 10 conhost) silently
|
||||
// discard the escape, while supporting terminals (Kitty, Ghostty,
|
||||
// Alacritty 0.13+, WezTerm, recent Konsole, recent xterm) report
|
||||
// unambiguous events for Option/Alt-modified keys, plain Esc, and
|
||||
// multi-byte sequences. Terminals that don't recognise the escape
|
||||
// silently discard it; behaviour is identical to today on legacy
|
||||
// terminals (iTerm2, Terminal.app, Windows 10 conhost).
|
||||
// unambiguous events for Option/Alt-modified keys and plain Esc.
|
||||
//
|
||||
// Only `DISAMBIGUATE_ESCAPE_CODES` is pushed — the higher tiers
|
||||
// (`REPORT_EVENT_TYPES`, `REPORT_ALL_KEYS_AS_ESCAPE_CODES`) emit
|
||||
// release events that the existing key handlers would mis-route
|
||||
// as duplicate presses. Best-effort: failure to push is logged
|
||||
// and ignored so a quirky terminal can't block startup.
|
||||
push_keyboard_enhancement_flags(&mut stdout);
|
||||
// as duplicate presses.
|
||||
recover_terminal_modes(&mut stdout, use_mouse_capture, use_bracketed_paste);
|
||||
let color_depth = palette::ColorDepth::detect();
|
||||
let palette_mode = palette::PaletteMode::detect();
|
||||
tracing::debug!(
|
||||
@@ -1586,24 +1580,18 @@ async fn run_event_loop(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Re-push keyboard enhancement flags on focus-gain and force a
|
||||
// full viewport reset before repainting. App-switching and
|
||||
// interactive handoffs can leave the host terminal scrolled away
|
||||
// from row 0; treating focus as a recapture point prevents the
|
||||
// native scrollback gutter / blank-top-row failure mode from
|
||||
// persisting after the user returns.
|
||||
// On macOS, switching away (Cmd+Tab) and back can reset the
|
||||
// terminal's keyboard mode, which breaks IME compositor state.
|
||||
// Acknowledging FocusGained and re-pushing the flags restores
|
||||
// the IME so CJK input methods work after a focus toggle.
|
||||
// The same reset can drop the terminal's mouse-tracking mode,
|
||||
// leaving wheel scroll dead until restart — re-arm mouse
|
||||
// capture on focus-gain so wheel events keep flowing.
|
||||
// Re-establish terminal mode flags on focus-gain and force a full
|
||||
// viewport reset before repainting. App-switching and interactive
|
||||
// handoffs can leave the host terminal scrolled away from row 0
|
||||
// and (on macOS) can drop the keyboard, mouse-tracking, or
|
||||
// bracketed-paste modes — recover_terminal_modes() is the
|
||||
// canonical place those flags live.
|
||||
if terminal_event_needs_viewport_recapture(&evt) {
|
||||
push_keyboard_enhancement_flags(terminal.backend_mut());
|
||||
if app.use_mouse_capture {
|
||||
let _ = execute!(terminal.backend_mut(), EnableMouseCapture);
|
||||
}
|
||||
recover_terminal_modes(
|
||||
terminal.backend_mut(),
|
||||
app.use_mouse_capture,
|
||||
app.use_bracketed_paste,
|
||||
);
|
||||
force_terminal_repaint = true;
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
@@ -6408,14 +6396,11 @@ fn resume_terminal(
|
||||
if use_alt_screen {
|
||||
execute!(terminal.backend_mut(), EnterAlternateScreen)?;
|
||||
}
|
||||
if use_mouse_capture {
|
||||
execute!(terminal.backend_mut(), EnableMouseCapture)?;
|
||||
}
|
||||
if use_bracketed_paste {
|
||||
execute!(terminal.backend_mut(), EnableBracketedPaste)?;
|
||||
}
|
||||
execute!(terminal.backend_mut(), EnableFocusChange)?;
|
||||
push_keyboard_enhancement_flags(terminal.backend_mut());
|
||||
recover_terminal_modes(
|
||||
terminal.backend_mut(),
|
||||
use_mouse_capture,
|
||||
use_bracketed_paste,
|
||||
);
|
||||
reset_terminal_viewport(terminal)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -6446,6 +6431,35 @@ fn push_keyboard_enhancement_flags<W: Write>(writer: &mut W) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-establish terminal mode flags. Idempotent and best-effort: each
|
||||
/// underlying flag is silently discarded by terminals that don't support
|
||||
/// it, and a single flag's failure doesn't prevent later flags from being
|
||||
/// attempted.
|
||||
///
|
||||
/// **Canonical location for terminal-mode setup.** If you add a new mode
|
||||
/// flag at startup or in `resume_terminal`, add it here too — `FocusGained`
|
||||
/// recovery calls this and will silently fall behind otherwise.
|
||||
///
|
||||
/// Excluded by design: raw mode and the alternate screen — those persist
|
||||
/// across focus events and are only re-established by `resume_terminal`
|
||||
/// after a suspension, which always runs a separate path.
|
||||
fn recover_terminal_modes<W: Write>(
|
||||
writer: &mut W,
|
||||
use_mouse_capture: bool,
|
||||
use_bracketed_paste: bool,
|
||||
) {
|
||||
push_keyboard_enhancement_flags(writer);
|
||||
if use_mouse_capture && let Err(err) = execute!(writer, EnableMouseCapture) {
|
||||
tracing::debug!(?err, "EnableMouseCapture ignored");
|
||||
}
|
||||
if use_bracketed_paste && let Err(err) = execute!(writer, EnableBracketedPaste) {
|
||||
tracing::debug!(?err, "EnableBracketedPaste ignored");
|
||||
}
|
||||
if let Err(err) = execute!(writer, EnableFocusChange) {
|
||||
tracing::debug!(?err, "EnableFocusChange ignored");
|
||||
}
|
||||
}
|
||||
|
||||
fn terminal_event_needs_viewport_recapture(evt: &Event) -> bool {
|
||||
matches!(evt, Event::FocusGained)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,43 @@ fn focus_gained_forces_terminal_viewport_recapture() {
|
||||
assert!(!terminal_event_needs_viewport_recapture(&Event::FocusLost));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recover_terminal_modes_emits_expected_csi_sequences_with_gating() {
|
||||
let mut all_on: Vec<u8> = Vec::new();
|
||||
let mut all_off: Vec<u8> = Vec::new();
|
||||
recover_terminal_modes(&mut all_on, true, true);
|
||||
recover_terminal_modes(&mut all_off, false, false);
|
||||
let on = String::from_utf8_lossy(&all_on);
|
||||
let off = String::from_utf8_lossy(&all_off);
|
||||
|
||||
assert!(
|
||||
on.contains("\x1b[?1004h") && off.contains("\x1b[?1004h"),
|
||||
"EnableFocusChange must be re-armed regardless of gating"
|
||||
);
|
||||
assert!(
|
||||
on.contains("\x1b[>1u") && off.contains("\x1b[>1u"),
|
||||
"Kitty keyboard disambiguation flag must be re-pushed regardless of gating"
|
||||
);
|
||||
|
||||
assert!(
|
||||
on.contains("\x1b[?1000h"),
|
||||
"EnableMouseCapture missing when use_mouse_capture=true"
|
||||
);
|
||||
assert!(
|
||||
!off.contains("\x1b[?1000h"),
|
||||
"EnableMouseCapture must be gated by use_mouse_capture"
|
||||
);
|
||||
|
||||
assert!(
|
||||
on.contains("\x1b[?2004h"),
|
||||
"EnableBracketedPaste missing when use_bracketed_paste=true"
|
||||
);
|
||||
assert!(
|
||||
!off.contains("\x1b[?2004h"),
|
||||
"EnableBracketedPaste must be gated by use_bracketed_paste"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_origin_reset_resets_scroll_region_origin_and_clears() {
|
||||
assert!(
|
||||
|
||||
Reference in New Issue
Block a user