Skip to main content

steer_core/
preferences.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3use strum::Display;
4
5#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default, Display)]
6#[strum(serialize_all = "kebab-case")]
7pub enum EditingMode {
8    #[default]
9    Simple, // Default - no modal editing
10    Vim, // Full vim keybindings
11}
12
13#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, Display)]
14#[serde(rename_all = "kebab-case")]
15#[strum(serialize_all = "kebab-case")]
16pub enum NotificationTransport {
17    #[default]
18    Auto,
19    Osc9,
20    Off,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, Default)]
24pub struct Preferences {
25    pub default_model: Option<String>,
26
27    #[serde(default)]
28    pub ui: UiPreferences,
29
30    #[serde(default)]
31    pub tools: ToolPreferences,
32
33    #[serde(default)]
34    pub telemetry: TelemetryPreferences,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, Default)]
38pub struct UiPreferences {
39    pub theme: Option<String>,
40    #[serde(default)]
41    pub notifications: NotificationPreferences,
42    pub history_limit: Option<usize>,
43    pub provider_priority: Option<Vec<String>>,
44    #[serde(default)]
45    pub editing_mode: EditingMode,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct NotificationPreferences {
50    #[serde(default)]
51    pub transport: NotificationTransport,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, Default)]
55pub struct ToolPreferences {
56    pub pre_approved: Vec<String>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct TelemetryPreferences {
61    #[serde(default = "default_telemetry_enabled")]
62    pub enabled: bool,
63    pub endpoint: Option<String>,
64}
65
66fn default_telemetry_enabled() -> bool {
67    true
68}
69
70impl Default for NotificationPreferences {
71    fn default() -> Self {
72        Self {
73            transport: NotificationTransport::Auto,
74        }
75    }
76}
77
78impl Default for TelemetryPreferences {
79    fn default() -> Self {
80        Self {
81            enabled: default_telemetry_enabled(),
82            endpoint: None,
83        }
84    }
85}
86
87impl Preferences {
88    /// Get the path to the preferences file
89    pub fn config_path() -> Result<PathBuf, crate::error::Error> {
90        let config_dir = dirs::config_dir().ok_or_else(|| {
91            crate::error::Error::Configuration("Could not determine config directory".to_string())
92        })?;
93        Ok(config_dir.join("steer").join("preferences.toml"))
94    }
95
96    /// Load preferences from disk, or return defaults if not found
97    pub fn load() -> Result<Self, crate::error::Error> {
98        let path = Self::config_path()?;
99
100        if path.exists() {
101            let contents = std::fs::read_to_string(&path)?;
102            match toml::from_str(&contents) {
103                Ok(prefs) => Ok(prefs),
104                Err(e) => {
105                    tracing::warn!(
106                        "Failed to parse preferences file at {:?}: {}. Using defaults.",
107                        path,
108                        e
109                    );
110                    Ok(Self::default())
111                }
112            }
113        } else {
114            Ok(Self::default())
115        }
116    }
117
118    /// Save preferences to disk
119    pub fn save(&self) -> Result<(), crate::error::Error> {
120        let path = Self::config_path()?;
121
122        // Create parent directory if it doesn't exist
123        if let Some(parent) = path.parent() {
124            std::fs::create_dir_all(parent)?;
125        }
126
127        let contents = toml::to_string_pretty(self).map_err(|e| {
128            crate::error::Error::Configuration(format!("Failed to serialize preferences: {e}"))
129        })?;
130
131        std::fs::write(&path, contents)?;
132
133        Ok(())
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn telemetry_preferences_default_to_enabled_and_no_endpoint() {
143        let telemetry = TelemetryPreferences::default();
144        assert!(telemetry.enabled);
145        assert_eq!(telemetry.endpoint, None);
146    }
147}