fix(tui): keep Ghostty motion override live

This commit is contained in:
CodeWhale Agent
2026-06-12 13:27:29 -07:00
parent a0724bc5af
commit d921b5670d
2 changed files with 102 additions and 11 deletions
@@ -658,6 +658,7 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) ->
if let Err(e) = settings.set(&key, value) {
return CommandResult::error(format!("{e}"));
}
settings.apply_env_overrides();
let mut action = None;
match key.as_str() {
@@ -835,6 +836,8 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) ->
},
),
"composer_vim_mode" | "vim_mode" | "vim" => settings.composer_vim_mode.clone(),
"low_motion" | "motion" => settings.low_motion.to_string(),
"fancy_animations" | "fancy" | "animations" => settings.fancy_animations.to_string(),
_ => value.to_string(),
};
@@ -1394,6 +1397,44 @@ mod tests {
);
}
#[test]
fn config_fancy_animations_obeys_ghostty_override() {
let temp_root = env::temp_dir().join(format!(
"codewhale-tui-ghostty-fancy-config-test-{}",
std::process::id()
));
fs::create_dir_all(&temp_root).unwrap();
let _guard = EnvGuard::new(&temp_root);
let prev_term_program = env::var_os("TERM_PROGRAM");
// Safety: test-only environment mutation guarded by EnvGuard's lock.
unsafe {
env::set_var("TERM_PROGRAM", "Ghostty");
}
let mut app = create_test_app();
assert!(!app.fancy_animations);
let result = set_config_value(&mut app, "fancy_animations", "true", false);
assert!(!result.is_error);
assert!(
!app.fancy_animations,
"Ghostty compatibility override must keep the water strip disabled"
);
assert_eq!(
result.message.as_deref(),
Some("fancy_animations = false (session only, add --save to persist)")
);
// Safety: cleanup under EnvGuard's lock.
unsafe {
match prev_term_program {
Some(v) => env::set_var("TERM_PROGRAM", v),
None => env::remove_var("TERM_PROGRAM"),
}
}
}
#[test]
fn config_model_accepts_future_deepseek_model_id() {
let mut app = create_test_app();
+61 -11
View File
@@ -456,18 +456,23 @@ impl Settings {
self.low_motion = true;
self.fancy_animations = false;
}
// VS Code (TERM_PROGRAM=vscode, #1356), Ghostty (TERM_PROGRAM=ghostty,
// #1445), and a few VTE terminals (#1470) produce visible flicker at
// 120 FPS. Drop to the 30 FPS low-motion cap for them automatically.
// VS Code (TERM_PROGRAM=vscode, #1356), Ghostty (#1445), and a few
// VTE terminals (#1470) produce visible flicker at 120 FPS. Drop to
// the 30 FPS low-motion cap for them automatically. Ghostty may report
// either TERM_PROGRAM=Ghostty/ghostty or TERM=xterm-ghostty.
// Like NO_ANIMATIONS above, this unconditionally overrides any
// disk-loaded value — consistent precedence: env signals always win.
let term_program = std::env::var("TERM_PROGRAM")
.unwrap_or_default()
.to_ascii_lowercase();
let term = std::env::var("TERM")
.unwrap_or_default()
.to_ascii_lowercase();
let term_forces_low_motion =
matches!(term_program.as_str(), "vscode" | "ghostty") || term.contains("ghostty");
let vte_env_forces_low_motion = std::env::var_os("TILIX_ID").is_some_and(|v| !v.is_empty())
|| std::env::var_os("TERMINATOR_UUID").is_some_and(|v| !v.is_empty());
if matches!(
std::env::var("TERM_PROGRAM").as_deref(),
Ok("vscode") | Ok("ghostty")
) || vte_env_forces_low_motion
{
if term_forces_low_motion || vte_env_forces_low_motion {
self.low_motion = true;
self.fancy_animations = false;
}
@@ -1673,6 +1678,7 @@ mod tests {
let prev_tmux = std::env::var_os("TMUX");
let prev_sty = std::env::var_os("STY");
let prev_term_program = std::env::var_os("TERM_PROGRAM");
let prev_term = std::env::var_os("TERM");
let prev_ssh_client = std::env::var_os("SSH_CLIENT");
let prev_ssh_tty = std::env::var_os("SSH_TTY");
let prev_tilix_id = std::env::var_os("TILIX_ID");
@@ -1690,6 +1696,7 @@ mod tests {
std::env::remove_var("TMUX");
std::env::remove_var("STY");
std::env::remove_var("TERM_PROGRAM");
std::env::remove_var("TERM");
std::env::remove_var("SSH_CLIENT");
std::env::remove_var("SSH_TTY");
std::env::remove_var("TILIX_ID");
@@ -1736,6 +1743,10 @@ mod tests {
Some(v) => std::env::set_var("TERM_PROGRAM", v),
None => std::env::remove_var("TERM_PROGRAM"),
}
match prev_term {
Some(v) => std::env::set_var("TERM", v),
None => std::env::remove_var("TERM"),
}
match prev_ssh_client {
Some(v) => std::env::set_var("SSH_CLIENT", v),
None => std::env::remove_var("SSH_CLIENT"),
@@ -1799,18 +1810,18 @@ mod tests {
let prev = std::env::var_os("TERM_PROGRAM");
// SAFETY: serialised by the guard.
unsafe {
std::env::set_var("TERM_PROGRAM", "ghostty");
std::env::set_var("TERM_PROGRAM", "Ghostty");
}
let mut settings = Settings::default();
assert!(!settings.low_motion, "default is animated");
settings.apply_env_overrides();
assert!(
settings.low_motion,
"TERM_PROGRAM=ghostty must enable low_motion to prevent flickering (#1445)"
"TERM_PROGRAM=Ghostty must enable low_motion to prevent flickering (#1445)"
);
assert!(
!settings.fancy_animations,
"TERM_PROGRAM=ghostty must disable fancy_animations"
"TERM_PROGRAM=Ghostty must disable fancy_animations"
);
// SAFETY: cleanup under the guard.
unsafe {
@@ -1821,10 +1832,44 @@ mod tests {
}
}
#[test]
fn ghostty_term_fallback_forces_low_motion_on() {
let _g = term_program_test_guard();
let prev_program = std::env::var_os("TERM_PROGRAM");
let prev_term = std::env::var_os("TERM");
// SAFETY: serialised by the guard.
unsafe {
std::env::remove_var("TERM_PROGRAM");
std::env::set_var("TERM", "xterm-ghostty");
}
let mut settings = Settings::default();
settings.apply_env_overrides();
assert!(
settings.low_motion,
"TERM=xterm-ghostty must enable low_motion when TERM_PROGRAM is absent"
);
assert!(
!settings.fancy_animations,
"TERM=xterm-ghostty must disable fancy_animations"
);
// SAFETY: cleanup under the guard.
unsafe {
match prev_program {
Some(v) => std::env::set_var("TERM_PROGRAM", v),
None => std::env::remove_var("TERM_PROGRAM"),
}
match prev_term {
Some(v) => std::env::set_var("TERM", v),
None => std::env::remove_var("TERM"),
}
}
}
#[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");
let prev_term = std::env::var_os("TERM");
let prev_ssh_client = std::env::var_os("SSH_CLIENT");
let prev_ssh_tty = std::env::var_os("SSH_TTY");
let prev_tilix_id = std::env::var_os("TILIX_ID");
@@ -1838,6 +1883,7 @@ mod tests {
unsafe {
std::env::remove_var("SSH_CLIENT");
std::env::remove_var("SSH_TTY");
std::env::remove_var("TERM");
std::env::remove_var("TILIX_ID");
std::env::remove_var("TERMINATOR_UUID");
std::env::remove_var("TMUX");
@@ -1861,6 +1907,10 @@ mod tests {
Some(v) => std::env::set_var("TERM_PROGRAM", v),
None => std::env::remove_var("TERM_PROGRAM"),
}
match prev_term {
Some(v) => std::env::set_var("TERM", v),
None => std::env::remove_var("TERM"),
}
if let Some(v) = prev_ssh_client {
std::env::set_var("SSH_CLIENT", v);
}