Skip to content

Commit cc5ce84

Browse files
committed
fix: prevent OSC response bytes leaking into input on theme reload
- fix: drain terminal event queue multiple times with short delays after OSC query. - fix: avoid OSC response bytes (e.g. rgb:...) entering search field on Ctrl+R theme reload. - refactor: extract drain logic into drain_stray_events() with configurable iterations/delay.
1 parent cdef9ad commit cc5ce84

1 file changed

Lines changed: 44 additions & 9 deletions

File tree

src/theme/terminal_query.rs

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,41 @@ use super::types::Theme;
1414
/// Increased to 250ms to allow slower terminals to respond.
1515
const OSC_QUERY_TIMEOUT_MS: u64 = 250;
1616

17+
/// Number of drain iterations to perform after OSC query.
18+
/// Multiple passes with small delays help catch delayed response bytes.
19+
const DRAIN_ITERATIONS: usize = 3;
20+
21+
/// Delay between drain iterations (milliseconds).
22+
/// Short enough to not noticeably slow down reload, but enough to catch stragglers.
23+
const DRAIN_DELAY_MS: u64 = 10;
24+
25+
/// Drain any stray events from crossterm's event queue.
26+
///
27+
/// OSC responses may arrive in chunks or with slight delays. Some terminals
28+
/// also echo the response to stdin in addition to /dev/tty. This function
29+
/// drains events multiple times with small delays to ensure all stray bytes
30+
/// are consumed before returning control to the main event loop.
31+
///
32+
/// Without this, OSC response bytes like `10;rgb:e0e0/dede/f4f4` can leak
33+
/// into the search input field when reloading theme with Ctrl+R.
34+
fn drain_stray_events() {
35+
use crossterm::event::{poll, read as crossterm_read};
36+
37+
for _ in 0..DRAIN_ITERATIONS {
38+
// Drain all immediately available events
39+
while poll(Duration::from_millis(0)).unwrap_or(false) {
40+
let _ = crossterm_read();
41+
}
42+
// Small delay to allow any in-flight bytes to arrive
43+
std::thread::sleep(Duration::from_millis(DRAIN_DELAY_MS));
44+
}
45+
46+
// Final drain after the last delay
47+
while poll(Duration::from_millis(0)).unwrap_or(false) {
48+
let _ = crossterm_read();
49+
}
50+
}
51+
1752
/// What: Query the terminal for foreground and background colors.
1853
///
1954
/// Inputs:
@@ -53,7 +88,7 @@ pub fn query_terminal_colors() -> Option<(Color, Color)> {
5388

5489
/// Perform the actual query with terminal in raw mode.
5590
fn query_with_raw_mode() -> Option<(Color, Color)> {
56-
use crossterm::event::{DisableMouseCapture, EnableMouseCapture, poll, read as crossterm_read};
91+
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
5792
use crossterm::execute;
5893
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, is_raw_mode_enabled};
5994

@@ -74,10 +109,9 @@ fn query_with_raw_mode() -> Option<(Color, Color)> {
74109
return None;
75110
}
76111

77-
// Drain any pending events before sending query
78-
while poll(Duration::from_millis(0)).unwrap_or(false) {
79-
let _ = crossterm_read();
80-
}
112+
// Drain any pending events before sending query.
113+
// This clears any events that accumulated before we started.
114+
drain_stray_events();
81115

82116
let result = (|| {
83117
let mut stdout = std::io::stdout();
@@ -114,10 +148,11 @@ fn query_with_raw_mode() -> Option<(Color, Color)> {
114148
Some((fg?, bg?))
115149
})();
116150

117-
// Drain any events that arrived during the query
118-
while poll(Duration::from_millis(0)).unwrap_or(false) {
119-
let _ = crossterm_read();
120-
}
151+
// Drain any events that arrived during the query.
152+
// The OSC response may arrive in chunks or be delayed, so we drain multiple times
153+
// with small delays to ensure all stray bytes are consumed before returning control
154+
// to the main event loop. This prevents OSC responses from leaking into input fields.
155+
drain_stray_events();
121156

122157
// Restore terminal mode if we enabled it
123158
if !was_raw_mode {

0 commit comments

Comments
 (0)