//! Settings modal — opens via F2, `/settings`, command palette, and //! shortcuts-help. //! //! ## State machine //! //! `SettingsModalState` carries a `UiConfig` snapshot plus a mode machine: //! //! - `Browse` — j/k navigates rows; Space toggles Bool; Enter opens //! a chooser/editor for Enum/String/Int. //! - `FilterFocused` — `/` enters filter mode; `invalidate_filter` //! recomputes `filtered_cache` on every mutation. //! - `PickingEnum { ... }` — enum chooser sub-pane. //! - `EditingValue { ... }` — inline string/int editor. //! //! ## Keyboard ↔ mouse parity //! //! Every keyboard interaction has a mouse equivalent via `handle_mouse`. //! //! ## Close-key interception //! //! F2/Ctrl+,/Cmd+, are intercepted before mode-specific routing. //! Esc-in-Browse is handled by the `ModalWindow` chrome (so //! `is_close_key` does NOT match Esc); Esc-in-FilterFocused exits //! filter mode without closing. use std::sync::Arc; use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEventKind}; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span}; use unicode_width::UnicodeWidthStr; use crate::app::actions::Action; use crate::render::line_utils::truncate_str; use crate::settings::{ EnumChoice, OwnedEnumChoice, PagerLocalSnapshot, SettingCategory, SettingKey, SettingKind, SettingMeta, SettingValue, SettingsRegistry, StringValidator, current_value_for, dynamic_enum_choices, }; use crate::theme::Theme; use crate::views::modal_window::{ self, ModalContentArea, ModalSizing, ModalWindowConfig, ModalWindowState, Shortcut, }; use kigi_shell::agent::config::UiConfig; // --------------------------------------------------------------------------- // Public constants // --------------------------------------------------------------------------- /// Public display title of the modal — also used by /// `views/modal.rs::ActiveModal::message` so renames stay in one place. pub const MODAL_TITLE: &str = "Settings"; /// Width of the `"─ "` leading decoration before the title in the /// modal's top border. Used to compute the breadcrumb hit-rect x offset. const TITLE_LEADING_DECORATION_W: u16 = 2; // `─ `: 1 cell box-drawing + 1 cell space. // Descriptions are now expand-on-demand via Right/Left arrows; // see `render_expanded_description`. /// Below this width the row list is skipped (chrome renders empty). const CONTENT_MIN_WIDTH: u16 = 10; /// Default max width for the modal. Keeps the row list compact on wide terminals. const STANDARD_MAX_WIDTH: u16 = 110; /// Per-side margin when editing `max_thoughts_width` (modal widens /// to `terminal_width - 2*margin` so the wrap preview is useful). const MAX_THOUGHTS_WIDTH_WIDENED_MARGIN: u16 = 8; /// Outcome of a key or mouse event. Separate from `InputOutcome` /// because the modal doesn't own `agent.active_modal` — close is /// the caller's responsibility. #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum SettingsKeyOutcome { /// Close the modal. Close, /// Forward to dispatch. Action(Action), /// Forward two actions in order (first must resolve before second). /// Used by `d`-reset-in-picker to revert preview before opening /// the reset-confirm overlay. ActionPair(Action, Action), /// Internal state mutation, no action. Changed, /// No-op. Unchanged, } // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- /// One row in the visible flat list — either a category header (non- /// selectable) or a setting row (selectable, dispatchable). #[derive(Debug, Clone)] pub enum RowEntry { Header { category: SettingCategory }, Setting { key: SettingKey, meta_index: usize }, } /// Mode state for the modal. #[derive(Debug, Clone)] pub enum SettingsModalMode { Browse, /// `/` was pressed; chars filter the visible rows. FilterFocused, /// Enum chooser sub-pane. `supports_preview` is cached at open /// time to avoid per-keystroke registry lookups. PickingEnum { key: SettingKey, choices_idx: usize, original_value: SettingValue, supports_preview: bool, }, /// Group sub-sheet: a list of the group's child Bool toggles. `child_idx` /// is the focused child within the group. Space/Enter toggles in place /// (the sheet stays open); Esc returns to Browse. Mirrors `PickingEnum`'s /// open/render/commit flow but for independent toggles. PickingGroup { key: SettingKey, child_idx: usize, }, /// Inline string/int editor. `cursor_byte` is always on a char /// boundary. `validation_error` shows live feedback; commit /// re-validates before dispatching. No `original_value` — these /// settings have no live preview, so Esc is a pure cancel. EditingValue { key: SettingKey, buffer: String, cursor_byte: usize, validation_error: Option, }, } /// Settings modal state. Boxed inside `ActiveModal::Settings` to /// avoid clippy `large_enum_variant`. pub struct SettingsModalState { pub window: ModalWindowState, pub registry: Arc, /// `UiConfig` snapshot, refreshed by the dispatcher on mutations. pub ui_snapshot: UiConfig, pub pager_snapshot: PagerLocalSnapshot, /// Computed row layout (headers + settings, in render order). pub rows: Vec, /// Index into `rows` of the focused row. pub selected: usize, /// Vertical scroll offset (line-granular). pub scroll_offset: usize, pub mode: SettingsModalMode, /// Filter query. Persists across FilterFocused→Browse on Enter; cleared by Esc. pub query: String, /// Byte offset of the editing cursor within `query`. pub query_cursor: usize, /// Row indices matching `query`, recomputed per mutation (not per frame). filtered_cache: Vec, // -- Mouse hit-test rects (populated by render) -- pub list_area: Rect, /// Click-hit rect per row, parallel to `rows`. pub row_rects: Vec, /// Click-hit rect for the value column on each row. Bool rows /// toggle on click; Enum/String/Int rows open the sub-pane. pub value_hit_rects: Vec, /// `(decrement_rect, increment_rect)` for the Int stepper's /// `‹`/`›` glyphs. Zero-sized when not in Int editing mode. pub editor_adornment_rects: (Rect, Rect), /// Click-hit rect per choice in `PickingEnum`. Each rect spans the /// full height of a choice (including wrapped description lines). pub picker_choice_rects: Vec, /// Hit-rect for the breadcrumb title in sub-pane modes /// (`PickingEnum`/`EditingValue`). Clicking anywhere on /// `Settings ›