steer_core/
preferences.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)]
5pub enum EditingMode {
6    #[default]
7    Simple, // Default - no modal editing
8    Vim, // Full vim keybindings
9}
10
11#[derive(Debug, Clone, Serialize, Deserialize, Default)]
12pub struct Preferences {
13    #[serde(default)]
14    pub ui: UiPreferences,
15
16    #[serde(default)]
17    pub keybindings: KeyBindings,
18
19    #[serde(default)]
20    pub tools: ToolPreferences,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, Default)]
24pub struct UiPreferences {
25    pub default_model: Option<String>,
26    pub theme: Option<String>,
27    pub notifications: NotificationPreferences,
28    pub history_limit: Option<usize>,
29    pub provider_priority: Option<Vec<String>>,
30    #[serde(default)]
31    pub editing_mode: EditingMode,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct NotificationPreferences {
36    pub sound: bool,
37    pub desktop: bool,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, Default)]
41pub struct KeyBindings {
42    pub cancel: Option<String>,
43    pub model_selection: Option<String>,
44    pub clear: Option<String>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, Default)]
48pub struct ToolPreferences {
49    pub pre_approved: Vec<String>,
50}
51
52impl Default for NotificationPreferences {
53    fn default() -> Self {
54        Self {
55            sound: true,
56            desktop: true,
57        }
58    }
59}
60
61impl Preferences {
62    /// Get the path to the preferences file
63    pub fn config_path() -> Result<PathBuf, crate::error::Error> {
64        let config_dir = dirs::config_dir().ok_or_else(|| {
65            crate::error::Error::Configuration("Could not determine config directory".to_string())
66        })?;
67        Ok(config_dir.join("steer").join("preferences.toml"))
68    }
69
70    /// Load preferences from disk, or return defaults if not found
71    pub fn load() -> Result<Self, crate::error::Error> {
72        let path = Self::config_path()?;
73
74        if path.exists() {
75            let contents = std::fs::read_to_string(&path)?;
76            match toml::from_str(&contents) {
77                Ok(prefs) => Ok(prefs),
78                Err(e) => {
79                    tracing::warn!(
80                        "Failed to parse preferences file at {:?}: {}. Using defaults.",
81                        path,
82                        e
83                    );
84                    Ok(Self::default())
85                }
86            }
87        } else {
88            Ok(Self::default())
89        }
90    }
91
92    /// Save preferences to disk
93    pub fn save(&self) -> Result<(), crate::error::Error> {
94        let path = Self::config_path()?;
95
96        // Create parent directory if it doesn't exist
97        if let Some(parent) = path.parent() {
98            std::fs::create_dir_all(parent)?;
99        }
100
101        let contents = toml::to_string_pretty(self).map_err(|e| {
102            crate::error::Error::Configuration(format!("Failed to serialize preferences: {e}"))
103        })?;
104
105        std::fs::write(&path, contents)?;
106
107        Ok(())
108    }
109}