Codex Handbook
tui/src/tui.rs 1136 lines
use std::fmt;use std::future::Future;use std::io::IsTerminal;use std::io::Result;use std::io::Stdout;use std::io::Write;use std::io::stdin;use std::io::stdout;use std::panic;use std::pin::Pin;use std::sync::Arc;use std::sync::atomic::AtomicBool;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;use crossterm::event::EnableBracketedPaste;use crossterm::event::EnableFocusChange;use crossterm::event::KeyEvent;use crossterm::terminal::EnterAlternateScreen;use crossterm::terminal::LeaveAlternateScreen;#[cfg(not(unix))]use crossterm::terminal::supports_keyboard_enhancement;use ratatui::backend::Backend;use ratatui::backend::CrosstermBackend;use ratatui::crossterm::execute;use ratatui::crossterm::terminal::disable_raw_mode;use ratatui::crossterm::terminal::enable_raw_mode;use ratatui::layout::Offset;use ratatui::layout::Position;use ratatui::layout::Rect;use ratatui::text::Line;use tokio::sync::broadcast;use tokio_stream::Stream;pub use self::frame_requester::FrameRequester;use crate::custom_terminal;use crate::custom_terminal::Terminal as CustomTerminal;use crate::insert_history::HistoryLineWrapPolicy;use crate::insert_history::InsertHistoryMode;use crate::notifications::DesktopNotificationBackend;use crate::notifications::detect_backend;use crate::terminal_hyperlinks::HyperlinkLine;use crate::terminal_hyperlinks::plain_hyperlink_lines;use crate::tui::event_stream::EventBroker;use crate::tui::event_stream::TuiEventStream;#[cfg(unix)]use crate::tui::job_control::SuspendContext;use codex_config::types::NotificationCondition;use codex_config::types::NotificationMethod;mod event_stream;mod frame_rate_limiter;mod frame_requester;#[cfg(unix)]mod job_control;mod keyboard_modes;mod terminal_stderr;/// Target frame interval for UI redraw scheduling.pub(crate) const TARGET_FRAME_INTERVAL: Duration = frame_rate_limiter::MIN_FRAME_INTERVAL;/// A type alias for the terminal type used in this applicationpub type Terminal = CustomTerminal<CrosstermBackend<Stdout>>;pub(crate) struct InitializedTerminal {    pub(crate) terminal: Terminal,    pub(crate) enhanced_keys_supported: bool,    pub(crate) stderr_guard: terminal_stderr::TerminalStderrGuard,}pub(crate) fn running_in_vscode_terminal() -> bool {    keyboard_modes::running_in_vscode_terminal()}fn should_emit_notification(condition: NotificationCondition, terminal_focused: bool) -> bool {    match condition {        NotificationCondition::Unfocused => !terminal_focused,        NotificationCondition::Always => true,    }}impl Drop for Tui {    fn drop(&mut self) {        if let Err(err) = self.clear_ambient_pet_image() {            tracing::debug!(error = %err, "failed to clear ambient pet image on TUI drop");        }    }}#[cfg(test)]mod tests {    use std::io::Write as _;    use super::clear_for_viewport_change;    use super::should_emit_notification;    use crate::custom_terminal::Terminal as CustomTerminal;    use crate::test_backend::VT100Backend;    use codex_config::types::NotificationCondition;    use ratatui::layout::Position;    use ratatui::layout::Rect;    #[test]    fn unfocused_notification_condition_is_suppressed_when_focused() {        assert!(!should_emit_notification(            NotificationCondition::Unfocused,            /*terminal_focused*/ true        ));    }    #[test]    fn always_notification_condition_emits_when_focused() {        assert!(should_emit_notification(            NotificationCondition::Always,            /*terminal_focused*/ true        ));    }    #[test]    fn unfocused_notification_condition_emits_when_unfocused() {        assert!(should_emit_notification(            NotificationCondition::Unfocused,            /*terminal_focused*/ false        ));    }    #[test]    fn first_viewport_change_clears_from_new_viewport_when_old_viewport_is_empty() {        let width = 12;        let height = 4;        let backend = VT100Backend::new(width, height);        let mut terminal =            CustomTerminal::with_options_and_cursor_position(backend, Position { x: 0, y: 1 })                .expect("terminal");        write!(            terminal.backend_mut(),            "shell line\r\nstale cells\r\nmore stale"        )        .expect("prefill terminal");        clear_for_viewport_change(            &mut terminal,            Rect::new(                /*x*/ 0,                /*y*/ 1,                /*width*/ width,                /*height*/ height - 1,            ),        )        .expect("clear transition");        let rows: Vec<String> = terminal            .backend()            .vt100()            .screen()            .rows(/*start*/ 0, width)            .collect();        assert!(            rows[0].contains("shell line"),            "expected content before the viewport to remain visible, rows: {rows:?}"        );        assert!(            !rows.iter().skip(1).any(|row| row.contains("stale")),            "expected stale cells inside the new viewport to be cleared, rows: {rows:?}"        );    }}pub fn set_modes() -> Result<()> {    ensure_virtual_terminal_processing()?;    execute!(stdout(), EnableBracketedPaste)?;    enable_raw_mode()?;    // Enable keyboard enhancement flags so modifiers for keys like Enter are disambiguated.    // chat_composer.rs is using a keyboard event listener to enter for any modified keys    // to create a new line that require this.    // Some terminals (notably legacy Windows consoles) do not support    // keyboard enhancement flags. Attempt to enable them, but continue    // gracefully if unsupported.    keyboard_modes::enable_keyboard_enhancement();    let _ = execute!(stdout(), EnableFocusChange);    Ok(())}#[derive(Debug, Clone, Copy, PartialEq, Eq)]struct EnableAlternateScroll;impl Command for EnableAlternateScroll {    fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {        write!(f, "\x1b[?1007h")    }    #[cfg(windows)]    fn execute_winapi(&self) -> Result<()> {        Err(std::io::Error::other(            "tried to execute EnableAlternateScroll using WinAPI; use ANSI instead",        ))    }    #[cfg(windows)]    fn is_ansi_code_supported(&self) -> bool {        true    }}#[derive(Debug, Clone, Copy, PartialEq, Eq)]struct DisableAlternateScroll;impl Command for DisableAlternateScroll {    fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {        write!(f, "\x1b[?1007l")    }    #[cfg(windows)]    fn execute_winapi(&self) -> Result<()> {        Err(std::io::Error::other(            "tried to execute DisableAlternateScroll using WinAPI; use ANSI instead",        ))    }    #[cfg(windows)]    fn is_ansi_code_supported(&self) -> bool {        true    }}#[derive(Debug, Clone, Copy, PartialEq, Eq)]enum RawModeRestore {    Disable,    Keep,}#[derive(Debug, Clone, Copy, PartialEq, Eq)]enum KeyboardRestore {    PopStack,    ResetAfterExit,}fn restore_common(    raw_mode_restore: RawModeRestore,    keyboard_restore: KeyboardRestore,) -> Result<()> {    let mut first_error = ensure_virtual_terminal_processing().err();    match keyboard_restore {        KeyboardRestore::PopStack => keyboard_modes::restore_keyboard_enhancement_stack(),        KeyboardRestore::ResetAfterExit => keyboard_modes::reset_keyboard_reporting_after_exit(),    }    if let Err(err) = execute!(stdout(), DisableBracketedPaste) {        first_error.get_or_insert(err);    }    let _ = execute!(stdout(), DisableFocusChange);    if matches!(raw_mode_restore, RawModeRestore::Disable)        && let Err(err) = disable_raw_mode()    {        first_error.get_or_insert(err);    }    if let Err(err) = execute!(        stdout(),        SetCursorStyle::DefaultUserShape,        crossterm::cursor::Show    ) {        first_error.get_or_insert(err);    }    match first_error {        Some(err) => Err(err),        None => Ok(()),    }}/// Restore the terminal to its original state./// Inverse of `set_modes`.pub fn restore() -> Result<()> {    restore_common(RawModeRestore::Disable, KeyboardRestore::PopStack)}/// Force crossterm's cached raw-mode state back in sync with the terminal after `fg`.////// A shell may restore the job's saved termios after the process receives `SIGCONT`. When that/// races with [`set_modes`], crossterm still believes raw mode is enabled even though the terminal/// has returned to canonical, echoing mode. Clearing crossterm's saved state before enabling raw/// mode again makes the kernel state authoritative once the shell has completed its handoff.#[cfg(unix)]pub(super) fn reapply_raw_mode_after_resume() -> Result<()> {    disable_raw_mode()?;    enable_raw_mode()}/// Restore the terminal after Codex is exiting.////// Uses a stronger keyboard reset than [`restore`] so the parent shell recovers even if a/// terminal missed the stack pop that normally pairs with [`set_modes`].pub fn restore_after_exit() -> Result<()> {    let mut first_error =        restore_common(RawModeRestore::Disable, KeyboardRestore::ResetAfterExit).err();    if let Err(err) = terminal_stderr::finish() {        first_error.get_or_insert(err);    }    match first_error {        Some(err) => Err(err),        None => Ok(()),    }}/// Restore the terminal to its original state, but keep raw mode enabled.pub fn restore_keep_raw() -> Result<()> {    restore_common(RawModeRestore::Keep, KeyboardRestore::PopStack)}#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum RestoreMode {    #[allow(dead_code)]    Full, // Fully restore the terminal (disables raw mode).    KeepRaw, // Restore the terminal but keep raw mode enabled.}impl RestoreMode {    fn restore(self) -> Result<()> {        match self {            RestoreMode::Full => restore(),            RestoreMode::KeepRaw => restore_keep_raw(),        }    }}/// Flush the underlying stdin buffer to clear any input that may be buffered at the terminal level./// For example, clears any user input that occurred while the crossterm EventStream was dropped.#[cfg(unix)]fn flush_terminal_input_buffer() {    // Safety: flushing the stdin queue is safe and does not move ownership.    let result = unsafe { libc::tcflush(libc::STDIN_FILENO, libc::TCIFLUSH) };    if result != 0 {        let err = std::io::Error::last_os_error();        tracing::warn!("failed to tcflush stdin: {err}");    }}/// Flush the underlying stdin buffer to clear any input that may be buffered at the terminal level./// For example, clears any user input that occurred while the crossterm EventStream was dropped.#[cfg(windows)]fn flush_terminal_input_buffer() {    use windows_sys::Win32::Foundation::GetLastError;    use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;    use windows_sys::Win32::System::Console::FlushConsoleInputBuffer;    use windows_sys::Win32::System::Console::GetStdHandle;    use windows_sys::Win32::System::Console::STD_INPUT_HANDLE;    let handle = unsafe { GetStdHandle(STD_INPUT_HANDLE) };    if handle == INVALID_HANDLE_VALUE || handle == 0 {        let err = unsafe { GetLastError() };        tracing::warn!("failed to get stdin handle for flush: error {err}");        return;    }    let result = unsafe { FlushConsoleInputBuffer(handle) };    if result == 0 {        let err = unsafe { GetLastError() };        tracing::warn!("failed to flush stdin buffer: error {err}");    }}#[cfg(not(any(unix, windows)))]pub(crate) fn flush_terminal_input_buffer() {}/// Initialize the terminal (inline viewport; history stays in normal scrollback)pub(crate) fn init() -> Result<InitializedTerminal> {    if !stdin().is_terminal() {        return Err(std::io::Error::other("stdin is not a terminal"));    }    if !stdout().is_terminal() {        return Err(std::io::Error::other("stdout is not a terminal"));    }    set_modes()?;    flush_terminal_input_buffer();    set_panic_hook();    #[cfg(unix)]    let backend = CrosstermBackend::new(stdout());    #[cfg(unix)]    let startup_probe = {        use crate::terminal_probe::StartupKeyboardEnhancementProbe;        let started_at = std::time::Instant::now();        let keyboard_probe = if keyboard_modes::keyboard_enhancement_disabled() {            StartupKeyboardEnhancementProbe::Skip        } else {            StartupKeyboardEnhancementProbe::Query        };        match crate::terminal_probe::startup(crate::terminal_probe::DEFAULT_TIMEOUT, keyboard_probe)        {            Ok(probe) => {                tracing::info!(                    duration_ms = %started_at.elapsed().as_millis(),                    cursor_position = probe.cursor_position.is_some(),                    default_colors = probe.default_colors.is_some(),                    keyboard_enhancement_supported = ?probe.keyboard_enhancement_supported,                    "terminal startup probes completed"                );                probe            }            Err(err) => {                tracing::warn!(                    duration_ms = %started_at.elapsed().as_millis(),                    "terminal startup probes failed: {err}"                );                crate::terminal_probe::StartupProbe {                    cursor_position: None,                    default_colors: None,                    keyboard_enhancement_supported: None,                }            }        }    };    #[cfg(unix)]    crate::terminal_palette::set_default_colors_from_startup_probe(startup_probe.default_colors);    #[cfg(unix)]    let cursor_pos = match startup_probe.cursor_position {        Some(pos) => pos,        None => {            tracing::warn!("initial cursor position probe timed out; defaulting to origin");            Position { x: 0, y: 0 }        }    };    #[cfg(unix)]    let enhanced_keys_supported = startup_probe        .keyboard_enhancement_supported        .unwrap_or(/*default*/ false);    #[cfg(not(unix))]    let mut backend = CrosstermBackend::new(stdout());    #[cfg(not(unix))]    let cursor_pos = cursor_position_with_crossterm(&mut backend);    #[cfg(not(unix))]    let enhanced_keys_supported =        !keyboard_modes::keyboard_enhancement_disabled() && detect_keyboard_enhancement_supported();    #[cfg(windows)]    probe_windows_default_colors();    let tui = CustomTerminal::with_options_and_cursor_position(backend, cursor_pos)?;    let stderr_guard = terminal_stderr::TerminalStderrGuard::install()?;    Ok(InitializedTerminal {        terminal: tui,        enhanced_keys_supported,        stderr_guard,    })}#[cfg(not(unix))]fn cursor_position_with_crossterm(backend: &mut CrosstermBackend<Stdout>) -> Position {    backend.get_cursor_position().unwrap_or_else(|err| {        tracing::warn!("failed to read initial cursor position; defaulting to origin: {err}");        Position { x: 0, y: 0 }    })}#[cfg(not(unix))]fn detect_keyboard_enhancement_supported() -> bool {    // Non-Unix startup keeps the existing crossterm keyboard probe path because it already knows    // how to interpret platform-specific event sources.    supports_keyboard_enhancement().unwrap_or(/*default*/ false)}#[cfg(windows)]fn probe_windows_default_colors() {    let started_at = std::time::Instant::now();    match crate::terminal_probe::default_colors(crate::terminal_probe::DEFAULT_TIMEOUT) {        Ok(colors) => {            tracing::info!(                duration_ms = %started_at.elapsed().as_millis(),                default_colors = colors.is_some(),                "terminal default color probe completed"            );            crate::terminal_palette::set_default_colors_from_startup_probe(colors);        }        Err(err) => {            tracing::warn!(                duration_ms = %started_at.elapsed().as_millis(),                "terminal default color probe failed: {err}"            );            crate::terminal_palette::set_default_colors_from_startup_probe(/*colors*/ None);        }    }}fn set_panic_hook() {    let hook = panic::take_hook();    panic::set_hook(Box::new(move |panic_info| {        let _ = restore_after_exit(); // ignore any errors as we are already failing        hook(panic_info);    }));}#[derive(Clone, Debug)]pub enum TuiEvent {    /// A terminal key event after focus, paste, and protocol bookkeeping has been handled.    Key(KeyEvent),    /// A bracketed paste payload normalized by the app layer before it reaches the composer.    Paste(String),    /// A terminal size notification that should be handled as resize-sensitive draw work.    ///    /// Resize is separate from `Draw` so the app can run feature-gated pre-render logic without    /// changing the default draw path for scheduled frames.    Resize,    /// A scheduled repaint that does not necessarily correspond to a terminal size change.    Draw,}pub struct Tui {    frame_requester: FrameRequester,    draw_tx: broadcast::Sender<()>,    event_broker: Arc<EventBroker>,    pub(crate) terminal: Terminal,    pending_history_lines: Vec<PendingHistoryLines>,    ambient_pet_image_state: crate::pets::PetImageRenderState,    pet_picker_preview_image_state: crate::pets::PetImageRenderState,    alt_saved_viewport: Option<ratatui::layout::Rect>,    #[cfg(unix)]    suspend_context: SuspendContext,    // True when overlay alt-screen UI is active    alt_screen_active: Arc<AtomicBool>,    // True when terminal/tab is focused; updated internally from crossterm events    terminal_focused: Arc<AtomicBool>,    enhanced_keys_supported: bool,    notification_backend: Option<DesktopNotificationBackend>,    notification_condition: NotificationCondition,    // Raw terminal-wrapped history needs a non-scroll-region insertion path in Zellij.    is_zellij: bool,    // When false, enter_alt_screen() becomes a no-op.    alt_screen_enabled: bool,    // Keeps unmanaged process stderr writes out of the inline viewport.    _stderr_guard: terminal_stderr::TerminalStderrGuard,}struct PendingHistoryLines {    lines: Vec<HyperlinkLine>,    wrap_policy: HistoryLineWrapPolicy,}fn clear_for_viewport_change<B>(terminal: &mut CustomTerminal<B>, new_area: Rect) -> Result<()>where    B: Backend + Write,{    let clear_position = if terminal.viewport_area.is_empty() {        new_area.as_position()    } else {        terminal.viewport_area.as_position()    };    terminal.clear_after_position(clear_position)}impl Tui {    pub(crate) fn new(        terminal: Terminal,        enhanced_keys_supported: bool,        stderr_guard: terminal_stderr::TerminalStderrGuard,    ) -> Self {        let (draw_tx, _) = broadcast::channel(1);        let frame_requester = FrameRequester::new(draw_tx.clone());        // Cache this to avoid contention with the event reader.        supports_color::on_cached(supports_color::Stream::Stdout);        let _ = crate::terminal_palette::default_colors();        let is_zellij = codex_terminal_detection::terminal_info().is_zellij();        Self {            frame_requester,            draw_tx,            event_broker: Arc::new(EventBroker::new()),            terminal,            pending_history_lines: vec![],            ambient_pet_image_state: crate::pets::PetImageRenderState::default(),            pet_picker_preview_image_state: crate::pets::PetImageRenderState::default(),            alt_saved_viewport: None,            #[cfg(unix)]            suspend_context: SuspendContext::new(),            alt_screen_active: Arc::new(AtomicBool::new(false)),            terminal_focused: Arc::new(AtomicBool::new(true)),            enhanced_keys_supported,            notification_backend: Some(detect_backend(NotificationMethod::default())),            notification_condition: NotificationCondition::default(),            is_zellij,            alt_screen_enabled: true,            _stderr_guard: stderr_guard,        }    }    /// Set whether alternate screen is enabled. When false, enter_alt_screen() becomes a no-op.    pub fn set_alt_screen_enabled(&mut self, enabled: bool) {        self.alt_screen_enabled = enabled;    }    pub fn set_notification_settings(        &mut self,        method: NotificationMethod,        condition: NotificationCondition,    ) {        self.notification_backend = Some(detect_backend(method));        self.notification_condition = condition;    }    pub fn frame_requester(&self) -> FrameRequester {        self.frame_requester.clone()    }    pub fn enhanced_keys_supported(&self) -> bool {        self.enhanced_keys_supported    }    pub fn is_alt_screen_active(&self) -> bool {        self.alt_screen_active.load(Ordering::Relaxed)    }    // Drop crossterm EventStream to avoid stdin conflicts with other processes.    pub fn pause_events(&mut self) {        self.event_broker.pause_events();    }    // Resume crossterm EventStream to resume stdin polling.    // Inverse of `pause_events`.    pub fn resume_events(&mut self) {        self.event_broker.resume_events();    }    /// Temporarily restore terminal state to run an external interactive program `f`.    ///    /// This pauses crossterm's stdin polling by dropping the underlying event stream, restores    /// terminal modes and stderr (optionally keeping raw mode enabled), then re-applies Codex TUI    /// modes and stderr suppression before resuming events.    pub async fn with_restored<R, F, Fut>(&mut self, mode: RestoreMode, f: F) -> R    where        F: FnOnce() -> Fut,        Fut: Future<Output = R>,    {        // Pause crossterm events to avoid stdin conflicts with external program `f`.        self.pause_events();        // Leave alt screen if active to avoid conflicts with external program `f`.        let was_alt_screen = self.is_alt_screen_active();        if was_alt_screen {            let _ = self.leave_alt_screen();        }        if let Err(err) = mode.restore() {            tracing::warn!("failed to restore terminal modes before external program: {err}");        }        if let Err(err) = terminal_stderr::pause() {            tracing::warn!("failed to restore terminal stderr before external program: {err}");        }        let output = f().await;        if let Err(err) = terminal_stderr::resume() {            tracing::warn!("failed to suppress terminal stderr after external program: {err}");        }        if let Err(err) = set_modes() {            tracing::warn!("failed to re-enable terminal modes after external program: {err}");        }        // After the external program `f` finishes, reset terminal state and flush any buffered keypresses.        flush_terminal_input_buffer();        if was_alt_screen {            let _ = self.enter_alt_screen();        }        self.resume_events();        output    }    /// Emit a desktop notification now if the terminal is unfocused.    /// Returns true if a notification was posted.    pub fn notify(&mut self, message: impl AsRef<str>) -> bool {        let terminal_focused = self.terminal_focused.load(Ordering::Relaxed);        if !should_emit_notification(self.notification_condition, terminal_focused) {            return false;        }        let Some(backend) = self.notification_backend.as_mut() else {            return false;        };        let message = message.as_ref().to_string();        match backend.notify(&message) {            Ok(()) => true,            Err(err) => {                let method = backend.method();                tracing::warn!(                    error = %err,                    method = %method,                    "Failed to emit terminal notification; disabling future notifications"                );                self.notification_backend = None;                false            }        }    }    pub fn event_stream(&self) -> Pin<Box<dyn Stream<Item = TuiEvent> + Send + 'static>> {        #[cfg(unix)]        let stream = TuiEventStream::new(            self.event_broker.clone(),            self.draw_tx.subscribe(),            self.terminal_focused.clone(),            self.suspend_context.clone(),            self.alt_screen_active.clone(),        );        #[cfg(not(unix))]        let stream = TuiEventStream::new(            self.event_broker.clone(),            self.draw_tx.subscribe(),            self.terminal_focused.clone(),        );        Box::pin(stream)    }    /// Enter alternate screen and expand the viewport to full terminal size, saving the current    /// inline viewport for restoration when leaving.    pub fn enter_alt_screen(&mut self) -> Result<()> {        if !self.alt_screen_enabled {            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);        if let Ok(size) = self.terminal.size() {            self.alt_saved_viewport = Some(self.terminal.viewport_area);            self.terminal.set_viewport_area(ratatui::layout::Rect::new(                0,                0,                size.width,                size.height,            ));            let _ = self.terminal.clear();        }        self.alt_screen_active.store(true, Ordering::Relaxed);        Ok(())    }    /// Leave alternate screen and restore the previously saved inline viewport, if any.    pub fn leave_alt_screen(&mut self) -> Result<()> {        if !self.alt_screen_enabled {            return Ok(());        }        // Disable alternate scroll when leaving alt-screen        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);        }        self.alt_screen_active.store(false, Ordering::Relaxed);        Ok(())    }    pub fn insert_history_lines(&mut self, lines: Vec<Line<'static>>) {        self.insert_history_lines_with_wrap_policy(lines, HistoryLineWrapPolicy::PreWrap);    }    pub fn insert_history_lines_with_wrap_policy(        &mut self,        lines: Vec<Line<'static>>,        wrap_policy: HistoryLineWrapPolicy,    ) {        self.insert_history_hyperlink_lines_with_wrap_policy(            plain_hyperlink_lines(lines),            wrap_policy,        );    }    pub(crate) fn insert_history_hyperlink_lines_with_wrap_policy(        &mut self,        lines: Vec<HyperlinkLine>,        wrap_policy: HistoryLineWrapPolicy,    ) {        if lines.is_empty() {            return;        }        if let Some(last) = self.pending_history_lines.last_mut()            && last.wrap_policy == wrap_policy        {            last.lines.extend(lines);        } else {            self.pending_history_lines                .push(PendingHistoryLines { lines, wrap_policy });        }        self.frame_requester().schedule_frame();    }    pub fn clear_pending_history_lines(&mut self) {        self.pending_history_lines.clear();    }    /// Resize the inline viewport for the resize-reflow path.    ///    /// Unlike the legacy draw path, this path does not scroll rows above the viewport when the    /// terminal shrinks. Resize reflow owns rebuilding those rows from transcript source, so    /// scrolling here would move the viewport once and then replay history into the wrong row.    fn update_inline_viewport_for_resize_reflow(        terminal: &mut Terminal,        height: u16,    ) -> Result<bool> {        let size = terminal.size()?;        let terminal_height_shrank = size.height < terminal.last_known_screen_size.height;        let terminal_height_grew = size.height > terminal.last_known_screen_size.height;        let viewport_was_bottom_aligned =            terminal.viewport_area.bottom() == terminal.last_known_screen_size.height;        let previous_area = terminal.viewport_area;        let mut area = terminal.viewport_area;        area.height = height.min(size.height);        area.width = size.width;        let mut needs_full_repaint = false;        if area.bottom() > size.height {            let scroll_by = area.bottom() - size.height;            if !terminal_height_shrank {                terminal                    .backend_mut()                    .scroll_region_up(0..area.top(), scroll_by)?;            }            area.y = size.height - area.height;        } else if terminal_height_grew && viewport_was_bottom_aligned {            area.y = size.height - area.height;        }        if area != terminal.viewport_area {            let clear_position = Position::new(/*x*/ 0, previous_area.y.min(area.y));            terminal.set_viewport_area(area);            terminal.clear_after_position(clear_position)?;            needs_full_repaint = true;        }        Ok(needs_full_repaint)    }    /// Write any buffered history lines above the viewport and clear the buffer.    fn flush_pending_history_lines(        terminal: &mut Terminal,        pending_history_lines: &mut Vec<PendingHistoryLines>,        is_zellij: bool,    ) -> Result<()> {        if pending_history_lines.is_empty() {            return Ok(());        }        for batch in pending_history_lines.iter() {            let mode = if is_zellij && batch.wrap_policy == HistoryLineWrapPolicy::Terminal {                InsertHistoryMode::ZellijRaw            } else {                InsertHistoryMode::Standard            };            crate::insert_history::insert_history_hyperlink_lines_with_mode_and_wrap_policy(                terminal,                batch.lines.clone(),                mode,                batch.wrap_policy,            )?;        }        pending_history_lines.clear();        Ok(())    }    pub fn draw(        &mut self,        height: u16,        draw_fn: impl FnOnce(&mut custom_terminal::Frame),    ) -> Result<()> {        // If we are resuming from ^Z, we need to prepare the resume action now so we can apply it        // in the synchronized update.        #[cfg(unix)]        let mut prepared_resume = self            .suspend_context            .prepare_resume_action(&mut self.alt_saved_viewport);        // Precompute any viewport updates that need a cursor-position query before entering        // the synchronized update, to avoid racing with the event reader.        let mut pending_viewport_area = self.pending_viewport_area()?;        ensure_virtual_terminal_processing()?;        stdout().sync_update(|_| {            #[cfg(unix)]            if let Some(prepared) = prepared_resume.take() {                prepared.apply(&mut self.terminal)?;            }            let terminal = &mut self.terminal;            if let Some(new_area) = pending_viewport_area.take() {                terminal.set_viewport_area(new_area);                terminal.clear()?;            }            let size = terminal.size()?;            let mut area = terminal.viewport_area;            area.height = height.min(size.height);            area.width = size.width;            // If the viewport has expanded, scroll everything else up to make room.            if area.bottom() > size.height {                terminal                    .backend_mut()                    .scroll_region_up(0..area.top(), area.bottom() - size.height)?;                area.y = size.height - area.height;            }            if area != terminal.viewport_area {                // On startup, the old viewport can still be empty. Clear from the                // new viewport top so stale shell cells do not show through spaces.                clear_for_viewport_change(terminal, area)?;                terminal.set_viewport_area(area);            }            Self::flush_pending_history_lines(                terminal,                &mut self.pending_history_lines,                self.is_zellij,            )?;            // Update the y position for suspending so Ctrl-Z can place the cursor correctly.            #[cfg(unix)]            {                let area = terminal.viewport_area;                let inline_area_bottom = if self.alt_screen_active.load(Ordering::Relaxed) {                    self.alt_saved_viewport                        .map(|r| r.bottom().saturating_sub(1))                        .unwrap_or_else(|| area.bottom().saturating_sub(1))                } else {                    area.bottom().saturating_sub(1)                };                self.suspend_context.set_cursor_y(inline_area_bottom);            }            terminal.draw(|frame| {                draw_fn(frame);            })        })?    }    pub fn draw_ambient_pet_image(        &mut self,        request: Option<crate::pets::AmbientPetDraw>,    ) -> std::result::Result<(), crate::pets::PetImageRenderError> {        if let Err(err) = ensure_virtual_terminal_processing() {            return Err(crate::pets::PetImageRenderError::Terminal(err));        }        let terminal = &mut self.terminal;        let state = &mut self.ambient_pet_image_state;        stdout().sync_update(|_| {            match crate::pets::render_ambient_pet_image(terminal.backend_mut(), state, request) {                Ok(()) => Ok(Ok(())),                Err(crate::pets::PetImageRenderError::Terminal(err)) => Err(err),                Err(err @ crate::pets::PetImageRenderError::Asset(_)) => Ok(Err(err)),            }        })??    }    pub fn draw_pet_picker_preview_image(        &mut self,        request: Option<crate::pets::AmbientPetDraw>,    ) -> std::result::Result<(), crate::pets::PetImageRenderError> {        if let Err(err) = ensure_virtual_terminal_processing() {            return Err(crate::pets::PetImageRenderError::Terminal(err));        }        let terminal = &mut self.terminal;        let state = &mut self.pet_picker_preview_image_state;        stdout().sync_update(|_| {            match crate::pets::render_pet_picker_preview_image(                terminal.backend_mut(),                state,                request,            ) {                Ok(()) => Ok(Ok(())),                Err(crate::pets::PetImageRenderError::Terminal(err)) => Err(err),                Err(err @ crate::pets::PetImageRenderError::Asset(_)) => Ok(Err(err)),            }        })??    }    pub fn clear_ambient_pet_image(        &mut self,    ) -> std::result::Result<(), crate::pets::PetImageRenderError> {        if let Err(err) = ensure_virtual_terminal_processing() {            return Err(crate::pets::PetImageRenderError::Terminal(err));        }        crate::pets::render_ambient_pet_image(            self.terminal.backend_mut(),            &mut self.ambient_pet_image_state,            /*request*/ None,        )    }    /// Draw a frame using the resize-reflow viewport and history insertion rules.    ///    /// This is the feature-gated counterpart to `draw`. It intentionally skips    /// `pending_viewport_area`, whose cursor-position heuristic is part of the legacy path, and    /// instead lets transcript reflow rebuild scrollback before the frame is rendered.    pub fn draw_with_resize_reflow(        &mut self,        height: u16,        draw_fn: impl FnOnce(&mut custom_terminal::Frame),    ) -> Result<()> {        // If we are resuming from ^Z, we need to prepare the resume action now so we can apply it        // in the synchronized update.        #[cfg(unix)]        let mut prepared_resume = self            .suspend_context            .prepare_resume_action(&mut self.alt_saved_viewport);        ensure_virtual_terminal_processing()?;        stdout().sync_update(|_| {            #[cfg(unix)]            if let Some(prepared) = prepared_resume.take() {                prepared.apply(&mut self.terminal)?;            }            let terminal = &mut self.terminal;            let needs_full_repaint =                Self::update_inline_viewport_for_resize_reflow(terminal, height)?;            Self::flush_pending_history_lines(                terminal,                &mut self.pending_history_lines,                self.is_zellij,            )?;            if needs_full_repaint {                terminal.invalidate_viewport();            }            // Update the y position for suspending so Ctrl-Z can place the cursor correctly.            #[cfg(unix)]            {                let area = terminal.viewport_area;                let inline_area_bottom = if self.alt_screen_active.load(Ordering::Relaxed) {                    self.alt_saved_viewport                        .map(|r| r.bottom().saturating_sub(1))                        .unwrap_or_else(|| area.bottom().saturating_sub(1))                } else {                    area.bottom().saturating_sub(1)                };                self.suspend_context.set_cursor_y(inline_area_bottom);            }            terminal.draw(|frame| {                draw_fn(frame);            })        })?    }    fn pending_viewport_area(&mut self) -> Result<Option<Rect>> {        let terminal = &mut self.terminal;        let screen_size = terminal.size()?;        let last_known_screen_size = terminal.last_known_screen_size;        if screen_size != last_known_screen_size            && let Ok(cursor_pos) = terminal.get_cursor_position()        {            let last_known_cursor_pos = terminal.last_known_cursor_pos;            // If we resized AND the cursor moved, we adjust the viewport area to keep the            // cursor in the same position. This is a heuristic that seems to work well            // at least in iTerm2.            if cursor_pos.y != last_known_cursor_pos.y {                let offset = Offset {                    x: 0,                    y: cursor_pos.y as i32 - last_known_cursor_pos.y as i32,                };                return Ok(Some(terminal.viewport_area.offset(offset)));            }        }        Ok(None)    }}#[cfg(windows)]fn ensure_virtual_terminal_processing() -> Result<()> {    use windows_sys::Win32::Foundation::HANDLE;    use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;    use windows_sys::Win32::System::Console::ENABLE_PROCESSED_OUTPUT;    use windows_sys::Win32::System::Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING;    use windows_sys::Win32::System::Console::GetConsoleMode;    use windows_sys::Win32::System::Console::GetStdHandle;    use windows_sys::Win32::System::Console::STD_ERROR_HANDLE;    use windows_sys::Win32::System::Console::STD_OUTPUT_HANDLE;    use windows_sys::Win32::System::Console::SetConsoleMode;    fn enable_for_handle(handle: HANDLE) -> Result<()> {        if handle == INVALID_HANDLE_VALUE || handle == 0 {            return Ok(());        }        let mut mode = 0;        if unsafe { GetConsoleMode(handle, &mut mode) } == 0 {            return Ok(());        }        let requested = ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING;        if mode & requested == requested {            return Ok(());        }        if unsafe { SetConsoleMode(handle, mode | requested) } == 0 {            return Err(std::io::Error::last_os_error());        }        Ok(())    }    let stdout_handle = unsafe { GetStdHandle(STD_OUTPUT_HANDLE) };    enable_for_handle(stdout_handle)?;    let stderr_handle = unsafe { GetStdHandle(STD_ERROR_HANDLE) };    enable_for_handle(stderr_handle)?;    Ok(())}#[cfg(not(windows))]fn ensure_virtual_terminal_processing() -> Result<()> {    Ok(())}