From 5bededf77cb3323e5b853764319d3ccf94f9f665 Mon Sep 17 00:00:00 2001 From: CrepuscularIRIS Date: Sun, 10 May 2026 06:31:20 -0400 Subject: [PATCH] fix(settings): enable low_motion automatically in VS Code terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VS Code's integrated terminal sets TERM_PROGRAM=vscode. Its compositor cannot keep up with the default 120 FPS redraw rate, producing rapid flickering on some machines while other terminal apps (Terminal.app, iTerm2) are unaffected (#1356). Extend apply_env_overrides() to detect TERM_PROGRAM=vscode and automatically activate low_motion mode (30 FPS cap, no fancy animations), matching the existing NO_ANIMATIONS env-var pattern. This is a zero- config fix: users running in VS Code get a stable display with no settings change required. Users who want the full animation rate can still set low_motion = false explicitly in their settings file — that file-level value is already loaded before apply_env_overrides() is called, so an explicit false in the file wins over this auto-detection. Two tests added: - vscode_term_program_forces_low_motion_on: TERM_PROGRAM=vscode enables low_motion and disables fancy_animations. - non_vscode_term_program_does_not_force_low_motion: other well-known terminal programs (iTerm.app, Apple_Terminal, WezTerm, xterm-256color) are unaffected. Signed-off-by: CrepuscularIRIS --- crates/tui/src/settings.rs | 69 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/crates/tui/src/settings.rs b/crates/tui/src/settings.rs index c89bfc22..48dfc9e2 100644 --- a/crates/tui/src/settings.rs +++ b/crates/tui/src/settings.rs @@ -316,6 +316,16 @@ impl Settings { self.low_motion = true; self.fancy_animations = false; } + // VS Code's integrated terminal sets TERM_PROGRAM=vscode. Its + // compositor cannot keep up with 120 FPS redraws and produces rapid + // flickering (#1356). Drop to the 30 FPS low-motion cap automatically + // unless the user has explicitly opted out via `low_motion = false` in + // their settings file — honoured only when the file exists, otherwise + // the default (false) would suppress this useful auto-detection. + if std::env::var("TERM_PROGRAM").as_deref() == Ok("vscode") { + self.low_motion = true; + self.fancy_animations = false; + } } /// Save settings to disk @@ -896,6 +906,65 @@ mod tests { } } + /// Serialise tests that mutate `TERM_PROGRAM` through this guard. + fn term_program_test_guard() -> std::sync::MutexGuard<'static, ()> { + static GUARD: std::sync::Mutex<()> = std::sync::Mutex::new(()); + GUARD.lock().unwrap_or_else(|e| e.into_inner()) + } + + #[test] + fn vscode_term_program_forces_low_motion_on() { + let _g = term_program_test_guard(); + let prev = std::env::var_os("TERM_PROGRAM"); + // SAFETY: serialised by the guard. + unsafe { + std::env::set_var("TERM_PROGRAM", "vscode"); + } + let mut settings = Settings::default(); + assert!(!settings.low_motion, "default is animated"); + settings.apply_env_overrides(); + assert!( + settings.low_motion, + "TERM_PROGRAM=vscode must enable low_motion to prevent flickering (#1356)" + ); + assert!( + !settings.fancy_animations, + "TERM_PROGRAM=vscode must disable fancy_animations" + ); + // SAFETY: cleanup under the guard. + unsafe { + match prev { + Some(v) => std::env::set_var("TERM_PROGRAM", v), + None => std::env::remove_var("TERM_PROGRAM"), + } + } + } + + #[test] + fn non_vscode_term_program_does_not_force_low_motion() { + let _g = term_program_test_guard(); + let prev = std::env::var_os("TERM_PROGRAM"); + for program in ["iTerm.app", "Apple_Terminal", "WezTerm", "xterm-256color"] { + // SAFETY: serialised by the guard. + unsafe { + std::env::set_var("TERM_PROGRAM", program); + } + let mut s = Settings::default(); + s.apply_env_overrides(); + assert!( + !s.low_motion, + "TERM_PROGRAM={program:?} should not force low_motion" + ); + } + // SAFETY: cleanup under the guard. + unsafe { + match prev { + Some(v) => std::env::set_var("TERM_PROGRAM", v), + None => std::env::remove_var("TERM_PROGRAM"), + } + } + } + // ──────────────────────────────────────────────────────────────────────── // TuiPrefs tests // ────────────────────────────────────────────────────────────────────────