Skip to main content

vtcode_tui/core_tui/session/
config.rs

1//! Configuration system for TUI session UI preferences
2//!
3//! Contains settings for customizable UI elements, colors, key bindings, and other preferences.
4
5use hashbrown::HashMap;
6use serde::{Deserialize, Serialize};
7use std::path::Path;
8
9/// Main configuration struct for TUI session preferences
10#[derive(Debug, Clone, Serialize, Deserialize, Default)]
11pub struct SessionConfig {
12    /// UI appearance settings
13    pub appearance: AppearanceConfig,
14
15    /// Key binding preferences
16    pub key_bindings: KeyBindingConfig,
17
18    /// Behavior preferences
19    pub behavior: BehaviorConfig,
20
21    /// Performance related settings
22    pub performance: PerformanceConfig,
23
24    /// Customization settings
25    pub customization: CustomizationConfig,
26}
27
28/// UI mode variants for quick presets
29#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
30#[serde(rename_all = "snake_case")]
31pub enum UiMode {
32    /// Full UI with all features (sidebar, footer)
33    #[default]
34    Full,
35    /// Minimal UI - no sidebar, no footer
36    Minimal,
37    /// Focused mode - transcript only, maximum content space
38    Focused,
39}
40
41/// Layout mode override for responsive UI
42#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
43#[serde(rename_all = "snake_case")]
44pub enum LayoutModeOverride {
45    /// Auto-detect based on terminal size
46    #[default]
47    Auto,
48    /// Force compact mode (no borders)
49    Compact,
50    /// Force standard mode (borders, no sidebar/footer)
51    Standard,
52    /// Force wide mode (sidebar + footer behavior)
53    Wide,
54}
55
56/// Reasoning visibility behavior in the transcript
57#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
58#[serde(rename_all = "snake_case")]
59pub enum ReasoningDisplayMode {
60    /// Always show reasoning output
61    Always,
62    /// Allow reasoning visibility to be toggled (uses default below at startup)
63    #[default]
64    Toggle,
65    /// Never show reasoning output
66    Hidden,
67}
68
69/// UI appearance configuration
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct AppearanceConfig {
72    /// Color theme to use
73    pub theme: String,
74
75    /// UI mode variant (full, minimal, focused)
76    pub ui_mode: UiMode,
77
78    /// Whether to show the right sidebar (queue, context, tools)
79    pub show_sidebar: bool,
80
81    /// Minimum width for content area
82    pub min_content_width: u16,
83
84    /// Minimum width for navigation area
85    pub min_navigation_width: u16,
86
87    /// Percentage of width for navigation area
88    pub navigation_width_percent: u8,
89
90    /// Transcript bottom padding
91    pub transcript_bottom_padding: u16,
92
93    /// Whether to dim completed todo items (- [x] and ~~strikethrough~~)
94    pub dim_completed_todos: bool,
95
96    /// Number of blank lines between message blocks (0-2)
97    pub message_block_spacing: u8,
98
99    /// Override responsive layout mode
100    #[serde(default)]
101    pub layout_mode: LayoutModeOverride,
102
103    /// Reasoning visibility mode
104    #[serde(default)]
105    pub reasoning_display_mode: ReasoningDisplayMode,
106
107    /// Default reasoning visibility when mode is "toggle"
108    #[serde(default)]
109    pub reasoning_visible_default: bool,
110
111    /// Enable Vim-style input editing for the prompt.
112    #[serde(default)]
113    pub vim_mode: bool,
114
115    /// Screen reader mode (disables animation-heavy rendering paths)
116    #[serde(default)]
117    pub screen_reader_mode: bool,
118
119    /// Reduce motion mode (disables shimmer/flashing animations)
120    #[serde(default)]
121    pub reduce_motion_mode: bool,
122
123    /// Keep progress animation while reduce motion mode is enabled
124    #[serde(default)]
125    pub reduce_motion_keep_progress_animation: bool,
126
127    /// Customization settings
128    pub customization: CustomizationConfig,
129}
130
131impl Default for AppearanceConfig {
132    fn default() -> Self {
133        Self {
134            theme: "default".to_owned(),
135            ui_mode: UiMode::Full,
136            show_sidebar: true,
137            min_content_width: 40,
138            min_navigation_width: 20,
139            navigation_width_percent: 25,
140            transcript_bottom_padding: 0,
141            dim_completed_todos: true,
142            message_block_spacing: 0,
143            layout_mode: LayoutModeOverride::Auto,
144            reasoning_display_mode: ReasoningDisplayMode::Toggle,
145            reasoning_visible_default: false,
146            vim_mode: false,
147            screen_reader_mode: false,
148            reduce_motion_mode: false,
149            reduce_motion_keep_progress_animation: false,
150            customization: CustomizationConfig::default(),
151        }
152    }
153}
154
155impl AppearanceConfig {
156    /// Check if sidebar should be shown based on ui_mode and show_sidebar
157    pub fn should_show_sidebar(&self) -> bool {
158        match self.ui_mode {
159            UiMode::Full => self.show_sidebar,
160            UiMode::Minimal | UiMode::Focused => false,
161        }
162    }
163
164    pub fn reasoning_visible(&self) -> bool {
165        match self.reasoning_display_mode {
166            ReasoningDisplayMode::Always => true,
167            ReasoningDisplayMode::Hidden => false,
168            ReasoningDisplayMode::Toggle => self.reasoning_visible_default,
169        }
170    }
171
172    pub fn motion_reduced(&self) -> bool {
173        self.screen_reader_mode || self.reduce_motion_mode
174    }
175
176    pub fn should_animate_progress_status(&self) -> bool {
177        !self.screen_reader_mode
178            && (!self.reduce_motion_mode || self.reduce_motion_keep_progress_animation)
179    }
180
181    /// Check if footer should be shown based on ui_mode
182    #[allow(dead_code)]
183    pub fn should_show_footer(&self) -> bool {
184        match self.ui_mode {
185            UiMode::Full => true,
186            UiMode::Minimal => false,
187            UiMode::Focused => false,
188        }
189    }
190}
191
192/// Key binding configuration
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct KeyBindingConfig {
195    /// Map of action to key sequences
196    pub bindings: HashMap<String, Vec<String>>,
197}
198
199impl Default for KeyBindingConfig {
200    fn default() -> Self {
201        let mut bindings = HashMap::new();
202
203        // Navigation
204        bindings.insert("scroll_up".to_owned(), vec!["up".to_owned()]);
205        bindings.insert("scroll_down".to_owned(), vec!["down".to_owned()]);
206        bindings.insert("page_up".to_owned(), vec!["pageup".to_owned()]);
207        bindings.insert("page_down".to_owned(), vec!["pagedown".to_owned()]);
208
209        // Input
210        bindings.insert("submit".to_owned(), vec!["enter".to_owned()]);
211        bindings.insert("submit_queue".to_owned(), vec!["tab".to_owned()]);
212        bindings.insert("cancel".to_owned(), vec!["esc".to_owned()]);
213        bindings.insert("interrupt".to_owned(), vec!["ctrl+c".to_owned()]);
214
215        Self { bindings }
216    }
217}
218
219/// Behavior configuration
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct BehaviorConfig {
222    /// Maximum lines for input area
223    pub max_input_lines: usize,
224
225    /// Whether to enable command history
226    pub enable_history: bool,
227
228    /// History size limit
229    pub history_size: usize,
230
231    /// Whether to enable double-tap escape to clear input
232    pub double_tap_escape_clears: bool,
233
234    /// Delay in milliseconds for double-tap detection
235    pub double_tap_delay_ms: u64,
236
237    /// Whether to auto-scroll to bottom
238    pub auto_scroll_to_bottom: bool,
239
240    /// Whether to show queued inputs
241    pub show_queued_inputs: bool,
242}
243
244impl Default for BehaviorConfig {
245    fn default() -> Self {
246        Self {
247            max_input_lines: 10,
248            enable_history: true,
249            history_size: 100,
250            double_tap_escape_clears: true,
251            double_tap_delay_ms: 300,
252            auto_scroll_to_bottom: true,
253            show_queued_inputs: true,
254        }
255    }
256}
257
258/// Performance configuration
259#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct PerformanceConfig {
261    /// Cache size for rendered elements
262    pub render_cache_size: usize,
263
264    /// Transcript cache size (number of messages to cache)
265    pub transcript_cache_size: usize,
266
267    /// Whether to enable transcript reflow caching
268    pub enable_transcript_caching: bool,
269
270    /// Size of LRU cache for expensive operations
271    pub lru_cache_size: usize,
272
273    /// Whether to enable smooth scrolling
274    pub enable_smooth_scrolling: bool,
275}
276
277impl Default for PerformanceConfig {
278    fn default() -> Self {
279        Self {
280            render_cache_size: 1000,
281            transcript_cache_size: 500,
282            enable_transcript_caching: true,
283            lru_cache_size: 128,
284            enable_smooth_scrolling: false,
285        }
286    }
287}
288
289/// Customization configuration
290#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct CustomizationConfig {
292    /// User-defined UI labels
293    pub ui_labels: HashMap<String, String>,
294
295    /// Custom styling options
296    pub custom_styles: HashMap<String, String>,
297
298    /// Enabled UI features
299    pub enabled_features: Vec<String>,
300}
301
302impl Default for CustomizationConfig {
303    fn default() -> Self {
304        Self {
305            ui_labels: HashMap::new(),
306            custom_styles: HashMap::new(),
307            enabled_features: vec![
308                "slash_commands".to_owned(),
309                "file_palette".to_owned(),
310                "modal_dialogs".to_owned(),
311            ],
312        }
313    }
314}
315
316impl SessionConfig {
317    /// Creates a new default configuration
318    #[allow(dead_code)]
319    pub fn new() -> Self {
320        Self::default()
321    }
322
323    /// Loads configuration from a file
324    #[allow(dead_code)]
325    pub fn load_from_file(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
326        let content = crate::utils::file_utils::read_file_with_context_sync(
327            Path::new(path),
328            "session config file",
329        )
330        .map_err(|err| -> Box<dyn std::error::Error> { Box::new(std::io::Error::other(err)) })?;
331        let config: SessionConfig = toml::from_str(&content)?;
332        Ok(config)
333    }
334
335    /// Saves configuration to a file
336    #[allow(dead_code)]
337    pub fn save_to_file(&self, path: &str) -> Result<(), Box<dyn std::error::Error>> {
338        let content = toml::to_string_pretty(self)?;
339        crate::utils::file_utils::write_file_with_context_sync(
340            Path::new(path),
341            &content,
342            "session config file",
343        )
344        .map_err(|err| -> Box<dyn std::error::Error> { Box::new(std::io::Error::other(err)) })?;
345        Ok(())
346    }
347
348    /// Updates a specific configuration value by key
349    #[allow(dead_code)]
350    pub fn set_value(&mut self, key: &str, value: &str) -> Result<(), String> {
351        // This is a simplified version - in a real implementation, we'd have more sophisticated
352        // parsing and validation for different configuration types
353        match key {
354            "behavior.max_input_lines" => {
355                self.behavior.max_input_lines = value
356                    .parse()
357                    .map_err(|_| format!("Cannot parse '{}' as number", value))?;
358            }
359            "performance.lru_cache_size" => {
360                self.performance.lru_cache_size = value
361                    .parse()
362                    .map_err(|_| format!("Cannot parse '{}' as number", value))?;
363            }
364            _ => return Err(format!("Unknown configuration key: {}", key)),
365        }
366        Ok(())
367    }
368
369    /// Gets a configuration value by key
370    #[allow(dead_code)]
371    pub fn get_value(&self, key: &str) -> Option<String> {
372        match key {
373            "behavior.max_input_lines" => Some(self.behavior.max_input_lines.to_string()),
374            "performance.lru_cache_size" => Some(self.performance.lru_cache_size.to_string()),
375            _ => None,
376        }
377    }
378
379    /// Validates the configuration to ensure all values are within acceptable ranges
380    #[allow(dead_code)]
381    pub fn validate(&self) -> Result<(), Vec<String>> {
382        let mut errors = Vec::new();
383
384        if self.behavior.history_size == 0 {
385            errors.push("history_size must be greater than 0".to_owned());
386        }
387
388        if self.performance.lru_cache_size == 0 {
389            errors.push("lru_cache_size must be greater than 0".to_owned());
390        }
391
392        if self.appearance.navigation_width_percent > 100 {
393            errors.push("navigation_width_percent must be between 0 and 100".to_owned());
394        }
395
396        if errors.is_empty() {
397            Ok(())
398        } else {
399            Err(errors)
400        }
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407
408    #[test]
409    fn test_default_config() {
410        let config = SessionConfig::new();
411        assert_eq!(config.behavior.history_size, 100);
412    }
413
414    #[test]
415    fn test_config_serialization() {
416        let config = SessionConfig::new();
417        let serialized = toml::to_string_pretty(&config).unwrap();
418        assert!(serialized.contains("theme"));
419    }
420
421    #[test]
422    fn test_config_value_setting() {
423        let mut config = SessionConfig::new();
424
425        config.set_value("behavior.max_input_lines", "15").unwrap();
426        assert_eq!(config.behavior.max_input_lines, 15);
427
428        assert!(
429            config
430                .set_value("behavior.max_input_lines", "not_a_number")
431                .is_err()
432        );
433    }
434
435    #[test]
436    fn test_config_value_getting() {
437        let config = SessionConfig::new();
438        assert_eq!(
439            config.get_value("behavior.max_input_lines"),
440            Some("10".to_owned())
441        );
442    }
443
444    #[test]
445    fn test_config_validation() {
446        let config = SessionConfig::new();
447        assert!(config.validate().is_ok());
448
449        // Test invalid history size
450        let mut invalid_config = config.clone();
451        invalid_config.behavior.history_size = 0;
452        assert!(invalid_config.validate().is_err());
453
454        // Test invalid cache size
455        let mut invalid_config2 = config.clone();
456        invalid_config2.performance.lru_cache_size = 0;
457        assert!(invalid_config2.validate().is_err());
458    }
459
460    #[test]
461    fn test_config_with_custom_values() {
462        let mut config = SessionConfig::new();
463
464        // Test setting custom values
465        config.behavior.max_input_lines = 20;
466        config.performance.lru_cache_size = 256;
467
468        assert_eq!(config.behavior.max_input_lines, 20);
469        assert_eq!(config.performance.lru_cache_size, 256);
470    }
471}