From 9038e70bcb0c98e84bd0b9dcd1707dee377216b5 Mon Sep 17 00:00:00 2001 From: v_wanghuanping01 Date: Wed, 13 May 2026 13:03:54 +0800 Subject: [PATCH] fix(tui): debounce FocusGained recovery to prevent flicker loop on Tabby MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/tui/src/tui/ui.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 70a8f00e..836f9a9f 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -745,6 +745,14 @@ async fn run_event_loop( let mut terminal_paused_at: Option = 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; }