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