fix(tui): debounce FocusGained recovery to prevent flicker loop on Tabby
Some terminal emulators (observed on Tabby / xterm.js) re-trigger a
FocusGained event when the application sends EnableFocusChange
(\e[?1004h) inside recover_terminal_modes(). This creates a tight
event→redraw→event loop that manifests as continuous screen flicker
whenever the terminal window has focus.
Root cause chain:
FocusGained → recover_terminal_modes() → EnableFocusChange
↑ ↓
└── Tabby retriggers FocusGained ←─────────────┘
Fix: add a 200 ms debounce window on FocusGained-triggered mode
recovery. If a FocusGained arrives within 200 ms of the last
recovery call, skip recover_terminal_modes() (avoiding the
EnableFocusChange that feeds the loop) but still mark a repaint.
The debounce constant FOCUS_RECOVERY_DEBOUNCE uses a 200 ms window,
which is short enough that legitimate app-switch focus events (>200 ms
apart) still get full mode recovery, while spurious back-to-back
events from terminal quirks are suppressed.
Fixes flickering reported when running deepseek-tui inside Tabby on
macOS (deepseek-tui 0.8.32).
--- Fixed with DeepSeek V4 Pro ---
Signed-off-by: DeepSeek V4 Pro <via deepseek-tui>
This commit is contained in:
@@ -745,6 +745,14 @@ async fn run_event_loop(
|
||||
let mut terminal_paused_at: Option<Instant> = None;
|
||||
let mut force_terminal_repaint = false;
|
||||
let mut draws_since_last_full_repaint: u64 = 0;
|
||||
// FocusGained debounce: some terminal emulators (e.g. Tabby) re-trigger
|
||||
// FocusGained when we re-arm focus-change reporting inside
|
||||
// recover_terminal_modes, creating a tight repaint loop. Skip
|
||||
// mode recovery (but still mark a repaint) within the debounce window.
|
||||
const FOCUS_RECOVERY_DEBOUNCE: Duration = Duration::from_millis(200);
|
||||
let mut last_focus_recovery = Instant::now()
|
||||
.checked_sub(Duration::from_secs(60))
|
||||
.unwrap_or_else(Instant::now);
|
||||
|
||||
loop {
|
||||
if !drain_web_config_events(&mut web_config_session, app, config, &engine_handle).await {
|
||||
@@ -1954,11 +1962,15 @@ async fn run_event_loop(
|
||||
// bracketed-paste modes — recover_terminal_modes() is the
|
||||
// canonical place those flags live.
|
||||
if terminal_event_needs_viewport_recapture(&evt) {
|
||||
recover_terminal_modes(
|
||||
terminal.backend_mut(),
|
||||
app.use_mouse_capture,
|
||||
app.use_bracketed_paste,
|
||||
);
|
||||
let now = Instant::now();
|
||||
if now.duration_since(last_focus_recovery) >= FOCUS_RECOVERY_DEBOUNCE {
|
||||
recover_terminal_modes(
|
||||
terminal.backend_mut(),
|
||||
app.use_mouse_capture,
|
||||
app.use_bracketed_paste,
|
||||
);
|
||||
last_focus_recovery = now;
|
||||
}
|
||||
force_terminal_repaint = true;
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user