1use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
16pub struct SnippetConfig {
17 pub id: String,
19
20 pub title: String,
22
23 pub content: String,
25
26 #[serde(default)]
28 pub keybinding: Option<String>,
29
30 #[serde(default = "crate::defaults::bool_true")]
33 pub keybinding_enabled: bool,
34
35 #[serde(default)]
37 pub folder: Option<String>,
38
39 #[serde(default = "crate::defaults::bool_true")]
41 pub enabled: bool,
42
43 #[serde(default)]
45 pub description: Option<String>,
46
47 #[serde(default)]
50 pub auto_execute: bool,
51
52 #[serde(default)]
54 pub variables: HashMap<String, String>,
55}
56
57impl SnippetConfig {
58 pub fn new(id: String, title: String, content: String) -> Self {
60 Self {
61 id,
62 title,
63 content,
64 keybinding: None,
65 keybinding_enabled: true,
66 folder: None,
67 enabled: true,
68 description: None,
69 auto_execute: false,
70 variables: HashMap::new(),
71 }
72 }
73
74 pub fn with_keybinding(mut self, keybinding: String) -> Self {
76 self.keybinding = Some(keybinding);
77 self
78 }
79
80 pub fn with_keybinding_disabled(mut self) -> Self {
82 self.keybinding_enabled = false;
83 self
84 }
85
86 pub fn with_folder(mut self, folder: String) -> Self {
88 self.folder = Some(folder);
89 self
90 }
91
92 pub fn with_variable(mut self, name: String, value: String) -> Self {
94 self.variables.insert(name, value);
95 self
96 }
97
98 pub fn with_auto_execute(mut self) -> Self {
100 self.auto_execute = true;
101 self
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct SnippetLibrary {
110 pub snippets: Vec<SnippetConfig>,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
118#[serde(tag = "type", rename_all = "snake_case")]
119pub enum CustomActionConfig {
120 ShellCommand {
122 id: String,
124
125 title: String,
127
128 command: String,
130
131 #[serde(default)]
133 args: Vec<String>,
134
135 #[serde(default)]
137 notify_on_success: bool,
138
139 #[serde(default)]
141 keybinding: Option<String>,
142
143 #[serde(default = "crate::defaults::bool_true")]
145 keybinding_enabled: bool,
146
147 #[serde(default)]
149 description: Option<String>,
150 },
151
152 InsertText {
154 id: String,
156
157 title: String,
159
160 text: String,
162
163 #[serde(default)]
165 variables: HashMap<String, String>,
166
167 #[serde(default)]
169 keybinding: Option<String>,
170
171 #[serde(default = "crate::defaults::bool_true")]
173 keybinding_enabled: bool,
174
175 #[serde(default)]
177 description: Option<String>,
178 },
179
180 KeySequence {
182 id: String,
184
185 title: String,
187
188 keys: String,
190
191 #[serde(default)]
193 keybinding: Option<String>,
194
195 #[serde(default = "crate::defaults::bool_true")]
197 keybinding_enabled: bool,
198
199 #[serde(default)]
201 description: Option<String>,
202 },
203}
204
205impl CustomActionConfig {
206 pub fn id(&self) -> &str {
208 match self {
209 Self::ShellCommand { id, .. } => id,
210 Self::InsertText { id, .. } => id,
211 Self::KeySequence { id, .. } => id,
212 }
213 }
214
215 pub fn title(&self) -> &str {
217 match self {
218 Self::ShellCommand { title, .. } => title,
219 Self::InsertText { title, .. } => title,
220 Self::KeySequence { title, .. } => title,
221 }
222 }
223
224 pub fn keybinding(&self) -> Option<&str> {
226 match self {
227 Self::ShellCommand { keybinding, .. }
228 | Self::InsertText { keybinding, .. }
229 | Self::KeySequence { keybinding, .. } => keybinding.as_deref(),
230 }
231 }
232
233 pub fn keybinding_enabled(&self) -> bool {
235 match self {
236 Self::ShellCommand {
237 keybinding_enabled, ..
238 }
239 | Self::InsertText {
240 keybinding_enabled, ..
241 }
242 | Self::KeySequence {
243 keybinding_enabled, ..
244 } => *keybinding_enabled,
245 }
246 }
247
248 pub fn set_keybinding(&mut self, kb: Option<String>) {
250 match self {
251 Self::ShellCommand { keybinding, .. }
252 | Self::InsertText { keybinding, .. }
253 | Self::KeySequence { keybinding, .. } => *keybinding = kb,
254 }
255 }
256
257 pub fn set_keybinding_enabled(&mut self, enabled: bool) {
259 match self {
260 Self::ShellCommand {
261 keybinding_enabled, ..
262 }
263 | Self::InsertText {
264 keybinding_enabled, ..
265 }
266 | Self::KeySequence {
267 keybinding_enabled, ..
268 } => *keybinding_enabled = enabled,
269 }
270 }
271
272 pub fn is_shell_command(&self) -> bool {
274 matches!(self, Self::ShellCommand { .. })
275 }
276
277 pub fn is_insert_text(&self) -> bool {
279 matches!(self, Self::InsertText { .. })
280 }
281
282 pub fn is_key_sequence(&self) -> bool {
284 matches!(self, Self::KeySequence { .. })
285 }
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
290pub enum BuiltInVariable {
291 Date,
293 Time,
295 DateTime,
297 Hostname,
299 User,
301 Path,
303 GitBranch,
305 GitCommit,
307 Uuid,
309 Random,
311}
312
313impl BuiltInVariable {
314 pub fn all() -> &'static [(&'static str, &'static str)] {
316 &[
317 ("date", "Current date (YYYY-MM-DD)"),
318 ("time", "Current time (HH:MM:SS)"),
319 ("datetime", "Current date and time"),
320 ("hostname", "System hostname"),
321 ("user", "Current username"),
322 ("path", "Current working directory"),
323 ("git_branch", "Current git branch"),
324 ("git_commit", "Current git commit hash"),
325 ("uuid", "Random UUID"),
326 ("random", "Random number (0-999999)"),
327 ]
328 }
329
330 pub fn parse(name: &str) -> Option<Self> {
332 match name {
333 "date" => Some(Self::Date),
334 "time" => Some(Self::Time),
335 "datetime" => Some(Self::DateTime),
336 "hostname" => Some(Self::Hostname),
337 "user" => Some(Self::User),
338 "path" => Some(Self::Path),
339 "git_branch" => Some(Self::GitBranch),
340 "git_commit" => Some(Self::GitCommit),
341 "uuid" => Some(Self::Uuid),
342 "random" => Some(Self::Random),
343 _ => None,
344 }
345 }
346
347 pub fn resolve(&self) -> String {
349 match self {
350 Self::Date => {
351 use std::time::{SystemTime, UNIX_EPOCH};
352 let duration = SystemTime::now()
353 .duration_since(UNIX_EPOCH)
354 .unwrap_or_default();
355 let secs = duration.as_secs();
356 let days_since_epoch = secs / 86400;
357
358 let years = 1970 + days_since_epoch / 365;
360 let day_of_year = (days_since_epoch % 365) as u32;
361 let month = (day_of_year / 30) + 1;
362 let day = (day_of_year % 30) + 1;
363
364 format!("{:04}-{:02}-{:02}", years, month, day)
365 }
366 Self::Time => {
367 use std::time::{SystemTime, UNIX_EPOCH};
368 let duration = SystemTime::now()
369 .duration_since(UNIX_EPOCH)
370 .unwrap_or_default();
371 let secs = duration.as_secs();
372 let hours = (secs % 86400) / 3600;
373 let minutes = (secs % 3600) / 60;
374 let seconds = secs % 60;
375
376 format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
377 }
378 Self::DateTime => {
379 format!("{} {}", Self::Date.resolve(), Self::Time.resolve())
380 }
381 Self::Hostname => {
382 std::env::var("HOSTNAME")
383 .or_else(|_| std::env::var("HOST"))
384 .unwrap_or_else(|_| {
385 hostname::get()
387 .ok()
388 .and_then(|s| s.into_string().ok())
389 .unwrap_or_else(|| "unknown".to_string())
390 })
391 }
392 Self::User => std::env::var("USER")
393 .or_else(|_| std::env::var("USERNAME"))
394 .unwrap_or_else(|_| "unknown".to_string()),
395 Self::Path => std::env::current_dir()
396 .ok()
397 .and_then(|p| p.to_str().map(|s| s.to_string()))
398 .unwrap_or_else(|| ".".to_string()),
399 Self::GitBranch => {
400 match std::env::var("GIT_BRANCH") {
402 Ok(branch) => branch,
403 Err(_) => {
404 std::process::Command::new("git")
406 .args(["rev-parse", "--abbrev-ref", "HEAD"])
407 .output()
408 .ok()
409 .and_then(|o| String::from_utf8(o.stdout).ok())
410 .map(|s| s.trim().to_string())
411 .unwrap_or_default()
412 }
413 }
414 }
415 Self::GitCommit => {
416 match std::env::var("GIT_COMMIT") {
418 Ok(commit) => commit,
419 Err(_) => std::process::Command::new("git")
420 .args(["rev-parse", "--short", "HEAD"])
421 .output()
422 .ok()
423 .and_then(|o| String::from_utf8(o.stdout).ok())
424 .map(|s| s.trim().to_string())
425 .unwrap_or_default(),
426 }
427 }
428 Self::Uuid => uuid::Uuid::new_v4().to_string(),
429 Self::Random => {
430 use std::time::{SystemTime, UNIX_EPOCH};
431 let duration = SystemTime::now()
432 .duration_since(UNIX_EPOCH)
433 .unwrap_or_default();
434 format!("{}", (duration.as_nanos() % 1_000_000) as u32)
435 }
436 }
437 }
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn test_snippet_new() {
446 let snippet = SnippetConfig::new(
447 "test".to_string(),
448 "Test Snippet".to_string(),
449 "echo 'hello'".to_string(),
450 );
451
452 assert_eq!(snippet.id, "test");
453 assert_eq!(snippet.title, "Test Snippet");
454 assert_eq!(snippet.content, "echo 'hello'");
455 assert!(snippet.enabled);
456 assert!(snippet.keybinding.is_none());
457 assert!(snippet.folder.is_none());
458 assert!(snippet.variables.is_empty());
459 }
460
461 #[test]
462 fn test_snippet_builder() {
463 let snippet = SnippetConfig::new(
464 "test".to_string(),
465 "Test Snippet".to_string(),
466 "echo 'hello'".to_string(),
467 )
468 .with_keybinding("Ctrl+Shift+T".to_string())
469 .with_folder("Test".to_string())
470 .with_variable("name".to_string(), "value".to_string());
471
472 assert_eq!(snippet.keybinding, Some("Ctrl+Shift+T".to_string()));
473 assert_eq!(snippet.folder, Some("Test".to_string()));
474 assert_eq!(snippet.variables.get("name"), Some(&"value".to_string()));
475 }
476
477 #[test]
478 fn test_builtin_variable_resolution() {
479 let date = BuiltInVariable::Date.resolve();
481 assert!(!date.is_empty());
482
483 let time = BuiltInVariable::Time.resolve();
484 assert!(!time.is_empty());
485
486 let user = BuiltInVariable::User.resolve();
487 assert!(!user.is_empty());
488
489 let path = BuiltInVariable::Path.resolve();
490 assert!(!path.is_empty());
491 }
492
493 #[test]
494 fn test_builtin_variable_parse() {
495 assert_eq!(BuiltInVariable::parse("date"), Some(BuiltInVariable::Date));
496 assert_eq!(BuiltInVariable::parse("time"), Some(BuiltInVariable::Time));
497 assert_eq!(BuiltInVariable::parse("unknown"), None);
498 }
499
500 #[test]
501 fn test_custom_action_id() {
502 let action = CustomActionConfig::ShellCommand {
503 id: "test-action".to_string(),
504 title: "Test Action".to_string(),
505 command: "echo".to_string(),
506 args: vec!["hello".to_string()],
507 notify_on_success: false,
508 keybinding: None,
509 keybinding_enabled: true,
510 description: None,
511 };
512
513 assert_eq!(action.id(), "test-action");
514 assert_eq!(action.title(), "Test Action");
515 assert!(action.is_shell_command());
516 assert!(!action.is_insert_text());
517 assert!(!action.is_key_sequence());
518 }
519}