mirror of
https://github.com/openai/codex.git
synced 2026-11-02 19:43:24 +00:00
docs(tui): theme picker, highlight lifecycle, diff rendering contracts
This commit is contained in:
@@ -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<dyn Fn(&AppEventSender) + Send + Sync>;
|
||||
|
||||
/// 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<Box<dyn Fn(usize, &AppEventSender) + Send + Sync>>;
|
||||
|
||||
/// 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<Box<dyn Fn(&AppEventSender) + Send + Sync>>;
|
||||
|
||||
/// One row in a [`ListSelectionView`] selection list.
|
||||
|
||||
@@ -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<RtSpan>` 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<Vec<RtSpan<'static>>> {
|
||||
let mut result: Vec<Vec<RtSpan<'static>>> = Vec::new();
|
||||
let mut current_line: Vec<RtSpan<'static>> = Vec::new();
|
||||
|
||||
@@ -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<SyntaxSet>` | Grammar database, immutable after init |
|
||||
//! | `THEME` | `OnceLock<RwLock<Theme>>` | Active color theme, swappable at runtime |
|
||||
//! | `THEME_OVERRIDE` | `OnceLock<Option<String>>` | Persisted user preference (write-once) |
|
||||
//! | `CODEX_HOME` | `OnceLock<Option<PathBuf>>` | 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<Vec<Span<'stati
|
||||
|
||||
// -- Public API ---------------------------------------------------------------
|
||||
|
||||
/// Highlight code in any supported language, returning styled ratatui Lines.
|
||||
/// Falls back to plain unstyled text when the language is not recognized.
|
||||
/// Highlight code in any supported language, returning styled ratatui `Line`s.
|
||||
///
|
||||
/// Falls back to plain unstyled text when the language is not recognized or the
|
||||
/// input exceeds safety guardrails. Callers can always render the result
|
||||
/// directly -- the fallback path produces equivalent plain-text lines.
|
||||
///
|
||||
/// Used by `markdown_render` for fenced code blocks and by `exec_cell` for bash
|
||||
/// command highlighting.
|
||||
pub(crate) fn highlight_code_to_lines(code: &str, lang: &str) -> Vec<Line<'static>> {
|
||||
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<Line<'static>> {
|
||||
}
|
||||
|
||||
/// 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<Span>` 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,
|
||||
|
||||
@@ -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<u16>)
|
||||
///
|
||||
/// 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>,
|
||||
|
||||
Reference in New Issue
Block a user