1use std::collections::BTreeSet;
2
3pub const SAFE_HOTKEY_PRESETS: [&str; 6] = [
4 "Ctrl+Shift+Space",
5 "Ctrl+Alt+Space",
6 "Alt+Shift+Space",
7 "Ctrl+Shift+P",
8 "Ctrl+Alt+P",
9 "Ctrl+Shift+O",
10];
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct SettingsDraft {
14 pub hotkey: String,
15 pub max_results: u16,
16 pub launch_at_startup: bool,
17}
18
19pub fn validate_hotkey(input: &str) -> Result<String, String> {
20 let raw_parts: Vec<&str> = input
21 .split('+')
22 .map(|part| part.trim())
23 .filter(|part| !part.is_empty())
24 .collect();
25
26 if raw_parts.len() < 2 {
27 return Err("Hotkey must include at least one modifier and one key.".to_string());
28 }
29
30 let key_raw = raw_parts[raw_parts.len() - 1];
31 let key = normalize_key(key_raw)?;
32
33 let mut modifiers: BTreeSet<&'static str> = BTreeSet::new();
34 for part in &raw_parts[..raw_parts.len() - 1] {
35 let modifier = normalize_modifier(part)?;
36 modifiers.insert(modifier);
37 }
38
39 if modifiers.is_empty() {
40 return Err("Hotkey must include at least one modifier.".to_string());
41 }
42
43 let canonical = canonical_hotkey(&modifiers, &key);
44 if is_reserved_hotkey(&canonical) {
45 return Err(
46 "This hotkey is commonly reserved by Windows. Choose a different one.".to_string(),
47 );
48 }
49
50 Ok(canonical)
51}
52
53pub fn validate_max_results(value: u16) -> Result<(), String> {
54 if (5..=100).contains(&value) {
55 Ok(())
56 } else {
57 Err("Max results must be between 5 and 100.".to_string())
58 }
59}
60
61pub fn suggested_hotkey_presets(current: &str, limit: usize) -> Vec<String> {
62 if limit == 0 {
63 return Vec::new();
64 }
65
66 let current_canonical = validate_hotkey(current).ok();
67 SAFE_HOTKEY_PRESETS
68 .iter()
69 .filter_map(|preset| validate_hotkey(preset).ok())
70 .filter(|preset| current_canonical.as_ref() != Some(preset))
71 .take(limit)
72 .collect()
73}
74
75fn normalize_modifier(input: &str) -> Result<&'static str, String> {
76 match input.to_ascii_lowercase().as_str() {
77 "ctrl" | "control" => Ok("Ctrl"),
78 "alt" => Ok("Alt"),
79 "shift" => Ok("Shift"),
80 "win" | "windows" | "meta" => Err("Win/Meta combinations are not supported.".to_string()),
81 _ => Err(format!(
82 "Unsupported modifier '{input}'. Use Ctrl, Alt, or Shift."
83 )),
84 }
85}
86
87fn normalize_key(input: &str) -> Result<String, String> {
88 let raw = input.trim();
89 if raw.is_empty() {
90 return Err("Hotkey key is required.".to_string());
91 }
92
93 let upper = raw.to_ascii_uppercase();
94 if upper == "SPACE" {
95 return Ok("Space".to_string());
96 }
97
98 if let Some(number) = upper.strip_prefix('F') {
99 if let Ok(parsed) = number.parse::<u8>() {
100 if (1..=24).contains(&parsed) {
101 return Ok(format!("F{parsed}"));
102 }
103 }
104 return Err("Function key must be between F1 and F24.".to_string());
105 }
106
107 if upper.len() == 1 {
108 let c = upper.chars().next().unwrap_or_default();
109 if c.is_ascii_alphanumeric() {
110 return Ok(upper);
111 }
112 }
113
114 Err("Key must be A-Z, 0-9, Space, or F1-F24.".to_string())
115}
116
117fn canonical_hotkey(modifiers: &BTreeSet<&'static str>, key: &str) -> String {
118 let mut ordered = Vec::new();
119 if modifiers.contains("Ctrl") {
120 ordered.push("Ctrl");
121 }
122 if modifiers.contains("Alt") {
123 ordered.push("Alt");
124 }
125 if modifiers.contains("Shift") {
126 ordered.push("Shift");
127 }
128 ordered.push(key);
129 ordered.join("+")
130}
131
132fn is_reserved_hotkey(canonical: &str) -> bool {
133 matches!(
134 canonical,
135 "Alt+Tab" | "Alt+F4" | "Ctrl+Esc" | "Alt+Esc" | "Ctrl+Shift+Esc" | "Alt+Space"
136 )
137}