Skip to content

Commit c1986e5

Browse files
authored
fix shift enter (#14598)
## Description <!-- Please remember to add your design buddy onto the PR for review, if it contains any UI changes! --> Fixes intermittent Shift+Enter handling in the TUI when crossterm incorrectly reports that the host terminal does not support Kitty keyboard enhancements. Retries a negative capability probe once, always requests the safe baseline Kitty flags (`3`), and keeps full modifier lifecycle reporting (`15`) conditional on confirmed support and push-to-talk configuration. ## Testing <!-- How did you test this change? What automated tests did you add? If you didn't add any new tests, what's your justification for not adding any? Manual testing is required for changes that can be manually tested, and almost all changes can be manually tested. If your change can be manually tested, please include screenshots or a screen recording that show it working end to end. You can run the app locally using `./script/run` - see AGENTS.md for more details on how to get set up. --> - [x] I have manually tested my changes locally with `./script/run` ## Agent Mode - [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode
1 parent fe8138b commit c1986e5

2 files changed

Lines changed: 76 additions & 32 deletions

File tree

crates/warpui_core/src/runtime/mod.rs

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,17 @@ impl<T: TuiView, R: TuiTerminal> TuiScreen<T, R> {
264264
}
265265
}
266266

267+
/// Crossterm treats the first primary-device-attributes response it reads as a
268+
/// definitive negative result. Retry that result once because an older queued
269+
/// response can precede the keyboard-flags response from the current query.
270+
fn probe_keyboard_enhancement_support(mut probe: impl FnMut() -> io::Result<bool>) -> bool {
271+
match probe() {
272+
Ok(true) => true,
273+
Ok(false) => matches!(probe(), Ok(true)),
274+
Err(_) => false,
275+
}
276+
}
277+
267278
/// A **development/test harness** that drives a single [`TuiView`] window with a
268279
/// *blocking* loop ([`run_until`](Self::run_until)): it redraws when dirty and
269280
/// polls the terminal for input. It backs the interactive `tui_*` examples and
@@ -464,7 +475,7 @@ impl TuiTerminalGuard {
464475
/// when the guard is dropped.
465476
pub fn enter(report_modifier_key_lifecycle: bool) -> io::Result<Self> {
466477
let keyboard_enhancement_supported =
467-
matches!(terminal::supports_keyboard_enhancement(), Ok(true));
478+
probe_keyboard_enhancement_support(terminal::supports_keyboard_enhancement);
468479
Ok(Self {
469480
_guard: RawModeGuard::enter(CrosstermModeControl {
470481
keyboard_enhancement_supported,
@@ -840,16 +851,13 @@ fn enter_terminal_screen(
840851
Hide
841852
)?;
842853

843-
// Opt into the Kitty keyboard protocol so protocol-aware terminals (Ghostty,
844-
// kitty, foot, WezTerm) report modified keys distinctly. This only affects
845-
// the TUI's own host terminal — the GUI never enters raw mode / the alt
846-
// screen and never runs this. The capability query happens before the input
847-
// reader starts because crossterm's query cannot run concurrently with
848-
// event polling.
849-
if keyboard_enhancement_supported {
850-
let flags = keyboard_enhancement_flags(report_modifier_key_lifecycle);
851-
let _ = execute!(out, PushKeyboardEnhancementFlags(flags));
852-
}
854+
// Always request the backwards-compatible baseline so modified keys remain
855+
// distinct even if capability detection produced a false negative.
856+
// Alternate/all-key reporting is more invasive and remains restricted to
857+
// confirmed terminals when modifier lifecycle events are required.
858+
let flags =
859+
keyboard_enhancement_flags(keyboard_enhancement_supported && report_modifier_key_lifecycle);
860+
let _ = execute!(out, PushKeyboardEnhancementFlags(flags));
853861
Ok(())
854862
}
855863

@@ -878,13 +886,8 @@ fn set_terminal_keyboard_enhancement_flags(
878886
out.flush()
879887
}
880888

881-
fn leave_terminal_screen(
882-
out: &mut impl Write,
883-
keyboard_enhancement_supported: bool,
884-
) -> io::Result<()> {
885-
if keyboard_enhancement_supported {
886-
let _ = execute!(out, PopKeyboardEnhancementFlags);
887-
}
889+
fn leave_terminal_screen(out: &mut impl Write) -> io::Result<()> {
890+
let _ = execute!(out, PopKeyboardEnhancementFlags);
888891
execute!(
889892
out,
890893
Show,
@@ -904,7 +907,7 @@ impl TerminalModeControl for CrosstermModeControl {
904907
self.keyboard_enhancement_supported,
905908
self.report_modifier_key_lifecycle,
906909
) {
907-
let _ = leave_terminal_screen(&mut out, self.keyboard_enhancement_supported);
910+
let _ = leave_terminal_screen(&mut out);
908911
let _ = terminal::disable_raw_mode();
909912
return Err(error);
910913
}
@@ -913,7 +916,7 @@ impl TerminalModeControl for CrosstermModeControl {
913916

914917
fn leave(&mut self) {
915918
let mut out = stdout();
916-
let _ = leave_terminal_screen(&mut out, self.keyboard_enhancement_supported);
919+
let _ = leave_terminal_screen(&mut out);
917920
let _ = terminal::disable_raw_mode();
918921
}
919922
}

crates/warpui_core/src/runtime/mod_tests.rs

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -700,7 +700,7 @@ fn terminal_screen_lifecycle_toggles_bracketed_paste() {
700700
);
701701

702702
let mut leave_output = Vec::new();
703-
leave_terminal_screen(&mut leave_output, true).unwrap();
703+
leave_terminal_screen(&mut leave_output).unwrap();
704704
assert!(
705705
leave_output
706706
.windows(b"\x1b[?2004l".len())
@@ -721,7 +721,7 @@ fn terminal_screen_lifecycle_toggles_focus_reporting() {
721721
);
722722

723723
let mut leave_output = Vec::new();
724-
leave_terminal_screen(&mut leave_output, true).unwrap();
724+
leave_terminal_screen(&mut leave_output).unwrap();
725725
assert!(
726726
leave_output
727727
.windows(b"\x1b[?1004l".len())
@@ -771,7 +771,7 @@ fn terminal_screen_lifecycle_toggles_keyboard_enhancement() {
771771
enter_terminal_screen(&mut enter_output, true, true).unwrap();
772772

773773
let mut leave_output = Vec::new();
774-
leave_terminal_screen(&mut leave_output, true).unwrap();
774+
leave_terminal_screen(&mut leave_output).unwrap();
775775

776776
#[cfg(not(windows))]
777777
{
@@ -824,23 +824,64 @@ fn terminal_screen_lifecycle_reconfigures_modifier_reporting() {
824824
}
825825

826826
#[test]
827-
fn terminal_screen_lifecycle_skips_unsupported_keyboard_enhancement() {
827+
fn terminal_screen_lifecycle_uses_baseline_keyboard_enhancement_when_unconfirmed() {
828828
let mut enter_output = Vec::new();
829829
enter_terminal_screen(&mut enter_output, false, true).unwrap();
830-
assert!(
831-
!enter_output
832-
.windows(b"\x1b[>15u".len())
833-
.any(|window| window == b"\x1b[>15u")
834-
);
830+
831+
#[cfg(not(windows))]
832+
{
833+
assert!(
834+
enter_output
835+
.windows(b"\x1b[>3u".len())
836+
.any(|window| window == b"\x1b[>3u"),
837+
"unconfirmed terminals should still receive safe baseline keyboard enhancements"
838+
);
839+
assert!(
840+
!enter_output
841+
.windows(b"\x1b[>15u".len())
842+
.any(|window| window == b"\x1b[>15u"),
843+
"unconfirmed terminals should not receive all-key reporting"
844+
);
845+
}
835846

836847
let mut leave_output = Vec::new();
837-
leave_terminal_screen(&mut leave_output, false).unwrap();
848+
leave_terminal_screen(&mut leave_output).unwrap();
849+
#[cfg(not(windows))]
838850
assert!(
839-
!leave_output
851+
leave_output
840852
.windows(b"\x1b[<1u".len())
841-
.any(|window| window == b"\x1b[<1u")
853+
.any(|window| window == b"\x1b[<1u"),
854+
"leaving should pop the baseline keyboard enhancement request"
842855
);
843856
}
857+
858+
#[test]
859+
fn keyboard_enhancement_probe_retries_a_negative_result_once() {
860+
let mut results = VecDeque::from([Ok(false), Ok(true)]);
861+
assert!(probe_keyboard_enhancement_support(|| {
862+
results.pop_front().expect("probe should run at most twice")
863+
}));
864+
assert!(results.is_empty());
865+
}
866+
867+
#[test]
868+
fn keyboard_enhancement_probe_does_not_retry_success_or_error() {
869+
let mut successful_results = VecDeque::from([Ok(true), Ok(false)]);
870+
assert!(probe_keyboard_enhancement_support(|| {
871+
successful_results
872+
.pop_front()
873+
.expect("successful probe should run once")
874+
}));
875+
assert_eq!(successful_results.len(), 1);
876+
877+
let mut failed_results = VecDeque::from([Err(io::Error::other("probe failed")), Ok(true)]);
878+
assert!(!probe_keyboard_enhancement_support(|| {
879+
failed_results
880+
.pop_front()
881+
.expect("failed probe should run once")
882+
}));
883+
assert_eq!(failed_results.len(), 1);
884+
}
844885
#[test]
845886
fn raw_mode_guard_restores_on_drop() {
846887
let log = Rc::new(RefCell::new(Vec::new()));

0 commit comments

Comments
 (0)