diff --git a/codex-rs/tui/src/bottom_pane/list_selection_view.rs b/codex-rs/tui/src/bottom_pane/list_selection_view.rs index 69920e47a1..ea49ff9083 100644 --- a/codex-rs/tui/src/bottom_pane/list_selection_view.rs +++ b/codex-rs/tui/src/bottom_pane/list_selection_view.rs @@ -42,12 +42,16 @@ const MIN_LIST_WIDTH_FOR_SIDE: u16 = 40; /// panel when side-by-side layout is active. const SIDE_CONTENT_GAP: u16 = 2; -/// Width mode for side content when side-by-side layout is active. +/// Controls how the side content panel is sized relative to the popup width. +/// +/// When the computed side width falls below `side_content_min_width` or the +/// remaining list area would be narrower than [`MIN_LIST_WIDTH_FOR_SIDE`], the +/// side-by-side layout is abandoned and the stacked fallback is used instead. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum SideContentWidth { - /// Fixed number of columns. + /// Fixed number of columns. `Fixed(0)` disables side content entirely. Fixed(u16), - /// Exact 50/50 split (minus inter-column gap). + /// Exact 50/50 split of the content area (minus the inter-column gap). Half, } @@ -60,10 +64,13 @@ impl Default for SideContentWidth { /// One selectable item in the generic selection list. pub(crate) type SelectionAction = Box; -/// Callback type for when the selection changes (navigation, filter, number-key). +/// Callback invoked whenever the highlighted item changes (arrow keys, search +/// filter, number-key jump). Receives the *actual* index into the unfiltered +/// `items` list and the event sender. Used by the theme picker for live preview. pub type OnSelectionChangedCallback = Option>; -/// Callback type for when the selection popup is dismissed without accepting (e.g. via Esc or Ctrl+C). +/// Callback invoked when the picker is dismissed without accepting (Esc or +/// Ctrl+C). Used by the theme picker to restore the pre-open theme. pub type OnCancelCallback = Option>; /// One row in a [`ListSelectionView`] selection list. diff --git a/codex-rs/tui/src/diff_render.rs b/codex-rs/tui/src/diff_render.rs index ad973a1cd4..6385b5a84e 100644 --- a/codex-rs/tui/src/diff_render.rs +++ b/codex-rs/tui/src/diff_render.rs @@ -1,3 +1,23 @@ +//! Renders unified diffs with line numbers, gutter signs, and optional syntax +//! highlighting. +//! +//! Each `FileChange` variant (Add / Delete / Update) is rendered as a block of +//! diff lines, each prefixed by a right-aligned line number, a gutter sign +//! (`+` / `-` / ` `), and the content text. When a recognized file extension +//! is present, the content text is syntax-highlighted using +//! [`crate::render::highlight`]. +//! +//! **Highlighting strategy for `Update` diffs:** the renderer highlights each +//! hunk as a single concatenated block rather than line-by-line. This +//! preserves syntect's parser state across consecutive lines within a hunk +//! (important for multi-line strings, block comments, etc.). Cross-hunk state +//! is intentionally *not* preserved because hunks are visually separated and +//! re-synchronize at context boundaries anyway. +//! +//! **Wrapping:** long lines are hard-wrapped at the available column width. +//! Syntax-highlighted spans are split at character boundaries with styles +//! preserved across the split so that no color information is lost. + use diffy::Hunk; use ratatui::buffer::Buffer; use ratatui::layout::Rect; @@ -28,7 +48,11 @@ use crate::render::renderable::Renderable; use codex_core::git_info::get_git_repo_root; use codex_protocol::protocol::FileChange; -// Diff line type used for gutter sign + style selection. +/// Classifies a diff line for gutter sign rendering and style selection. +/// +/// `Insert` renders with a `+` sign and green text, `Delete` with `-` and red +/// text (plus dim overlay when syntax-highlighted), and `Context` with a space +/// and default styling. #[derive(Clone, Copy)] pub(crate) enum DiffLineType { Insert, @@ -465,6 +489,9 @@ pub(crate) fn calculate_add_remove_from_diff(diff: &str) -> (usize, usize) { } } +/// Render a single diff line (no syntax highlighting) as one or more wrapped +/// ratatui `Line`s. The first output line carries the gutter sign; continuation +/// lines are indented under the line number column. pub(crate) fn push_wrapped_diff_line( line_number: usize, kind: DiffLineType, @@ -475,6 +502,9 @@ pub(crate) fn push_wrapped_diff_line( push_wrapped_diff_line_inner(line_number, kind, text, width, line_number_width, None) } +/// Render a single diff line with pre-computed syntax spans. The sign character +/// uses the diff color; content gets syntax colors with a dim overlay for delete +/// lines. pub(crate) fn push_wrapped_diff_line_with_syntax( line_number: usize, kind: DiffLineType, @@ -598,8 +628,16 @@ fn push_wrapped_diff_line_inner( } /// Split styled spans into chunks that fit within `max_cols` display columns. +/// /// Returns one `Vec` per output line. Styles are preserved across /// split boundaries so that wrapping never loses syntax coloring. +/// +/// The algorithm walks characters using their Unicode display width (with tabs +/// expanded to [`TAB_WIDTH`] columns). When a character would overflow the +/// current line, the accumulated text is flushed and a new line begins. A +/// single character wider than the remaining space forces a line break *before* +/// the character so that progress is always made (avoiding infinite loops on +/// CJK characters or tabs at the end of a line). fn wrap_styled_spans(spans: &[RtSpan<'static>], max_cols: usize) -> Vec>> { let mut result: Vec>> = Vec::new(); let mut current_line: Vec> = Vec::new(); diff --git a/codex-rs/tui/src/render/highlight.rs b/codex-rs/tui/src/render/highlight.rs index 6c7b41ca1f..6cbd9c8c37 100644 --- a/codex-rs/tui/src/render/highlight.rs +++ b/codex-rs/tui/src/render/highlight.rs @@ -1,3 +1,26 @@ +//! Syntax highlighting engine for the TUI. +//! +//! Wraps [syntect] with the [two_face] grammar and theme bundles to provide +//! ~250-language syntax highlighting and 32 bundled color themes. The module +//! owns four process-global singletons: +//! +//! | Singleton | Type | Purpose | +//! |---|---|---| +//! | `SYNTAX_SET` | `OnceLock` | Grammar database, immutable after init | +//! | `THEME` | `OnceLock>` | Active color theme, swappable at runtime | +//! | `THEME_OVERRIDE` | `OnceLock>` | Persisted user preference (write-once) | +//! | `CODEX_HOME` | `OnceLock>` | Root for custom `.tmTheme` discovery | +//! +//! **Lifecycle:** call [`set_theme_override`] once at startup (after the final +//! config is resolved) to persist the user preference and seed the `THEME` +//! lock. After that, [`set_syntax_theme`] and [`current_syntax_theme`] can +//! swap/snapshot the theme for live preview. All highlighting functions read +//! the theme via `theme_lock()`. +//! +//! **Guardrails:** inputs exceeding 512 KB or 10 000 lines are rejected early +//! (returns `None`) to prevent pathological CPU/memory usage. Callers must +//! fall back to plain unstyled text. + use ratatui::style::Color as RtColor; use ratatui::style::Modifier; use ratatui::style::Style; @@ -235,9 +258,13 @@ pub(crate) fn resolve_theme_by_name(name: &str, codex_home: Option<&Path>) -> Op None } -/// A theme available in the picker. +/// A theme available in the picker, either bundled or loaded from a custom +/// `.tmTheme` file under `{CODEX_HOME}/themes/`. pub(crate) struct ThemeEntry { + /// Kebab-case identifier used for config persistence and theme resolution. pub name: String, + /// `true` when this entry was discovered from a `.tmTheme` file on disk + /// rather than the embedded two-face bundle. pub is_custom: bool, } @@ -450,8 +477,14 @@ fn highlight_to_line_spans(code: &str, lang: &str) -> Option Vec> { if let Some(line_spans) = highlight_to_line_spans(code, lang) { line_spans.into_iter().map(Line::from).collect() @@ -474,7 +507,14 @@ pub(crate) fn highlight_bash_to_lines(script: &str) -> Vec> { } /// Highlight code and return per-line styled spans for diff integration. -/// Returns None if the language is unsupported. +/// +/// Returns `None` when the language is unrecognized or the input exceeds +/// guardrails. The caller (`diff_render`) uses this signal to fall back to +/// plain diff coloring. +/// +/// Each inner `Vec` corresponds to one source line. Styles are derived +/// from the active theme but backgrounds are intentionally omitted so the +/// terminal's own background shows through. pub(crate) fn highlight_code_to_styled_spans( code: &str, lang: &str, diff --git a/codex-rs/tui/src/theme_picker.rs b/codex-rs/tui/src/theme_picker.rs index 368f4949f1..fa89876c25 100644 --- a/codex-rs/tui/src/theme_picker.rs +++ b/codex-rs/tui/src/theme_picker.rs @@ -1,3 +1,24 @@ +//! Builds the `/theme` picker dialog for the TUI. +//! +//! The picker lists all bundled themes plus any custom `.tmTheme` files found +//! under `{CODEX_HOME}/themes/`. It provides: +//! +//! - **Live preview:** the `on_selection_changed` callback swaps the runtime +//! syntax theme as the user navigates, giving instant visual feedback in both +//! the preview panel and any visible code blocks. +//! - **Cancel-restore:** on dismiss (Esc / Ctrl+C) the `on_cancel` callback +//! restores the theme snapshot taken when the picker opened. +//! - **Persist on confirm:** the `AppEvent::SyntaxThemeSelected` action persists +//! `[tui] theme = "..."` to `config.toml` via `ConfigEditsBuilder`. +//! +//! Two preview renderables adapt to terminal width: +//! +//! - `ThemePreviewWideRenderable` -- vertically centered, inset by 2 columns, +//! shown in the side panel when the terminal is wide enough for side-by-side +//! layout (>= 44-column side panel and >= 40-column list). +//! - `ThemePreviewNarrowRenderable` -- compact 4-line snippet stacked below the +//! list when side-by-side does not fit. + use std::path::Path; use crate::app_event::AppEvent; @@ -122,9 +143,12 @@ const SIDE_CONTENT_GAP: u16 = 2; /// Minimum list width required for side-by-side mode in the selection popup. const MIN_LIST_WIDTH_FOR_SIDE: u16 = 40; -/// Renders a syntax-highlighted code snippet below the theme list so users can -/// preview what each theme looks like on real code. +/// Side-by-side preview: syntax-highlighted Rust diff snippet, vertically +/// centered with a 2-column left inset. Fills the entire side panel height. struct ThemePreviewWideRenderable; + +/// Stacked preview: compact 4-line Rust diff snippet shown below the list +/// when the terminal is too narrow for side-by-side layout. struct ThemePreviewNarrowRenderable; fn preview_diff_line_type(kind: PreviewDiffKind) -> DiffLineType { @@ -272,6 +296,12 @@ fn theme_picker_subtitle(codex_home: Option<&Path>, terminal_width: Option) /// /// Lists all bundled themes plus custom `.tmTheme` files, with live preview /// on cursor movement and cancel-restore. +/// +/// `current_name` should be the value of `Config::tui_theme` (the persisted +/// preference). When it names a theme that is currently available the picker +/// pre-selects it; otherwise the picker falls back to the active runtime theme +/// (from adaptive auto-detection) so opening the picker without a persisted +/// preference still highlights the correct entry. pub(crate) fn build_theme_picker_params( current_name: Option<&str>, codex_home: Option<&Path>,