steer_core/
preferences.rs1use 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, Vim, }
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 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 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 pub fn save(&self) -> Result<(), crate::error::Error> {
120 let path = Self::config_path()?;
121
122 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}