1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
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 screen_reader_mode: bool,
114
115 #[serde(default)]
117 pub reduce_motion_mode: bool,
118
119 #[serde(default)]
121 pub reduce_motion_keep_progress_animation: bool,
122
123 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 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 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct KeyBindingConfig {
190 pub bindings: HashMap<String, Vec<String>>,
192}
193
194impl Default for KeyBindingConfig {
195 fn default() -> Self {
196 let mut bindings = HashMap::new();
197
198 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct BehaviorConfig {
217 pub max_input_lines: usize,
219
220 pub enable_history: bool,
222
223 pub history_size: usize,
225
226 pub double_tap_escape_clears: bool,
228
229 pub double_tap_delay_ms: u64,
231
232 pub auto_scroll_to_bottom: bool,
234
235 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#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct PerformanceConfig {
256 pub render_cache_size: usize,
258
259 pub transcript_cache_size: usize,
261
262 pub enable_transcript_caching: bool,
264
265 pub lru_cache_size: usize,
267
268 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#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct CustomizationConfig {
287 pub ui_labels: HashMap<String, String>,
289
290 pub custom_styles: HashMap<String, String>,
292
293 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 #[allow(dead_code)]
314 pub fn new() -> Self {
315 Self::default()
316 }
317
318 #[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 #[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 #[allow(dead_code)]
345 pub fn set_value(&mut self, key: &str, value: &str) -> Result<(), String> {
346 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 #[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 #[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 let mut invalid_config = config.clone();
446 invalid_config.behavior.history_size = 0;
447 assert!(invalid_config.validate().is_err());
448
449 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 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}