use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use std::path::Path;
use vtcode_commons::fs::{read_file_with_context_sync, write_file_with_context_sync};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SessionConfig {
pub appearance: AppearanceConfig,
pub key_bindings: KeyBindingConfig,
pub behavior: BehaviorConfig,
pub performance: PerformanceConfig,
pub customization: CustomizationConfig,
}
pub use vtcode_commons::ui_protocol::{LayoutModeOverride, ReasoningDisplayMode, UiMode};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppearanceConfig {
pub theme: String,
pub ui_mode: UiMode,
pub show_sidebar: bool,
pub min_content_width: u16,
pub min_navigation_width: u16,
pub navigation_width_percent: u8,
pub transcript_bottom_padding: u16,
pub dim_completed_todos: bool,
pub message_block_spacing: u8,
#[serde(default)]
pub layout_mode: LayoutModeOverride,
#[serde(default)]
pub reasoning_display_mode: ReasoningDisplayMode,
#[serde(default)]
pub reasoning_visible_default: bool,
#[serde(default)]
pub vim_mode: bool,
#[serde(default)]
pub screen_reader_mode: bool,
#[serde(default)]
pub reduce_motion_mode: bool,
#[serde(default)]
pub reduce_motion_keep_progress_animation: bool,
pub customization: CustomizationConfig,
}
impl Default for AppearanceConfig {
fn default() -> Self {
Self {
theme: "default".to_owned(),
ui_mode: UiMode::Full,
show_sidebar: true,
min_content_width: 40,
min_navigation_width: 20,
navigation_width_percent: 25,
transcript_bottom_padding: 0,
dim_completed_todos: true,
message_block_spacing: 0,
layout_mode: LayoutModeOverride::Auto,
reasoning_display_mode: ReasoningDisplayMode::Toggle,
reasoning_visible_default: crate::config::constants::ui::DEFAULT_REASONING_VISIBLE,
vim_mode: false,
screen_reader_mode: false,
reduce_motion_mode: false,
reduce_motion_keep_progress_animation: false,
customization: CustomizationConfig::default(),
}
}
}
impl AppearanceConfig {
pub fn should_show_sidebar(&self) -> bool {
match self.ui_mode {
UiMode::Full => self.show_sidebar,
UiMode::Minimal | UiMode::Focused => false,
}
}
pub fn reasoning_visible(&self) -> bool {
match self.reasoning_display_mode {
ReasoningDisplayMode::Always => true,
ReasoningDisplayMode::Hidden => false,
ReasoningDisplayMode::Toggle => self.reasoning_visible_default,
}
}
pub fn motion_reduced(&self) -> bool {
self.screen_reader_mode || self.reduce_motion_mode
}
pub fn should_animate_progress_status(&self) -> bool {
!self.screen_reader_mode
&& (!self.reduce_motion_mode || self.reduce_motion_keep_progress_animation)
}
#[allow(dead_code)]
pub fn should_show_footer(&self) -> bool {
match self.ui_mode {
UiMode::Full => true,
UiMode::Minimal => false,
UiMode::Focused => false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyBindingConfig {
pub bindings: HashMap<String, Vec<String>>,
}
impl Default for KeyBindingConfig {
fn default() -> Self {
let mut bindings = HashMap::new();
bindings.insert("scroll_up".to_owned(), vec!["up".to_owned()]);
bindings.insert("scroll_down".to_owned(), vec!["down".to_owned()]);
bindings.insert("page_up".to_owned(), vec!["pageup".to_owned()]);
bindings.insert("page_down".to_owned(), vec!["pagedown".to_owned()]);
bindings.insert("submit".to_owned(), vec!["enter".to_owned()]);
bindings.insert("submit_queue".to_owned(), vec!["tab".to_owned()]);
bindings.insert("cancel".to_owned(), vec!["esc".to_owned()]);
bindings.insert("interrupt".to_owned(), vec!["ctrl+c".to_owned()]);
Self { bindings }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BehaviorConfig {
pub max_input_lines: usize,
pub enable_history: bool,
pub history_size: usize,
pub double_tap_escape_clears: bool,
pub double_tap_delay_ms: u64,
pub auto_scroll_to_bottom: bool,
pub show_queued_inputs: bool,
}
impl Default for BehaviorConfig {
fn default() -> Self {
Self {
max_input_lines: 10,
enable_history: true,
history_size: 100,
double_tap_escape_clears: true,
double_tap_delay_ms: 300,
auto_scroll_to_bottom: true,
show_queued_inputs: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceConfig {
pub render_cache_size: usize,
pub transcript_cache_size: usize,
pub enable_transcript_caching: bool,
pub lru_cache_size: usize,
pub enable_smooth_scrolling: bool,
}
impl Default for PerformanceConfig {
fn default() -> Self {
Self {
render_cache_size: 1000,
transcript_cache_size: 500,
enable_transcript_caching: true,
lru_cache_size: 128,
enable_smooth_scrolling: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomizationConfig {
pub ui_labels: HashMap<String, String>,
pub custom_styles: HashMap<String, String>,
pub enabled_features: Vec<String>,
}
impl Default for CustomizationConfig {
fn default() -> Self {
Self {
ui_labels: HashMap::new(),
custom_styles: HashMap::new(),
enabled_features: vec![
"slash_commands".to_owned(),
"file_palette".to_owned(),
"modal_dialogs".to_owned(),
],
}
}
}
impl SessionConfig {
#[allow(dead_code)]
pub fn new() -> Self {
Self::default()
}
#[allow(dead_code)]
pub fn load_from_file(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
let content = read_file_with_context_sync(Path::new(path), "session config file").map_err(
|err| -> Box<dyn std::error::Error> { Box::new(std::io::Error::other(err)) },
)?;
let config: SessionConfig = toml::from_str(&content)?;
Ok(config)
}
#[allow(dead_code)]
pub fn save_to_file(&self, path: &str) -> Result<(), Box<dyn std::error::Error>> {
let content = toml::to_string_pretty(self)?;
write_file_with_context_sync(Path::new(path), &content, "session config file").map_err(
|err| -> Box<dyn std::error::Error> { Box::new(std::io::Error::other(err)) },
)?;
Ok(())
}
#[allow(dead_code)]
pub fn set_value(&mut self, key: &str, value: &str) -> Result<(), String> {
match key {
"behavior.max_input_lines" => {
self.behavior.max_input_lines = value
.parse()
.map_err(|_| format!("Cannot parse '{}' as number", value))?;
}
"performance.lru_cache_size" => {
self.performance.lru_cache_size = value
.parse()
.map_err(|_| format!("Cannot parse '{}' as number", value))?;
}
_ => return Err(format!("Unknown configuration key: {}", key)),
}
Ok(())
}
#[allow(dead_code)]
pub fn get_value(&self, key: &str) -> Option<String> {
match key {
"behavior.max_input_lines" => Some(self.behavior.max_input_lines.to_string()),
"performance.lru_cache_size" => Some(self.performance.lru_cache_size.to_string()),
_ => None,
}
}
#[allow(dead_code)]
pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
if self.behavior.history_size == 0 {
errors.push("history_size must be greater than 0".to_owned());
}
if self.performance.lru_cache_size == 0 {
errors.push("lru_cache_size must be greater than 0".to_owned());
}
if self.appearance.navigation_width_percent > 100 {
errors.push("navigation_width_percent must be between 0 and 100".to_owned());
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = SessionConfig::new();
assert_eq!(config.behavior.history_size, 100);
assert_eq!(
config.appearance.reasoning_display_mode,
ReasoningDisplayMode::Toggle
);
assert!(config.appearance.reasoning_visible_default);
assert!(config.appearance.reasoning_visible());
}
#[test]
fn test_config_serialization() {
let config = SessionConfig::new();
let serialized = toml::to_string_pretty(&config).unwrap();
assert!(serialized.contains("theme"));
}
#[test]
fn test_config_value_setting() {
let mut config = SessionConfig::new();
config.set_value("behavior.max_input_lines", "15").unwrap();
assert_eq!(config.behavior.max_input_lines, 15);
assert!(
config
.set_value("behavior.max_input_lines", "not_a_number")
.is_err()
);
}
#[test]
fn test_config_value_getting() {
let config = SessionConfig::new();
assert_eq!(
config.get_value("behavior.max_input_lines"),
Some("10".to_owned())
);
}
#[test]
fn test_config_validation() {
let config = SessionConfig::new();
assert!(config.validate().is_ok());
let mut invalid_config = config.clone();
invalid_config.behavior.history_size = 0;
assert!(invalid_config.validate().is_err());
let mut invalid_config2 = config.clone();
invalid_config2.performance.lru_cache_size = 0;
assert!(invalid_config2.validate().is_err());
}
#[test]
fn test_config_with_custom_values() {
let mut config = SessionConfig::new();
config.behavior.max_input_lines = 20;
config.performance.lru_cache_size = 256;
assert_eq!(config.behavior.max_input_lines, 20);
assert_eq!(config.performance.lru_cache_size, 256);
}
}