2026-07-16 06:46:02 +01:00
|
|
|
|
//! Appearance configuration for the pager.
|
|
|
|
|
|
//!
|
|
|
|
|
|
//! Two-layer design:
|
|
|
|
|
|
//! - `RawAppearanceConfig`: Serde-friendly types for TOML (de)serialization
|
|
|
|
|
|
//! - `AppearanceConfig`: Runtime types with ratatui::Color, BlockBackground, etc.
|
|
|
|
|
|
|
|
|
|
|
|
use documented::{Documented, DocumentedFields};
|
|
|
|
|
|
use ratatui::style::Color;
|
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
use toml_edit::{DocumentMut, Item, RawString};
|
|
|
|
|
|
use xai_grok_shared::ui_config::UiConfig;
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// Runtime Config (used by render code)
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
/// Background style for block content area.
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
|
|
|
|
|
|
pub enum BlockBackground {
|
|
|
|
|
|
#[default]
|
|
|
|
|
|
None,
|
|
|
|
|
|
Light,
|
|
|
|
|
|
Dark,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Runtime appearance configuration with resolved types.
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
|
pub struct AppearanceConfig {
|
|
|
|
|
|
pub animation: AnimationConfig,
|
|
|
|
|
|
pub prompt: PromptViewConfig,
|
|
|
|
|
|
pub scrollback: ScrollbackConfig,
|
|
|
|
|
|
pub todo: TodoConfig,
|
|
|
|
|
|
pub turn_status: TurnStatusConfig,
|
|
|
|
|
|
/// Show timestamps on user/agent messages. Toggled via `/timestamps`.
|
|
|
|
|
|
pub show_timestamps: bool,
|
|
|
|
|
|
/// Timeline sidebar (per-turn tick rail). Toggled via `/timeline`.
|
|
|
|
|
|
pub show_timeline: bool,
|
|
|
|
|
|
/// Whether hooks & plugins UI is disabled (hides /hooks, /plugins commands
|
|
|
|
|
|
/// and scrollback annotations). `false` by default (plugins enabled).
|
|
|
|
|
|
pub disable_plugins: bool,
|
|
|
|
|
|
/// Always show the "plan" chip in the status bar when plan content is
|
|
|
|
|
|
/// available, even after the user exits plan mode.
|
|
|
|
|
|
/// `false` by default (chip hidden once plan mode ends).
|
|
|
|
|
|
pub show_plan_chip: bool,
|
|
|
|
|
|
/// Alt-screen (fullscreen) policy from the `[terminal]` section.
|
|
|
|
|
|
pub alt_screen: crate::terminal::AltScreenMode,
|
|
|
|
|
|
/// Experimental scrollback-native minimal mode (`[terminal] minimal`).
|
|
|
|
|
|
pub minimal: bool,
|
|
|
|
|
|
/// Pinned live-region height (rows) in minimal mode. Clamped to
|
|
|
|
|
|
/// `[3, term_height - 1]` at runtime.
|
|
|
|
|
|
pub minimal_live_rows: u16,
|
|
|
|
|
|
/// Maximum rows a single committed block may occupy in minimal mode before
|
|
|
|
|
|
/// it is truncated with a "… N more lines" footer.
|
|
|
|
|
|
pub minimal_max_commit_rows: u16,
|
2026-07-28 22:50:19 +00:00
|
|
|
|
/// Resolved `[terminal] minimal_collapse_thinking`.
|
|
|
|
|
|
pub minimal_collapse_thinking: bool,
|
2026-07-16 06:46:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for AppearanceConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
RawAppearanceConfig::default().into()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Turn status line configuration.
|
|
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
|
|
|
|
pub struct TurnStatusConfig {
|
|
|
|
|
|
/// When true, add a 1-line gap between the turn status line and the prompt
|
|
|
|
|
|
/// widget. Allows visual separation; also enables future background styling
|
|
|
|
|
|
/// without merging with the prompt's lighter background.
|
|
|
|
|
|
pub gap: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for TurnStatusConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self { gap: true }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Prompt input view configuration (the editor widget, not the scrollback block).
|
|
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
|
|
|
|
pub struct PromptViewConfig {
|
|
|
|
|
|
/// When true, the prompt collapses to its minimum height (single-line)
|
|
|
|
|
|
/// when focus is in the scrollback pane. Expands back when focused.
|
|
|
|
|
|
pub collapse_unfocused: bool,
|
|
|
|
|
|
/// Show hover highlight box when mousing over the prompt widget.
|
|
|
|
|
|
pub mouse_hover: bool,
|
|
|
|
|
|
/// Show the ❯ prefix character in the prompt editor.
|
|
|
|
|
|
pub show_prefix: bool,
|
|
|
|
|
|
/// Compact mode: remove top padding and reduce info block padding.
|
|
|
|
|
|
/// Toggled at runtime via `/compact-mode`. This is the DERIVED render
|
|
|
|
|
|
/// value, which the app may force on for short terminals (the persisted
|
|
|
|
|
|
/// user setting is `UiConfig::compact_mode`) — in the pager, write it
|
|
|
|
|
|
/// only via `AppView::apply_effective_compact`.
|
|
|
|
|
|
pub compact: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for PromptViewConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
collapse_unfocused: true,
|
|
|
|
|
|
mouse_hover: true,
|
|
|
|
|
|
show_prefix: true,
|
|
|
|
|
|
compact: false,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Scrollback pane configuration (layout, scrollbar, scroll, block rendering).
|
|
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
|
|
|
|
pub struct ScrollbackConfig {
|
|
|
|
|
|
pub layout: LayoutConfig,
|
|
|
|
|
|
pub scrollbar: ScrollbarConfig,
|
|
|
|
|
|
pub scroll: ScrollConfig,
|
|
|
|
|
|
pub blocks: BlocksConfig,
|
|
|
|
|
|
pub display: ScrollbackDisplayConfig,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Scrollback display options (grouping, accents, etc.).
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
|
pub struct ScrollbackDisplayConfig {
|
|
|
|
|
|
/// Render a subtle horizontal line below the last entry.
|
|
|
|
|
|
/// Visual marker for "end of content".
|
|
|
|
|
|
pub line_under_last_entry: bool,
|
|
|
|
|
|
/// Accent character for collapsed groupable blocks (default: "❙").
|
|
|
|
|
|
/// Used instead of "┃" to prevent adjacent accents from merging visually.
|
|
|
|
|
|
pub collapsed_accent_char: String,
|
|
|
|
|
|
/// Blend factor for dimmed accents on collapsed groupable blocks (0.0–1.0).
|
|
|
|
|
|
/// 0.0 = invisible (fully bg), 1.0 = full accent color. Default: 0.5.
|
|
|
|
|
|
pub dim_accent: f32,
|
|
|
|
|
|
/// Group selection box mode.
|
|
|
|
|
|
/// When `true` (Mode B / "split"): selection box wraps only the contiguous
|
|
|
|
|
|
/// collapsed sub-group around the selected entry. Expanded blocks within a
|
|
|
|
|
|
/// group get their own individual selection box.
|
|
|
|
|
|
/// When `false` (Mode A / "always"): selection box wraps the entire group
|
|
|
|
|
|
/// regardless of expanded blocks.
|
|
|
|
|
|
/// Default: `true` (Mode B).
|
|
|
|
|
|
pub group_selection_split: bool,
|
|
|
|
|
|
/// When true, the active-block highlight within a group extends over the
|
|
|
|
|
|
/// selection box border columns (│). When false (default), the highlight is
|
|
|
|
|
|
/// inset by 1 column on each side so the borders remain uncolored.
|
|
|
|
|
|
pub highlight_overlays_border: bool,
|
|
|
|
|
|
/// When true, the bullet character of the selected entry is replaced with
|
|
|
|
|
|
/// an expand indicator (e.g., "›") if the block is foldable and collapsed.
|
|
|
|
|
|
/// Helps indicate which entries can be expanded with 'l' or 'e'.
|
|
|
|
|
|
/// Default: true.
|
|
|
|
|
|
pub expandable_indicator: bool,
|
|
|
|
|
|
/// When true, also show the expand indicator on running entries that are
|
|
|
|
|
|
/// in their minimum fold mode (e.g., Truncated for execute/thinking blocks).
|
|
|
|
|
|
/// The indicator inherits the block's animated accent style (blinking).
|
|
|
|
|
|
/// Default: true.
|
|
|
|
|
|
pub expandable_indicator_running: bool,
|
|
|
|
|
|
/// Character to use as the expand indicator. Default: "›".
|
|
|
|
|
|
pub expandable_indicator_char: String,
|
|
|
|
|
|
/// Show ⧉ (copy) and ↗ (view) buttons on the selection box.
|
|
|
|
|
|
/// Default: false (opt-in while testing).
|
|
|
|
|
|
pub selection_buttons: bool,
|
|
|
|
|
|
/// Pin user prompts as sticky headers when scrolled past.
|
|
|
|
|
|
/// Default: true.
|
|
|
|
|
|
pub sticky_headers: bool,
|
|
|
|
|
|
/// Number of spaces to use when expanding tab characters (\t) in content.
|
|
|
|
|
|
/// Tabs in model output are replaced with this many spaces before rendering.
|
|
|
|
|
|
/// Default: 4. Set to 0 to pass through tabs unchanged.
|
|
|
|
|
|
pub tab_width: u8,
|
|
|
|
|
|
/// Maximum number of visible entries in a group of consecutive collapsed
|
|
|
|
|
|
/// tool-call / thinking blocks. Older entries beyond this limit are hidden
|
|
|
|
|
|
/// behind a compact "╶╶ N more" header. 0 disables group truncation.
|
|
|
|
|
|
/// Default: 10.
|
|
|
|
|
|
pub group_max_visible: u16,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for ScrollbackDisplayConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
line_under_last_entry: false,
|
|
|
|
|
|
collapsed_accent_char: crate::glyphs::collapsed_accent().to_string(),
|
|
|
|
|
|
dim_accent: 0.5,
|
|
|
|
|
|
group_selection_split: true, // Mode B by default
|
|
|
|
|
|
highlight_overlays_border: false,
|
|
|
|
|
|
expandable_indicator: true,
|
|
|
|
|
|
expandable_indicator_running: true,
|
|
|
|
|
|
expandable_indicator_char: "›".to_string(),
|
|
|
|
|
|
selection_buttons: false,
|
|
|
|
|
|
sticky_headers: true,
|
|
|
|
|
|
tab_width: 4,
|
|
|
|
|
|
group_max_visible: 10,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Layout configuration for viewport padding and block spacing.
|
|
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
|
|
|
|
pub struct LayoutConfig {
|
|
|
|
|
|
/// Vertical padding (top/bottom) for outer viewport.
|
|
|
|
|
|
pub outer_vpad: u16,
|
|
|
|
|
|
/// Left horizontal padding for outer viewport.
|
|
|
|
|
|
pub outer_hpad_left: u16,
|
|
|
|
|
|
/// Right horizontal padding for outer viewport.
|
|
|
|
|
|
pub outer_hpad_right: u16,
|
|
|
|
|
|
/// Padding after accent line, before content (inside block bg).
|
|
|
|
|
|
pub block_pad_left: u16,
|
|
|
|
|
|
/// Padding after content, at right edge (inside block bg).
|
|
|
|
|
|
pub block_pad_right: u16,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for LayoutConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
outer_vpad: 1,
|
|
|
|
|
|
outer_hpad_left: 2,
|
|
|
|
|
|
outer_hpad_right: 2,
|
|
|
|
|
|
block_pad_left: 2,
|
|
|
|
|
|
block_pad_right: 2, // Match left padding for symmetry
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl LayoutConfig {
|
|
|
|
|
|
/// Minimum value for horizontal padding (must have room for selection border).
|
|
|
|
|
|
pub const MIN_HPAD: u16 = 1;
|
|
|
|
|
|
|
|
|
|
|
|
/// Effective outer vertical padding (0 in compact mode).
|
|
|
|
|
|
pub fn eff_outer_vpad(&self, compact: bool) -> u16 {
|
|
|
|
|
|
if compact { 0 } else { self.outer_vpad }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Effective left horizontal padding (MIN_HPAD in compact mode).
|
|
|
|
|
|
pub fn eff_hpad_left(&self, compact: bool) -> u16 {
|
|
|
|
|
|
if compact {
|
|
|
|
|
|
Self::MIN_HPAD
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.outer_hpad_left
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Effective right horizontal padding (MIN_HPAD in compact mode).
|
|
|
|
|
|
pub fn eff_hpad_right(&self, compact: bool) -> u16 {
|
|
|
|
|
|
if compact {
|
|
|
|
|
|
Self::MIN_HPAD
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.outer_hpad_right
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Validate and clamp values to valid ranges.
|
|
|
|
|
|
pub fn validated(self) -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
outer_vpad: self.outer_vpad,
|
|
|
|
|
|
outer_hpad_left: self.outer_hpad_left.max(Self::MIN_HPAD),
|
|
|
|
|
|
outer_hpad_right: self.outer_hpad_right.max(Self::MIN_HPAD),
|
|
|
|
|
|
block_pad_left: self.block_pad_left,
|
|
|
|
|
|
block_pad_right: self.block_pad_right,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Scrollbar configuration.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Positioning
|
|
|
|
|
|
///
|
|
|
|
|
|
/// The scrollbar position is computed as:
|
|
|
|
|
|
/// - `scrollbar_x = screen_right - gap_right - 1`
|
|
|
|
|
|
/// - Content ends at: `scrollbar_x - gap_left`
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Content Width Clamping
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Content width is automatically clamped to not extend beyond the outer
|
|
|
|
|
|
/// viewport padding. This means:
|
|
|
|
|
|
/// - With `gap_right=0` (scrollbar at screen edge), the scrollbar is in `outer_hpad_right`
|
|
|
|
|
|
/// - With `gap_left=0`, content extends to just before the scrollbar
|
|
|
|
|
|
/// - But content will never exceed `outer_hpad_right` boundary on the right
|
|
|
|
|
|
///
|
|
|
|
|
|
/// This allows flexible scrollbar positioning without content overflow.
|
|
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
|
|
|
|
pub struct ScrollbarConfig {
|
|
|
|
|
|
/// Whether scrollbar is enabled.
|
|
|
|
|
|
pub enabled: bool,
|
|
|
|
|
|
/// Gap between content/selection edge and scrollbar track.
|
|
|
|
|
|
/// 0 = adjacent to content, 1+ = space between content and scrollbar.
|
|
|
|
|
|
pub gap_left: u16,
|
|
|
|
|
|
/// Gap between scrollbar track and screen edge.
|
|
|
|
|
|
/// 0 = scrollbar at screen edge (in outer_hpad_right if > 0).
|
|
|
|
|
|
pub gap_right: u16,
|
|
|
|
|
|
/// Override scrollbar background color (None = use theme default).
|
|
|
|
|
|
pub scrollbar_bg: Option<Color>,
|
|
|
|
|
|
/// Override scrollbar foreground/thumb color (None = use theme default).
|
|
|
|
|
|
pub scrollbar_fg: Option<Color>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for ScrollbarConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
gap_left: 0, // Content adjacent to scrollbar
|
|
|
|
|
|
gap_right: 0, // Scrollbar at screen edge
|
|
|
|
|
|
scrollbar_bg: None,
|
|
|
|
|
|
scrollbar_fg: None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl ScrollbarConfig {
|
|
|
|
|
|
/// Total width reserved for scrollbar (gap_left + track + gap_right).
|
|
|
|
|
|
pub fn total_width(&self) -> u16 {
|
|
|
|
|
|
if self.enabled {
|
|
|
|
|
|
self.gap_left + 1 + self.gap_right
|
|
|
|
|
|
} else {
|
|
|
|
|
|
0
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Whether scrollbar fits entirely within outer_hpad_right.
|
|
|
|
|
|
pub fn is_outside(&self, outer_hpad_right: u16) -> bool {
|
|
|
|
|
|
self.gap_right < outer_hpad_right
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Scroll behavior configuration.
|
|
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
|
|
|
|
pub struct ScrollConfig {
|
|
|
|
|
|
/// Minimum lines of context to keep above/below selected entry.
|
|
|
|
|
|
/// When navigating, ensure at least this many lines of adjacent entries
|
|
|
|
|
|
/// remain visible. 0 = scroll to edge (default).
|
|
|
|
|
|
pub margin: u16,
|
|
|
|
|
|
/// Minimum scroll as a fraction of viewport height (0-100).
|
|
|
|
|
|
/// If a scroll would be less than this percentage of the viewport,
|
|
|
|
|
|
/// scroll by this amount instead. 0 = minimal scroll (default).
|
|
|
|
|
|
/// 100 = always scroll by full page.
|
|
|
|
|
|
pub min_page_fraction: u8,
|
|
|
|
|
|
/// Follow indicator style in the gap row below scrollback.
|
|
|
|
|
|
pub follow_indicator: FollowIndicator,
|
|
|
|
|
|
/// When follow mode scrolls to new content, auto-select the latest entry.
|
|
|
|
|
|
pub follow_auto_select: bool,
|
|
|
|
|
|
/// Scrolling past the bottom (j, Ctrl-D, page-down, mousewheel) engages follow mode.
|
|
|
|
|
|
pub follow_by_overscroll: bool,
|
|
|
|
|
|
/// When true (default), expanding/collapsing a block adjusts scroll_offset so
|
|
|
|
|
|
/// the block's header line stays at the same screen position. When false, uses
|
|
|
|
|
|
/// ensure_selected_visible (the block may shift on screen).
|
|
|
|
|
|
pub anchor_on_fold: bool,
|
|
|
|
|
|
pub respect_manual_folds: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for ScrollConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
margin: 0,
|
|
|
|
|
|
min_page_fraction: 0,
|
|
|
|
|
|
follow_indicator: FollowIndicator::Center,
|
|
|
|
|
|
follow_auto_select: true,
|
|
|
|
|
|
follow_by_overscroll: true,
|
|
|
|
|
|
anchor_on_fold: true,
|
|
|
|
|
|
respect_manual_folds: false,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-03 08:17:57 +00:00
|
|
|
|
/// Scroll indicator display mode: the ▼ jump-to-bottom arrow below
|
|
|
|
|
|
/// scrollback and its ▲ jump-to-response-top mirror under the sticky
|
|
|
|
|
|
/// prompt header.
|
2026-07-16 06:46:02 +01:00
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
|
|
|
|
pub enum FollowIndicator {
|
2026-08-03 08:17:57 +00:00
|
|
|
|
/// No scroll indicators.
|
2026-07-16 06:46:02 +01:00
|
|
|
|
None,
|
|
|
|
|
|
/// Show ▼ centered in the gap row below scrollback when not following
|
2026-08-03 08:17:57 +00:00
|
|
|
|
/// and there's content below the viewport, and ▲ centered under the
|
|
|
|
|
|
/// sticky prompt header while the answer being read starts above the
|
|
|
|
|
|
/// viewport top.
|
2026-07-16 06:46:02 +01:00
|
|
|
|
#[default]
|
|
|
|
|
|
Center,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl ScrollConfig {
|
|
|
|
|
|
/// Compute the minimum scroll amount in lines for a given viewport height.
|
|
|
|
|
|
pub fn min_scroll_lines(&self, viewport_height: u16) -> u16 {
|
|
|
|
|
|
if self.min_page_fraction == 0 {
|
|
|
|
|
|
0
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let fraction =
|
|
|
|
|
|
(self.min_page_fraction.min(100) as u32) * (viewport_height as u32) / 100;
|
|
|
|
|
|
fraction as u16
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Animation configuration.
|
|
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
|
|
|
|
pub struct AnimationConfig {
|
|
|
|
|
|
/// Animation frame rate (ticks per second).
|
|
|
|
|
|
/// Higher = smoother but more CPU. Default: 30.
|
|
|
|
|
|
pub fps: u8,
|
|
|
|
|
|
/// Rows per wave cycle for accent line animation.
|
|
|
|
|
|
/// Lower = faster wave, higher = slower/smoother wave. Default: 32.
|
|
|
|
|
|
pub wave_rows: u16,
|
|
|
|
|
|
/// Show an FPS counter overlay in the top-right corner (debug/dev builds only).
|
|
|
|
|
|
/// Also enabled by the `GROK_FPS=1` env var. Default: false.
|
|
|
|
|
|
pub show_fps: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for AnimationConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
fps: 30,
|
|
|
|
|
|
wave_rows: 32,
|
|
|
|
|
|
show_fps: false,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl AnimationConfig {
|
|
|
|
|
|
/// Get the tick interval as a Duration.
|
|
|
|
|
|
pub fn tick_interval(&self) -> std::time::Duration {
|
|
|
|
|
|
let fps = self.fps.max(1) as u64;
|
|
|
|
|
|
std::time::Duration::from_millis(1000 / fps)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Badge format for the todo status counts in the status bar.
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
|
|
|
|
pub enum TodoBadgeFormat {
|
|
|
|
|
|
/// Colon format: `[▶:1 □:4 ✓:3 ✗:2]` — compact, icon:count.
|
|
|
|
|
|
Colon,
|
|
|
|
|
|
/// Comma format: `[1 ▶, 4 □, 3 ✓, 2 ✗]` — count icon, comma-separated.
|
|
|
|
|
|
Comma,
|
|
|
|
|
|
/// Default format: `2/5` — a `done/total` progress fraction (done =
|
|
|
|
|
|
/// completed, total = all tasks except cancelled).
|
|
|
|
|
|
#[default]
|
|
|
|
|
|
Default,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Todo pane configuration.
|
|
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
|
|
|
|
pub struct TodoConfig {
|
|
|
|
|
|
/// Badge format in the status bar.
|
|
|
|
|
|
pub badge_format: TodoBadgeFormat,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for TodoConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
badge_format: TodoBadgeFormat::Default,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
|
|
|
|
pub struct BlocksConfig {
|
|
|
|
|
|
pub edit: EditBlockConfig,
|
|
|
|
|
|
pub prompt: PromptConfig,
|
|
|
|
|
|
pub thinking: ThinkingConfig,
|
|
|
|
|
|
pub tool: ToolConfig,
|
|
|
|
|
|
pub list_dir: ListDirConfig,
|
|
|
|
|
|
pub execute: ExecuteConfig,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Runtime config for EditBlock with resolved ratatui types.
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
|
pub struct EditBlockConfig {
|
|
|
|
|
|
pub indent: bool,
|
|
|
|
|
|
pub vpad: bool,
|
|
|
|
|
|
pub bg: BlockBackground,
|
|
|
|
|
|
pub accent_bg: bool,
|
|
|
|
|
|
pub accent: Option<Color>,
|
|
|
|
|
|
pub gutter_bg: bool,
|
|
|
|
|
|
pub indent_bg: bool,
|
|
|
|
|
|
/// Show the +N/-M line summary in the collapsed header. `None` (default)
|
|
|
|
|
|
/// follows the shell-owned `collapsed_edit_blocks` flag; an explicit
|
|
|
|
|
|
/// pager.toml value pins the shape regardless of the flag.
|
|
|
|
|
|
pub line_summary: Option<bool>,
|
|
|
|
|
|
/// When true, Edit blocks start in Expanded mode showing the diff; when
|
|
|
|
|
|
/// false, they start Collapsed (one-line summary). `None` (default)
|
|
|
|
|
|
/// follows the shell-owned `collapsed_edit_blocks` flag; an explicit
|
|
|
|
|
|
/// pager.toml value pins the shape regardless of the flag.
|
|
|
|
|
|
pub expanded_by_default: Option<bool>,
|
|
|
|
|
|
/// Separator between diff hunks.
|
|
|
|
|
|
/// Options: "…" (ellipsis, default), "───" (line), "⋯" (midline), "" (none).
|
|
|
|
|
|
pub hunk_separator: String,
|
|
|
|
|
|
/// Show two line-number columns (old + new) like GitHub's unified diff.
|
|
|
|
|
|
/// When false (default), show a single column with the new-file line number.
|
|
|
|
|
|
pub dual_line_numbers: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for EditBlockConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
indent: true,
|
|
|
|
|
|
vpad: false,
|
|
|
|
|
|
bg: BlockBackground::None,
|
|
|
|
|
|
accent_bg: false,
|
|
|
|
|
|
accent: None,
|
|
|
|
|
|
gutter_bg: false,
|
|
|
|
|
|
indent_bg: false,
|
|
|
|
|
|
line_summary: None,
|
|
|
|
|
|
expanded_by_default: None,
|
|
|
|
|
|
hunk_separator: "…".to_string(),
|
|
|
|
|
|
dual_line_numbers: false,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl EditBlockConfig {
|
|
|
|
|
|
/// Effective "Edit blocks start expanded" default. The single policy
|
|
|
|
|
|
/// point pairing the two owners: an explicit pager.toml value wins;
|
|
|
|
|
|
/// unset defers to the shell-owned `collapsed_edit_blocks` flag
|
|
|
|
|
|
/// (flag on = collapsed one-liner, off = legacy expanded diff).
|
|
|
|
|
|
pub fn effective_expanded(&self, collapsed_edit_blocks: bool) -> bool {
|
|
|
|
|
|
self.expanded_by_default.unwrap_or(!collapsed_edit_blocks)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Effective collapsed-header `+N/-M` diffstat toggle. Same pairing as
|
|
|
|
|
|
/// [`Self::effective_expanded`]: explicit value wins; unset shows the
|
|
|
|
|
|
/// diffstat exactly when the flag collapses Edits (the one-liner view
|
|
|
|
|
|
/// is what the summary exists for).
|
|
|
|
|
|
pub fn effective_line_summary(&self, collapsed_edit_blocks: bool) -> bool {
|
|
|
|
|
|
self.line_summary.unwrap_or(collapsed_edit_blocks)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Runtime config for user prompt block (rendered inside scrollback).
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
|
pub struct PromptConfig {
|
|
|
|
|
|
/// Whether to apply vertical padding (blank lines above/below).
|
|
|
|
|
|
pub vpad: bool,
|
|
|
|
|
|
/// Block background color.
|
|
|
|
|
|
pub bg: BlockBackground,
|
|
|
|
|
|
/// Whether accent column gets block's background.
|
|
|
|
|
|
pub accent_bg: bool,
|
|
|
|
|
|
/// Minimum content lines to show in truncated/sticky header mode.
|
|
|
|
|
|
/// This is the number of actual content lines, not including vpad.
|
|
|
|
|
|
pub min_lines: u16,
|
|
|
|
|
|
/// Show the ❯ prefix character before the prompt text.
|
|
|
|
|
|
pub show_prefix: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for PromptConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
vpad: true,
|
|
|
|
|
|
bg: BlockBackground::Light,
|
|
|
|
|
|
accent_bg: false,
|
|
|
|
|
|
min_lines: 2,
|
|
|
|
|
|
show_prefix: true,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Runtime config for thinking/reasoning block.
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
|
pub struct ThinkingConfig {
|
|
|
|
|
|
/// Accent color for the thinking block.
|
|
|
|
|
|
pub accent: Color,
|
|
|
|
|
|
/// Whether accent line is enabled. When false, no accent in any mode.
|
|
|
|
|
|
pub accent_enabled: bool,
|
|
|
|
|
|
/// How much to blend markdown colors with background (0.0-1.0).
|
|
|
|
|
|
/// 0.8 means 80% original color, 20% background.
|
|
|
|
|
|
pub bg_blend: f32,
|
|
|
|
|
|
/// Number of visual lines to show in truncated mode (before and after ellipsis).
|
|
|
|
|
|
pub truncated_lines: u16,
|
|
|
|
|
|
/// Whether the accent line animates (traveling wave) while thinking is active.
|
|
|
|
|
|
pub animate: bool,
|
|
|
|
|
|
/// Show header line ("Thinking..." / "Thought for Xs") in all display modes.
|
|
|
|
|
|
/// When false (default), the header only appears in collapsed mode.
|
|
|
|
|
|
/// When true, it appears as the first line in truncated and expanded modes too.
|
|
|
|
|
|
pub header: bool,
|
|
|
|
|
|
/// When true, the header uses brighter styling in non-collapsed modes
|
|
|
|
|
|
/// (matching tool block title style), and respects muted_collapsed when collapsed.
|
|
|
|
|
|
/// When false (default), the header is always dim/muted gray.
|
|
|
|
|
|
pub header_bright: bool,
|
2026-07-28 22:50:19 +00:00
|
|
|
|
/// Render the reasoning body de-emphasized (SGR dim + italic) on top of the
|
|
|
|
|
|
/// `bg_blend` fade, for surfaces where the fade alone cannot separate
|
|
|
|
|
|
/// reasoning from the answer. **Not a TOML key** — minimal mode sets it;
|
|
|
|
|
|
/// see the minimal-mode design doc §6.16.
|
|
|
|
|
|
pub body_dim_italic: bool,
|
|
|
|
|
|
/// Append a dim "(ctrl+e to expand)" affordance to the *collapsed* header
|
|
|
|
|
|
/// when it fits on the same row (never adds a row). **Not a TOML key** —
|
|
|
|
|
|
/// minimal mode sets it, being the only surface where a folded block cannot
|
|
|
|
|
|
/// be unfolded in place.
|
|
|
|
|
|
pub collapsed_expand_hint: bool,
|
2026-07-16 06:46:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for ThinkingConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
accent: crate::theme::Theme::current().gray_dim,
|
|
|
|
|
|
accent_enabled: true,
|
|
|
|
|
|
bg_blend: 0.7,
|
|
|
|
|
|
truncated_lines: 3,
|
|
|
|
|
|
animate: true,
|
|
|
|
|
|
header: true,
|
|
|
|
|
|
header_bright: false,
|
2026-07-28 22:50:19 +00:00
|
|
|
|
body_dim_italic: false,
|
|
|
|
|
|
collapsed_expand_hint: false,
|
2026-07-16 06:46:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Runtime config for tool call blocks (Read, Search, ListDir, etc).
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
|
pub struct ToolConfig {
|
|
|
|
|
|
/// When true, collapsed tool calls render entirely in muted gray.
|
|
|
|
|
|
/// When false, collapsed tool calls show normal colors (paths, patterns, etc).
|
|
|
|
|
|
pub muted_collapsed: bool,
|
|
|
|
|
|
/// When true, parenthetical details use gray_dim (dimmest gray):
|
|
|
|
|
|
/// Read "(1-50)", Search "(N matches)", Edit "(N edits)", Thinking "for Xs".
|
|
|
|
|
|
/// When false, they use the normal muted gray.
|
|
|
|
|
|
pub dim_details: bool,
|
|
|
|
|
|
/// Bullet/icon character rendered before tool call headers.
|
|
|
|
|
|
pub bullet: ToolBullet,
|
|
|
|
|
|
// Note: bullet_accent and bullet_color were removed in the scrollback-v2 refactor.
|
|
|
|
|
|
// Bullet color is now determined by BlockContent::bullet() — each block type
|
|
|
|
|
|
// decides its own bullet color based on state (accent color, error, default).
|
|
|
|
|
|
// Dimming for collapsed+groupable blocks is handled by EntryRenderer.
|
|
|
|
|
|
// TODO(dim_muted): add a dim factor for collapsed text styling (not just bullet/accent).
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for ToolConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
muted_collapsed: true,
|
|
|
|
|
|
dim_details: true,
|
|
|
|
|
|
bullet: ToolBullet::Diamond,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Bullet/icon style for tool call headers.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Rendered before the tool title, e.g. `⊙ Read src/main.rs`.
|
|
|
|
|
|
/// Respects `muted_collapsed`: when the tool is collapsed and muting is
|
|
|
|
|
|
/// enabled, the bullet color blends with the muted palette.
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
|
|
|
|
pub enum ToolBullet {
|
|
|
|
|
|
/// No bullet (default).
|
|
|
|
|
|
#[default]
|
|
|
|
|
|
None,
|
|
|
|
|
|
/// `·` (middle dot — smallest).
|
|
|
|
|
|
Dot,
|
|
|
|
|
|
/// `•` (bullet — between dot and circle).
|
|
|
|
|
|
SmallCircle,
|
|
|
|
|
|
/// `●` (filled circle).
|
|
|
|
|
|
Circle,
|
|
|
|
|
|
/// `▸` (right-pointing small triangle).
|
|
|
|
|
|
SmallTriangle,
|
|
|
|
|
|
/// `▶` (right-pointing triangle).
|
|
|
|
|
|
Triangle,
|
|
|
|
|
|
/// `◆` (filled diamond).
|
|
|
|
|
|
Diamond,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl ToolBullet {
|
|
|
|
|
|
/// The display character for this bullet, or `None` for no bullet.
|
|
|
|
|
|
pub fn char(&self) -> Option<&'static str> {
|
|
|
|
|
|
match self {
|
|
|
|
|
|
Self::None => Option::None,
|
|
|
|
|
|
Self::Dot => Some("·"),
|
|
|
|
|
|
Self::SmallCircle => Some("•"),
|
|
|
|
|
|
Self::Circle => Some(crate::glyphs::filled_dot()),
|
|
|
|
|
|
Self::SmallTriangle => Some("▸"),
|
|
|
|
|
|
Self::Triangle => Some("▶"),
|
|
|
|
|
|
// Routed through `glyphs` so the default scrollback bullet
|
|
|
|
|
|
// (used by tool calls, thinking, the running-subagent block,
|
|
|
|
|
|
// etc.) degrades to the CP437 `♦` on legacy Windows consoles
|
|
|
|
|
|
// that can't render U+25C6.
|
|
|
|
|
|
Self::Diamond => Some(crate::glyphs::diamond_filled()),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Runtime config for ListDir block.
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
|
pub struct ListDirConfig {
|
|
|
|
|
|
/// When true, output has terminal-style dark background.
|
|
|
|
|
|
/// When false, output has no background (default).
|
|
|
|
|
|
pub terminal_bg: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for ListDirConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
terminal_bg: true, // Default: dark background for output
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Header display style for execute blocks.
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
|
|
|
|
pub enum ExecuteHeaderStyle {
|
|
|
|
|
|
/// Shell style: `$ command` (default).
|
|
|
|
|
|
/// The `$` prompt is dim/muted, command may or may not be colored.
|
|
|
|
|
|
#[default]
|
|
|
|
|
|
Shell,
|
|
|
|
|
|
/// Label style: `Run command` (like Edit/Search blocks).
|
|
|
|
|
|
/// "Run" is bold (muted when collapsed, primary when expanded).
|
|
|
|
|
|
Label,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Runtime config for Execute tool call block.
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
|
pub struct ExecuteConfig {
|
|
|
|
|
|
/// Number of output lines to show at the start in truncated mode.
|
|
|
|
|
|
pub first_lines: u16,
|
|
|
|
|
|
/// Number of output lines to show at the end in truncated mode.
|
|
|
|
|
|
pub last_lines: u16,
|
|
|
|
|
|
/// Whether accent line is enabled. When false, no accent (running/success/error).
|
|
|
|
|
|
pub accent_enabled: bool,
|
|
|
|
|
|
/// Accent color for running execute blocks (animated).
|
|
|
|
|
|
pub running_accent: Color,
|
|
|
|
|
|
/// Header display style (shell `$` vs label `Run`).
|
|
|
|
|
|
pub header_style: ExecuteHeaderStyle,
|
|
|
|
|
|
/// When true, command text is muted/uncolored when collapsed.
|
|
|
|
|
|
pub muted_command_collapsed: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for ExecuteConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
first_lines: 2,
|
|
|
|
|
|
last_lines: 3,
|
|
|
|
|
|
accent_enabled: true,
|
|
|
|
|
|
running_accent: crate::theme::Theme::current().accent_running,
|
|
|
|
|
|
header_style: ExecuteHeaderStyle::Label,
|
|
|
|
|
|
muted_command_collapsed: true,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// Raw Config (for TOML serde)
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
//
|
|
|
|
|
|
// ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
|
|
|
|
// ║ MAINTAINER NOTE: When adding/changing fields or sections: ║
|
|
|
|
|
|
// ║ ║
|
|
|
|
|
|
// ║ 1. Add doc comments (///) to ALL fields in Raw* structs - they become ║
|
|
|
|
|
|
// ║ TOML comments via the `DocumentedFields` derive macro. ║
|
|
|
|
|
|
// ║ ║
|
|
|
|
|
|
// ║ 2. If adding a new section (e.g., RawNewBlockConfig): ║
|
|
|
|
|
|
// ║ - Add it to RawBlocksConfig (or appropriate parent) ║
|
|
|
|
|
|
// ║ - Add corresponding runtime config (NewBlockConfig) ║
|
|
|
|
|
|
// ║ - Add From<RawNewBlockConfig> for NewBlockConfig conversion ║
|
|
|
|
|
|
// ║ - Add annotate_table call in to_toml_with_comments() below! ║
|
|
|
|
|
|
// ║ ║
|
|
|
|
|
|
// ║ 3. The to_toml_with_comments() method generates the default config file ║
|
|
|
|
|
|
// ║ with comments. Update it when adding new sections. ║
|
|
|
|
|
|
// ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
|
|
|
|
|
|
|
|
|
|
/// Root appearance configuration (TOML format).
|
|
|
|
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, Documented, DocumentedFields)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct RawAppearanceConfig {
|
|
|
|
|
|
/// Terminal behavior settings (fullscreen policy).
|
|
|
|
|
|
pub terminal: RawTerminalConfig,
|
|
|
|
|
|
/// Animation settings.
|
|
|
|
|
|
pub animation: RawAnimationConfig,
|
|
|
|
|
|
/// Prompt input view settings (collapse, hover).
|
|
|
|
|
|
pub prompt: RawPromptViewConfig,
|
|
|
|
|
|
/// Scrollback pane settings (layout, scrollbar, scroll, blocks).
|
|
|
|
|
|
pub scrollback: RawScrollbackConfig,
|
|
|
|
|
|
/// Todo pane settings (badge format).
|
|
|
|
|
|
pub todo: RawTodoConfig,
|
|
|
|
|
|
/// Disable hooks & plugins UI (/hooks and /plugins commands, scrollback annotations).
|
|
|
|
|
|
/// Defaults to false (plugins enabled).
|
|
|
|
|
|
pub disable_plugins: bool,
|
|
|
|
|
|
/// Always show the "plan" chip in the status bar when plan content is
|
|
|
|
|
|
/// available, even after the user exits plan mode.
|
|
|
|
|
|
/// Defaults to false (chip hidden once plan mode ends).
|
|
|
|
|
|
pub show_plan_chip: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Terminal behavior configuration (TOML format).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Controls fullscreen (alternate screen) policy and related terminal
|
|
|
|
|
|
/// interaction settings.
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Documented, DocumentedFields)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct RawTerminalConfig {
|
|
|
|
|
|
/// Alt-screen (fullscreen) policy.
|
|
|
|
|
|
/// "auto" — fullscreen in plain terminals and normal tmux, inline in
|
|
|
|
|
|
/// tmux control mode and Zellij. (default)
|
|
|
|
|
|
/// "always" — always enter fullscreen, even in control mode / Zellij.
|
|
|
|
|
|
/// "never" — never enter fullscreen; run inline in main scrollback.
|
|
|
|
|
|
pub alt_screen: RawAltScreenMode,
|
|
|
|
|
|
/// Experimental scrollback-native rendering mode. Finalized blocks are
|
|
|
|
|
|
/// printed into the terminal's native scrollback. Default false.
|
|
|
|
|
|
pub minimal: bool,
|
|
|
|
|
|
/// Pinned live-region height (rows) for minimal mode. Default 10.
|
|
|
|
|
|
pub minimal_live_rows: Option<u16>,
|
|
|
|
|
|
/// Maximum rows for a single committed block in minimal mode. Default 2000.
|
|
|
|
|
|
pub minimal_max_commit_rows: Option<u16>,
|
2026-07-28 22:50:19 +00:00
|
|
|
|
/// Commit reasoning ("Thought for Xs") to native scrollback COLLAPSED to
|
|
|
|
|
|
/// its one-line header instead of in full. Default false — minimal
|
|
|
|
|
|
/// deliberately keeps the whole reasoning body in the transcript (K9); this
|
|
|
|
|
|
/// is the opt-out for a terser scrollback. The body stays reachable with
|
|
|
|
|
|
/// `Ctrl+E` / `/expand` and `/transcript`.
|
|
|
|
|
|
pub minimal_collapse_thinking: bool,
|
2026-07-16 06:46:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for RawTerminalConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
alt_screen: RawAltScreenMode::Auto,
|
|
|
|
|
|
minimal: false,
|
|
|
|
|
|
minimal_live_rows: None,
|
|
|
|
|
|
minimal_max_commit_rows: None,
|
2026-07-28 22:50:19 +00:00
|
|
|
|
minimal_collapse_thinking: false,
|
2026-07-16 06:46:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Raw alt-screen mode for TOML (de)serialization.
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
|
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
|
pub enum RawAltScreenMode {
|
|
|
|
|
|
/// Automatic: fullscreen in healthy environments, inline in degraded ones.
|
|
|
|
|
|
#[default]
|
|
|
|
|
|
Auto,
|
|
|
|
|
|
/// Always enter the alternate screen.
|
|
|
|
|
|
Always,
|
|
|
|
|
|
/// Never enter the alternate screen.
|
|
|
|
|
|
Never,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl From<RawAltScreenMode> for crate::terminal::AltScreenMode {
|
|
|
|
|
|
fn from(raw: RawAltScreenMode) -> Self {
|
|
|
|
|
|
match raw {
|
|
|
|
|
|
RawAltScreenMode::Auto => crate::terminal::AltScreenMode::Auto,
|
|
|
|
|
|
RawAltScreenMode::Always => crate::terminal::AltScreenMode::Always,
|
|
|
|
|
|
RawAltScreenMode::Never => crate::terminal::AltScreenMode::Never,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
/// Prompt input view configuration (TOML format).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// This configures the prompt editor widget at the bottom of the screen,
|
|
|
|
|
|
/// NOT the user prompt block rendered inside the scrollback.
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Documented, DocumentedFields)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct RawPromptViewConfig {
|
|
|
|
|
|
/// When true, the prompt collapses to its minimum height (single-line)
|
|
|
|
|
|
/// when focus is in the scrollback pane. Expands back when focused.
|
|
|
|
|
|
pub collapse_unfocused: bool,
|
|
|
|
|
|
/// Show hover highlight box when mousing over the prompt widget.
|
|
|
|
|
|
pub mouse_hover: bool,
|
|
|
|
|
|
/// Show the ❯ prefix character in the prompt editor.
|
|
|
|
|
|
pub show_prefix: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for RawPromptViewConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
collapse_unfocused: true,
|
|
|
|
|
|
mouse_hover: true,
|
|
|
|
|
|
show_prefix: true,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Scrollback pane configuration (TOML format).
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, Documented, DocumentedFields)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct RawScrollbackConfig {
|
|
|
|
|
|
/// Layout settings (padding, spacing).
|
|
|
|
|
|
pub layout: RawLayoutConfig,
|
|
|
|
|
|
/// Scrollbar settings.
|
|
|
|
|
|
pub scrollbar: RawScrollbarConfig,
|
|
|
|
|
|
/// Scroll behavior settings.
|
|
|
|
|
|
pub scroll: RawScrollConfig,
|
|
|
|
|
|
/// Block rendering settings.
|
|
|
|
|
|
pub blocks: RawBlocksConfig,
|
|
|
|
|
|
/// Miscellaneous display options.
|
|
|
|
|
|
pub display: RawScrollbackDisplayConfig,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Scrollback display options (TOML format).
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Documented, DocumentedFields)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct RawScrollbackDisplayConfig {
|
|
|
|
|
|
/// Render a subtle horizontal line below the last entry ("end of content" marker).
|
|
|
|
|
|
pub line_under_last_entry: bool,
|
|
|
|
|
|
/// Accent character for collapsed groupable blocks. Default: "❙".
|
|
|
|
|
|
pub collapsed_accent_char: Option<String>,
|
|
|
|
|
|
/// Blend factor for dimmed accents on collapsed groupable blocks (0.0–1.0). Default: 0.5.
|
|
|
|
|
|
pub dim_accent: Option<f32>,
|
|
|
|
|
|
/// Group selection box mode. true = "split" (Mode B), false = "always" (Mode A). Default: true.
|
|
|
|
|
|
pub group_selection_split: Option<bool>,
|
|
|
|
|
|
/// Whether active-block highlight overlays selection box borders. Default: false.
|
|
|
|
|
|
pub highlight_overlays_border: Option<bool>,
|
|
|
|
|
|
/// Show expand indicator (e.g., "›") on selected foldable collapsed entries. Default: true.
|
|
|
|
|
|
pub expandable_indicator: Option<bool>,
|
|
|
|
|
|
/// Show expand indicator on running entries in their minimum fold mode. Default: true.
|
|
|
|
|
|
pub expandable_indicator_running: Option<bool>,
|
|
|
|
|
|
/// Character for the expand indicator. Default: "›".
|
|
|
|
|
|
pub expandable_indicator_char: Option<String>,
|
|
|
|
|
|
/// Show ⧉/↗ buttons on the selection box. Default: false.
|
|
|
|
|
|
pub selection_buttons: Option<bool>,
|
|
|
|
|
|
/// Pin user prompts as sticky headers when scrolled past. Default: true.
|
|
|
|
|
|
pub sticky_headers: Option<bool>,
|
|
|
|
|
|
/// Number of spaces to use when expanding tab characters (\t) in content.
|
|
|
|
|
|
/// Tabs in model output are replaced with this many spaces before rendering.
|
|
|
|
|
|
/// Default: 4. Set to 0 to pass through tabs unchanged.
|
|
|
|
|
|
pub tab_width: Option<u8>,
|
|
|
|
|
|
/// Maximum visible entries in a consecutive group of collapsed tool/thinking blocks.
|
|
|
|
|
|
/// Older entries are hidden behind a compact header. 0 = disable. Default: 10.
|
|
|
|
|
|
pub group_max_visible: Option<u16>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for RawScrollbackDisplayConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
line_under_last_entry: false,
|
|
|
|
|
|
collapsed_accent_char: Some(crate::glyphs::collapsed_accent().to_string()),
|
|
|
|
|
|
dim_accent: Some(0.5),
|
|
|
|
|
|
group_selection_split: Some(true),
|
|
|
|
|
|
highlight_overlays_border: Some(false),
|
|
|
|
|
|
expandable_indicator: Some(true),
|
|
|
|
|
|
expandable_indicator_running: Some(true),
|
|
|
|
|
|
expandable_indicator_char: Some("›".to_string()),
|
|
|
|
|
|
selection_buttons: Some(false),
|
|
|
|
|
|
sticky_headers: Some(true),
|
|
|
|
|
|
tab_width: Some(4),
|
|
|
|
|
|
group_max_visible: Some(10),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Layout configuration (TOML format).
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Documented, DocumentedFields)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct RawLayoutConfig {
|
|
|
|
|
|
/// Vertical padding (top/bottom) for outer viewport.
|
|
|
|
|
|
pub outer_vpad: u16,
|
|
|
|
|
|
/// Left horizontal padding for outer viewport (min 1).
|
|
|
|
|
|
pub outer_hpad_left: u16,
|
|
|
|
|
|
/// Right horizontal padding for outer viewport (min 1).
|
|
|
|
|
|
pub outer_hpad_right: u16,
|
|
|
|
|
|
/// Padding after accent line, before content.
|
|
|
|
|
|
pub block_pad_left: u16,
|
|
|
|
|
|
/// Padding after content, at right edge.
|
|
|
|
|
|
pub block_pad_right: u16,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for RawLayoutConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
outer_vpad: 1,
|
|
|
|
|
|
outer_hpad_left: 2,
|
|
|
|
|
|
outer_hpad_right: 2,
|
|
|
|
|
|
block_pad_left: 2,
|
|
|
|
|
|
block_pad_right: 2,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Scrollbar configuration (TOML format).
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Documented, DocumentedFields)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct RawScrollbarConfig {
|
|
|
|
|
|
/// Whether scrollbar is enabled.
|
|
|
|
|
|
pub enabled: bool,
|
|
|
|
|
|
/// Gap between content/selection edge and scrollbar track.
|
|
|
|
|
|
/// 0 = adjacent to content, 1+ = space between.
|
|
|
|
|
|
/// Note: Content width is clamped to outer_hpad boundaries.
|
|
|
|
|
|
pub gap_left: u16,
|
|
|
|
|
|
/// Gap between scrollbar track and screen edge.
|
|
|
|
|
|
/// 0 = scrollbar at screen edge (uses outer_hpad_right if available).
|
|
|
|
|
|
pub gap_right: u16,
|
|
|
|
|
|
/// Override scrollbar background color.
|
|
|
|
|
|
/// Use "none" to use theme default, or a color value.
|
|
|
|
|
|
pub scrollbar_bg: OptionalColor,
|
|
|
|
|
|
/// Override scrollbar foreground/thumb color.
|
|
|
|
|
|
/// Use "none" to use theme default, or a color value.
|
|
|
|
|
|
pub scrollbar_fg: OptionalColor,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for RawScrollbarConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
gap_left: 0,
|
|
|
|
|
|
gap_right: 0,
|
|
|
|
|
|
scrollbar_bg: OptionalColor::None,
|
|
|
|
|
|
scrollbar_fg: OptionalColor::None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Scroll behavior configuration (TOML format).
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Documented, DocumentedFields)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct RawScrollConfig {
|
|
|
|
|
|
/// Minimum lines of context to keep above/below selected entry.
|
|
|
|
|
|
/// When navigating, ensure at least this many lines of adjacent entries
|
|
|
|
|
|
/// remain visible. 0 = scroll to edge (default).
|
|
|
|
|
|
pub margin: u16,
|
|
|
|
|
|
/// Minimum scroll as percentage of viewport height (0-100).
|
|
|
|
|
|
/// If a scroll would be less than this percentage, scroll by this amount instead.
|
|
|
|
|
|
/// 0 = minimal scroll (default), 25 = quarter page, 100 = full page.
|
|
|
|
|
|
pub min_page_fraction: u8,
|
2026-08-03 08:17:57 +00:00
|
|
|
|
/// Scroll indicators: the ▼ below scrollback and the ▲ under the sticky
|
|
|
|
|
|
/// prompt header. "none" = hidden, "center" = centered arrows.
|
2026-07-16 06:46:02 +01:00
|
|
|
|
pub follow_indicator: RawFollowIndicator,
|
|
|
|
|
|
/// When follow mode scrolls to new content, auto-select the latest entry.
|
|
|
|
|
|
pub follow_auto_select: bool,
|
|
|
|
|
|
/// Scrolling past the bottom (j, Ctrl-D, page-down, mousewheel) engages follow mode.
|
|
|
|
|
|
pub follow_by_overscroll: bool,
|
|
|
|
|
|
/// Anchor scroll on fold: keep the toggled block's header at the same screen y. Default: true.
|
|
|
|
|
|
pub anchor_on_fold: bool,
|
|
|
|
|
|
/// Opt-in: keep manually folded blocks as-is during streaming and when they finish,
|
|
|
|
|
|
/// and stop auto-scroll when a fold expands a block while following. Default: false.
|
|
|
|
|
|
pub respect_manual_folds: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for RawScrollConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
margin: 0,
|
|
|
|
|
|
min_page_fraction: 0,
|
|
|
|
|
|
follow_indicator: RawFollowIndicator::Center,
|
|
|
|
|
|
follow_auto_select: true,
|
|
|
|
|
|
follow_by_overscroll: true,
|
|
|
|
|
|
anchor_on_fold: true,
|
|
|
|
|
|
respect_manual_folds: false,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-03 08:17:57 +00:00
|
|
|
|
/// Scroll indicator display mode (TOML format).
|
2026-07-16 06:46:02 +01:00
|
|
|
|
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
|
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
|
pub enum RawFollowIndicator {
|
2026-08-03 08:17:57 +00:00
|
|
|
|
/// No scroll indicators.
|
2026-07-16 06:46:02 +01:00
|
|
|
|
None,
|
2026-08-03 08:17:57 +00:00
|
|
|
|
/// Show ▼ centered below scrollback and ▲ under the sticky prompt header.
|
2026-07-16 06:46:02 +01:00
|
|
|
|
#[default]
|
|
|
|
|
|
Center,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Tool bullet style (TOML format).
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
|
|
|
|
|
|
#[serde(rename_all = "kebab-case")]
|
|
|
|
|
|
pub enum RawToolBullet {
|
|
|
|
|
|
/// No bullet.
|
|
|
|
|
|
#[default]
|
|
|
|
|
|
None,
|
|
|
|
|
|
/// `·` (middle dot — smallest).
|
|
|
|
|
|
Dot,
|
|
|
|
|
|
/// `•` (bullet — between dot and circle).
|
|
|
|
|
|
SmallCircle,
|
|
|
|
|
|
/// `●` (filled circle).
|
|
|
|
|
|
Circle,
|
|
|
|
|
|
/// `▸` (right-pointing small triangle).
|
|
|
|
|
|
SmallTriangle,
|
|
|
|
|
|
/// `▶` (right-pointing triangle).
|
|
|
|
|
|
Triangle,
|
|
|
|
|
|
/// `◆` (filled diamond).
|
|
|
|
|
|
Diamond,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Todo pane configuration (TOML format).
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Documented, DocumentedFields)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct RawTodoConfig {
|
|
|
|
|
|
/// Badge format in the status bar.
|
|
|
|
|
|
/// "default" = colored numbers only [1 2 3 4].
|
|
|
|
|
|
/// "colon" = icon:count [▶:1 □:4 ✓:3].
|
|
|
|
|
|
/// "comma" = count icon, comma-separated [1 ▶, 4 □, 3 ✓].
|
|
|
|
|
|
pub badge_format: RawTodoBadgeFormat,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for RawTodoConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
badge_format: RawTodoBadgeFormat::Default,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Badge format for the todo status counts (TOML format).
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
|
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
|
pub enum RawTodoBadgeFormat {
|
|
|
|
|
|
/// Colored numbers only: `[1 2 3 4]`.
|
|
|
|
|
|
#[default]
|
|
|
|
|
|
Default,
|
|
|
|
|
|
/// Icon:count: `[▶:1 □:4 ✓:3 ✗:2]`.
|
|
|
|
|
|
Colon,
|
|
|
|
|
|
/// Count icon, comma-separated: `[1 ▶, 4 □, 3 ✓, 2 ✗]`.
|
|
|
|
|
|
Comma,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl From<RawTodoBadgeFormat> for TodoBadgeFormat {
|
|
|
|
|
|
fn from(raw: RawTodoBadgeFormat) -> Self {
|
|
|
|
|
|
match raw {
|
|
|
|
|
|
RawTodoBadgeFormat::Default => TodoBadgeFormat::Default,
|
|
|
|
|
|
RawTodoBadgeFormat::Colon => TodoBadgeFormat::Colon,
|
|
|
|
|
|
RawTodoBadgeFormat::Comma => TodoBadgeFormat::Comma,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Animation configuration (TOML format).
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Documented, DocumentedFields)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct RawAnimationConfig {
|
|
|
|
|
|
/// Animation frame rate (ticks per second).
|
|
|
|
|
|
/// Higher = smoother but more CPU. Range: 1-60. Default: 30.
|
|
|
|
|
|
pub fps: u8,
|
|
|
|
|
|
/// Rows per wave cycle for accent line animation.
|
|
|
|
|
|
/// Lower = faster wave, higher = slower/smoother wave. Default: 32.
|
|
|
|
|
|
pub wave_rows: u16,
|
|
|
|
|
|
/// Show an FPS counter overlay in the top-right corner.
|
|
|
|
|
|
/// Requires a debug build. Also enabled by GROK_FPS=1 env var. Default: false.
|
|
|
|
|
|
pub show_fps: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for RawAnimationConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
fps: 30,
|
|
|
|
|
|
wave_rows: 32,
|
|
|
|
|
|
show_fps: false,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Configuration for all block types (TOML format).
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, Documented, DocumentedFields)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct RawBlocksConfig {
|
|
|
|
|
|
/// Edit block settings.
|
|
|
|
|
|
pub edit: RawEditBlockConfig,
|
|
|
|
|
|
/// User prompt block settings.
|
|
|
|
|
|
pub prompt: RawPromptConfig,
|
|
|
|
|
|
/// Thinking/reasoning block settings.
|
|
|
|
|
|
pub thinking: RawThinkingConfig,
|
|
|
|
|
|
/// Tool call block settings (Read, Search, ListDir, etc).
|
|
|
|
|
|
pub tool: RawToolConfig,
|
|
|
|
|
|
/// ListDir block settings.
|
|
|
|
|
|
pub list_dir: RawListDirConfig,
|
|
|
|
|
|
/// Execute tool call settings.
|
|
|
|
|
|
pub execute: RawExecuteConfig,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Configuration for EditBlock rendering (TOML format).
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Documented, DocumentedFields)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct RawEditBlockConfig {
|
|
|
|
|
|
/// Whether to apply 2-char indent before line numbers.
|
|
|
|
|
|
pub indent: bool,
|
|
|
|
|
|
/// Whether to apply vertical padding.
|
|
|
|
|
|
pub vpad: bool,
|
|
|
|
|
|
/// Block background: "none", "dark", or "light".
|
|
|
|
|
|
pub bg: RawBlockBackground,
|
|
|
|
|
|
/// Whether to show background behind accent.
|
|
|
|
|
|
pub accent_bg: bool,
|
|
|
|
|
|
/// Accent color for vertical line.
|
|
|
|
|
|
/// Use "none" to disable, or a color value.
|
|
|
|
|
|
/// Formats: [r, g, b], "#rrggbb", "#rgb", or named color.
|
|
|
|
|
|
/// Named: BLUE, CYAN, GREEN, YELLOW, ORANGE, RED, MAGENTA, COMMENT, etc.
|
|
|
|
|
|
pub accent: OptionalColor,
|
|
|
|
|
|
/// Whether diff line background extends to include gutter (line numbers).
|
|
|
|
|
|
pub gutter_bg: bool,
|
|
|
|
|
|
/// Whether to skip indent columns in diff line background (keep them clean).
|
|
|
|
|
|
/// true = skip indent, false = include indent in background.
|
|
|
|
|
|
pub indent_bg: bool,
|
|
|
|
|
|
/// Show the +N/-M line summary in the collapsed header.
|
|
|
|
|
|
/// Commented out (unset), it follows the `[ui] collapsed_edit_blocks`
|
|
|
|
|
|
/// flag in config.toml; uncomment to pin either way.
|
|
|
|
|
|
pub line_summary: Option<bool>,
|
|
|
|
|
|
/// Start Edit blocks expanded (showing the diff) instead of as a
|
|
|
|
|
|
/// collapsed one-line summary. Commented out (unset), it follows the
|
|
|
|
|
|
/// `[ui] collapsed_edit_blocks` flag in config.toml (flag on =
|
|
|
|
|
|
/// collapsed); uncomment to pin either way.
|
|
|
|
|
|
pub expanded_by_default: Option<bool>,
|
|
|
|
|
|
/// Separator between diff hunks. Options: "…" (default), "───", "⋯", "" (none).
|
|
|
|
|
|
pub hunk_separator: Option<String>,
|
|
|
|
|
|
/// Show two line-number columns (old + new) like GitHub's unified diff.
|
|
|
|
|
|
/// When false (default), show a single column with the new-file line number.
|
|
|
|
|
|
pub dual_line_numbers: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for RawEditBlockConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
indent: true,
|
|
|
|
|
|
vpad: false,
|
|
|
|
|
|
bg: RawBlockBackground::None,
|
|
|
|
|
|
accent_bg: false,
|
|
|
|
|
|
accent: OptionalColor::None,
|
|
|
|
|
|
gutter_bg: false,
|
|
|
|
|
|
indent_bg: false,
|
|
|
|
|
|
line_summary: None,
|
|
|
|
|
|
expanded_by_default: None,
|
|
|
|
|
|
hunk_separator: Some("…".to_string()),
|
|
|
|
|
|
dual_line_numbers: false,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Configuration for user prompt block rendering (TOML format).
|
|
|
|
|
|
///
|
|
|
|
|
|
/// This configures how user prompts appear inside the scrollback,
|
|
|
|
|
|
/// NOT the prompt editor widget (see [prompt] section).
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Documented, DocumentedFields)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct RawPromptConfig {
|
|
|
|
|
|
/// Whether to apply vertical padding (blank lines above/below).
|
|
|
|
|
|
pub vpad: bool,
|
|
|
|
|
|
/// Block background: "none", "dark", or "light".
|
|
|
|
|
|
pub bg: RawBlockBackground,
|
|
|
|
|
|
/// Whether accent column gets block's background.
|
|
|
|
|
|
pub accent_bg: bool,
|
|
|
|
|
|
/// Minimum content lines to show in truncated/sticky header mode.
|
|
|
|
|
|
/// This is the number of actual content lines, not including vpad.
|
|
|
|
|
|
pub min_lines: u16,
|
|
|
|
|
|
/// Show the ❯ prefix character before the prompt text.
|
|
|
|
|
|
pub show_prefix: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for RawPromptConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
vpad: true,
|
|
|
|
|
|
bg: RawBlockBackground::Light,
|
|
|
|
|
|
accent_bg: false,
|
|
|
|
|
|
min_lines: 2,
|
|
|
|
|
|
show_prefix: true,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Configuration for thinking/reasoning block rendering (TOML format).
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Documented, DocumentedFields)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct RawThinkingConfig {
|
|
|
|
|
|
/// Accent color for the vertical line.
|
|
|
|
|
|
/// Use "default" (or "none") for theme default, or a named/hex/RGB color.
|
|
|
|
|
|
pub accent: OptionalColor,
|
|
|
|
|
|
/// Whether accent line is shown. Set to false to hide accent in all modes.
|
|
|
|
|
|
pub accent_enabled: bool,
|
|
|
|
|
|
/// Blend factor for markdown colors with background (0-100).
|
|
|
|
|
|
/// Lower values = more faded. 70 = 70% original color, 30% background.
|
|
|
|
|
|
pub bg_blend: u8,
|
|
|
|
|
|
/// Number of visual lines to show at start and end in truncated mode.
|
|
|
|
|
|
/// If content exceeds this, shows first N lines, ellipsis, last N lines.
|
|
|
|
|
|
pub truncated_lines: u16,
|
|
|
|
|
|
/// Whether the accent line animates (traveling wave) while thinking is active.
|
|
|
|
|
|
/// When false, the accent line is static.
|
|
|
|
|
|
pub animate: bool,
|
|
|
|
|
|
/// Show header line in truncated/expanded modes.
|
|
|
|
|
|
/// When true: "Thinking..." (running) or "Thought for Xs" (done) appears
|
|
|
|
|
|
/// as the first line above thinking content, with accent line and bullet.
|
|
|
|
|
|
pub header: bool,
|
|
|
|
|
|
/// When true, header uses brighter styling in non-collapsed modes (like tool titles).
|
|
|
|
|
|
/// Respects muted_collapsed when collapsed. When false, header is always dim gray.
|
|
|
|
|
|
pub header_bright: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for RawThinkingConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
accent: OptionalColor::None,
|
|
|
|
|
|
accent_enabled: true,
|
|
|
|
|
|
bg_blend: 70,
|
|
|
|
|
|
truncated_lines: 3,
|
|
|
|
|
|
animate: true,
|
|
|
|
|
|
header: true,
|
|
|
|
|
|
header_bright: false,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Configuration for tool call blocks (TOML format).
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Documented, DocumentedFields)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct RawToolConfig {
|
|
|
|
|
|
/// When true, collapsed tool calls render entirely in muted gray.
|
|
|
|
|
|
/// When false, collapsed tool calls show normal colors.
|
|
|
|
|
|
pub muted_collapsed: bool,
|
|
|
|
|
|
/// When true, parenthetical details use the dimmest gray (gray_dim):
|
|
|
|
|
|
/// Read "(1-50)", Search "(N matches)", Edit "(N edits)", Thinking "for Xs".
|
|
|
|
|
|
/// When false, they use the normal muted gray.
|
|
|
|
|
|
pub dim_details: bool,
|
|
|
|
|
|
/// Bullet/icon before tool call headers.
|
|
|
|
|
|
/// "none", "dot" (·), "small-circle" (•), "circle" (●),
|
|
|
|
|
|
/// "small-triangle" (▸), "triangle" (▶), "diamond" (◆).
|
|
|
|
|
|
pub bullet: RawToolBullet,
|
|
|
|
|
|
// Note: bullet_accent and bullet_color removed — see ToolConfig comment.
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for RawToolConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
muted_collapsed: true,
|
|
|
|
|
|
dim_details: true,
|
|
|
|
|
|
bullet: RawToolBullet::Diamond,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Configuration for ListDir block (TOML format).
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Documented, DocumentedFields)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct RawListDirConfig {
|
|
|
|
|
|
/// When true, output has terminal-style dark background.
|
|
|
|
|
|
/// When false, output has no background.
|
|
|
|
|
|
pub terminal_bg: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for RawListDirConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
terminal_bg: true, // Default: dark background for output
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Configuration for Execute tool call block (TOML format).
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Documented, DocumentedFields)]
|
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
|
pub struct RawExecuteConfig {
|
|
|
|
|
|
/// Number of output lines to show at the start in truncated mode.
|
|
|
|
|
|
/// In truncated mode: first_lines, then "…", then last_lines.
|
|
|
|
|
|
pub first_lines: u16,
|
|
|
|
|
|
/// Number of output lines to show at the end in truncated mode.
|
|
|
|
|
|
pub last_lines: u16,
|
|
|
|
|
|
/// Whether accent line is shown. Set to false to hide all accents
|
|
|
|
|
|
/// (running/success/error).
|
|
|
|
|
|
pub accent_enabled: bool,
|
|
|
|
|
|
/// Accent color for running execute blocks (animated wave).
|
|
|
|
|
|
/// Default: MAGENTA. Use named colors, hex, or RGB array.
|
|
|
|
|
|
pub running_accent: OptionalColor,
|
|
|
|
|
|
/// Header display style: "shell" = `$ command` (default), "label" = `Run command`.
|
|
|
|
|
|
/// Shell style shows a dim `$` prompt. Label style shows bold "Run" like Edit/Search.
|
|
|
|
|
|
pub header_style: RawExecuteHeaderStyle,
|
|
|
|
|
|
/// When true, command text is muted/uncolored when collapsed.
|
|
|
|
|
|
/// When false, command text keeps its color when collapsed.
|
|
|
|
|
|
pub muted_command_collapsed: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for RawExecuteConfig {
|
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
first_lines: 2,
|
|
|
|
|
|
last_lines: 3,
|
|
|
|
|
|
accent_enabled: true,
|
|
|
|
|
|
running_accent: OptionalColor::None,
|
|
|
|
|
|
header_style: RawExecuteHeaderStyle::Label,
|
|
|
|
|
|
muted_command_collapsed: true,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Raw header style for execute blocks (TOML format).
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
|
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
|
pub enum RawExecuteHeaderStyle {
|
|
|
|
|
|
/// Shell style: `$ command` (default).
|
|
|
|
|
|
#[default]
|
|
|
|
|
|
Shell,
|
|
|
|
|
|
/// Label style: `Run command` (like Edit/Search blocks).
|
|
|
|
|
|
Label,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl From<RawExecuteHeaderStyle> for ExecuteHeaderStyle {
|
|
|
|
|
|
fn from(raw: RawExecuteHeaderStyle) -> Self {
|
|
|
|
|
|
match raw {
|
|
|
|
|
|
RawExecuteHeaderStyle::Shell => ExecuteHeaderStyle::Shell,
|
|
|
|
|
|
RawExecuteHeaderStyle::Label => ExecuteHeaderStyle::Label,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
|
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
|
pub enum RawBlockBackground {
|
|
|
|
|
|
#[default]
|
|
|
|
|
|
None,
|
|
|
|
|
|
Dark,
|
|
|
|
|
|
Light,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl From<RawBlockBackground> for BlockBackground {
|
|
|
|
|
|
fn from(raw: RawBlockBackground) -> Self {
|
|
|
|
|
|
match raw {
|
|
|
|
|
|
RawBlockBackground::None => BlockBackground::None,
|
|
|
|
|
|
RawBlockBackground::Dark => BlockBackground::Dark,
|
|
|
|
|
|
RawBlockBackground::Light => BlockBackground::Light,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// Raw → Runtime Conversion
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
impl From<RawAppearanceConfig> for AppearanceConfig {
|
|
|
|
|
|
fn from(raw: RawAppearanceConfig) -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
animation: raw.animation.into(),
|
|
|
|
|
|
prompt: PromptViewConfig {
|
|
|
|
|
|
collapse_unfocused: raw.prompt.collapse_unfocused,
|
|
|
|
|
|
mouse_hover: raw.prompt.mouse_hover,
|
|
|
|
|
|
show_prefix: raw.prompt.show_prefix,
|
|
|
|
|
|
compact: false, // runtime-only, not persisted in TOML
|
|
|
|
|
|
},
|
|
|
|
|
|
scrollback: ScrollbackConfig {
|
|
|
|
|
|
layout: raw.scrollback.layout.into(),
|
|
|
|
|
|
scrollbar: raw.scrollback.scrollbar.into(),
|
|
|
|
|
|
scroll: raw.scrollback.scroll.into(),
|
|
|
|
|
|
blocks: raw.scrollback.blocks.into(),
|
|
|
|
|
|
display: ScrollbackDisplayConfig {
|
|
|
|
|
|
line_under_last_entry: raw.scrollback.display.line_under_last_entry,
|
|
|
|
|
|
collapsed_accent_char: raw
|
|
|
|
|
|
.scrollback
|
|
|
|
|
|
.display
|
|
|
|
|
|
.collapsed_accent_char
|
|
|
|
|
|
.unwrap_or_else(|| crate::glyphs::collapsed_accent().to_string()),
|
|
|
|
|
|
dim_accent: raw.scrollback.display.dim_accent.unwrap_or(0.5),
|
|
|
|
|
|
group_selection_split: raw
|
|
|
|
|
|
.scrollback
|
|
|
|
|
|
.display
|
|
|
|
|
|
.group_selection_split
|
|
|
|
|
|
.unwrap_or(true),
|
|
|
|
|
|
highlight_overlays_border: raw
|
|
|
|
|
|
.scrollback
|
|
|
|
|
|
.display
|
|
|
|
|
|
.highlight_overlays_border
|
|
|
|
|
|
.unwrap_or(false),
|
|
|
|
|
|
expandable_indicator: raw
|
|
|
|
|
|
.scrollback
|
|
|
|
|
|
.display
|
|
|
|
|
|
.expandable_indicator
|
|
|
|
|
|
.unwrap_or(true),
|
|
|
|
|
|
expandable_indicator_running: raw
|
|
|
|
|
|
.scrollback
|
|
|
|
|
|
.display
|
|
|
|
|
|
.expandable_indicator_running
|
|
|
|
|
|
.unwrap_or(true),
|
|
|
|
|
|
expandable_indicator_char: raw
|
|
|
|
|
|
.scrollback
|
|
|
|
|
|
.display
|
|
|
|
|
|
.expandable_indicator_char
|
|
|
|
|
|
.unwrap_or_else(|| "›".to_string()),
|
|
|
|
|
|
selection_buttons: raw.scrollback.display.selection_buttons.unwrap_or(false),
|
|
|
|
|
|
sticky_headers: raw.scrollback.display.sticky_headers.unwrap_or(true),
|
|
|
|
|
|
tab_width: raw.scrollback.display.tab_width.unwrap_or(4),
|
|
|
|
|
|
group_max_visible: raw.scrollback.display.group_max_visible.unwrap_or(10),
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
todo: TodoConfig {
|
|
|
|
|
|
badge_format: raw.todo.badge_format.into(),
|
|
|
|
|
|
},
|
|
|
|
|
|
turn_status: TurnStatusConfig::default(),
|
|
|
|
|
|
show_timestamps: true, // runtime-only, loaded from config.toml via persist
|
|
|
|
|
|
// Single source: UiConfig::SHOW_TIMELINE_DEFAULT (loaded from config.toml via persist).
|
|
|
|
|
|
show_timeline: UiConfig::SHOW_TIMELINE_DEFAULT,
|
|
|
|
|
|
disable_plugins: raw.disable_plugins,
|
|
|
|
|
|
show_plan_chip: raw.show_plan_chip,
|
|
|
|
|
|
alt_screen: raw.terminal.alt_screen.into(),
|
|
|
|
|
|
minimal: raw.terminal.minimal,
|
|
|
|
|
|
minimal_live_rows: raw.terminal.minimal_live_rows.unwrap_or(10),
|
|
|
|
|
|
minimal_max_commit_rows: raw.terminal.minimal_max_commit_rows.unwrap_or(2000),
|
2026-07-28 22:50:19 +00:00
|
|
|
|
minimal_collapse_thinking: raw.terminal.minimal_collapse_thinking,
|
2026-07-16 06:46:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl From<RawAnimationConfig> for AnimationConfig {
|
|
|
|
|
|
fn from(raw: RawAnimationConfig) -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
fps: raw.fps.clamp(1, 60),
|
|
|
|
|
|
wave_rows: raw.wave_rows.max(1),
|
|
|
|
|
|
show_fps: raw.show_fps,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl From<RawFollowIndicator> for FollowIndicator {
|
|
|
|
|
|
fn from(raw: RawFollowIndicator) -> Self {
|
|
|
|
|
|
match raw {
|
|
|
|
|
|
RawFollowIndicator::None => Self::None,
|
|
|
|
|
|
RawFollowIndicator::Center => Self::Center,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl From<RawToolBullet> for ToolBullet {
|
|
|
|
|
|
fn from(raw: RawToolBullet) -> Self {
|
|
|
|
|
|
match raw {
|
|
|
|
|
|
RawToolBullet::None => Self::None,
|
|
|
|
|
|
RawToolBullet::Dot => Self::Dot,
|
|
|
|
|
|
RawToolBullet::SmallCircle => Self::SmallCircle,
|
|
|
|
|
|
RawToolBullet::Circle => Self::Circle,
|
|
|
|
|
|
RawToolBullet::SmallTriangle => Self::SmallTriangle,
|
|
|
|
|
|
RawToolBullet::Triangle => Self::Triangle,
|
|
|
|
|
|
RawToolBullet::Diamond => Self::Diamond,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl From<RawScrollConfig> for ScrollConfig {
|
|
|
|
|
|
fn from(raw: RawScrollConfig) -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
margin: raw.margin,
|
|
|
|
|
|
min_page_fraction: raw.min_page_fraction.min(100),
|
|
|
|
|
|
follow_indicator: raw.follow_indicator.into(),
|
|
|
|
|
|
follow_auto_select: raw.follow_auto_select,
|
|
|
|
|
|
follow_by_overscroll: raw.follow_by_overscroll,
|
|
|
|
|
|
anchor_on_fold: raw.anchor_on_fold,
|
|
|
|
|
|
respect_manual_folds: raw.respect_manual_folds,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl From<RawLayoutConfig> for LayoutConfig {
|
|
|
|
|
|
fn from(raw: RawLayoutConfig) -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
outer_vpad: raw.outer_vpad,
|
|
|
|
|
|
outer_hpad_left: raw.outer_hpad_left,
|
|
|
|
|
|
outer_hpad_right: raw.outer_hpad_right,
|
|
|
|
|
|
block_pad_left: raw.block_pad_left,
|
|
|
|
|
|
block_pad_right: raw.block_pad_right,
|
|
|
|
|
|
}
|
|
|
|
|
|
.validated()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl From<RawScrollbarConfig> for ScrollbarConfig {
|
|
|
|
|
|
fn from(raw: RawScrollbarConfig) -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
enabled: raw.enabled,
|
|
|
|
|
|
gap_left: raw.gap_left,
|
|
|
|
|
|
gap_right: raw.gap_right,
|
|
|
|
|
|
scrollbar_bg: raw.scrollbar_bg.to_option(),
|
|
|
|
|
|
scrollbar_fg: raw.scrollbar_fg.to_option(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl From<RawBlocksConfig> for BlocksConfig {
|
|
|
|
|
|
fn from(raw: RawBlocksConfig) -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
edit: raw.edit.into(),
|
|
|
|
|
|
prompt: raw.prompt.into(),
|
|
|
|
|
|
thinking: raw.thinking.into(),
|
|
|
|
|
|
tool: raw.tool.into(),
|
|
|
|
|
|
list_dir: raw.list_dir.into(),
|
|
|
|
|
|
execute: raw.execute.into(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl From<RawToolConfig> for ToolConfig {
|
|
|
|
|
|
fn from(raw: RawToolConfig) -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
muted_collapsed: raw.muted_collapsed,
|
|
|
|
|
|
dim_details: raw.dim_details,
|
|
|
|
|
|
bullet: raw.bullet.into(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl From<RawListDirConfig> for ListDirConfig {
|
|
|
|
|
|
fn from(raw: RawListDirConfig) -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
terminal_bg: raw.terminal_bg,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl From<RawExecuteConfig> for ExecuteConfig {
|
|
|
|
|
|
fn from(raw: RawExecuteConfig) -> Self {
|
|
|
|
|
|
// Default accent is accent_running from theme — already quantized.
|
|
|
|
|
|
let default_accent = crate::theme::Theme::current().accent_running;
|
|
|
|
|
|
let accent = raw.running_accent.to_option().unwrap_or(default_accent);
|
|
|
|
|
|
Self {
|
|
|
|
|
|
first_lines: raw.first_lines.max(1),
|
|
|
|
|
|
last_lines: raw.last_lines.max(1),
|
|
|
|
|
|
accent_enabled: raw.accent_enabled,
|
|
|
|
|
|
running_accent: accent,
|
|
|
|
|
|
header_style: raw.header_style.into(),
|
|
|
|
|
|
muted_command_collapsed: raw.muted_command_collapsed,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl From<RawEditBlockConfig> for EditBlockConfig {
|
|
|
|
|
|
fn from(raw: RawEditBlockConfig) -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
indent: raw.indent,
|
|
|
|
|
|
vpad: raw.vpad,
|
|
|
|
|
|
bg: raw.bg.into(),
|
|
|
|
|
|
accent_bg: raw.accent_bg,
|
|
|
|
|
|
accent: raw.accent.to_option(),
|
|
|
|
|
|
gutter_bg: raw.gutter_bg,
|
|
|
|
|
|
indent_bg: raw.indent_bg,
|
|
|
|
|
|
line_summary: raw.line_summary,
|
|
|
|
|
|
expanded_by_default: raw.expanded_by_default,
|
|
|
|
|
|
hunk_separator: raw.hunk_separator.unwrap_or_else(|| "…".to_string()),
|
|
|
|
|
|
dual_line_numbers: raw.dual_line_numbers,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl From<RawPromptConfig> for PromptConfig {
|
|
|
|
|
|
fn from(raw: RawPromptConfig) -> Self {
|
|
|
|
|
|
Self {
|
|
|
|
|
|
vpad: raw.vpad,
|
|
|
|
|
|
bg: raw.bg.into(),
|
|
|
|
|
|
accent_bg: raw.accent_bg,
|
|
|
|
|
|
min_lines: raw.min_lines,
|
|
|
|
|
|
show_prefix: raw.show_prefix,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl From<RawThinkingConfig> for ThinkingConfig {
|
|
|
|
|
|
fn from(raw: RawThinkingConfig) -> Self {
|
|
|
|
|
|
// Default accent is gray_dim from theme — already quantized.
|
|
|
|
|
|
let default_accent = crate::theme::Theme::current().gray_dim;
|
|
|
|
|
|
let accent = raw.accent.to_option().unwrap_or(default_accent);
|
|
|
|
|
|
// Convert 0-100 integer to 0.0-1.0 float
|
|
|
|
|
|
let bg_blend = (raw.bg_blend.min(100) as f32) / 100.0;
|
|
|
|
|
|
Self {
|
|
|
|
|
|
accent,
|
|
|
|
|
|
accent_enabled: raw.accent_enabled,
|
|
|
|
|
|
bg_blend,
|
|
|
|
|
|
truncated_lines: raw.truncated_lines.max(1),
|
|
|
|
|
|
animate: raw.animate,
|
|
|
|
|
|
header: raw.header,
|
|
|
|
|
|
header_bright: raw.header_bright,
|
2026-07-28 22:50:19 +00:00
|
|
|
|
body_dim_italic: false,
|
|
|
|
|
|
collapsed_expand_hint: false,
|
2026-07-16 06:46:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// Color Parsing
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
/// An optional color that can be "none" or a color value.
|
|
|
|
|
|
/// This allows TOML to represent None values explicitly.
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Default)]
|
|
|
|
|
|
pub enum OptionalColor {
|
|
|
|
|
|
#[default]
|
|
|
|
|
|
None,
|
|
|
|
|
|
Some(Color),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl OptionalColor {
|
|
|
|
|
|
/// Convert to `Option<Color>`, quantizing to the terminal's color level.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// User-configured colors arrive as raw RGB from TOML. Unlike theme
|
|
|
|
|
|
/// colors (which are pre-quantized by [`Theme::current()`]), these
|
|
|
|
|
|
/// need quantization here to work correctly on 256-color terminals.
|
|
|
|
|
|
pub fn to_option(&self) -> Option<Color> {
|
|
|
|
|
|
match self {
|
|
|
|
|
|
OptionalColor::None => None,
|
|
|
|
|
|
OptionalColor::Some(c) => Some(crate::theme::quantize(*c)),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Convert to `Option<Color>` without quantization.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Returns the raw parsed color value. Used for serialization
|
|
|
|
|
|
/// round-trip tests and anywhere the original RGB is needed.
|
|
|
|
|
|
pub fn to_option_raw(&self) -> Option<Color> {
|
|
|
|
|
|
match self {
|
|
|
|
|
|
OptionalColor::None => None,
|
|
|
|
|
|
OptionalColor::Some(c) => Some(*c),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Serialize for OptionalColor {
|
|
|
|
|
|
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
|
|
|
|
match self {
|
|
|
|
|
|
OptionalColor::None => serializer.serialize_str("none"),
|
|
|
|
|
|
OptionalColor::Some(Color::Rgb(r, g, b)) => [*r, *g, *b].serialize(serializer),
|
|
|
|
|
|
OptionalColor::Some(Color::Indexed(n)) => {
|
|
|
|
|
|
// Convert indexed to RGB for serialization so the TOML round-trips.
|
|
|
|
|
|
let (r, g, b) = crate::render::color::indexed_to_rgb(*n);
|
|
|
|
|
|
[r, g, b].serialize(serializer)
|
|
|
|
|
|
}
|
|
|
|
|
|
OptionalColor::Some(_) => serializer.serialize_str("unknown"),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl<'de> Deserialize<'de> for OptionalColor {
|
|
|
|
|
|
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
|
|
|
|
|
let value = RawColorValue::deserialize(deserializer)?;
|
|
|
|
|
|
match value {
|
|
|
|
|
|
RawColorValue::Rgb([r, g, b]) => Ok(OptionalColor::Some(Color::Rgb(r, g, b))),
|
|
|
|
|
|
RawColorValue::Text(s) => {
|
|
|
|
|
|
let s = s.trim().to_lowercase();
|
|
|
|
|
|
if s == "none" || s == "null" {
|
|
|
|
|
|
Ok(OptionalColor::None)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
parse_color_string(&s)
|
|
|
|
|
|
.map(OptionalColor::Some)
|
|
|
|
|
|
.map_err(serde::de::Error::custom)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
|
#[serde(untagged)]
|
|
|
|
|
|
enum RawColorValue {
|
|
|
|
|
|
Rgb([u8; 3]),
|
|
|
|
|
|
Text(String),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn parse_color_string(s: &str) -> Result<Color, String> {
|
|
|
|
|
|
let s = s.trim();
|
|
|
|
|
|
if let Some(hex) = s.strip_prefix('#') {
|
|
|
|
|
|
return parse_hex_color(hex);
|
|
|
|
|
|
}
|
|
|
|
|
|
lookup_named_color(s)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn parse_hex_color(hex: &str) -> Result<Color, String> {
|
|
|
|
|
|
let hex = hex.trim_start_matches('#');
|
|
|
|
|
|
let (r, g, b) = match hex.len() {
|
|
|
|
|
|
3 => {
|
|
|
|
|
|
let r = u8::from_str_radix(&hex[0..1], 16).map_err(|e| e.to_string())? * 17;
|
|
|
|
|
|
let g = u8::from_str_radix(&hex[1..2], 16).map_err(|e| e.to_string())? * 17;
|
|
|
|
|
|
let b = u8::from_str_radix(&hex[2..3], 16).map_err(|e| e.to_string())? * 17;
|
|
|
|
|
|
(r, g, b)
|
|
|
|
|
|
}
|
|
|
|
|
|
6 => {
|
|
|
|
|
|
let r = u8::from_str_radix(&hex[0..2], 16).map_err(|e| e.to_string())?;
|
|
|
|
|
|
let g = u8::from_str_radix(&hex[2..4], 16).map_err(|e| e.to_string())?;
|
|
|
|
|
|
let b = u8::from_str_radix(&hex[4..6], 16).map_err(|e| e.to_string())?;
|
|
|
|
|
|
(r, g, b)
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => return Err(format!("invalid hex color: #{hex}")),
|
|
|
|
|
|
};
|
|
|
|
|
|
Ok(Color::Rgb(r, g, b))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn lookup_named_color(name: &str) -> Result<Color, String> {
|
|
|
|
|
|
// Named colors use the GrokNight RGB palette. They are quantized via
|
|
|
|
|
|
// `parse_color_string` → `quantize()` to match the terminal's capabilities.
|
|
|
|
|
|
let color = match name.to_uppercase().as_str() {
|
|
|
|
|
|
// Background colors
|
|
|
|
|
|
"BG" | "BG_BASE" => Color::Rgb(20, 20, 20), // #141414
|
|
|
|
|
|
"BG_LIGHT" | "BG_HIGHLIGHT" => Color::Rgb(30, 30, 30), // #1e1e1e
|
|
|
|
|
|
"BG_DARK" => Color::Rgb(17, 17, 17), // #111111
|
|
|
|
|
|
"BG_TERMINAL" | "BG_NIGHT" => Color::Rgb(10, 10, 10), // #0a0a0a
|
|
|
|
|
|
"BG_VISUAL" => Color::Rgb(30, 32, 45), // blue-tinted selection
|
|
|
|
|
|
"BG_SEARCH" => Color::Rgb(48, 48, 52), // #303034
|
|
|
|
|
|
|
|
|
|
|
|
// Accent colors (TokyoNight Night)
|
|
|
|
|
|
"BLUE" => Color::Rgb(77, 121, 255), // #4D79FF
|
|
|
|
|
|
"BLUE0" => Color::Rgb(61, 89, 161), // #3d59a1
|
|
|
|
|
|
"BLUE1" => Color::Rgb(42, 195, 222), // #2ac3de
|
|
|
|
|
|
"BLUE2" => Color::Rgb(13, 185, 215), // #0db9d7
|
|
|
|
|
|
"BLUE5" => Color::Rgb(137, 221, 255), // #89ddff
|
|
|
|
|
|
"BLUE6" => Color::Rgb(180, 249, 248), // #b4f9f8
|
|
|
|
|
|
"BLUE7" => Color::Rgb(57, 75, 112), // #394b70
|
|
|
|
|
|
"CYAN" => Color::Rgb(125, 207, 255), // #7dcfff
|
|
|
|
|
|
"GREEN" => Color::Rgb(36, 196, 116), // #24C474
|
|
|
|
|
|
"GREEN1" => Color::Rgb(115, 218, 202), // #73daca
|
|
|
|
|
|
"GREEN2" => Color::Rgb(65, 166, 181), // #41a6b5
|
|
|
|
|
|
"YELLOW" => Color::Rgb(255, 219, 141), // #FFDB8D
|
|
|
|
|
|
"ORANGE" => Color::Rgb(255, 158, 100), // #ff9e64
|
|
|
|
|
|
"RED" => Color::Rgb(248, 114, 122), // #F8727A
|
|
|
|
|
|
"RED1" => Color::Rgb(219, 75, 75), // #db4b4b
|
|
|
|
|
|
"MAGENTA" => Color::Rgb(187, 154, 247), // #bb9af7
|
|
|
|
|
|
"PURPLE" => Color::Rgb(131, 113, 211), // #8371D3
|
|
|
|
|
|
"MAGENTA2" => Color::Rgb(255, 0, 124), // #ff007c
|
|
|
|
|
|
"TEAL" | "HINT" => Color::Rgb(26, 188, 156), // #1abc9c
|
|
|
|
|
|
|
|
|
|
|
|
// Text colors
|
|
|
|
|
|
"FG" | "TEXT" | "TEXT_PRIMARY" => Color::Rgb(243, 243, 243), // #f3f3f3
|
|
|
|
|
|
"FG_DARK" | "TEXT_SECONDARY" => Color::Rgb(200, 200, 200), // #c8c8c8
|
|
|
|
|
|
"FG_GUTTER" => Color::Rgb(65, 65, 65), // #414141
|
|
|
|
|
|
"COMMENT" | "MUTED" | "TEXT_MUTED" => Color::Rgb(98, 98, 98), // #626262
|
|
|
|
|
|
"DARK3" => Color::Rgb(90, 90, 90), // #5a5a5a
|
|
|
|
|
|
"DARK5" | "TOOL" => Color::Rgb(120, 120, 120), // #787878
|
|
|
|
|
|
|
|
|
|
|
|
// Semantic colors
|
|
|
|
|
|
"ERROR" => Color::Rgb(247, 118, 142), // RED
|
|
|
|
|
|
"SUCCESS" => Color::Rgb(158, 206, 106), // GREEN
|
|
|
|
|
|
"WARNING" => Color::Rgb(224, 175, 104), // YELLOW
|
|
|
|
|
|
"INFO" => Color::Rgb(125, 207, 255), // CYAN
|
|
|
|
|
|
|
|
|
|
|
|
// Basic colors
|
|
|
|
|
|
"BLACK" => Color::Black,
|
|
|
|
|
|
"WHITE" => Color::White,
|
|
|
|
|
|
"GRAY" | "GREY" => Color::Gray,
|
|
|
|
|
|
|
|
|
|
|
|
_ => return Err(format!("unknown color name: {name}")),
|
|
|
|
|
|
};
|
|
|
|
|
|
Ok(color)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// TOML Generation with Comments
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
impl RawAppearanceConfig {
|
|
|
|
|
|
pub fn to_toml_with_comments() -> String {
|
|
|
|
|
|
let mut config = Self::default();
|
|
|
|
|
|
// Template-only materialization: these default to None (the shell's
|
|
|
|
|
|
// `[ui] collapsed_edit_blocks` flag decides), which the serializer
|
|
|
|
|
|
// would omit entirely. Show the flag-off shape as commented lines so
|
|
|
|
|
|
// the keys stay discoverable; commenting-out below keeps them inert.
|
|
|
|
|
|
config.scrollback.blocks.edit.expanded_by_default = Some(true);
|
|
|
|
|
|
config.scrollback.blocks.edit.line_summary = Some(false);
|
|
|
|
|
|
let toml_str = toml_edit::ser::to_string_pretty(&config).expect("serialize default");
|
|
|
|
|
|
let mut doc: DocumentMut = toml_str.parse().expect("parse toml");
|
|
|
|
|
|
|
|
|
|
|
|
let pager_path = crate::util::display_user_grok_path("pager.toml");
|
|
|
|
|
|
let header = format!(
|
|
|
|
|
|
"\
|
|
|
|
|
|
# Grok Pager Appearance Configuration ({pager_path})
|
|
|
|
|
|
# Every value below is a commented-out built-in default: uncomment a line and
|
|
|
|
|
|
# save to override it. Values left commented track future default changes.
|
|
|
|
|
|
# Delete the file to regenerate this template.
|
|
|
|
|
|
#
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
# TOKYO NIGHT STORM COLOR PALETTE
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
#
|
|
|
|
|
|
# Colors can be specified as:
|
|
|
|
|
|
# - Named color: \"BLUE\", \"cyan\", \"Comment\" (case-insensitive)
|
|
|
|
|
|
# - Hex string: \"#7aa2f7\" or \"#f7f\" (3 or 6 digits)
|
|
|
|
|
|
# - RGB array: [122, 162, 247]
|
|
|
|
|
|
#
|
|
|
|
|
|
# Background colors:
|
|
|
|
|
|
# BG, BG_BASE #141414 (neutral background)
|
|
|
|
|
|
# BG_LIGHT #1c1c1c (highlighted background)
|
|
|
|
|
|
# BG_DARK #111111 (darker background)
|
|
|
|
|
|
# BG_TERMINAL #0e0e0e (terminal background)
|
|
|
|
|
|
# BG_VISUAL #262628 (visual selection background)
|
|
|
|
|
|
# BG_SEARCH #303034 (search highlight background)
|
|
|
|
|
|
#
|
|
|
|
|
|
# Primary colors:
|
|
|
|
|
|
# BLUE #7aa2f7
|
|
|
|
|
|
# BLUE0 #3d59a1 (dark blue, used for search/visual)
|
|
|
|
|
|
# BLUE1 #2ac3de (bright cyan-blue)
|
|
|
|
|
|
# BLUE2 #0db9d7 (teal-blue)
|
|
|
|
|
|
# BLUE5 #89ddff (light cyan)
|
|
|
|
|
|
# BLUE6 #b4f9f8 (pale cyan)
|
|
|
|
|
|
# BLUE7 #394b70 (dark muted blue)
|
|
|
|
|
|
# CYAN #7dcfff
|
|
|
|
|
|
# GREEN #9ece6a
|
|
|
|
|
|
# GREEN1 #73daca (teal-green)
|
|
|
|
|
|
# GREEN2 #41a6b5 (dark teal)
|
|
|
|
|
|
# YELLOW #e0af68
|
|
|
|
|
|
# ORANGE #ff9e64
|
|
|
|
|
|
# RED #f7768e
|
|
|
|
|
|
# RED1 #db4b4b (dark red)
|
|
|
|
|
|
# MAGENTA #bb9af7
|
|
|
|
|
|
# PURPLE #9d7cd8
|
|
|
|
|
|
# MAGENTA2 #ff007c (hot pink)
|
|
|
|
|
|
# TEAL, HINT #1abc9c
|
|
|
|
|
|
#
|
|
|
|
|
|
# Text colors:
|
|
|
|
|
|
# FG, TEXT #c8c8c8 (primary text)
|
|
|
|
|
|
# FG_DARK #b2b2b2 (secondary text)
|
|
|
|
|
|
# FG_GUTTER #414141 (line number gutter)
|
|
|
|
|
|
# COMMENT, MUTED #5f5f5f (muted/comment text)
|
|
|
|
|
|
# DARK3 #5a5a5a (medium gray)
|
|
|
|
|
|
# DARK5, TOOL #787878 (tool accent, system prompt)
|
|
|
|
|
|
#
|
|
|
|
|
|
# Semantic colors:
|
|
|
|
|
|
# ERROR #f7768e (same as RED)
|
|
|
|
|
|
# SUCCESS #9ece6a (same as GREEN)
|
|
|
|
|
|
# WARNING #e0af68 (same as YELLOW)
|
|
|
|
|
|
# INFO #7dcfff (same as CYAN)
|
|
|
|
|
|
#
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(terminal) = doc.get_mut("terminal").and_then(Item::as_table_mut) {
|
|
|
|
|
|
annotate_table::<RawTerminalConfig>(terminal);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(todo) = doc.get_mut("todo").and_then(Item::as_table_mut) {
|
|
|
|
|
|
annotate_table::<RawTodoConfig>(todo);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(animation) = doc.get_mut("animation").and_then(Item::as_table_mut) {
|
|
|
|
|
|
annotate_table::<RawAnimationConfig>(animation);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(prompt) = doc.get_mut("prompt").and_then(Item::as_table_mut) {
|
|
|
|
|
|
annotate_table::<RawPromptViewConfig>(prompt);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(scrollback) = doc.get_mut("scrollback").and_then(Item::as_table_mut) {
|
|
|
|
|
|
if let Some(display) = scrollback.get_mut("display").and_then(Item::as_table_mut) {
|
|
|
|
|
|
annotate_table::<RawScrollbackDisplayConfig>(display);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(layout) = scrollback.get_mut("layout").and_then(Item::as_table_mut) {
|
|
|
|
|
|
annotate_table::<RawLayoutConfig>(layout);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(scrollbar) = scrollback.get_mut("scrollbar").and_then(Item::as_table_mut) {
|
|
|
|
|
|
annotate_table::<RawScrollbarConfig>(scrollbar);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(scroll) = scrollback.get_mut("scroll").and_then(Item::as_table_mut) {
|
|
|
|
|
|
annotate_table::<RawScrollConfig>(scroll);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(blocks) = scrollback.get_mut("blocks").and_then(Item::as_table_mut) {
|
|
|
|
|
|
if let Some(edit) = blocks.get_mut("edit").and_then(Item::as_table_mut) {
|
|
|
|
|
|
annotate_table::<RawEditBlockConfig>(edit);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(prompt) = blocks.get_mut("prompt").and_then(Item::as_table_mut) {
|
|
|
|
|
|
annotate_table::<RawPromptConfig>(prompt);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(thinking) = blocks.get_mut("thinking").and_then(Item::as_table_mut) {
|
|
|
|
|
|
annotate_table::<RawThinkingConfig>(thinking);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(tool) = blocks.get_mut("tool").and_then(Item::as_table_mut) {
|
|
|
|
|
|
annotate_table::<RawToolConfig>(tool);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(list_dir) = blocks.get_mut("list_dir").and_then(Item::as_table_mut) {
|
|
|
|
|
|
annotate_table::<RawListDirConfig>(list_dir);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(execute) = blocks.get_mut("execute").and_then(Item::as_table_mut) {
|
|
|
|
|
|
annotate_table::<RawExecuteConfig>(execute);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
format!("{header}{}", comment_out_values(&doc.to_string()))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Comment out every key-value line, keeping section headers and comments.
|
|
|
|
|
|
/// The generated template documents defaults without pinning them, so a
|
|
|
|
|
|
/// future built-in default change reaches installs holding an old file
|
|
|
|
|
|
/// (active values would freeze the defaults of the day forever).
|
|
|
|
|
|
fn comment_out_values(toml: &str) -> String {
|
|
|
|
|
|
let mut out = String::with_capacity(toml.len() + 256);
|
|
|
|
|
|
for line in toml.lines() {
|
|
|
|
|
|
let trimmed = line.trim_start();
|
|
|
|
|
|
if !(trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with('[')) {
|
|
|
|
|
|
out.push_str("# ");
|
|
|
|
|
|
}
|
|
|
|
|
|
out.push_str(line);
|
|
|
|
|
|
out.push('\n');
|
|
|
|
|
|
}
|
|
|
|
|
|
out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Serializes the pager.toml read-modify-write so two rapid settings
|
|
|
|
|
|
/// toggles can't interleave and clobber each other (mirrors the shell's
|
|
|
|
|
|
/// `save_config` `SAVE_LOCK`).
|
|
|
|
|
|
static PAGER_TOML_SAVE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
|
|
|
|
|
|
|
|
|
|
|
pub fn persist_respect_manual_folds(enabled: bool) -> std::io::Result<()> {
|
|
|
|
|
|
use std::io::{Error, ErrorKind};
|
|
|
|
|
|
|
|
|
|
|
|
if xai_grok_config::user_grok_home().is_none() {
|
|
|
|
|
|
return Err(Error::new(
|
|
|
|
|
|
ErrorKind::NotFound,
|
|
|
|
|
|
"no user grok home resolved; refusing to write a cwd-relative pager.toml \
|
|
|
|
|
|
that startup would never read",
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
let _guard = PAGER_TOML_SAVE_LOCK
|
|
|
|
|
|
.lock()
|
|
|
|
|
|
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
|
|
|
|
|
|
|
|
|
|
let path = crate::util::pager_toml_path();
|
|
|
|
|
|
let content = match std::fs::read_to_string(&path) {
|
|
|
|
|
|
Ok(c) => c,
|
|
|
|
|
|
Err(e) if e.kind() == ErrorKind::NotFound => String::new(),
|
|
|
|
|
|
Err(e) => return Err(e),
|
|
|
|
|
|
};
|
|
|
|
|
|
let updated = upsert_respect_manual_folds(&content, enabled)
|
|
|
|
|
|
.map_err(|e| Error::new(ErrorKind::InvalidData, e))?;
|
|
|
|
|
|
if let Some(dir) = path.parent() {
|
|
|
|
|
|
std::fs::create_dir_all(dir)?;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
|
let prior_mode: Option<u32> = std::fs::metadata(&path).ok().map(|m| {
|
|
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
|
|
m.permissions().mode()
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let suffix = {
|
|
|
|
|
|
let nanos = std::time::SystemTime::now()
|
|
|
|
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
|
|
|
|
.map(|d| d.as_nanos())
|
|
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
|
format!("toml.tmp.{}.{}", std::process::id(), nanos)
|
|
|
|
|
|
};
|
|
|
|
|
|
let tmp = path.with_extension(suffix);
|
|
|
|
|
|
std::fs::write(&tmp, updated)?;
|
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
|
{
|
|
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
|
|
if let Some(mode) = prior_mode {
|
|
|
|
|
|
let _ = std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(mode));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
std::fs::rename(&tmp, &path)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn upsert_respect_manual_folds(content: &str, enabled: bool) -> Result<String, String> {
|
|
|
|
|
|
let mut doc: DocumentMut = content
|
|
|
|
|
|
.parse()
|
|
|
|
|
|
.map_err(|e: toml_edit::TomlError| e.to_string())?;
|
|
|
|
|
|
let scrollback = doc
|
|
|
|
|
|
.entry("scrollback")
|
|
|
|
|
|
.or_insert_with(implicit_table)
|
|
|
|
|
|
.as_table_mut()
|
|
|
|
|
|
.ok_or_else(|| "pager.toml `scrollback` is not a table".to_string())?;
|
|
|
|
|
|
let scroll = scrollback
|
|
|
|
|
|
.entry("scroll")
|
|
|
|
|
|
.or_insert(toml_edit::Item::Table(toml_edit::Table::new()))
|
|
|
|
|
|
.as_table_mut()
|
|
|
|
|
|
.ok_or_else(|| "pager.toml `scrollback.scroll` is not a table".to_string())?;
|
|
|
|
|
|
scroll.insert("respect_manual_folds", toml_edit::value(enabled));
|
|
|
|
|
|
Ok(doc.to_string())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn implicit_table() -> Item {
|
|
|
|
|
|
let mut table = toml_edit::Table::new();
|
|
|
|
|
|
table.set_implicit(true);
|
|
|
|
|
|
Item::Table(table)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn annotate_table<T: DocumentedFields>(table: &mut toml_edit::Table) {
|
|
|
|
|
|
for (mut key, _value) in table.iter_mut() {
|
|
|
|
|
|
let field_name = key.get();
|
|
|
|
|
|
if let Ok(docs) = T::get_field_docs(field_name) {
|
|
|
|
|
|
let comment: String = docs
|
|
|
|
|
|
.lines()
|
|
|
|
|
|
.map(|l| {
|
|
|
|
|
|
if l.is_empty() {
|
|
|
|
|
|
"#\n".to_string()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
format!("# {l}\n")
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let decor = key.leaf_decor_mut();
|
|
|
|
|
|
let prefix = decor.prefix().and_then(RawString::as_str).unwrap_or("");
|
|
|
|
|
|
decor.set_prefix(format!("{prefix}{comment}"));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// Tests
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
mod tests {
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_parse_hex_color() {
|
|
|
|
|
|
assert_eq!(parse_hex_color("ff8800"), Ok(Color::Rgb(255, 136, 0)));
|
|
|
|
|
|
assert_eq!(parse_hex_color("f80"), Ok(Color::Rgb(255, 136, 0)));
|
|
|
|
|
|
assert_eq!(parse_hex_color("000"), Ok(Color::Rgb(0, 0, 0)));
|
|
|
|
|
|
assert_eq!(parse_hex_color("fff"), Ok(Color::Rgb(255, 255, 255)));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_parse_named_color() {
|
|
|
|
|
|
assert_eq!(lookup_named_color("BLUE"), Ok(Color::Rgb(77, 121, 255)));
|
|
|
|
|
|
assert_eq!(lookup_named_color("blue"), Ok(Color::Rgb(77, 121, 255)));
|
|
|
|
|
|
assert!(lookup_named_color("NOTACOLOR").is_err());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_deserialize_rgb_array() {
|
|
|
|
|
|
let toml = r#"color = [255, 128, 0]"#;
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
|
struct Test {
|
|
|
|
|
|
color: OptionalColor,
|
|
|
|
|
|
}
|
|
|
|
|
|
let t: Test = toml::from_str(toml).unwrap();
|
|
|
|
|
|
assert_eq!(t.color.to_option_raw(), Some(Color::Rgb(255, 128, 0)));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_deserialize_hex_string() {
|
|
|
|
|
|
let toml = r##"color = "#ff8800""##;
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
|
struct Test {
|
|
|
|
|
|
color: OptionalColor,
|
|
|
|
|
|
}
|
|
|
|
|
|
let t: Test = toml::from_str(toml).unwrap();
|
|
|
|
|
|
assert_eq!(t.color.to_option_raw(), Some(Color::Rgb(255, 136, 0)));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_deserialize_named_color() {
|
|
|
|
|
|
let toml = r#"color = "BLUE""#;
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
|
struct Test {
|
|
|
|
|
|
color: OptionalColor,
|
|
|
|
|
|
}
|
|
|
|
|
|
let t: Test = toml::from_str(toml).unwrap();
|
|
|
|
|
|
assert_eq!(t.color.to_option_raw(), Some(Color::Rgb(77, 121, 255)));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_deserialize_none_color() {
|
|
|
|
|
|
let toml = r#"color = "none""#;
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
|
struct Test {
|
|
|
|
|
|
color: OptionalColor,
|
|
|
|
|
|
}
|
|
|
|
|
|
let t: Test = toml::from_str(toml).unwrap();
|
|
|
|
|
|
assert_eq!(t.color.to_option(), None);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_deserialize_full_config() {
|
|
|
|
|
|
let toml = r#"
|
|
|
|
|
|
[scrollback.blocks.edit]
|
|
|
|
|
|
indent = false
|
|
|
|
|
|
vpad = true
|
|
|
|
|
|
bg = "dark"
|
|
|
|
|
|
accent_bg = true
|
|
|
|
|
|
accent = "CYAN"
|
|
|
|
|
|
gutter_bg = true
|
|
|
|
|
|
"#;
|
|
|
|
|
|
let raw: RawAppearanceConfig = toml::from_str(toml).unwrap();
|
|
|
|
|
|
let cfg: AppearanceConfig = raw.into();
|
|
|
|
|
|
assert!(!cfg.scrollback.blocks.edit.indent);
|
|
|
|
|
|
assert!(cfg.scrollback.blocks.edit.vpad);
|
|
|
|
|
|
assert_eq!(cfg.scrollback.blocks.edit.bg, BlockBackground::Dark);
|
|
|
|
|
|
assert!(cfg.scrollback.blocks.edit.accent_bg);
|
|
|
|
|
|
// CYAN from lookup_named_color — quantized by OptionalColor::to_option().
|
|
|
|
|
|
// In non-TTY test environments, quantize() may downgrade to ANSI 16,
|
|
|
|
|
|
// so compare against the quantized value.
|
|
|
|
|
|
assert!(cfg.scrollback.blocks.edit.accent.is_some());
|
|
|
|
|
|
assert!(cfg.scrollback.blocks.edit.gutter_bg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_default_config() {
|
|
|
|
|
|
let cfg = AppearanceConfig::default();
|
|
|
|
|
|
assert!(cfg.scrollback.blocks.edit.indent);
|
|
|
|
|
|
assert!(!cfg.scrollback.blocks.edit.vpad);
|
|
|
|
|
|
assert_eq!(cfg.scrollback.blocks.edit.bg, BlockBackground::None);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Legacy configs still contain the removed `invert` key (it was written
|
|
|
|
|
|
/// by old generated `pager.toml`s); parsing must keep ignoring it.
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn removed_prompt_invert_key_is_ignored() {
|
|
|
|
|
|
let raw: RawAppearanceConfig =
|
|
|
|
|
|
toml::from_str("[scrollback.blocks.prompt]\ninvert = true\nvpad = false").unwrap();
|
|
|
|
|
|
let cfg: AppearanceConfig = raw.into();
|
|
|
|
|
|
assert!(!cfg.scrollback.blocks.prompt.vpad);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_respect_manual_folds_defaults_off_and_round_trips() {
|
|
|
|
|
|
let cfg = AppearanceConfig::default();
|
|
|
|
|
|
assert!(!cfg.scrollback.scroll.respect_manual_folds);
|
|
|
|
|
|
|
|
|
|
|
|
let raw: RawAppearanceConfig =
|
|
|
|
|
|
toml::from_str("[scrollback.scroll]\nrespect_manual_folds = true").unwrap();
|
|
|
|
|
|
let cfg: AppearanceConfig = raw.into();
|
|
|
|
|
|
assert!(cfg.scrollback.scroll.respect_manual_folds);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_upsert_respect_manual_folds_writes_key_and_preserves_siblings() {
|
|
|
|
|
|
let updated = upsert_respect_manual_folds("", true).unwrap();
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
updated.contains("[scrollback.scroll]"),
|
|
|
|
|
|
"missing section must render in section form, not as inline tables:\n{updated}"
|
|
|
|
|
|
);
|
|
|
|
|
|
let raw: RawAppearanceConfig = toml::from_str(&updated).unwrap();
|
|
|
|
|
|
let cfg: AppearanceConfig = raw.into();
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
cfg.scrollback.scroll.respect_manual_folds,
|
|
|
|
|
|
"empty pager.toml: upserted key must parse back as true:\n{updated}"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let existing = "[scrollback.scroll]\nanchor_on_fold = false\nrespect_manual_folds = true\n";
|
|
|
|
|
|
let updated = upsert_respect_manual_folds(existing, false).unwrap();
|
|
|
|
|
|
let raw: RawAppearanceConfig = toml::from_str(&updated).unwrap();
|
|
|
|
|
|
let cfg: AppearanceConfig = raw.into();
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
!cfg.scrollback.scroll.respect_manual_folds,
|
|
|
|
|
|
"key updated in place"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
!cfg.scrollback.scroll.anchor_on_fold,
|
|
|
|
|
|
"sibling keys must be preserved:\n{updated}"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
upsert_respect_manual_folds("scrollback = 5", true).is_err(),
|
|
|
|
|
|
"non-table `scrollback` must be a hard error, not a panic"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
upsert_respect_manual_folds("[scrollback]\nscroll = 5", true).is_err(),
|
|
|
|
|
|
"non-table `scrollback.scroll` must be a hard error, not a panic"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn test_to_toml_with_comments() {
|
|
|
|
|
|
let toml = RawAppearanceConfig::to_toml_with_comments();
|
|
|
|
|
|
// Check scrollback sections — check for key names rather than section
|
|
|
|
|
|
// headers since toml_edit may format subtables differently when the
|
|
|
|
|
|
// parent table has inline keys (like line_under_last_entry).
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("margin = "),
|
|
|
|
|
|
"Missing scroll margin in:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("min_page_fraction = "),
|
|
|
|
|
|
"Missing scroll min_page_fraction in:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("follow_indicator = "),
|
|
|
|
|
|
"Missing follow_indicator in:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("follow_auto_select = "),
|
|
|
|
|
|
"Missing follow_auto_select in:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("follow_by_overscroll = "),
|
|
|
|
|
|
"Missing follow_by_overscroll in:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("respect_manual_folds = "),
|
|
|
|
|
|
"Missing respect_manual_folds in:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("# Opt-in: keep manually folded blocks as-is"),
|
|
|
|
|
|
"Missing respect_manual_folds comment in:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("line_under_last_entry"),
|
|
|
|
|
|
"Missing line_under_last_entry in:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
// Check tool bullet fields
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("bullet = "),
|
|
|
|
|
|
"Missing tool bullet in:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
// Note: bullet_color and bullet_accent removed in scrollback-v2 refactor.
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("outer_vpad = "),
|
|
|
|
|
|
"Missing layout outer_vpad in:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("enabled = "),
|
|
|
|
|
|
"Missing scrollbar enabled in:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
// Check animation section
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("[animation]"),
|
|
|
|
|
|
"Missing [animation] section in:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(toml.contains("fps = "), "Missing animation fps in:\n{toml}");
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("wave_rows = "),
|
|
|
|
|
|
"Missing animation wave_rows in:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
// Check edit block comments
|
|
|
|
|
|
assert!(toml.contains("# Whether to apply 2-char indent"));
|
|
|
|
|
|
assert!(toml.contains("indent = true"));
|
|
|
|
|
|
// Check prompt block comments
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("# Whether to apply vertical padding"),
|
|
|
|
|
|
"Missing prompt vpad comment in:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("# Minimum content lines to show"),
|
|
|
|
|
|
"Missing prompt min_lines comment in:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
// Check prompt view section
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("[prompt]"),
|
|
|
|
|
|
"Missing [prompt] section in:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("collapse_unfocused = "),
|
|
|
|
|
|
"Missing collapse_unfocused in:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("mouse_hover = "),
|
|
|
|
|
|
"Missing mouse_hover in:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
// Check blocks are under scrollback
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("[scrollback.blocks.edit]"),
|
|
|
|
|
|
"Missing [scrollback.blocks.edit] section in:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("[scrollback.blocks.execute]"),
|
|
|
|
|
|
"Missing [scrollback.blocks.execute] section in:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
// Check execute block has running_accent
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("running_accent = "),
|
|
|
|
|
|
"Missing execute running_accent in:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
// Ensure old top-level sections are gone
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
!toml.contains("\n[scroll]\n"),
|
|
|
|
|
|
"Old [scroll] section should not exist:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
!toml.contains("[prompt_input]"),
|
|
|
|
|
|
"Old [prompt_input] section should not exist:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
!toml.contains("\n[mouse]\n"),
|
|
|
|
|
|
"Old [mouse] section should not exist:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
!toml.contains("\n[blocks."),
|
|
|
|
|
|
"Old top-level [blocks.*] should not exist:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// The generated template must be inert: parsing it (and an empty file)
|
|
|
|
|
|
/// yields exactly the built-in defaults, so old dev-generated files can
|
|
|
|
|
|
/// never pin a superseded default again.
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn template_parses_to_builtin_defaults() {
|
|
|
|
|
|
let serialize = |cfg: &RawAppearanceConfig| toml_edit::ser::to_string_pretty(cfg).unwrap();
|
|
|
|
|
|
let defaults = serialize(&RawAppearanceConfig::default());
|
|
|
|
|
|
|
|
|
|
|
|
let template = RawAppearanceConfig::to_toml_with_comments();
|
|
|
|
|
|
let parsed: RawAppearanceConfig = toml::from_str(&template).expect("template must parse");
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
|
serialize(&parsed),
|
|
|
|
|
|
defaults,
|
|
|
|
|
|
"inert template must round-trip to built-in defaults"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let empty: RawAppearanceConfig = toml::from_str("").expect("empty config must parse");
|
|
|
|
|
|
assert_eq!(serialize(&empty), defaults);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Every value line in the template is commented out; only section
|
|
|
|
|
|
/// headers (empty tables) and comments are active.
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn template_has_no_active_value_lines() {
|
|
|
|
|
|
let template = RawAppearanceConfig::to_toml_with_comments();
|
|
|
|
|
|
for line in template.lines() {
|
|
|
|
|
|
let trimmed = line.trim_start();
|
|
|
|
|
|
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with('[') {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
panic!("active value line in generated template: {line:?}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// The single policy point pairing the pager.toml shape keys with the
|
|
|
|
|
|
/// shell-owned `collapsed_edit_blocks` flag: unset keys follow the flag
|
|
|
|
|
|
/// (on = collapsed one-liner with diffstat, off = legacy expanded diff
|
|
|
|
|
|
/// without it); explicit values pin the shape in both directions.
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn effective_edit_shape_follows_flag_unless_pinned() {
|
|
|
|
|
|
let unset = EditBlockConfig::default();
|
|
|
|
|
|
assert!(unset.effective_expanded(false), "flag off: expanded");
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
!unset.effective_line_summary(false),
|
|
|
|
|
|
"flag off: no diffstat"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(!unset.effective_expanded(true), "flag on: collapsed");
|
|
|
|
|
|
assert!(unset.effective_line_summary(true), "flag on: diffstat");
|
|
|
|
|
|
|
|
|
|
|
|
let pinned = EditBlockConfig {
|
|
|
|
|
|
expanded_by_default: Some(true),
|
|
|
|
|
|
line_summary: Some(true),
|
|
|
|
|
|
..EditBlockConfig::default()
|
|
|
|
|
|
};
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
pinned.effective_expanded(true),
|
|
|
|
|
|
"explicit expanded beats the flag"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
pinned.effective_line_summary(false),
|
|
|
|
|
|
"explicit diffstat beats the flag"
|
|
|
|
|
|
);
|
|
|
|
|
|
let pinned = EditBlockConfig {
|
|
|
|
|
|
expanded_by_default: Some(false),
|
|
|
|
|
|
line_summary: Some(false),
|
|
|
|
|
|
..EditBlockConfig::default()
|
|
|
|
|
|
};
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
!pinned.effective_expanded(false),
|
|
|
|
|
|
"explicit collapse beats the flag"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
!pinned.effective_line_summary(true),
|
|
|
|
|
|
"explicit no-diffstat beats the flag"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// The two flag-deferred edit keys default to `None` (omitted by the
|
|
|
|
|
|
/// serializer), but the template must still document them as commented
|
|
|
|
|
|
/// lines showing the flag-off shape.
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn template_documents_flag_deferred_edit_keys() {
|
|
|
|
|
|
let template = RawAppearanceConfig::to_toml_with_comments();
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
template.contains("# expanded_by_default = true"),
|
|
|
|
|
|
"expanded_by_default missing from template:\n{template}"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
template.contains("# line_summary = false"),
|
|
|
|
|
|
"line_summary missing from template:\n{template}"
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
template.contains("collapsed_edit_blocks"),
|
|
|
|
|
|
"doc comments must point at the [ui] flag:\n{template}"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// The `/settings` writer must insert an ACTIVE key into the inert
|
|
|
|
|
|
/// template (whose keys are all comments) rather than edit a commented
|
|
|
|
|
|
/// line or fail.
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn upsert_into_inert_template_inserts_active_key() {
|
|
|
|
|
|
let template = RawAppearanceConfig::to_toml_with_comments();
|
|
|
|
|
|
let updated = upsert_respect_manual_folds(&template, true).expect("upsert");
|
|
|
|
|
|
let parsed: RawAppearanceConfig =
|
|
|
|
|
|
toml::from_str(&updated).expect("updated template must parse");
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
parsed.scrollback.scroll.respect_manual_folds,
|
|
|
|
|
|
"inserted key must be active:\n{updated}"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Terminal config (alt_screen) parsing ─────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn terminal_alt_screen_auto_default() {
|
|
|
|
|
|
let raw: RawAppearanceConfig = toml::from_str("").unwrap();
|
|
|
|
|
|
assert_eq!(raw.terminal.alt_screen, RawAltScreenMode::Auto);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn terminal_alt_screen_never() {
|
|
|
|
|
|
let raw: RawAppearanceConfig =
|
|
|
|
|
|
toml::from_str("[terminal]\nalt_screen = \"never\"").unwrap();
|
|
|
|
|
|
assert_eq!(raw.terminal.alt_screen, RawAltScreenMode::Never);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn terminal_alt_screen_always() {
|
|
|
|
|
|
let raw: RawAppearanceConfig =
|
|
|
|
|
|
toml::from_str("[terminal]\nalt_screen = \"always\"").unwrap();
|
|
|
|
|
|
assert_eq!(raw.terminal.alt_screen, RawAltScreenMode::Always);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn terminal_alt_screen_auto_explicit() {
|
|
|
|
|
|
let raw: RawAppearanceConfig = toml::from_str("[terminal]\nalt_screen = \"auto\"").unwrap();
|
|
|
|
|
|
assert_eq!(raw.terminal.alt_screen, RawAltScreenMode::Auto);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn terminal_config_to_runtime_conversion() {
|
|
|
|
|
|
use crate::terminal::AltScreenMode;
|
|
|
|
|
|
|
|
|
|
|
|
let raw = RawAltScreenMode::Auto;
|
|
|
|
|
|
assert_eq!(AltScreenMode::from(raw), AltScreenMode::Auto);
|
|
|
|
|
|
|
|
|
|
|
|
let raw = RawAltScreenMode::Always;
|
|
|
|
|
|
assert_eq!(AltScreenMode::from(raw), AltScreenMode::Always);
|
|
|
|
|
|
|
|
|
|
|
|
let raw = RawAltScreenMode::Never;
|
|
|
|
|
|
assert_eq!(AltScreenMode::from(raw), AltScreenMode::Never);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn terminal_section_appears_in_generated_toml() {
|
|
|
|
|
|
let toml = RawAppearanceConfig::to_toml_with_comments();
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
toml.contains("alt_screen = "),
|
|
|
|
|
|
"Missing alt_screen in generated config:\n{toml}"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-07-28 22:50:19 +00:00
|
|
|
|
|
|
|
|
|
|
/// A config written before the key existed must still parse and keep K9.
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn minimal_collapse_thinking_defaults_off_and_old_configs_parse() {
|
|
|
|
|
|
let empty: RawAppearanceConfig = toml::from_str("").expect("empty config must parse");
|
|
|
|
|
|
assert!(!empty.terminal.minimal_collapse_thinking);
|
|
|
|
|
|
assert!(!AppearanceConfig::from(empty).minimal_collapse_thinking);
|
|
|
|
|
|
|
|
|
|
|
|
let legacy: RawAppearanceConfig =
|
|
|
|
|
|
toml::from_str("[terminal]\nminimal = true\nminimal_live_rows = 12\n")
|
|
|
|
|
|
.expect("legacy config must parse");
|
|
|
|
|
|
let cfg: AppearanceConfig = legacy.into();
|
|
|
|
|
|
assert!(cfg.minimal);
|
|
|
|
|
|
assert_eq!(cfg.minimal_live_rows, 12);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
!cfg.minimal_collapse_thinking,
|
|
|
|
|
|
"a config written before the key existed must keep the K9 default"
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
assert!(!AppearanceConfig::default().minimal_collapse_thinking);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn minimal_collapse_thinking_opt_in_parses() {
|
|
|
|
|
|
let raw: RawAppearanceConfig =
|
|
|
|
|
|
toml::from_str("[terminal]\nminimal_collapse_thinking = true\n").unwrap();
|
|
|
|
|
|
assert!(AppearanceConfig::from(raw).minimal_collapse_thinking);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// The reasoning-legibility toggles must stay un-settable from pager.toml.
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn thinking_body_treatment_is_off_by_default_and_not_a_toml_key() {
|
|
|
|
|
|
let cfg = AppearanceConfig::default();
|
|
|
|
|
|
assert!(!cfg.scrollback.blocks.thinking.body_dim_italic);
|
|
|
|
|
|
assert!(!cfg.scrollback.blocks.thinking.collapsed_expand_hint);
|
|
|
|
|
|
|
|
|
|
|
|
let template = RawAppearanceConfig::to_toml_with_comments();
|
|
|
|
|
|
assert!(!template.contains("body_dim_italic"));
|
|
|
|
|
|
assert!(!template.contains("collapsed_expand_hint"));
|
|
|
|
|
|
}
|
2026-07-16 06:46:02 +01:00
|
|
|
|
}
|