Skip to main content

nex_core/
settings.rs

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}