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    /// Screen reader mode (disables animation-heavy rendering paths)
112    #[serde(default)]
113    pub screen_reader_mode: bool,
114
115    /// Reduce motion mode (disables shimmer/flashing animations)
116    #[serde(default)]
117    pub reduce_motion_mode: bool,
118
119    /// Keep progress animation while reduce motion mode is enabled
120    #[serde(default)]
121    pub reduce_motion_keep_progress_animation: bool,
122
123    /// Customization settings
124    pub customization: CustomizationConfig,
125}
126
127impl Default for AppearanceConfig {
128    fn default() -> Self {
129        Self {
130            theme: "default".to_owned(),
131            ui_mode: UiMode::Full,
132            show_sidebar: true,
133            min_content_width: 40,
134            min_navigation_width: 20,
135            navigation_width_percent: 25,
136            transcript_bottom_padding: 0,
137            dim_completed_todos: true,
138            message_block_spacing: 0,
139            layout_mode: LayoutModeOverride::Auto,
140            reasoning_display_mode: ReasoningDisplayMode::Toggle,
141            reasoning_visible_default: false,
142            screen_reader_mode: false,
143            reduce_motion_mode: false,
144            reduce_motion_keep_progress_animation: false,
145            customization: CustomizationConfig::default(),
146        }
147    }
148}
149
150impl AppearanceConfig {
151    /// Check if sidebar should be shown based on ui_mode and show_sidebar
152    pub fn should_show_sidebar(&self) -> bool {
153        match self.ui_mode {
154            UiMode::Full => self.show_sidebar,
155            UiMode::Minimal | UiMode::Focused => false,
156        }
157    }
158
159    pub fn reasoning_visible(&self) -> bool {
160        match self.reasoning_display_mode {
161            ReasoningDisplayMode::Always => true,
162            ReasoningDisplayMode::Hidden => false,
163            ReasoningDisplayMode::Toggle => self.reasoning_visible_default,
164        }
165    }
166
167    pub fn motion_reduced(&self) -> bool {
168        self.screen_reader_mode || self.reduce_motion_mode
169    }
170
171    pub fn should_animate_progress_status(&self) -> bool {
172        !self.screen_reader_mode
173            && (!self.reduce_motion_mode || self.reduce_motion_keep_progress_animation)
174    }
175
176    /// Check if footer should be shown based on ui_mode
177    #[allow(dead_code)]
178    pub fn should_show_footer(&self) -> bool {
179        match self.ui_mode {
180            UiMode::Full => true,
181            UiMode::Minimal => false,
182            UiMode::Focused => false,
183        }
184    }
185}
186
187/// Key binding configuration
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct KeyBindingConfig {
190    /// Map of action to key sequences
191    pub bindings: HashMap<String, Vec<String>>,
192}
193
194impl Default for KeyBindingConfig {
195    fn default() -> Self {
196        let mut bindings = HashMap::new();
197
198        // Navigation
199        bindings.insert("scroll_up".to_owned(), vec!["up".to_owned()]);
200        bindings.insert("scroll_down".to_owned(), vec!["down".to_owned()]);
201        bindings.insert("page_up".to_owned(), vec!["pageup".to_owned()]);
202        bindings.insert("page_down".to_owned(), vec!["pagedown".to_owned()]);
203
204        // Input
205        bindings.insert("submit".to_owned(), vec!["enter".to_owned()]);
206        bindings.insert("submit_queue".to_owned(), vec!["tab".to_owned()]);
207        bindings.insert("cancel".to_owned(), vec!["esc".to_owned()]);
208        bindings.insert("interrupt".to_owned(), vec!["ctrl+c".to_owned()]);
209
210        Self { bindings }
211    }
212}
213
214/// Behavior configuration
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct BehaviorConfig {
217    /// Maximum lines for input area
218    pub max_input_lines: usize,
219
220    /// Whether to enable command history
221    pub enable_history: bool,
222
223    /// History size limit
224    pub history_size: usize,
225
226    /// Whether to enable double-tap escape to clear input
227    pub double_tap_escape_clears: bool,
228
229    /// Delay in milliseconds for double-tap detection
230    pub double_tap_delay_ms: u64,
231
232    /// Whether to auto-scroll to bottom
233    pub auto_scroll_to_bottom: bool,
234
235    /// Whether to show queued inputs
236    pub show_queued_inputs: bool,
237}
238
239impl Default for BehaviorConfig {
240    fn default() -> Self {
241        Self {
242            max_input_lines: 10,
243            enable_history: true,
244            history_size: 100,
245            double_tap_escape_clears: true,
246            double_tap_delay_ms: 300,
247            auto_scroll_to_bottom: true,
248            show_queued_inputs: true,
249        }
250    }
251}
252
253/// Performance configuration
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct PerformanceConfig {
256    /// Cache size for rendered elements
257    pub render_cache_size: usize,
258
259    /// Transcript cache size (number of messages to cache)
260    pub transcript_cache_size: usize,
261
262    /// Whether to enable transcript reflow caching
263    pub enable_transcript_caching: bool,
264
265    /// Size of LRU cache for expensive operations
266    pub lru_cache_size: usize,
267
268    /// Whether to enable smooth scrolling
269    pub enable_smooth_scrolling: bool,
270}
271
272impl Default for PerformanceConfig {
273    fn default() -> Self {
274        Self {
275            render_cache_size: 1000,
276            transcript_cache_size: 500,
277            enable_transcript_caching: true,
278            lru_cache_size: 128,
279            enable_smooth_scrolling: false,
280        }
281    }
282}
283
284/// Customization configuration
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct CustomizationConfig {
287    /// User-defined UI labels
288    pub ui_labels: HashMap<String, String>,
289
290    /// Custom styling options
291    pub custom_styles: HashMap<String, String>,
292
293    /// Enabled UI features
294    pub enabled_features: Vec<String>,
295}
296
297impl Default for CustomizationConfig {
298    fn default() -> Self {
299        Self {
300            ui_labels: HashMap::new(),
301            custom_styles: HashMap::new(),
302            enabled_features: vec![
303                "slash_commands".to_owned(),
304                "file_palette".to_owned(),
305                "modal_dialogs".to_owned(),
306            ],
307        }
308    }
309}
310
311impl SessionConfig {
312    /// Creates a new default configuration
313    #[allow(dead_code)]
314    pub fn new() -> Self {
315        Self::default()
316    }
317
318    /// Loads configuration from a file
319    #[allow(dead_code)]
320    pub fn load_from_file(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
321        let content = crate::utils::file_utils::read_file_with_context_sync(
322            Path::new(path),
323            "session config file",
324        )
325        .map_err(|err| -> Box<dyn std::error::Error> { Box::new(std::io::Error::other(err)) })?;
326        let config: SessionConfig = toml::from_str(&content)?;
327        Ok(config)
328    }
329
330    /// Saves configuration to a file
331    #[allow(dead_code)]
332    pub fn save_to_file(&self, path: &str) -> Result<(), Box<dyn std::error::Error>> {
333        let content = toml::to_string_pretty(self)?;
334        crate::utils::file_utils::write_file_with_context_sync(
335            Path::new(path),
336            &content,
337            "session config file",
338        )
339        .map_err(|err| -> Box<dyn std::error::Error> { Box::new(std::io::Error::other(err)) })?;
340        Ok(())
341    }
342
343    /// Updates a specific configuration value by key
344    #[allow(dead_code)]
345    pub fn set_value(&mut self, key: &str, value: &str) -> Result<(), String> {
346        // This is a simplified version - in a real implementation, we'd have more sophisticated
347        // parsing and validation for different configuration types
348        match key {
349            "behavior.max_input_lines" => {
350                self.behavior.max_input_lines = value
351                    .parse()
352                    .map_err(|_| format!("Cannot parse '{}' as number", value))?;
353            }
354            "performance.lru_cache_size" => {
355                self.performance.lru_cache_size = value
356                    .parse()
357                    .map_err(|_| format!("Cannot parse '{}' as number", value))?;
358            }
359            _ => return Err(format!("Unknown configuration key: {}", key)),
360        }
361        Ok(())
362    }
363
364    /// Gets a configuration value by key
365    #[allow(dead_code)]
366    pub fn get_value(&self, key: &str) -> Option<String> {
367        match key {
368            "behavior.max_input_lines" => Some(self.behavior.max_input_lines.to_string()),
369            "performance.lru_cache_size" => Some(self.performance.lru_cache_size.to_string()),
370            _ => None,
371        }
372    }
373
374    /// Validates the configuration to ensure all values are within acceptable ranges
375    #[allow(dead_code)]
376    pub fn validate(&self) -> Result<(), Vec<String>> {
377        let mut errors = Vec::new();
378
379        if self.behavior.history_size == 0 {
380            errors.push("history_size must be greater than 0".to_owned());
381        }
382
383        if self.performance.lru_cache_size == 0 {
384            errors.push("lru_cache_size must be greater than 0".to_owned());
385        }
386
387        if self.appearance.navigation_width_percent > 100 {
388            errors.push("navigation_width_percent must be between 0 and 100".to_owned());
389        }
390
391        if errors.is_empty() {
392            Ok(())
393        } else {
394            Err(errors)
395        }
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402
403    #[test]
404    fn test_default_config() {
405        let config = SessionConfig::new();
406        assert_eq!(config.behavior.history_size, 100);
407    }
408
409    #[test]
410    fn test_config_serialization() {
411        let config = SessionConfig::new();
412        let serialized = toml::to_string_pretty(&config).unwrap();
413        assert!(serialized.contains("theme"));
414    }
415
416    #[test]
417    fn test_config_value_setting() {
418        let mut config = SessionConfig::new();
419
420        config.set_value("behavior.max_input_lines", "15").unwrap();
421        assert_eq!(config.behavior.max_input_lines, 15);
422
423        assert!(
424            config
425                .set_value("behavior.max_input_lines", "not_a_number")
426                .is_err()
427        );
428    }
429
430    #[test]
431    fn test_config_value_getting() {
432        let config = SessionConfig::new();
433        assert_eq!(
434            config.get_value("behavior.max_input_lines"),
435            Some("10".to_owned())
436        );
437    }
438
439    #[test]
440    fn test_config_validation() {
441        let config = SessionConfig::new();
442        assert!(config.validate().is_ok());
443
444        // Test invalid history size
445        let mut invalid_config = config.clone();
446        invalid_config.behavior.history_size = 0;
447        assert!(invalid_config.validate().is_err());
448
449        // Test invalid cache size
450        let mut invalid_config2 = config.clone();
451        invalid_config2.performance.lru_cache_size = 0;
452        assert!(invalid_config2.validate().is_err());
453    }
454
455    #[test]
456    fn test_config_with_custom_values() {
457        let mut config = SessionConfig::new();
458
459        // Test setting custom values
460        config.behavior.max_input_lines = 20;
461        config.performance.lru_cache_size = 256;
462
463        assert_eq!(config.behavior.max_input_lines, 20);
464        assert_eq!(config.performance.lru_cache_size, 256);
465    }
466}