vtcode_tui/core_tui/session/
config.rs1use 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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
12pub struct SessionConfig {
13 pub appearance: AppearanceConfig,
15
16 pub key_bindings: KeyBindingConfig,
18
19 pub behavior: BehaviorConfig,
21
22 pub performance: PerformanceConfig,
24
25 pub customization: CustomizationConfig,
27}
28
29pub use vtcode_commons::ui_protocol::{LayoutModeOverride, ReasoningDisplayMode, UiMode};
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct AppearanceConfig {
35 pub theme: String,
37
38 pub ui_mode: UiMode,
40
41 pub show_sidebar: bool,
43
44 pub min_content_width: u16,
46
47 pub min_navigation_width: u16,
49
50 pub navigation_width_percent: u8,
52
53 pub transcript_bottom_padding: u16,
55
56 pub dim_completed_todos: bool,
58
59 pub message_block_spacing: u8,
61
62 #[serde(default)]
64 pub layout_mode: LayoutModeOverride,
65
66 #[serde(default)]
68 pub reasoning_display_mode: ReasoningDisplayMode,
69
70 #[serde(default)]
72 pub reasoning_visible_default: bool,
73
74 #[serde(default)]
76 pub vim_mode: bool,
77
78 #[serde(default)]
82 pub readline_mode: bool,
83
84 #[serde(default)]
86 pub screen_reader_mode: bool,
87
88 #[serde(default)]
90 pub reduce_motion_mode: bool,
91
92 #[serde(default)]
94 pub reduce_motion_keep_progress_animation: bool,
95
96 #[serde(default)]
98 pub hide_header: bool,
99
100 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 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 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct KeyBindingConfig {
170 pub bindings: HashMap<String, Vec<String>>,
172}
173
174impl Default for KeyBindingConfig {
175 fn default() -> Self {
176 let mut bindings = HashMap::new();
177
178 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct BehaviorConfig {
197 pub max_input_lines: usize,
199
200 pub enable_history: bool,
202
203 pub history_size: usize,
205
206 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#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct PerformanceConfig {
224 pub render_cache_size: usize,
226
227 pub transcript_cache_size: usize,
229
230 pub enable_transcript_caching: bool,
232
233 pub lru_cache_size: usize,
235
236 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#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct CustomizationConfig {
255 pub ui_labels: HashMap<String, String>,
257
258 pub custom_styles: HashMap<String, String>,
260
261 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 #[expect(dead_code)]
282 pub fn new() -> Self {
283 Self::default()
284 }
285
286 #[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 #[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 #[expect(dead_code)]
308 pub fn set_value(&mut self, key: &str, value: &str) -> Result<(), String> {
309 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 #[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 #[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 let mut invalid_config = config.clone();
415 invalid_config.behavior.history_size = 0;
416 assert!(invalid_config.validate().is_err());
417
418 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 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}