fix(settings): auto low-motion in Termius + every SSH session (#1433)

Closes #1433. Harvested from PR #1479 by @CrepuscularIRIS / autoghclaw,
with two changes from the original PR:

  * `is_some_and` instead of `.map_or(false, |v| !v.is_empty())` —
    the latter trips `clippy::unnecessary_map_or` on Rust 1.94+
    under `-D warnings`, which is what blocked the PR's Lint check
    in CI. is_some_and reads cleaner and ships the same behavior.
  * `non_vscode_term_program_does_not_force_low_motion` now clears
    SSH_CLIENT / SSH_TTY before iterating its negative-case
    fixtures so the suite still passes when run from a developer's
    actual SSH session.

Detection logic mirrors the existing VS Code (#1356) and Ghostty
(#1445) overrides: any of TERM_PROGRAM=Termius, SSH_CLIENT set, or
SSH_TTY set unconditionally flips low_motion = true and
fancy_animations = false. The 120 FPS cursor-repositioning that
caused the cursor to cycle through input boxes over SSH is dropped
to the 30 FPS cap the typewriter path already uses.

Two new tests: termius_term_program_forces_low_motion_on and
ssh_session_forces_low_motion_on. Both serialise through the
existing term_program_test_guard / crate-wide test lock to avoid
racing concurrent env-var-mutating tests in the suite.
This commit is contained in:
Hunter Bown
2026-05-11 22:13:53 -05:00
parent 1f7cd9cc2f
commit cc71ec191f
2 changed files with 122 additions and 0 deletions
+10
View File
@@ -9,6 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- **Termius and every SSH session auto-enable low-motion**
(#1433, harvested from PR #1479 by **@CrepuscularIRIS / autoghclaw**).
Termius desktop sets `TERM_PROGRAM=Termius`; sshd exports
`SSH_CLIENT` for every TCP session and `SSH_TTY` for interactive
PTY logins. Any of those signals now flips `low_motion` and
`fancy_animations` like the existing VS Code / Ghostty path, so
the 120 FPS cursor-repositioning that races the SSH round-trip
no longer flickers a remote TUI. Disk-loaded `fancy_animations =
true` is unconditionally overridden under these signals,
matching the existing env-precedence contract.
- **DEC 2026 synchronized output is auto-disabled on Ptyxis** (the new
default terminal on Ubuntu 26.04 and an increasingly common Linux
TUI host). Ptyxis 50.x ships on VTE 0.84.x, which parses the
+112
View File
@@ -370,6 +370,25 @@ impl Settings {
self.fancy_animations = false;
}
// Termius (TERM_PROGRAM=Termius) and SSH sessions exhibit the
// same 120-FPS flicker class as VS Code — the SSH round-trip
// races ahead of what the remote renderer can flush, so rapid
// cursor-positioning sequences cycle through input boxes.
// Drop both to the 30 FPS low-motion cap. Harvested from
// PR #1479 by @CrepuscularIRIS / autoghclaw (closes #1433).
//
// SSH_CLIENT is exported by sshd for every TCP SSH session;
// SSH_TTY is exported only for interactive PTY logins, so we
// check both so non-PTY-allocating tools (rsync wrappers, etc.)
// still pick this up if they end up running the TUI.
let term_is_termius = std::env::var("TERM_PROGRAM").as_deref() == Ok("Termius");
let in_ssh_session = std::env::var_os("SSH_CLIENT").is_some_and(|v| !v.is_empty())
|| std::env::var_os("SSH_TTY").is_some_and(|v| !v.is_empty());
if term_is_termius || in_ssh_session {
self.low_motion = true;
self.fancy_animations = false;
}
// Ptyxis 50.x (the new default terminal on Ubuntu 26.04) ships with
// VTE 0.84.x which mishandles DEC mode 2026 synchronized output: the
// begin/end pair is parsed but each wrapped frame still triggers a
@@ -1138,6 +1157,16 @@ mod tests {
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_ssh_client = std::env::var_os("SSH_CLIENT");
let prev_ssh_tty = std::env::var_os("SSH_TTY");
// SAFETY: serialised by the guard. Clear SSH_* so a real
// SSH session running the test suite doesn't make this
// assertion trivially fail — the SSH path is exercised
// separately by `ssh_session_forces_low_motion_on`.
unsafe {
std::env::remove_var("SSH_CLIENT");
std::env::remove_var("SSH_TTY");
}
for program in ["iTerm.app", "Apple_Terminal", "WezTerm", "xterm-256color"] {
// SAFETY: serialised by the guard.
unsafe {
@@ -1156,6 +1185,89 @@ mod tests {
Some(v) => std::env::set_var("TERM_PROGRAM", v),
None => std::env::remove_var("TERM_PROGRAM"),
}
if let Some(v) = prev_ssh_client {
std::env::set_var("SSH_CLIENT", v);
}
if let Some(v) = prev_ssh_tty {
std::env::set_var("SSH_TTY", v);
}
}
}
#[test]
fn termius_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", "Termius");
}
let mut settings = Settings::default();
assert!(!settings.low_motion, "default is animated");
settings.apply_env_overrides();
assert!(
settings.low_motion,
"TERM_PROGRAM=Termius must enable low_motion to prevent flickering (#1433)"
);
assert!(
!settings.fancy_animations,
"TERM_PROGRAM=Termius 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 ssh_session_forces_low_motion_on() {
let _g = term_program_test_guard();
let prev_client = std::env::var_os("SSH_CLIENT");
let prev_tty = std::env::var_os("SSH_TTY");
let prev_term_program = std::env::var_os("TERM_PROGRAM");
for (var, val) in [
("SSH_CLIENT", "192.168.1.100 50000 22"),
("SSH_TTY", "/dev/pts/0"),
] {
// SAFETY: serialised by the guard.
unsafe {
std::env::remove_var("SSH_CLIENT");
std::env::remove_var("SSH_TTY");
// Clear TERM_PROGRAM so the test isolates the SSH signal
// — otherwise a leaked `TERM_PROGRAM=vscode` from a
// concurrent test would already have forced low_motion
// and the SSH-only assertion below would be a tautology.
std::env::remove_var("TERM_PROGRAM");
std::env::set_var(var, val);
}
let mut s = Settings::default();
s.apply_env_overrides();
assert!(
s.low_motion,
"{var}={val:?} must enable low_motion to prevent flickering in SSH sessions (#1433)"
);
assert!(
!s.fancy_animations,
"{var}={val:?} must disable fancy_animations in SSH sessions (#1433)"
);
}
// SAFETY: cleanup under the guard.
unsafe {
std::env::remove_var("SSH_CLIENT");
std::env::remove_var("SSH_TTY");
if let Some(v) = prev_client {
std::env::set_var("SSH_CLIENT", v);
}
if let Some(v) = prev_tty {
std::env::set_var("SSH_TTY", v);
}
match prev_term_program {
Some(v) => std::env::set_var("TERM_PROGRAM", v),
None => std::env::remove_var("TERM_PROGRAM"),
}
}
}