1use hashbrown::HashMap;
6use serde::{Deserialize, Serialize};
7use std::path::Path;
8
9#[derive(Debug, Clone, Serialize, Deserialize, Default)]
11pub struct SessionConfig {
12 pub appearance: AppearanceConfig,
14
15 pub key_bindings: KeyBindingConfig,
17
18 pub behavior: BehaviorConfig,
20
21 pub performance: PerformanceConfig,
23
24 pub customization: CustomizationConfig,
26}
27
28#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
30#[serde(rename_all = "snake_case")]
31pub enum UiMode {
32 #[default]
34 Full,
35 Minimal,
37 Focused,
39}
40
41#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
43#[serde(rename_all = "snake_case")]
44pub enum LayoutModeOverride {
45 #[default]
47 Auto,
48 Compact,
50 Standard,
52 Wide,
54}
55
56#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
58#[serde(rename_all = "snake_case")]
59pub enum ReasoningDisplayMode {
60 Always,
62 #[default]
64 Toggle,
65 Hidden,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct AppearanceConfig {
72 pub theme: String,
74
75 pub ui_mode: UiMode,
77
78 pub show_sidebar: bool,
80
81 pub min_content_width: u16,
83
84 pub min_navigation_width: u16,
86
87 pub navigation_width_percent: u8,
89
90 pub transcript_bottom_padding: u16,
92
93 pub dim_completed_todos: bool,
95
96 pub message_block_spacing: u8,
98
99 #[serde(default)]
101 pub layout_mode: LayoutModeOverride,
102
103 #[serde(default)]
105 pub reasoning_display_mode: ReasoningDisplayMode,
106
107 #[serde(default)]
109 pub reasoning_visible_default: bool,
110
111 #[serde(default)]
113 pub vim_mode: bool,
114
115 #[serde(default)]
117 pub screen_reader_mode: bool,
118
119 #[serde(default)]
121 pub reduce_motion_mode: bool,
122
123 #[serde(default)]
125 pub reduce_motion_keep_progress_animation: bool,
126
127 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: crate::config::constants::ui::DEFAULT_REASONING_VISIBLE,
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 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 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct KeyBindingConfig {
195 pub bindings: HashMap<String, Vec<String>>,
197}
198
199impl Default for KeyBindingConfig {
200 fn default() -> Self {
201 let mut bindings = HashMap::new();
202
203 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct BehaviorConfig {
222 pub max_input_lines: usize,
224
225 pub enable_history: bool,
227
228 pub history_size: usize,
230
231 pub double_tap_escape_clears: bool,
233
234 pub double_tap_delay_ms: u64,
236
237 pub auto_scroll_to_bottom: bool,
239
240 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#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct PerformanceConfig {
261 pub render_cache_size: usize,
263
264 pub transcript_cache_size: usize,
266
267 pub enable_transcript_caching: bool,
269
270 pub lru_cache_size: usize,
272
273 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#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct CustomizationConfig {
292 pub ui_labels: HashMap<String, String>,
294
295 pub custom_styles: HashMap<String, String>,
297
298 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 #[allow(dead_code)]
319 pub fn new() -> Self {
320 Self::default()
321 }
322
323 #[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 #[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 #[allow(dead_code)]
350 pub fn set_value(&mut self, key: &str, value: &str) -> Result<(), String> {
351 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 #[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 #[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 assert_eq!(
413 config.appearance.reasoning_display_mode,
414 ReasoningDisplayMode::Toggle
415 );
416 assert!(config.appearance.reasoning_visible_default);
417 assert!(config.appearance.reasoning_visible());
418 }
419
420 #[test]
421 fn test_config_serialization() {
422 let config = SessionConfig::new();
423 let serialized = toml::to_string_pretty(&config).unwrap();
424 assert!(serialized.contains("theme"));
425 }
426
427 #[test]
428 fn test_config_value_setting() {
429 let mut config = SessionConfig::new();
430
431 config.set_value("behavior.max_input_lines", "15").unwrap();
432 assert_eq!(config.behavior.max_input_lines, 15);
433
434 assert!(
435 config
436 .set_value("behavior.max_input_lines", "not_a_number")
437 .is_err()
438 );
439 }
440
441 #[test]
442 fn test_config_value_getting() {
443 let config = SessionConfig::new();
444 assert_eq!(
445 config.get_value("behavior.max_input_lines"),
446 Some("10".to_owned())
447 );
448 }
449
450 #[test]
451 fn test_config_validation() {
452 let config = SessionConfig::new();
453 assert!(config.validate().is_ok());
454
455 let mut invalid_config = config.clone();
457 invalid_config.behavior.history_size = 0;
458 assert!(invalid_config.validate().is_err());
459
460 let mut invalid_config2 = config.clone();
462 invalid_config2.performance.lru_cache_size = 0;
463 assert!(invalid_config2.validate().is_err());
464 }
465
466 #[test]
467 fn test_config_with_custom_values() {
468 let mut config = SessionConfig::new();
469
470 config.behavior.max_input_lines = 20;
472 config.performance.lru_cache_size = 256;
473
474 assert_eq!(config.behavior.max_input_lines, 20);
475 assert_eq!(config.performance.lru_cache_size, 256);
476 }
477}