Rust · 7119 bytes Raw Blame History
1 //! TOML configuration for garshot.
2 //!
3 //! Fallback configuration when not using Lua (gar integration).
4
5 use std::path::PathBuf;
6
7 use serde::{Deserialize, Serialize};
8
9 use crate::error::{GarshotError, Result};
10
11 /// Main configuration structure.
12 #[derive(Debug, Clone, Serialize, Deserialize)]
13 #[serde(default)]
14 pub struct Config {
15 /// General settings.
16 pub general: GeneralSettings,
17 /// Selection overlay settings.
18 pub selection: SelectionSettings,
19 /// Naming settings.
20 pub naming: NamingSettings,
21 }
22
23 impl Default for Config {
24 fn default() -> Self {
25 Self {
26 general: GeneralSettings::default(),
27 selection: SelectionSettings::default(),
28 naming: NamingSettings::default(),
29 }
30 }
31 }
32
33 /// General screenshot settings.
34 #[derive(Debug, Clone, Serialize, Deserialize)]
35 #[serde(default)]
36 pub struct GeneralSettings {
37 /// Directory to save screenshots.
38 pub save_dir: PathBuf,
39 /// Default output format (png, jpeg, webp, ppm).
40 pub format: String,
41 /// JPEG/WebP quality (1-100).
42 pub quality: u8,
43 /// Include cursor in screenshots by default.
44 pub include_cursor: bool,
45 }
46
47 impl Default for GeneralSettings {
48 fn default() -> Self {
49 let save_dir = dirs::picture_dir()
50 .unwrap_or_else(|| PathBuf::from("."))
51 .join("Screenshots");
52
53 Self {
54 save_dir,
55 format: "png".to_string(),
56 quality: 90,
57 include_cursor: false,
58 }
59 }
60 }
61
62 /// Selection overlay settings.
63 #[derive(Debug, Clone, Serialize, Deserialize)]
64 #[serde(default)]
65 pub struct SelectionSettings {
66 /// Blur radius in pixels.
67 pub blur_radius: usize,
68 /// Selection line color (hex format: #RRGGBB).
69 pub line_color: String,
70 /// Selection line width in pixels.
71 pub line_width: u32,
72 }
73
74 impl Default for SelectionSettings {
75 fn default() -> Self {
76 Self {
77 blur_radius: 15,
78 line_color: "#ff6600".to_string(),
79 line_width: 2,
80 }
81 }
82 }
83
84 impl SelectionSettings {
85 /// Parse the line color as a u32 RGB value.
86 pub fn line_color_u32(&self) -> u32 {
87 parse_color(&self.line_color).unwrap_or(0xFF6600)
88 }
89 }
90
91 /// Naming settings for screenshot filenames.
92 #[derive(Debug, Clone, Serialize, Deserialize)]
93 #[serde(default)]
94 pub struct NamingSettings {
95 /// Filename pattern with strftime and custom tokens.
96 /// Supports: %Y, %m, %d, %H, %M, %S (strftime)
97 /// Custom: %n (daily counter), %N (global counter), %w (window name)
98 pub pattern: String,
99 }
100
101 impl Default for NamingSettings {
102 fn default() -> Self {
103 Self {
104 pattern: "screenshot-%Y%m%d-%H%M%S".to_string(),
105 }
106 }
107 }
108
109 /// Load configuration from file.
110 ///
111 /// Tries to load from:
112 /// 1. `~/.config/garshot/config.toml`
113 /// 2. `~/.config/gar/init.lua` (extracts gar.shot table) - TODO
114 /// 3. Default configuration
115 pub fn load_config() -> Result<Config> {
116 let config_path = config_path();
117
118 if config_path.exists() {
119 tracing::debug!("Loading config from {}", config_path.display());
120 let content = std::fs::read_to_string(&config_path).map_err(|e| {
121 GarshotError::ConfigError(format!("Failed to read config file: {}", e))
122 })?;
123
124 let mut config: Config = toml::from_str(&content).map_err(|e| {
125 GarshotError::ConfigError(format!("Failed to parse config file: {}", e))
126 })?;
127
128 // Expand tilde in save_dir
129 config.general.save_dir = expand_tilde(&config.general.save_dir);
130
131 Ok(config)
132 } else {
133 tracing::debug!("No config file found, using defaults");
134 Ok(Config::default())
135 }
136 }
137
138 /// Expand leading `~` in a path to the user's home directory.
139 fn expand_tilde(path: &PathBuf) -> PathBuf {
140 let path_str = path.to_string_lossy();
141 if path_str == "~" {
142 dirs::home_dir().unwrap_or_else(|| PathBuf::from("."))
143 } else if let Some(rest) = path_str.strip_prefix("~/") {
144 dirs::home_dir()
145 .unwrap_or_else(|| PathBuf::from("."))
146 .join(rest)
147 } else {
148 path.clone()
149 }
150 }
151
152 /// Get the config file path.
153 pub fn config_path() -> PathBuf {
154 dirs::config_dir()
155 .unwrap_or_else(|| PathBuf::from("."))
156 .join("garshot")
157 .join("config.toml")
158 }
159
160 /// Parse a color string (#RRGGBB or RRGGBB) to u32.
161 fn parse_color(s: &str) -> Option<u32> {
162 let s = s.trim_start_matches('#');
163 if s.len() != 6 {
164 return None;
165 }
166 u32::from_str_radix(s, 16).ok()
167 }
168
169 #[cfg(test)]
170 mod tests {
171 use super::*;
172
173 #[test]
174 fn test_parse_color() {
175 assert_eq!(parse_color("#ff6600"), Some(0xFF6600));
176 assert_eq!(parse_color("ff6600"), Some(0xFF6600));
177 assert_eq!(parse_color("#000000"), Some(0x000000));
178 assert_eq!(parse_color("#ffffff"), Some(0xFFFFFF));
179 assert_eq!(parse_color("invalid"), None);
180 }
181
182 #[test]
183 fn test_default_config() {
184 let config = Config::default();
185 assert_eq!(config.general.format, "png");
186 assert_eq!(config.selection.blur_radius, 15);
187 assert!(config.naming.pattern.contains("screenshot"));
188 }
189
190 #[test]
191 fn test_deserialize_config() {
192 let toml_str = concat!(
193 "[general]\n",
194 "format = \"jpeg\"\n",
195 "quality = 85\n",
196 "include_cursor = true\n",
197 "\n",
198 "[selection]\n",
199 "blur_radius = 20\n",
200 "line_color = \"#00ff00\"\n",
201 "line_width = 3\n",
202 "\n",
203 "[naming]\n",
204 "pattern = \"shot-date\"\n",
205 );
206 let config: Config = toml::from_str(toml_str).unwrap();
207 assert_eq!(config.general.format, "jpeg");
208 assert_eq!(config.general.quality, 85);
209 assert!(config.general.include_cursor);
210 assert_eq!(config.selection.blur_radius, 20);
211 assert_eq!(config.selection.line_color, "#00ff00");
212 assert_eq!(config.naming.pattern, "shot-date");
213 }
214
215 #[test]
216 fn test_expand_tilde() {
217 let home = dirs::home_dir().unwrap();
218
219 // ~/foo/bar should expand
220 let path = PathBuf::from("~/Pictures/Screenshots");
221 let expanded = expand_tilde(&path);
222 assert_eq!(expanded, home.join("Pictures/Screenshots"));
223
224 // Just ~ should expand to home
225 let path = PathBuf::from("~");
226 let expanded = expand_tilde(&path);
227 assert_eq!(expanded, home);
228
229 // Absolute paths should not change
230 let path = PathBuf::from("/tmp/screenshots");
231 let expanded = expand_tilde(&path);
232 assert_eq!(expanded, PathBuf::from("/tmp/screenshots"));
233
234 // Relative paths should not change
235 let path = PathBuf::from("screenshots");
236 let expanded = expand_tilde(&path);
237 assert_eq!(expanded, PathBuf::from("screenshots"));
238
239 // Tilde in middle should not expand
240 let path = PathBuf::from("/foo/~/bar");
241 let expanded = expand_tilde(&path);
242 assert_eq!(expanded, PathBuf::from("/foo/~/bar"));
243 }
244 }
245