vtcode_ui/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 appearance: AppearanceConfig,
15
16 key_bindings: KeyBindingConfig,
18
19 behavior: BehaviorConfig,
21
22 performance: PerformanceConfig,
24
25 customization: CustomizationConfig,
27}
28
29pub use vtcode_commons::ui_protocol::{
31 DiffPreviewMode, LayoutModeOverride, ReasoningDisplayMode, ThinkingBlockState, UiMode,
32};
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct AppearanceConfig {
37 pub theme: String,
39
40 pub ui_mode: UiMode,
42
43 pub show_sidebar: bool,
45
46 pub min_content_width: u16,
48
49 pub min_navigation_width: u16,
51
52 pub navigation_width_percent: u8,
54
55 pub transcript_bottom_padding: u16,
63
64 pub dim_completed_todos: bool,
66
67 #[serde(default)]
70 pub show_task_panel: bool,
71
72 pub message_block_spacing: u8,
74
75 #[serde(default)]
77 pub layout_mode: LayoutModeOverride,
78
79 #[serde(default)]
81 pub reasoning_display_mode: ReasoningDisplayMode,
82
83 #[serde(default)]
85 pub reasoning_visible_default: bool,
86
87 #[serde(default)]
89 pub thinking_display: ThinkingBlockState,
90
91 #[serde(default)]
93 pub vim_mode: bool,
94
95 #[serde(default)]
99 pub readline_mode: bool,
100
101 #[serde(default)]
103 pub screen_reader_mode: bool,
104
105 #[serde(default)]
107 pub reduce_motion_mode: bool,
108
109 #[serde(default)]
112 pub reduce_motion_keep_progress_animation: bool,
113
114 #[serde(default)]
116 pub hide_header: bool,
117
118 #[serde(default = "default_transcript_review_control")]
120 pub show_transcript_review_hints: bool,
121
122 #[serde(default = "default_transcript_review_control")]
124 pub show_transcript_review_shortcut_guide: bool,
125
126 #[serde(default = "default_transcript_review_control")]
128 pub show_transcript_review_close_button: bool,
129
130 #[serde(default)]
132 pub diff_preview_mode: DiffPreviewMode,
133
134 pub customization: CustomizationConfig,
136}
137
138impl Default for AppearanceConfig {
139 fn default() -> Self {
140 Self {
141 theme: "default".to_owned(),
142 ui_mode: UiMode::Full,
143 show_sidebar: true,
144 min_content_width: 40,
145 min_navigation_width: 20,
146 navigation_width_percent: 25,
147 transcript_bottom_padding: 0,
148 dim_completed_todos: true,
149 show_task_panel: false,
150 message_block_spacing: 1,
154 layout_mode: LayoutModeOverride::Auto,
155 reasoning_display_mode: ReasoningDisplayMode::Toggle,
156 reasoning_visible_default: crate::tui::config::constants::ui::DEFAULT_REASONING_VISIBLE,
157 thinking_display: ThinkingBlockState::Collapsed,
158 vim_mode: false,
159 readline_mode: false,
160 screen_reader_mode: false,
161 reduce_motion_mode: false,
162 reduce_motion_keep_progress_animation: false,
163 hide_header: true,
164 show_transcript_review_hints: default_transcript_review_control(),
165 show_transcript_review_shortcut_guide: default_transcript_review_control(),
166 show_transcript_review_close_button: default_transcript_review_control(),
167 diff_preview_mode: DiffPreviewMode::Inline,
168 customization: CustomizationConfig::default(),
169 }
170 }
171}
172
173fn default_transcript_review_control() -> bool {
174 true
175}
176
177impl AppearanceConfig {
178 pub(crate) fn should_show_sidebar(&self) -> bool {
180 match self.ui_mode {
181 UiMode::Full => self.show_sidebar,
182 UiMode::Minimal | UiMode::Focused => false,
183 }
184 }
185
186 pub(crate) fn should_show_task_panel(&self) -> bool {
188 self.show_task_panel
189 }
190
191 fn reasoning_visible(&self) -> bool {
192 match self.reasoning_display_mode {
193 ReasoningDisplayMode::Always => true,
194 ReasoningDisplayMode::Hidden => false,
195 ReasoningDisplayMode::Toggle => self.reasoning_visible_default,
196 }
197 }
198
199 pub(crate) fn thinking_collapsed_by_default(&self) -> bool {
201 self.thinking_display == ThinkingBlockState::Collapsed
202 }
203
204 pub(crate) fn should_animate_progress_status(&self) -> bool {
206 !self.screen_reader_mode && (!self.reduce_motion_mode || self.reduce_motion_keep_progress_animation)
207 }
208
209 pub fn should_show_footer(&self) -> bool {
211 match self.ui_mode {
212 UiMode::Full => true,
213 UiMode::Minimal => false,
214 UiMode::Focused => false,
215 }
216 }
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct KeyBindingConfig {
222 bindings: HashMap<String, Vec<String>>,
224}
225
226impl Default for KeyBindingConfig {
227 fn default() -> Self {
228 let mut bindings = HashMap::new();
229
230 bindings.insert("scroll_up".to_owned(), vec!["up".to_owned()]);
232 bindings.insert("scroll_down".to_owned(), vec!["down".to_owned()]);
233 bindings.insert("page_up".to_owned(), vec!["pageup".to_owned()]);
234 bindings.insert("page_down".to_owned(), vec!["pagedown".to_owned()]);
235
236 bindings.insert("submit".to_owned(), vec!["enter".to_owned()]);
238 bindings.insert("submit_queue".to_owned(), vec!["tab".to_owned()]);
239 bindings.insert("cancel".to_owned(), vec!["esc".to_owned()]);
240 bindings.insert("interrupt".to_owned(), vec!["ctrl+c".to_owned()]);
241
242 Self { bindings }
243 }
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct BehaviorConfig {
249 max_input_lines: usize,
251
252 enable_history: bool,
254
255 history_size: usize,
257
258 show_queued_inputs: bool,
260}
261
262impl Default for BehaviorConfig {
263 fn default() -> Self {
264 Self {
265 max_input_lines: 10,
266 enable_history: true,
267 history_size: 100,
268 show_queued_inputs: true,
269 }
270 }
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct PerformanceConfig {
276 render_cache_size: usize,
278
279 transcript_cache_size: usize,
281
282 enable_transcript_caching: bool,
284
285 lru_cache_size: usize,
287
288 enable_smooth_scrolling: bool,
290}
291
292impl Default for PerformanceConfig {
293 fn default() -> Self {
294 Self {
295 render_cache_size: 1000,
296 transcript_cache_size: 500,
297 enable_transcript_caching: true,
298 lru_cache_size: 128,
299 enable_smooth_scrolling: false,
300 }
301 }
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct CustomizationConfig {
307 ui_labels: HashMap<String, String>,
309
310 custom_styles: HashMap<String, String>,
312
313 enabled_features: Vec<String>,
315}
316
317impl Default for CustomizationConfig {
318 fn default() -> Self {
319 Self {
320 ui_labels: HashMap::new(),
321 custom_styles: HashMap::new(),
322 enabled_features: vec![
323 "slash_commands".to_owned(),
324 "file_palette".to_owned(),
325 "modal_dialogs".to_owned(),
326 ],
327 }
328 }
329}
330
331impl SessionConfig {
332 fn new() -> Self {
334 Self::default()
335 }
336
337 pub fn load_from_file(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
339 let content = read_file_with_context_sync(Path::new(path), "session config file")
340 .map_err(|err| -> Box<dyn std::error::Error> { Box::new(std::io::Error::other(err)) })?;
341 let config: SessionConfig = toml::from_str(&content)?;
342 Ok(config)
343 }
344
345 pub fn save_to_file(&self, path: &str) -> Result<(), Box<dyn std::error::Error>> {
347 let content = toml::to_string_pretty(self)?;
348 write_file_with_context_sync(Path::new(path), &content, "session config file")
349 .map_err(|err| -> Box<dyn std::error::Error> { Box::new(std::io::Error::other(err)) })?;
350 Ok(())
351 }
352
353 fn set_value(&mut self, key: &str, value: &str) -> Result<(), String> {
355 match key {
358 "behavior.max_input_lines" => {
359 self.behavior.max_input_lines =
360 value.parse().map_err(|_| format!("Cannot parse '{value}' as number"))?;
361 }
362 "performance.lru_cache_size" => {
363 self.performance.lru_cache_size =
364 value.parse().map_err(|_| format!("Cannot parse '{value}' as number"))?;
365 }
366 _ => return Err(format!("Unknown configuration key: {key}")),
367 }
368 Ok(())
369 }
370
371 fn get_value(&self, key: &str) -> Option<String> {
373 match key {
374 "behavior.max_input_lines" => Some(self.behavior.max_input_lines.to_string()),
375 "performance.lru_cache_size" => Some(self.performance.lru_cache_size.to_string()),
376 _ => None,
377 }
378 }
379
380 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() { Ok(()) } else { Err(errors) }
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 #[test]
405 fn test_default_config() {
406 let config = SessionConfig::new();
407 assert_eq!(config.behavior.history_size, 100);
408 assert_eq!(config.appearance.reasoning_display_mode, ReasoningDisplayMode::Toggle);
409 assert!(config.appearance.reasoning_visible_default);
410 assert!(config.appearance.reasoning_visible());
411 assert_eq!(config.appearance.thinking_display, ThinkingBlockState::Collapsed);
412 assert!(config.appearance.thinking_collapsed_by_default());
413 }
414
415 #[test]
416 fn test_task_panel_default_hidden() {
417 let mut config = SessionConfig::new();
418 assert!(!config.appearance.show_task_panel);
419 assert!(!config.appearance.should_show_task_panel());
420
421 config.appearance.show_task_panel = true;
422 assert!(config.appearance.should_show_task_panel());
423 }
424
425 #[test]
426 fn test_thinking_display_default_collapsed() {
427 let config = SessionConfig::new();
428 assert_eq!(config.appearance.thinking_display, ThinkingBlockState::Collapsed);
429 assert!(config.appearance.thinking_collapsed_by_default());
430 }
431
432 #[test]
433 fn test_thinking_display_override_extended() {
434 let mut config = SessionConfig::new();
435 config.appearance.thinking_display = ThinkingBlockState::Extended;
436 assert!(!config.appearance.thinking_collapsed_by_default());
437 }
438
439 #[test]
440 fn test_config_serialization() {
441 let config = SessionConfig::new();
442 let serialized = toml::to_string_pretty(&config).unwrap();
443 assert!(serialized.contains("theme"));
444 }
445
446 #[test]
447 fn test_config_value_setting() {
448 let mut config = SessionConfig::new();
449
450 config.set_value("behavior.max_input_lines", "15").unwrap();
451 assert_eq!(config.behavior.max_input_lines, 15);
452
453 assert!(config.set_value("behavior.max_input_lines", "not_a_number").is_err());
454 }
455
456 #[test]
457 fn test_config_value_getting() {
458 let config = SessionConfig::new();
459 assert_eq!(config.get_value("behavior.max_input_lines"), Some("10".to_owned()));
460 }
461
462 #[test]
463 fn test_config_validation() {
464 let config = SessionConfig::new();
465 config.validate().unwrap();
466
467 let mut invalid_config = config.clone();
469 invalid_config.behavior.history_size = 0;
470 assert!(invalid_config.validate().is_err());
471
472 let mut invalid_config2 = config.clone();
474 invalid_config2.performance.lru_cache_size = 0;
475 assert!(invalid_config2.validate().is_err());
476 }
477
478 #[test]
479 fn test_config_with_custom_values() {
480 let mut config = SessionConfig::new();
481
482 config.behavior.max_input_lines = 20;
484 config.performance.lru_cache_size = 256;
485
486 assert_eq!(config.behavior.max_input_lines, 20);
487 assert_eq!(config.performance.lru_cache_size, 256);
488 }
489}