fix(tui): calm legacy Windows console rendering (#1655)
This commit is contained in:
+7
-1
@@ -43,6 +43,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
`composer_arrows_scroll` now defaults on for Windows terminals even when
|
||||
mouse capture is enabled, so wheel events that arrive as arrow keys scroll the
|
||||
transcript instead of cycling composer history (#1578).
|
||||
- **Plain Windows PowerShell / ConHost uses calmer rendering.** Unmarked
|
||||
legacy Windows console hosts now automatically enable low-motion rendering,
|
||||
disable fancy animations, and resolve `synchronized_output = "auto"` to off
|
||||
so streaming redraws do not overlap or visibly flicker (#1590).
|
||||
|
||||
### Thanks
|
||||
|
||||
@@ -53,7 +57,9 @@ switching fixes harvested from #1642. Thanks to **Photo
|
||||
([@eng2007](https://github.com/eng2007))** for the provider-aware `/model`
|
||||
picker catalog work harvested from #1201. Thanks to
|
||||
**[@kunpeng-ai-lab](https://github.com/kunpeng-ai-lab)** for the Windows
|
||||
composer scroll fix harvested from #1578.
|
||||
composer scroll fix harvested from #1578, and **WuMing
|
||||
([@asdfg314284230](https://github.com/asdfg314284230))** for the Windows
|
||||
PowerShell flicker fix harvested from #1591.
|
||||
|
||||
## [0.8.37] - 2026-05-14
|
||||
|
||||
|
||||
@@ -43,6 +43,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
`composer_arrows_scroll` now defaults on for Windows terminals even when
|
||||
mouse capture is enabled, so wheel events that arrive as arrow keys scroll the
|
||||
transcript instead of cycling composer history (#1578).
|
||||
- **Plain Windows PowerShell / ConHost uses calmer rendering.** Unmarked
|
||||
legacy Windows console hosts now automatically enable low-motion rendering,
|
||||
disable fancy animations, and resolve `synchronized_output = "auto"` to off
|
||||
so streaming redraws do not overlap or visibly flicker (#1590).
|
||||
|
||||
### Thanks
|
||||
|
||||
@@ -53,7 +57,9 @@ switching fixes harvested from #1642. Thanks to **Photo
|
||||
([@eng2007](https://github.com/eng2007))** for the provider-aware `/model`
|
||||
picker catalog work harvested from #1201. Thanks to
|
||||
**[@kunpeng-ai-lab](https://github.com/kunpeng-ai-lab)** for the Windows
|
||||
composer scroll fix harvested from #1578.
|
||||
composer scroll fix harvested from #1578, and **WuMing
|
||||
([@asdfg314284230](https://github.com/asdfg314284230))** for the Windows
|
||||
PowerShell flicker fix harvested from #1591.
|
||||
|
||||
## [0.8.37] - 2026-05-14
|
||||
|
||||
|
||||
@@ -2398,6 +2398,13 @@ async fn run_doctor(config: &Config, workspace: &Path, config_path_override: Opt
|
||||
);
|
||||
any_quirk = true;
|
||||
}
|
||||
if crate::settings::detected_legacy_windows_console_host() {
|
||||
println!(
|
||||
" {} legacy Windows console host → low_motion + fancy_animations=false + synchronized_output=off (auto)",
|
||||
"•".truecolor(sky_r, sky_g, sky_b)
|
||||
);
|
||||
any_quirk = true;
|
||||
}
|
||||
if !any_quirk {
|
||||
println!(
|
||||
" {} no env-driven terminal-quirk overrides active",
|
||||
|
||||
@@ -411,6 +411,18 @@ impl Settings {
|
||||
self.fancy_animations = false;
|
||||
}
|
||||
|
||||
// Plain Windows PowerShell / cmd.exe under legacy ConHost exposes none
|
||||
// of the modern terminal markers below. Keep rendering calmer there:
|
||||
// lower the motion rate, disable animated chrome, and avoid DEC 2026
|
||||
// synchronized-output wrapping unless the user explicitly forced it on.
|
||||
if detected_legacy_windows_console_host() {
|
||||
self.low_motion = true;
|
||||
self.fancy_animations = false;
|
||||
if self.synchronized_output.eq_ignore_ascii_case("auto") {
|
||||
self.synchronized_output = "off".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -907,6 +919,31 @@ pub fn detected_ptyxis_terminal() -> bool {
|
||||
matches!(std::env::var("PTYXIS_VERSION"), Ok(v) if !v.trim().is_empty())
|
||||
}
|
||||
|
||||
/// Returns `true` for the unmarked Windows console-host path used by plain
|
||||
/// PowerShell / cmd.exe. Modern Windows terminals set at least one marker that
|
||||
/// lets us keep the richer rendering path.
|
||||
pub fn detected_legacy_windows_console_host() -> bool {
|
||||
cfg!(windows)
|
||||
&& legacy_windows_console_host_env([
|
||||
std::env::var_os("WT_SESSION").as_deref(),
|
||||
std::env::var_os("ConEmuPID").as_deref(),
|
||||
std::env::var_os("TERM_PROGRAM").as_deref(),
|
||||
std::env::var_os("WEZTERM_EXECUTABLE").as_deref(),
|
||||
std::env::var_os("WEZTERM_PANE").as_deref(),
|
||||
std::env::var_os("ALACRITTY_WINDOW_ID").as_deref(),
|
||||
std::env::var_os("ANSICON").as_deref(),
|
||||
std::env::var_os("TERM").as_deref(),
|
||||
])
|
||||
}
|
||||
|
||||
fn legacy_windows_console_host_env(markers: [Option<&std::ffi::OsStr>; 8]) -> bool {
|
||||
fn has_value(value: Option<&std::ffi::OsStr>) -> bool {
|
||||
value.is_some_and(|v| !v.is_empty())
|
||||
}
|
||||
|
||||
markers.into_iter().all(|value| !has_value(value))
|
||||
}
|
||||
|
||||
fn normalize_optional_background_color(value: Option<&str>) -> Option<String> {
|
||||
value.and_then(|raw| normalize_background_color_setting(raw).ok().flatten())
|
||||
}
|
||||
@@ -1200,6 +1237,14 @@ mod tests {
|
||||
#[test]
|
||||
fn no_animations_env_recognises_truthy_spellings_only() {
|
||||
let _g = no_animations_test_guard();
|
||||
let prev_wt_session = std::env::var_os("WT_SESSION");
|
||||
// The test is about NO_ANIMATIONS only. On Windows CI, an unmarked
|
||||
// console host now independently enables low_motion, so mark the host
|
||||
// as non-legacy while checking falsy spellings.
|
||||
#[cfg(windows)]
|
||||
unsafe {
|
||||
std::env::set_var("WT_SESSION", "test");
|
||||
}
|
||||
for truthy in ["1", "true", "True", "YES", "on"] {
|
||||
// SAFETY: serialised by the guard.
|
||||
unsafe {
|
||||
@@ -1221,6 +1266,10 @@ mod tests {
|
||||
// SAFETY: cleanup under the guard.
|
||||
unsafe {
|
||||
std::env::remove_var("NO_ANIMATIONS");
|
||||
match prev_wt_session {
|
||||
Some(v) => std::env::set_var("WT_SESSION", v),
|
||||
None => std::env::remove_var("WT_SESSION"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1417,6 +1466,93 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_windows_console_host_detects_unmarked_shell() {
|
||||
assert!(legacy_windows_console_host_env([
|
||||
None, None, None, None, None, None, None, None
|
||||
]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_windows_console_host_excludes_modern_terminal_markers() {
|
||||
use std::ffi::OsStr;
|
||||
|
||||
let marker = Some(OsStr::new("1"));
|
||||
assert!(!legacy_windows_console_host_env([
|
||||
marker, None, None, None, None, None, None, None
|
||||
]));
|
||||
assert!(!legacy_windows_console_host_env([
|
||||
None, marker, None, None, None, None, None, None
|
||||
]));
|
||||
assert!(!legacy_windows_console_host_env([
|
||||
None, None, marker, None, None, None, None, None
|
||||
]));
|
||||
assert!(!legacy_windows_console_host_env([
|
||||
None, None, None, marker, None, None, None, None
|
||||
]));
|
||||
assert!(!legacy_windows_console_host_env([
|
||||
None, None, None, None, marker, None, None, None
|
||||
]));
|
||||
assert!(!legacy_windows_console_host_env([
|
||||
None, None, None, None, None, marker, None, None
|
||||
]));
|
||||
assert!(!legacy_windows_console_host_env([
|
||||
None, None, None, None, None, None, marker, None
|
||||
]));
|
||||
assert!(!legacy_windows_console_host_env([
|
||||
None, None, None, None, None, None, None, marker
|
||||
]));
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn unmarked_windows_console_forces_calm_rendering() {
|
||||
let _g = term_program_test_guard();
|
||||
let vars = [
|
||||
"WT_SESSION",
|
||||
"ConEmuPID",
|
||||
"TERM_PROGRAM",
|
||||
"WEZTERM_EXECUTABLE",
|
||||
"WEZTERM_PANE",
|
||||
"ALACRITTY_WINDOW_ID",
|
||||
"ANSICON",
|
||||
"TERM",
|
||||
"SSH_CLIENT",
|
||||
"SSH_TTY",
|
||||
"NO_ANIMATIONS",
|
||||
"PTYXIS_VERSION",
|
||||
];
|
||||
let prev: Vec<_> = vars
|
||||
.iter()
|
||||
.map(|name| (*name, std::env::var_os(name)))
|
||||
.collect();
|
||||
|
||||
// SAFETY: serialised by the guard.
|
||||
unsafe {
|
||||
for name in vars {
|
||||
std::env::remove_var(name);
|
||||
}
|
||||
}
|
||||
|
||||
let mut settings = Settings::default();
|
||||
assert!(!settings.low_motion, "default is animated");
|
||||
assert_eq!(settings.synchronized_output, "auto");
|
||||
settings.apply_env_overrides();
|
||||
assert!(settings.low_motion);
|
||||
assert!(!settings.fancy_animations);
|
||||
assert_eq!(settings.synchronized_output, "off");
|
||||
|
||||
// SAFETY: cleanup under the guard.
|
||||
unsafe {
|
||||
for (name, value) in prev {
|
||||
match value {
|
||||
Some(value) => std::env::set_var(name, value),
|
||||
None => std::env::remove_var(name),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_session_forces_low_motion_on() {
|
||||
let _g = term_program_test_guard();
|
||||
|
||||
@@ -327,16 +327,16 @@ pub async fn run_tui(config: &Config, options: TuiOptions) -> Result<()> {
|
||||
let backend = ColorCompatBackend::new(stdout, color_depth, palette_mode);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
// At this point Settings hasn't loaded yet, so we can't read the
|
||||
// user's `synchronized_output` knob. Use the same env-based Ptyxis
|
||||
// detection that `Settings::apply_env_overrides` uses, so the
|
||||
// user's `synchronized_output` knob. Use the same env-based terminal
|
||||
// quirk detection that `Settings::apply_env_overrides` uses, so the
|
||||
// startup viewport reset matches what every later draw will do on
|
||||
// this terminal. A user who has explicitly set
|
||||
// `synchronized_output = "on"` to override Ptyxis detection will
|
||||
// get sync wrap from the main draw loop onward; the one-time
|
||||
// startup viewport reset stays opt-out for them, which is the safe
|
||||
// default because the cost is at most brief tearing on the first
|
||||
// frame.
|
||||
let sync_output_at_init = !crate::settings::detected_ptyxis_terminal();
|
||||
// flicker-sensitive hosts. A user who has explicitly set
|
||||
// `synchronized_output = "on"` to override detection will get sync wrap
|
||||
// from the main draw loop onward; the one-time startup viewport reset
|
||||
// stays opt-out for them, which is the safe default because the cost is
|
||||
// at most brief tearing on the first frame.
|
||||
let sync_output_at_init = !crate::settings::detected_ptyxis_terminal()
|
||||
&& !crate::settings::detected_legacy_windows_console_host();
|
||||
reset_terminal_viewport(&mut terminal, sync_output_at_init)?;
|
||||
let event_broker = EventBroker::new();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user