From 364c3fbe219b60ffb6dbd888edc2db7f14448e79 Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Wed, 20 May 2026 15:05:04 -0300 Subject: [PATCH] fix(tui): harden windows vt output --- codex-rs/Cargo.lock | 1 + codex-rs/cli/src/doctor.rs | 85 ++++++++++++ codex-rs/terminal-detection/Cargo.toml | 6 + codex-rs/terminal-detection/src/lib.rs | 122 ++++++++++++++++++ .../tui/src/chatwidget/status_surfaces.rs | 1 + codex-rs/tui/src/lib.rs | 1 + codex-rs/tui/src/raw_ansi.rs | 97 ++++++++++++++ codex-rs/tui/src/terminal_title.rs | 21 +++ codex-rs/tui/src/tui.rs | 105 +++++++++++++-- 9 files changed, 425 insertions(+), 14 deletions(-) create mode 100644 codex-rs/tui/src/raw_ansi.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 6dfc44d844..dc027cf50b 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3712,6 +3712,7 @@ version = "0.0.0" dependencies = [ "pretty_assertions", "tracing", + "windows-sys 0.52.0", ] [[package]] diff --git a/codex-rs/cli/src/doctor.rs b/codex-rs/cli/src/doctor.rs index 3f8c3ff094..47922c76d6 100644 --- a/codex-rs/cli/src/doctor.rs +++ b/codex-rs/cli/src/doctor.rs @@ -57,7 +57,9 @@ use codex_protocol::protocol::AskForApproval; use codex_terminal_detection::Multiplexer; use codex_terminal_detection::TerminalInfo; use codex_terminal_detection::TerminalName; +use codex_terminal_detection::WindowsStdoutVtState; use codex_terminal_detection::terminal_info; +use codex_terminal_detection::windows_stdout_vt_state; use codex_tui::Cli as TuiCli; use codex_utils_cli::CliConfigOverrides; use http::HeaderMap; @@ -1580,6 +1582,7 @@ struct TerminalCheckInputs { stream_supports_color: bool, terminal_size: Result<(u16, u16), String>, tmux_details: Vec, + windows_stdout_vt_state: Option, } impl TerminalCheckInputs { @@ -1604,6 +1607,7 @@ impl TerminalCheckInputs { stream_supports_color: supports_color::on(Stream::Stdout).is_some(), terminal_size, tmux_details, + windows_stdout_vt_state: cfg!(windows).then(windows_stdout_vt_state), } } @@ -1643,6 +1647,9 @@ fn terminal_check_from_inputs(inputs: TerminalCheckInputs) -> DoctorCheck { Ok((columns, rows)) => details.push(format!("terminal size: {columns}x{rows}")), Err(err) => details.push(format!("terminal size: unavailable ({err})")), } + if let Some(windows_stdout_vt_state) = inputs.windows_stdout_vt_state { + push_windows_stdout_vt_details(&mut details, windows_stdout_vt_state); + } push_terminal_env_values(&mut details, &inputs, TERMINAL_DIMENSION_ENV_VARS); details.push(format!("color output: {}", color_output_summary(&inputs))); push_terminal_env_values(&mut details, &inputs, COLOR_ENV_VARS); @@ -1711,6 +1718,26 @@ fn terminal_check_from_inputs(inputs: TerminalCheckInputs) -> DoctorCheck { check } +fn push_windows_stdout_vt_details( + details: &mut Vec, + windows_stdout_vt_state: WindowsStdoutVtState, +) { + match windows_stdout_vt_state { + WindowsStdoutVtState::Enabled { console_mode } => { + details.push(format!("Windows stdout console mode: {console_mode}")); + details.push("Windows stdout VT processing: enabled".to_string()); + } + WindowsStdoutVtState::Disabled { console_mode } => { + details.push(format!("Windows stdout console mode: {console_mode}")); + details.push("Windows stdout VT processing: disabled".to_string()); + } + WindowsStdoutVtState::Unavailable => { + details.push("Windows stdout console mode: unavailable".to_string()); + details.push("Windows stdout VT processing: unavailable".to_string()); + } + } +} + fn terminal_name(info: &TerminalInfo) -> &'static str { match info.name { TerminalName::AppleTerminal => "Apple Terminal", @@ -3724,6 +3751,7 @@ mod tests { stream_supports_color: true, terminal_size: Ok((120, 40)), tmux_details: Vec::new(), + windows_stdout_vt_state: None, } } @@ -3868,6 +3896,63 @@ mod tests { assert_eq!(check.summary, "terminal metadata was detected"); } + #[test] + fn terminal_check_reports_windows_stdout_vt_processing_when_enabled() { + let mut inputs = terminal_inputs(); + inputs.windows_stdout_vt_state = Some(WindowsStdoutVtState::Enabled { console_mode: 7 }); + + let check = terminal_check_from_inputs(inputs); + + assert!( + check + .details + .contains(&"Windows stdout console mode: 7".to_string()) + ); + assert!( + check + .details + .contains(&"Windows stdout VT processing: enabled".to_string()) + ); + } + + #[test] + fn terminal_check_reports_windows_stdout_vt_processing_when_disabled() { + let mut inputs = terminal_inputs(); + inputs.windows_stdout_vt_state = Some(WindowsStdoutVtState::Disabled { console_mode: 3 }); + + let check = terminal_check_from_inputs(inputs); + + assert!( + check + .details + .contains(&"Windows stdout console mode: 3".to_string()) + ); + assert!( + check + .details + .contains(&"Windows stdout VT processing: disabled".to_string()) + ); + } + + #[test] + fn terminal_check_reports_unavailable_windows_stdout_vt_processing() { + let mut inputs = terminal_inputs(); + inputs.windows_stdout_vt_state = Some(WindowsStdoutVtState::Unavailable); + + let check = terminal_check_from_inputs(inputs); + + assert!( + check + .details + .contains(&"Windows stdout console mode: unavailable".to_string()) + ); + assert!( + check + .details + .contains(&"Windows stdout VT processing: unavailable".to_string()) + ); + } + #[test] fn color_output_summary_reports_disabled_reasons() { let mut inputs = terminal_inputs(); diff --git a/codex-rs/terminal-detection/Cargo.toml b/codex-rs/terminal-detection/Cargo.toml index 9b1bf3a516..54276b96ec 100644 --- a/codex-rs/terminal-detection/Cargo.toml +++ b/codex-rs/terminal-detection/Cargo.toml @@ -15,5 +15,11 @@ workspace = true [dependencies] tracing = { workspace = true } +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.52", features = [ + "Win32_Foundation", + "Win32_System_Console", +] } + [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/codex-rs/terminal-detection/src/lib.rs b/codex-rs/terminal-detection/src/lib.rs index c42733380d..6d6af39e6c 100644 --- a/codex-rs/terminal-detection/src/lib.rs +++ b/codex-rs/terminal-detection/src/lib.rs @@ -5,6 +5,128 @@ use std::sync::OnceLock; +/// Visibility into the Windows stdout console VT-processing mode. +/// +/// Windows console handles expose a mode bit that controls whether emitted VT +/// sequences are interpreted by the terminal. PTY-style stdout handles do not +/// necessarily support this console-mode inspection; those land in +/// [`WindowsStdoutVtState::Unavailable`] instead of being treated as broken. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WindowsStdoutVtState { + /// Stdout is an inspectable Windows console handle and VT processing is enabled. + Enabled { + /// Console mode bits reported for stdout. + console_mode: u32, + }, + /// Stdout is an inspectable Windows console handle and VT processing is disabled. + Disabled { + /// Console mode bits reported for stdout. + console_mode: u32, + }, + /// Stdout is not an inspectable Windows console handle. + Unavailable, +} + +impl WindowsStdoutVtState { + /// Returns the inspected stdout console mode, if one is visible. + pub fn console_mode(self) -> Option { + match self { + Self::Enabled { console_mode } | Self::Disabled { console_mode } => Some(console_mode), + Self::Unavailable => None, + } + } + + /// Returns whether stdout VT processing is enabled when the mode is visible. + pub fn vt_processing_enabled(self) -> Option { + match self { + Self::Enabled { .. } => Some(true), + Self::Disabled { .. } => Some(false), + Self::Unavailable => None, + } + } +} + +/// Inspects the Windows stdout console VT-processing mode without mutating it. +/// +/// On non-Windows targets, and for stdout handles that do not support Windows +/// console-mode inspection, this returns [`WindowsStdoutVtState::Unavailable`]. +pub fn windows_stdout_vt_state() -> WindowsStdoutVtState { + #[cfg(windows)] + { + let Some((_handle, console_mode)) = windows_stdout_console_mode() else { + return WindowsStdoutVtState::Unavailable; + }; + windows_stdout_vt_state_from_mode(console_mode) + } + + #[cfg(not(windows))] + { + WindowsStdoutVtState::Unavailable + } +} + +/// Attempts to enable Windows stdout VT processing and returns the verified mode state. +/// +/// This preserves the inspection result for PTY-style or otherwise unavailable +/// stdout handles instead of assuming those handles cannot interpret ANSI. +pub fn enable_windows_stdout_vt_processing() -> WindowsStdoutVtState { + #[cfg(windows)] + { + use windows_sys::Win32::System::Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING; + use windows_sys::Win32::System::Console::SetConsoleMode; + + let Some((handle, console_mode)) = windows_stdout_console_mode() else { + return WindowsStdoutVtState::Unavailable; + }; + let state = windows_stdout_vt_state_from_mode(console_mode); + if matches!(state, WindowsStdoutVtState::Disabled { .. }) { + // Safety: `handle` and `console_mode` came from `GetConsoleMode` for stdout. + let _ = unsafe { + SetConsoleMode(handle, console_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + }; + } + windows_stdout_vt_state() + } + + #[cfg(not(windows))] + { + WindowsStdoutVtState::Unavailable + } +} + +#[cfg(windows)] +fn windows_stdout_vt_state_from_mode(console_mode: u32) -> WindowsStdoutVtState { + use windows_sys::Win32::System::Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING; + + if console_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0 { + WindowsStdoutVtState::Enabled { console_mode } + } else { + WindowsStdoutVtState::Disabled { console_mode } + } +} + +#[cfg(windows)] +fn windows_stdout_console_mode() -> Option<(windows_sys::Win32::Foundation::HANDLE, u32)> { + use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; + use windows_sys::Win32::System::Console::GetConsoleMode; + use windows_sys::Win32::System::Console::GetStdHandle; + use windows_sys::Win32::System::Console::STD_OUTPUT_HANDLE; + + // Safety: requesting the process stdout handle does not transfer ownership. + let handle = unsafe { GetStdHandle(STD_OUTPUT_HANDLE) }; + if handle == INVALID_HANDLE_VALUE || handle == 0 { + return None; + } + + let mut console_mode = 0; + // Safety: `console_mode` is a valid output pointer for the stdout handle query. + if unsafe { GetConsoleMode(handle, &mut console_mode) } == 0 { + return None; + } + + Some((handle, console_mode)) +} + /// Structured terminal identification data. #[derive(Clone, Debug, Eq, PartialEq)] pub struct TerminalInfo { diff --git a/codex-rs/tui/src/chatwidget/status_surfaces.rs b/codex-rs/tui/src/chatwidget/status_surfaces.rs index 2b2b7d52df..87973f2746 100644 --- a/codex-rs/tui/src/chatwidget/status_surfaces.rs +++ b/codex-rs/tui/src/chatwidget/status_surfaces.rs @@ -237,6 +237,7 @@ impl ChatWidget { tracing::debug!(error = %err, "failed to clear terminal title"); } } + Ok(SetTerminalTitleResult::SkippedUnsupported) => {} Err(err) => { tracing::debug!(error = %err, "failed to set terminal title"); } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 40cd121c71..2e4120db0c 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -160,6 +160,7 @@ mod oss_selection; mod pager_overlay; mod permission_compat; pub(crate) mod public_widgets; +mod raw_ansi; mod render; mod resize_reflow_cap; mod resume_picker; diff --git a/codex-rs/tui/src/raw_ansi.rs b/codex-rs/tui/src/raw_ansi.rs new file mode 100644 index 0000000000..8d768d995f --- /dev/null +++ b/codex-rs/tui/src/raw_ansi.rs @@ -0,0 +1,97 @@ +//! Gate forced-ANSI TUI output that can leak raw bytes on Windows. + +use std::io; +use std::io::stdout; + +use codex_terminal_detection::WindowsStdoutVtState; +use crossterm::SynchronizedUpdate; + +/// Whether raw ANSI writes should be emitted through the current stdout path. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum RawAnsiCapability { + /// Raw ANSI should follow the existing TUI write path. + Available, + /// Stdout is an inspectable Windows console handle whose VT mode is still disabled. + Unavailable, +} + +impl RawAnsiCapability { + pub(crate) fn is_available(self) -> bool { + matches!(self, Self::Available) + } +} + +/// Enables and verifies Windows stdout VT processing when that mode is inspectable. +/// +/// Handles that do not expose Windows console mode inspection preserve the +/// existing PTY behavior and continue to allow raw ANSI output. +pub(crate) fn stdout_capability() -> RawAnsiCapability { + #[cfg(windows)] + { + let state = codex_terminal_detection::enable_windows_stdout_vt_processing(); + let capability = capability_for_windows_stdout_vt_state(state); + if !capability.is_available() { + warn_windows_stdout_vt_disabled(state); + } + capability + } + + #[cfg(not(windows))] + { + capability_for_windows_stdout_vt_state(WindowsStdoutVtState::Unavailable) + } +} + +/// Runs a draw update with synchronized-update wrappers only when raw ANSI is safe. +pub(crate) fn synchronized_update(update: impl FnOnce() -> T) -> io::Result { + match stdout_capability() { + RawAnsiCapability::Available => stdout().sync_update(|_| update()), + RawAnsiCapability::Unavailable => Ok(update()), + } +} + +fn capability_for_windows_stdout_vt_state(state: WindowsStdoutVtState) -> RawAnsiCapability { + match state { + WindowsStdoutVtState::Disabled { .. } => RawAnsiCapability::Unavailable, + WindowsStdoutVtState::Enabled { .. } | WindowsStdoutVtState::Unavailable => { + RawAnsiCapability::Available + } + } +} + +#[cfg(windows)] +fn warn_windows_stdout_vt_disabled(state: WindowsStdoutVtState) { + use std::sync::Once; + + static WARNED: Once = Once::new(); + + WARNED.call_once(|| { + tracing::warn!( + ?state, + "Windows stdout VT processing is disabled; skipping raw ANSI TUI output" + ); + }); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unavailable_windows_stdout_state_keeps_raw_ansi_available() { + assert_eq!( + capability_for_windows_stdout_vt_state(WindowsStdoutVtState::Unavailable), + RawAnsiCapability::Available + ); + } + + #[test] + fn disabled_windows_stdout_state_blocks_raw_ansi() { + assert_eq!( + capability_for_windows_stdout_vt_state(WindowsStdoutVtState::Disabled { + console_mode: 3, + }), + RawAnsiCapability::Unavailable + ); + } +} diff --git a/codex-rs/tui/src/terminal_title.rs b/codex-rs/tui/src/terminal_title.rs index c21e6b8bc4..9d7a67f508 100644 --- a/codex-rs/tui/src/terminal_title.rs +++ b/codex-rs/tui/src/terminal_title.rs @@ -23,6 +23,8 @@ use std::io::stdout; use crossterm::Command; use ratatui::crossterm::execute; +use crate::raw_ansi; + /// Practical upper bound on title length, measured in Rust `char`s. /// /// Most terminals silently truncate titles beyond a few hundred characters. @@ -41,6 +43,8 @@ pub(crate) enum SetTerminalTitleResult { /// empty post-sanitization value should result in no-op behavior, clearing /// the title Codex manages, or some other fallback. NoVisibleContent, + /// The title would require a raw ANSI write while stdout cannot safely emit one. + SkippedUnsupported, } /// Writes a sanitized OSC window-title sequence to stdout. @@ -57,6 +61,9 @@ pub(crate) fn set_terminal_title(title: &str) -> io::Result io::Result<()> { if !stdout().is_terminal() { return Ok(()); } + if !title_output_supported(raw_ansi::stdout_capability()) { + return Ok(()); + } execute!(stdout(), SetWindowTitle(String::new())) } +fn title_output_supported(raw_ansi_capability: raw_ansi::RawAnsiCapability) -> bool { + raw_ansi_capability.is_available() +} + #[derive(Debug, Clone)] struct SetWindowTitle(String); @@ -181,6 +195,8 @@ mod tests { use super::MAX_TERMINAL_TITLE_CHARS; use super::SetWindowTitle; use super::sanitize_terminal_title; + use super::title_output_supported; + use crate::raw_ansi::RawAnsiCapability; use crossterm::Command; use pretty_assertions::assert_eq; @@ -222,4 +238,9 @@ mod tests { .expect("encode terminal title"); assert_eq!(out, "\x1b]0;hello\x07"); } + + #[test] + fn raw_ansi_disabled_skips_terminal_title_output() { + assert!(!title_output_supported(RawAnsiCapability::Unavailable)); + } } diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs index 32123b787c..8a5c2ee376 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -14,7 +14,6 @@ use std::sync::atomic::Ordering; use std::time::Duration; use crossterm::Command; -use crossterm::SynchronizedUpdate; use crossterm::cursor::SetCursorStyle; use crossterm::event::DisableBracketedPaste; use crossterm::event::DisableFocusChange; @@ -43,6 +42,7 @@ use crate::custom_terminal::Terminal as CustomTerminal; use crate::insert_history::HistoryLineWrapPolicy; use crate::notifications::DesktopNotificationBackend; use crate::notifications::detect_backend; +use crate::raw_ansi; use crate::tui::event_stream::EventBroker; use crate::tui::event_stream::TuiEventStream; #[cfg(unix)] @@ -91,9 +91,13 @@ impl Drop for Tui { mod tests { use std::io::Write as _; + use super::PendingHistoryLines; + use super::Tui; use super::clear_for_viewport_change; use super::should_emit_notification; use crate::custom_terminal::Terminal as CustomTerminal; + use crate::insert_history::HistoryLineWrapPolicy; + use crate::raw_ansi::RawAnsiCapability; use crate::test_backend::VT100Backend; use codex_config::types::NotificationCondition; use ratatui::layout::Position; @@ -163,10 +167,45 @@ mod tests { "expected stale cells inside the new viewport to be cleared, rows: {rows:?}" ); } + + #[test] + fn raw_ansi_disabled_skips_pending_history_writes() { + let width = 12; + let backend = VT100Backend::new(width, /*height*/ 4); + let mut terminal = + CustomTerminal::with_options_and_cursor_position(backend, Position { x: 0, y: 1 }) + .expect("terminal"); + let mut pending_history_lines = vec![PendingHistoryLines { + lines: vec![ratatui::text::Line::from("history")], + wrap_policy: HistoryLineWrapPolicy::PreWrap, + }]; + + Tui::flush_pending_history_lines_with_raw_ansi_capability( + &mut terminal, + &mut pending_history_lines, + RawAnsiCapability::Unavailable, + ) + .expect("skip history writes"); + + assert!(pending_history_lines.is_empty()); + let rows: Vec = terminal + .backend() + .vt100() + .screen() + .rows(/*start*/ 0, width) + .collect(); + assert!( + !rows.iter().any(|row| row.contains("history")), + "expected no history writes while raw ANSI is unavailable, rows: {rows:?}" + ); + } } pub fn set_modes() -> Result<()> { - execute!(stdout(), EnableBracketedPaste)?; + let raw_ansi_capability = raw_ansi::stdout_capability(); + if raw_ansi_capability.is_available() { + execute!(stdout(), EnableBracketedPaste)?; + } enable_raw_mode()?; // Enable keyboard enhancement flags so modifiers for keys like Enter are disambiguated. @@ -177,7 +216,9 @@ pub fn set_modes() -> Result<()> { // gracefully if unsupported. keyboard_modes::enable_keyboard_enhancement(); - let _ = execute!(stdout(), EnableFocusChange); + if raw_ansi_capability.is_available() { + let _ = execute!(stdout(), EnableFocusChange); + } Ok(()) } @@ -244,8 +285,15 @@ fn restore_common( KeyboardRestore::ResetAfterExit => keyboard_modes::reset_keyboard_reporting_after_exit(), } - let mut first_error = execute!(stdout(), DisableBracketedPaste).err(); - let _ = execute!(stdout(), DisableFocusChange); + let raw_ansi_capability = raw_ansi::stdout_capability(); + let mut first_error = if raw_ansi_capability.is_available() { + execute!(stdout(), DisableBracketedPaste).err() + } else { + None + }; + if raw_ansi_capability.is_available() { + let _ = execute!(stdout(), DisableFocusChange); + } if matches!(raw_mode_restore, RawModeRestore::Disable) && let Err(err) = disable_raw_mode() { @@ -659,6 +707,10 @@ impl Tui { if !self.alt_screen_enabled { return Ok(()); } + let raw_ansi_capability = raw_ansi::stdout_capability(); + if !raw_ansi_capability.is_available() { + return Ok(()); + } let _ = execute!(self.terminal.backend_mut(), EnterAlternateScreen); // Enable "alternate scroll" so terminals may translate wheel to arrows let _ = execute!(self.terminal.backend_mut(), EnableAlternateScroll); @@ -681,9 +733,12 @@ impl Tui { if !self.alt_screen_enabled { return Ok(()); } + let raw_ansi_capability = raw_ansi::stdout_capability(); // Disable alternate scroll when leaving alt-screen - let _ = execute!(self.terminal.backend_mut(), DisableAlternateScroll); - let _ = execute!(self.terminal.backend_mut(), LeaveAlternateScreen); + if raw_ansi_capability.is_available() { + let _ = execute!(self.terminal.backend_mut(), DisableAlternateScroll); + let _ = execute!(self.terminal.backend_mut(), LeaveAlternateScreen); + } if let Some(saved) = self.alt_saved_viewport.take() { self.terminal.set_viewport_area(saved); } @@ -762,13 +817,35 @@ impl Tui { } /// Write any buffered history lines above the viewport and clear the buffer. - fn flush_pending_history_lines( - terminal: &mut Terminal, + fn flush_pending_history_lines( + terminal: &mut CustomTerminal, pending_history_lines: &mut Vec, - ) -> Result<()> { + ) -> Result<()> + where + B: Backend + Write, + { + Self::flush_pending_history_lines_with_raw_ansi_capability( + terminal, + pending_history_lines, + raw_ansi::stdout_capability(), + ) + } + + fn flush_pending_history_lines_with_raw_ansi_capability( + terminal: &mut CustomTerminal, + pending_history_lines: &mut Vec, + raw_ansi_capability: raw_ansi::RawAnsiCapability, + ) -> Result<()> + where + B: Backend + Write, + { if pending_history_lines.is_empty() { return Ok(()); } + if !raw_ansi_capability.is_available() { + pending_history_lines.clear(); + return Ok(()); + } for batch in pending_history_lines.iter() { crate::insert_history::insert_history_lines_with_wrap_policy( @@ -797,7 +874,7 @@ impl Tui { // the synchronized update, to avoid racing with the event reader. let mut pending_viewport_area = self.pending_viewport_area()?; - stdout().sync_update(|_| { + raw_ansi::synchronized_update(|| { #[cfg(unix)] if let Some(prepared) = prepared_resume.take() { prepared.apply(&mut self.terminal)?; @@ -856,7 +933,7 @@ impl Tui { ) -> std::result::Result<(), crate::pets::PetImageRenderError> { let terminal = &mut self.terminal; let state = &mut self.ambient_pet_image_state; - stdout().sync_update(|_| { + raw_ansi::synchronized_update(|| { match crate::pets::render_ambient_pet_image(terminal.backend_mut(), state, request) { Ok(()) => Ok(Ok(())), Err(crate::pets::PetImageRenderError::Terminal(err)) => Err(err), @@ -871,7 +948,7 @@ impl Tui { ) -> std::result::Result<(), crate::pets::PetImageRenderError> { let terminal = &mut self.terminal; let state = &mut self.pet_picker_preview_image_state; - stdout().sync_update(|_| { + raw_ansi::synchronized_update(|| { match crate::pets::render_pet_picker_preview_image( terminal.backend_mut(), state, @@ -911,7 +988,7 @@ impl Tui { .suspend_context .prepare_resume_action(&mut self.terminal, &mut self.alt_saved_viewport); - stdout().sync_update(|_| { + raw_ansi::synchronized_update(|| { #[cfg(unix)] if let Some(prepared) = prepared_resume.take() { prepared.apply(&mut self.terminal)?;