@@ -101,8 +101,7 @@ use crate::terminal_theme::{
101101
102102const ESC : u8 = 0x1b ;
103103pub ( crate ) const RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS : i32 = 10 ;
104- #[ cfg( any( not( windows) , test) ) ]
105- pub ( crate ) const MOUSE_ACTIVE_LONE_ESCAPE_FLUSH_TIMEOUT_MS : i32 = 150 ;
104+ pub ( crate ) const MOUSE_ACTIVE_ESCAPE_SEQUENCE_FLUSH_TIMEOUT_MS : i32 = 150 ;
106105pub ( crate ) const GHOSTTY_COLOR_SCHEME_DARK_REPORT : & [ u8 ] = b"\x1b [?997;1n" ;
107106pub ( crate ) const GHOSTTY_COLOR_SCHEME_LIGHT_REPORT : & [ u8 ] = b"\x1b [?997;2n" ;
108107const BRACKETED_PASTE_START : & [ u8 ] = b"\x1b [200~" ;
@@ -145,6 +144,10 @@ impl RawInputFramer {
145144 self . byte_framer . has_pending_input ( )
146145 }
147146
147+ pub ( crate ) fn has_pending_incomplete_sgr_mouse_sequence ( & self ) -> bool {
148+ self . byte_framer . has_pending_incomplete_sgr_mouse_sequence ( )
149+ }
150+
148151 #[ cfg( any( windows, test) ) ]
149152 pub ( crate ) fn has_pending_bracketed_paste ( & self ) -> bool {
150153 self . byte_framer . has_pending_bracketed_paste ( )
@@ -213,6 +216,10 @@ impl RawInputByteFramer {
213216 self . buffer . as_slice ( ) == [ ESC ]
214217 }
215218
219+ pub ( crate ) fn has_pending_incomplete_sgr_mouse_sequence ( & self ) -> bool {
220+ starts_with_incomplete_sgr_mouse_sequence ( & self . buffer )
221+ }
222+
216223 #[ cfg( any( windows, test) ) ]
217224 pub ( crate ) fn has_pending_bracketed_paste ( & self ) -> bool {
218225 self . buffer . starts_with ( BRACKETED_PASTE_START )
@@ -223,6 +230,13 @@ impl RawInputByteFramer {
223230 let mut chunks = self . drain_available_chunks ( ) ;
224231
225232 if let Some ( family) = self . discard_until {
233+ if family == ControlStringFamily :: OrphanedSgrMouseTail {
234+ self . buffer . clear ( ) ;
235+ self . discard_until = None ;
236+ self . discarded_tail_bytes = 0 ;
237+ return chunks;
238+ }
239+
226240 let keep_split_st = self . buffer . last ( ) == Some ( & ESC ) ;
227241 let keep_discarding = plausible_control_string_tail ( family, & self . buffer ) ;
228242 self . discarded_tail_bytes = self . discarded_tail_bytes . saturating_add ( self . buffer . len ( ) ) ;
@@ -256,6 +270,18 @@ impl RawInputByteFramer {
256270 return chunks;
257271 }
258272
273+ if starts_with_incomplete_sgr_mouse_sequence ( & self . buffer ) {
274+ tracing:: debug!(
275+ bytes = ?self . buffer,
276+ "discarding incomplete SGR mouse sequence after input timeout"
277+ ) ;
278+ self . discarded_tail_bytes = self . buffer . len ( ) ;
279+ self . discard_until = ( self . discarded_tail_bytes <= MAX_DISCARDED_CONTROL_TAIL_BYTES )
280+ . then_some ( ControlStringFamily :: OrphanedSgrMouseTail ) ;
281+ self . buffer . clear ( ) ;
282+ return chunks;
283+ }
284+
259285 if self . buffer . starts_with ( BRACKETED_PASTE_START )
260286 && find_subsequence ( & self . buffer , BRACKETED_PASTE_END ) . is_none ( )
261287 {
@@ -356,6 +382,18 @@ impl RawInputByteFramer {
356382 }
357383
358384 if let Some ( family) = self . discard_until {
385+ if family == ControlStringFamily :: OrphanedSgrMouseTail {
386+ if discard_orphaned_sgr_mouse_tail (
387+ & mut self . buffer ,
388+ & mut self . discarded_tail_bytes ,
389+ ) {
390+ self . discard_until = None ;
391+ self . discarded_tail_bytes = 0 ;
392+ continue ;
393+ }
394+ break ;
395+ }
396+
359397 let Some ( terminator_len) =
360398 control_string_terminator_for_family ( & self . buffer , family)
361399 else {
@@ -438,6 +476,14 @@ pub(crate) fn events_require_host_terminal_theme_query(events: &[RawInputEvent])
438476 . any ( |event| matches ! ( event, RawInputEvent :: HostColorSchemeChanged ( _) ) )
439477}
440478
479+ fn input_flush_timeout_ms ( framer : & RawInputFramer ) -> i32 {
480+ if framer. has_pending_incomplete_sgr_mouse_sequence ( ) {
481+ MOUSE_ACTIVE_ESCAPE_SEQUENCE_FLUSH_TIMEOUT_MS
482+ } else {
483+ RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS
484+ }
485+ }
486+
441487pub fn spawn_input_reader ( ) -> mpsc:: Receiver < RawInputEvent > {
442488 let ( tx, rx) = mpsc:: channel ( 256 ) ;
443489
@@ -455,7 +501,7 @@ pub fn spawn_input_reader() -> mpsc::Receiver<RawInputEvent> {
455501 Ok ( n) => {
456502 send_raw_input_events ( framer. push ( & scratch[ ..n] ) , & tx) ;
457503
458- if stdin_read_ready ( & reader, RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS ) == Some ( false ) {
504+ if stdin_read_ready ( & reader, input_flush_timeout_ms ( & framer ) ) == Some ( false ) {
459505 let had_pending = framer. has_pending_input ( ) ;
460506 let events = framer. flush_timeout ( ) ;
461507 let held_escape = had_pending && events. is_empty ( ) ;
@@ -753,6 +799,13 @@ fn complete_escape_sequence_len(buffer: &[u8]) -> Option<usize> {
753799 Some ( 1 + escaped_char_width)
754800}
755801
802+ fn starts_with_incomplete_sgr_mouse_sequence ( buffer : & [ u8 ] ) -> bool {
803+ buffer. starts_with ( b"\x1b [<" )
804+ && buffer[ 3 ..]
805+ . iter ( )
806+ . all ( |byte| byte. is_ascii_digit ( ) || * byte == b';' )
807+ }
808+
756809fn starts_with_incomplete_orphaned_sgr_mouse_tail ( buffer : & [ u8 ] ) -> bool {
757810 if buffer. len ( ) > MAX_ORPHANED_SGR_MOUSE_TAIL_BYTES {
758811 return false ;
@@ -792,10 +845,39 @@ fn discard_or_buffer_orphaned_sgr_mouse_tail(
792845 discarded_tail_bytes : & mut usize ,
793846) {
794847 if !discard_complete_orphaned_sgr_mouse_tail ( buffer) {
795- * discard_until = Some ( ControlStringFamily :: OrphanedSgrMouseTail ) ;
796- * discarded_tail_bytes = 0 ;
848+ * discarded_tail_bytes = buffer. len ( ) ;
849+ * discard_until = ( * discarded_tail_bytes <= MAX_DISCARDED_CONTROL_TAIL_BYTES )
850+ . then_some ( ControlStringFamily :: OrphanedSgrMouseTail ) ;
851+ buffer. clear ( ) ;
852+ }
853+ }
854+
855+ fn discard_orphaned_sgr_mouse_tail ( buffer : & mut Vec < u8 > , discarded_tail_bytes : & mut usize ) -> bool {
856+ let remaining = MAX_DISCARDED_CONTROL_TAIL_BYTES . saturating_sub ( * discarded_tail_bytes) ;
857+ let inspected = buffer. len ( ) . min ( remaining) ;
858+
859+ for index in 0 ..inspected {
860+ match buffer[ index] {
861+ b'0' ..=b'9' | b';' => { }
862+ b'M' | b'm' => {
863+ buffer. drain ( ..=index) ;
864+ return true ;
865+ }
866+ _ => {
867+ buffer. drain ( ..index) ;
868+ return true ;
869+ }
870+ }
871+ }
872+
873+ * discarded_tail_bytes = discarded_tail_bytes. saturating_add ( inspected) ;
874+ if buffer. len ( ) > inspected {
797875 buffer. clear ( ) ;
876+ return true ;
798877 }
878+
879+ buffer. clear ( ) ;
880+ false
799881}
800882
801883fn osc_string_terminator ( buffer : & [ u8 ] ) -> Option < usize > {
@@ -1490,6 +1572,68 @@ mod tests {
14901572 ) ) ;
14911573 }
14921574
1575+ #[ test]
1576+ fn legacy_reader_extends_only_incomplete_sgr_mouse_timeout ( ) {
1577+ let mut mouse = RawInputFramer :: default ( ) ;
1578+ assert ! ( mouse. push( b"\x1b [<3" ) . is_empty( ) ) ;
1579+ assert_eq ! (
1580+ input_flush_timeout_ms( & mouse) ,
1581+ MOUSE_ACTIVE_ESCAPE_SEQUENCE_FLUSH_TIMEOUT_MS
1582+ ) ;
1583+
1584+ let mut escape = RawInputFramer :: default ( ) ;
1585+ assert ! ( escape. push( b"\x1b " ) . is_empty( ) ) ;
1586+ assert_eq ! (
1587+ input_flush_timeout_ms( & escape) ,
1588+ RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS
1589+ ) ;
1590+ }
1591+
1592+ #[ test]
1593+ fn sgr_mouse_sequence_split_after_button_prefix_is_reassembled_before_timeout ( ) {
1594+ let mut framer = RawInputFramer :: default ( ) ;
1595+
1596+ assert ! ( framer. push( b"\x1b [<3" ) . is_empty( ) ) ;
1597+ let events = framer. push ( b"5;58;30M" ) ;
1598+
1599+ assert_eq ! ( events. len( ) , 1 ) ;
1600+ assert ! ( matches!(
1601+ events[ 0 ] ,
1602+ RawInputEvent :: Mouse ( MouseEvent {
1603+ kind: MouseEventKind :: Moved ,
1604+ column: 57 ,
1605+ row: 29 ,
1606+ ..
1607+ } )
1608+ ) ) ;
1609+ }
1610+
1611+ #[ test]
1612+ fn timed_out_split_sgr_mouse_tail_is_discarded_and_following_input_is_preserved ( ) {
1613+ let mut framer = RawInputFramer :: default ( ) ;
1614+
1615+ assert ! ( framer. push( b"\x1b [<3" ) . is_empty( ) ) ;
1616+ assert ! ( framer. flush_timeout( ) . is_empty( ) ) ;
1617+ let events = framer. push ( b"5;58;30Mx" ) ;
1618+
1619+ assert_eq ! ( events. len( ) , 1 ) ;
1620+ assert_raw_key (
1621+ events. into_iter ( ) . next ( ) . unwrap ( ) ,
1622+ KeyCode :: Char ( 'x' ) ,
1623+ KeyModifiers :: empty ( ) ,
1624+ ) ;
1625+ }
1626+
1627+ #[ test]
1628+ fn timed_out_sgr_mouse_discard_state_clears_at_quiescence ( ) {
1629+ let mut framer = RawInputByteFramer :: default ( ) ;
1630+
1631+ assert ! ( framer. push( b"\x1b [<3" ) . is_empty( ) ) ;
1632+ assert ! ( framer. flush_timeout( ) . is_empty( ) ) ;
1633+ assert ! ( framer. flush_timeout( ) . is_empty( ) ) ;
1634+ assert_eq ! ( framer. push( b"M" ) , vec![ b"M" . to_vec( ) ] ) ;
1635+ }
1636+
14931637 #[ test]
14941638 fn sgr_mouse_tail_after_lone_escape_timeout_is_discarded ( ) {
14951639 let mut framer = RawInputFramer :: default ( ) ;
0 commit comments