From 2dd4cf236148c6d04873b78bc8d17f05a7a84d3b Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Thu, 30 Oct 2025 18:12:08 -0700 Subject: [PATCH] docs: add tui.rs doc comments Codex generated based on the PRs, commits, PR comments. This helps someone coming fresh to this code understand how most of the changes from the normal Ratatui approach work as well as the tui events and widget triggerd drawing. --- codex-rs/tui/src/tui.rs | 245 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 230 insertions(+), 15 deletions(-) diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs index ca672355dc..4d887ee03e 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -45,6 +45,22 @@ use tokio_stream::Stream; /// A type alias for the terminal type used in this application pub type Terminal = CustomTerminal>; +/// Enable the terminal capabilities the Codex TUI depends on. +/// +/// - Enables bracketed paste so multi-line submissions reach [`ChatComposer`] as a single +/// payload. +/// - Switches to raw mode to expose low-level key events without line buffering. +/// - Attempts to push Crossterm keyboard enhancement flags so modifier-aware keys are visible. +/// [`ChatComposer`] listens for modifier-rich enter presses, so we best-effort enable the +/// extension even when consoles may ignore it. +/// - Enables focus reporting so +/// [`ChatWidget::maybe_post_pending_notification`][ChatWidgetNotif] can gate alerts. +/// +/// Ratatui leaves these switches to callers; centralizing them here guarantees the inline viewport +/// starts from a consistent configuration. +/// +/// [`ChatComposer`]: crate::bottom_pane::chat_composer::ChatComposer +/// [ChatWidgetNotif]: crate::chatwidget::ChatWidget::maybe_post_pending_notification pub fn set_modes() -> Result<()> { execute!(stdout(), EnableBracketedPaste)?; @@ -68,6 +84,9 @@ pub fn set_modes() -> Result<()> { Ok(()) } +/// Crossterm command that enables "alternate scroll" (SGR/DECSET 1007) so mouse wheels translate +/// into arrow keys while the alt screen is active. See the xterm control sequence reference +/// () for details. #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct EnableAlternateScroll; @@ -89,6 +108,8 @@ impl Command for EnableAlternateScroll { } } +/// Crossterm command that disables SGR/DECSET 1007 so scroll wheels go back to native terminal +/// behavior; refer to the same xterm control sequence documentation for details. #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct DisableAlternateScroll; @@ -111,7 +132,15 @@ impl Command for DisableAlternateScroll { } /// Restore the terminal to its original state. -/// Inverse of `set_modes`. +/// +/// Undo the side effects of [`set_modes`]. +/// +/// - Pops any keyboard enhancement flags that were pushed. +/// - Disables bracketed paste and focus tracking. +/// - Leaves raw mode and ensures the cursor is visible. +/// +/// The disable calls are best-effort because some terminals refuse the matching sequences, +/// especially after a suspend/resume cycle. pub fn restore() -> Result<()> { // Pop may fail on platforms that didn't support the push; ignore errors. let _ = execute!(stdout(), PopKeyboardEnhancementFlags); @@ -122,7 +151,18 @@ pub fn restore() -> Result<()> { Ok(()) } -/// Initialize the terminal (inline viewport; history stays in normal scrollback) +/// Initialize the inline viewport while preserving scrollback. +/// +/// - Rejects initialization if stdout is not a TTY. +/// - Enables the raw-mode feature set via [`set_modes`]. +/// - Installs a panic hook that restores the terminal before unwinding. +/// +/// Existing terminal contents are left untouched so scrollback stays intact, even on terminals that +/// treat `Clear` as destructive. +/// +/// Unlike [`ratatui::Terminal::new`], this uses our `CustomTerminal` wrapper so the interactive +/// viewport stays inline and history remains accessible in the native scrollback buffer—the main +/// architectural difference from Ratatui's default alt-screen workflow. pub fn init() -> Result { if !stdout().is_terminal() { return Err(std::io::Error::other("stdout is not a terminal")); @@ -136,6 +176,11 @@ pub fn init() -> Result { Ok(tui) } +/// Ensure panics drop the terminal back to cooked mode before surfacing. +/// +/// Ratatui defers this to the consumer; we override the default panic hook so that even unexpected +/// crashes clear raw mode and cursor hiding. The hook delegates to the previous handler after +/// restoration so panic reporting (and test harness output) remains unchanged. fn set_panic_hook() { let hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |panic_info| { @@ -146,25 +191,71 @@ fn set_panic_hook() { #[derive(Debug)] pub enum TuiEvent { + /// Raw `crossterm` key event, including modifier-rich variants unlocked by [`set_modes`]. Key(KeyEvent), + /// Bracketed paste payload delivered as a single string. Paste(String), + /// Request to redraw the UI, typically due to focus changes, resizes, or coalesced frame + /// requests. Draw, } +/// Drives the Codex UI on top of an inline [`CustomTerminal`]. +/// +/// Light fork of Ratatui's terminal that adapts to Codex requirements. +/// +/// - Keeps transcript history in the native scrollback while reserving a configurable inline +/// viewport for interactive UI using [`crate::insert_history::insert_history_lines`]. +/// - Owns the scheduler that coalesces draw requests to avoid redundant rendering work, exposing +/// handles via [`FrameRequester`]. +/// - Manages alt-screen transitions so overlays can temporarily take over the full display through +/// [`enter_alt_screen`] and [`leave_alt_screen`]. +/// - Caches capability probes such as keyboard enhancement support and default palette refreshes. +/// +/// [`FrameRequester`]: crate::tui::FrameRequester +/// [`enter_alt_screen`]: Tui::enter_alt_screen +/// [`leave_alt_screen`]: Tui::leave_alt_screen pub struct Tui { + /// Channel used to schedule future frames; owned by the background coalescer task and cloned + /// into every [`FrameRequester`] handed to widgets. frame_schedule_tx: tokio::sync::mpsc::UnboundedSender, + + /// Broadcast channel that delivers draw notifications to the event stream so the UI loop can + /// wake without holding mutable access to the terminal. draw_tx: tokio::sync::broadcast::Sender<()>, + + /// Inline terminal wrapper that keeps the active viewport and history buffers in sync with + /// Ratatui widgets. pub(crate) terminal: Terminal, + + /// History lines waiting to be spliced above the viewport on the next draw; populated by + /// background tasks that stream transcript updates. pending_history_lines: Vec>, + + /// Saved viewport rectangle from inline mode so alt-screen overlays can be restored to the + /// exact scroll position users left. alt_saved_viewport: Option, + + /// Pending resume action recorded by the event loop when `Ctrl+Z` is processed; applied during + /// the next synchronized update to avoid cursor-query races. #[cfg(unix)] - resume_pending: Arc, // Stores a ResumeAction + resume_pending: Arc, + + /// Cached cursor row where the inline viewport ends, ensuring the shell prompt lands beneath + /// the UI after a suspend. #[cfg(unix)] - suspend_cursor_y: Arc, // Bottom line of inline viewport - // True when overlay alt-screen UI is active + suspend_cursor_y: Arc, + + /// Tracks whether an alt-screen overlay currently owns the terminal so mouse wheel handling and + /// viewport restoration behave correctly. alt_screen_active: Arc, - // True when terminal/tab is focused; updated internally from crossterm events + + /// Reflects the window focus state based on Crossterm events; used to gate OSC 9 notifications + /// and palette refreshes. terminal_focused: Arc, + + /// Whether the terminal acknowledged Crossterm's keyboard enhancement flags; controls key hint + /// rendering and modifier handling in the composer. enhanced_keys_supported: bool, } @@ -172,17 +263,28 @@ pub struct Tui { #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[repr(u8)] enum ResumeAction { + /// No post-resume work is required. None = 0, + /// Recenter the inline viewport around the cursor the shell left behind. RealignInline = 1, + /// Re-enter the alternate screen for overlays active when suspend happened. RestoreAlt = 2, } #[cfg(unix)] enum PreparedResumeAction { + /// Restore the alternate screen overlay and refresh its viewport. RestoreAltScreen, + /// Realign the inline viewport to the provided area. RealignViewport(ratatui::layout::Rect), } +/// Swap the pending resume action out of the atomic flag. +/// +/// The event loop records the desired action (realign inline viewport versus restore the alt +/// screen) before invoking [`suspend`]. We clear the flag with relaxed ordering because only the +/// main thread reads it during draw. The integer representation keeps the atomic size small when +/// shared across threads. #[cfg(unix)] fn take_resume_action(pending: &AtomicU8) -> ResumeAction { match pending.swap(ResumeAction::None as u8, Ordering::Relaxed) { @@ -192,14 +294,33 @@ fn take_resume_action(pending: &AtomicU8) -> ResumeAction { } } +/// Handle that lets subsystems ask for a redraw without locking the terminal. +/// +/// Requests are queued onto an unbounded channel and coalesced by the background task spawned in +/// [`Tui::new`]. `schedule_frame_in` exists so call sites can defer work—for example to debounce +/// status updates in [`BottomPane`]. +/// +/// [`BottomPane`]: crate::bottom_pane::BottomPane #[derive(Clone, Debug)] pub struct FrameRequester { + /// Handle to the shared frame scheduler; sending instants through this channel triggers draws + /// once the coalescer decides the deadline has arrived. frame_schedule_tx: tokio::sync::mpsc::UnboundedSender, } + impl FrameRequester { + /// Request an immediate redraw. + /// + /// The scheduler collapses concurrent requests into a single `Draw`, so callers can + /// fire-and-forget without coordinating. pub fn schedule_frame(&self) { let _ = self.frame_schedule_tx.send(Instant::now()); } + /// Request a redraw no earlier than `dur` in the future. + /// + /// Callers use this to debounce follow-up frames (for example, to animate a spinner while + /// waiting on the network). The scheduler still collapses multiple pending deadlines to the + /// earliest instant. pub fn schedule_frame_in(&self, dur: Duration) { let _ = self.frame_schedule_tx.send(Instant::now() + dur); } @@ -217,8 +338,11 @@ impl FrameRequester { } impl Tui { - /// Emit a desktop notification now if the terminal is unfocused. - /// Returns true if a notification was posted. + /// Emit an OSC 9 desktop notification if the terminal pane lacks focus. + /// + /// Returns `true` when a notification escape sequence was written. We only notify when the pane + /// is unfocused so OSC 9 terminals (iTerm2, Kitty, WezTerm) avoid redundant alerts. Terminals + /// that ignore OSC 9 fail silently. pub fn notify(&mut self, message: impl AsRef) -> bool { if !self.terminal_focused.load(Ordering::Relaxed) { let _ = execute!(stdout(), PostNotification(message.as_ref().to_string())); @@ -227,6 +351,15 @@ impl Tui { false } } + + /// Construct the controller around an already-configured terminal backend. + /// + /// - Spawns the background task that coalesces frame requests into `Draw` events, avoiding + /// flicker when history lines arrive in bursts. + /// - Primes capability checks that require talking to the terminal driver so they do not race + /// the async event reader. + /// - Differs from Ratatui's `Terminal::new`, which performs these probes lazily and assumes + /// exclusive control of the event loop. pub fn new(terminal: Terminal) -> Self { let (frame_schedule_tx, frame_schedule_rx) = tokio::sync::mpsc::unbounded_channel(); let (draw_tx, _) = tokio::sync::broadcast::channel(1); @@ -295,16 +428,35 @@ impl Tui { } } + /// Return a cloneable handle for requesting future draws. + /// + /// Widgets hold onto this so they can redraw from async callbacks without needing mutable + /// access to the `Tui`. The returned handle is cheap to clone because it only contains the + /// underlying channel sender. pub fn frame_requester(&self) -> FrameRequester { FrameRequester { frame_schedule_tx: self.frame_schedule_tx.clone(), } } + /// Returns whether the current terminal reported support for Crossterm's keyboard enhancement + /// flags. + /// + /// Consumers use this to decide whether to show modifier-specific key hints. We cache the probe + /// result because the underlying API talks to the terminal driver, and repeated checks would + /// contend with the event reader. pub fn enhanced_keys_supported(&self) -> bool { self.enhanced_keys_supported } + /// Build the async stream that drives the UI loop. + /// + /// - Merges raw `crossterm` events with draw notifications from the frame scheduler. + /// - Handles suspend/resume (`Ctrl+Z`) without exposing implementation details to callers. + /// - Translates focus events into redraws so palette refreshes take effect immediately. + /// - Surfaces bracketed paste as a first-class variant instead of leaving it to widgets. + /// - Disables the alternate scroll escape on exit from alt-screen mode so the shell prompt + /// behaves normally. pub fn event_stream(&self) -> Pin + Send + 'static>> { use tokio_stream::StreamExt; let mut crossterm_events = crossterm::event::EventStream::new(); @@ -337,9 +489,15 @@ impl Tui { // Disable alternate scroll when suspending from alt-screen let _ = execute!(stdout(), DisableAlternateScroll); let _ = execute!(stdout(), LeaveAlternateScreen); - resume_pending.store(ResumeAction::RestoreAlt as u8, Ordering::Relaxed); + resume_pending.store( + ResumeAction::RestoreAlt as u8, + Ordering::Relaxed, + ); } else { - resume_pending.store(ResumeAction::RealignInline as u8, Ordering::Relaxed); + resume_pending.store( + ResumeAction::RealignInline as u8, + Ordering::Relaxed, + ); } #[cfg(unix)] { @@ -376,7 +534,7 @@ impl Tui { yield TuiEvent::Draw; } Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => { - // We dropped one or more draw notifications; coalesce to a single draw. + // We dropped draw notifications; merge the backlog into one draw. yield TuiEvent::Draw; } Err(tokio::sync::broadcast::error::RecvError::Closed) => { @@ -389,6 +547,12 @@ impl Tui { }; Box::pin(event_stream) } + + /// Suspend the process after restoring terminal modes. + /// + /// Triggered internally when the user presses `Ctrl+Z`. The cursor is moved below the inline + /// viewport before the signal so the shell prompt appears beneath the UI once the process + /// stops. On resume [`Tui::draw`] applies any queued viewport adjustments. #[cfg(unix)] fn suspend() -> Result<()> { restore()?; @@ -397,6 +561,12 @@ impl Tui { Ok(()) } + /// Figure out what needs to happen after a suspend before we enter the synchronized update. + /// + /// We determine whether to realign the inline viewport or re-enter the alt-screen outside of + /// the synchronized update lock because querying the cursor while holding the lock can hang in + /// some terminals (WezTerm in particular). Errors from `get_cursor_position` are tolerated so + /// that resume never panics; we fall back to the last known coordinates instead. #[cfg(unix)] fn prepare_resume_action( &mut self, @@ -424,6 +594,11 @@ impl Tui { } } + /// Apply the previously prepared post-resume action inside the synchronized update. + /// + /// Replaying the action here ensures the viewport changes happen atomically with the frame + /// render. When resuming an alt-screen overlay we re-enable the alternate scroll escape so + /// mouse wheels keep mapping to arrow keys. #[cfg(unix)] fn apply_prepared_resume_action(&mut self, prepared: PreparedResumeAction) -> Result<()> { match prepared { @@ -448,8 +623,12 @@ impl Tui { Ok(()) } - /// Enter alternate screen and expand the viewport to full terminal size, saving the current - /// inline viewport for restoration when leaving. + /// Enter the alternate screen, expanding the viewport to the full terminal. + /// + /// We snapshot the inline viewport bounds so that leaving the alt screen can restore the inline + /// history view exactly where it left off. Alternate scroll support is enabled here so mouse + /// wheels map to arrow presses while the overlay is active—a deliberate deviation from + /// Ratatui, where alt screen mode is the default rather than an opt-in overlay. pub fn enter_alt_screen(&mut self) -> Result<()> { let _ = execute!(self.terminal.backend_mut(), EnterAlternateScreen); // Enable "alternate scroll" so terminals may translate wheel to arrows @@ -468,7 +647,11 @@ impl Tui { Ok(()) } - /// Leave alternate screen and restore the previously saved inline viewport, if any. + /// Leave the alternate screen and restore the inline viewport, if present. + /// + /// Alternate scroll is disabled before dropping back to the inline viewport so that shells do + /// not inherit the mapping. If we resumed from a suspend while in the alt screen, the viewport + /// coordinates were already updated in [`prepare_resume_action`]. pub fn leave_alt_screen(&mut self) -> Result<()> { // Disable alternate scroll when leaving alt-screen let _ = execute!(self.terminal.backend_mut(), DisableAlternateScroll); @@ -480,11 +663,37 @@ impl Tui { Ok(()) } + /// Queue history lines to be spliced above the inline viewport. + /// + /// The lines are copied into a pending buffer and applied immediately before the next draw (see + /// [`draw`]). This matches our approach of letting the terminal own the transcript so selection + /// and scrollback behave like a regular terminal log. Callers such as + /// [`App::handle_event`] use it whenever new transcript cells render. + /// + /// [`App::handle_event`]: crate::app::App::handle_event pub fn insert_history_lines(&mut self, lines: Vec>) { self.pending_history_lines.extend(lines); self.frame_requester().schedule_frame(); } + /// Render a frame inside the managed viewport. + /// + /// - `height` caps how tall the inline viewport may grow for this frame; the draw closure + /// receives the same `Frame` type that Ratatui exposes. + /// - Gathers cursor-dependent state (notably viewport alignment) before the synchronized update + /// lock so terminals such as WezTerm avoid cursor-query deadlocks. + /// - Applies suspend/resume bookkeeping so `Ctrl+Z` returns to the same viewport layout. + /// - Updates the viewport, splices pending history lines, refreshes the suspend cursor marker, + /// and finally delegates to the caller's draw logic. + /// + /// Compared to Ratatui's stock `Terminal::draw`, the key differences are inline viewport + /// management and history injection; the rest of the rendering pipeline remains unchanged. + /// Primary callers include [`App::handle_tui_event`] for the main chat loop and overlay flows + /// such as [`run_update_prompt_if_needed`] and [`run_resume_picker`]. + /// + /// [`App::handle_tui_event`]: crate::app::App::handle_tui_event + /// [`run_update_prompt_if_needed`]: crate::update_prompt::run_update_prompt_if_needed + /// [`run_resume_picker`]: crate::resume_picker::run_resume_picker pub fn draw( &mut self, height: u16, @@ -572,8 +781,14 @@ impl Tui { } /// Command that emits an OSC 9 desktop notification with a message. +/// +/// Only a subset of terminals (iTerm2, Kitty, WezTerm) honor OSC 9; others ignore the escape +/// sequence, which is acceptable because [`Tui::notify`] treats write errors as non-fatal. #[derive(Debug, Clone)] -pub struct PostNotification(pub String); +pub struct PostNotification( + /// Message to surface via the OSC 9 escape sequence. + pub String, +); impl Command for PostNotification { fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {