win_ctx/
entry.rs

1use super::path::*;
2use std::collections::HashMap;
3use std::io::{self, ErrorKind};
4use winreg::{RegKey, enums::*};
5
6const HKCR: RegKey = RegKey::predef(HKEY_CLASSES_ROOT);
7
8/// Entry activation type
9#[derive(Debug, Clone)]
10pub enum ActivationType {
11    /// Entry activation on files (must be an extension (e.g., `.rs`) or `*` for all files)
12    File(String),
13    /// Entry activation on folders
14    Folder,
15    /// Entry activation on directory backgrounds
16    Background,
17}
18
19/// Entry position in the context menu
20#[derive(Debug, Clone)]
21pub enum MenuPosition {
22    Top,
23    Bottom,
24}
25
26/// Context menu separator
27#[derive(Debug, Clone)]
28pub enum Separator {
29    Before,
30    After,
31    Both,
32}
33
34#[derive(Debug)]
35pub struct CtxEntry {
36    /// The path to the entry as a list of entry names
37    pub name_path: Vec<String>,
38    pub entry_type: ActivationType,
39}
40
41/// Options for further customizing an entry
42#[derive(Debug, Clone)]
43pub struct EntryOptions {
44    /// Command to run when the entry is selected
45    pub command: Option<String>,
46    /// Icon to display beside the entry
47    pub icon: Option<String>,
48    /// Entry position in the context menu
49    pub position: Option<MenuPosition>,
50    /// Separators to include around the entry
51    pub separator: Option<Separator>,
52    /// Whether the entry should only appear with Shift+RClick
53    pub extended: bool,
54}
55
56impl CtxEntry {
57    /// Gets an existing entry at the given name path. The last name
58    /// corresponds to the returned entry.
59    ///
60    /// # Examples
61    ///
62    /// ```no_run
63    /// let name_path = &["Root entry", "Sub entry", "Sub sub entry"];
64    /// let entry = CtxEntry::get(name_path, &ActivationType::Folder)?;
65    /// ``````
66    pub fn get<N: AsRef<str>>(name_path: &[N], entry_type: &ActivationType) -> Option<CtxEntry> {
67        if name_path.is_empty() {
68            return None;
69        }
70
71        let mut str_path = get_base_path(entry_type);
72
73        for entry_name in name_path.iter().map(|x| x.as_ref()) {
74            str_path.push_str(&format!("\\shell\\{entry_name}"));
75        }
76
77        let key = get_key(&str_path);
78
79        if key
80            .as_ref()
81            .err()
82            .is_some_and(|e| e.kind() == ErrorKind::NotFound)
83        {
84            return None;
85        }
86
87        Some(CtxEntry {
88            name_path: name_path.iter().map(|x| x.as_ref().to_string()).collect(),
89            entry_type: entry_type.clone(),
90        })
91    }
92
93    /// Gets all root entries with the given entry type.
94    ///
95    /// # Examples
96    ///
97    /// ```no_run
98    /// let entries = CtxEntry::get_all_of_type(&ActivationType::Folder);
99    /// ``````
100    pub fn get_all_of_type(entry_type: &ActivationType) -> HashMap<String, CtxEntry> {
101        let mut entries = HashMap::new();
102
103        let base_path = get_base_path(entry_type);
104        let shell_path = format!("{base_path}\\shell");
105        let shell_key = match get_key(&shell_path) {
106            Ok(key) => key,
107            Err(_) => return entries,
108        };
109
110        for entry_name in shell_key.enum_keys().map(|x| x.unwrap()) {
111            if let Some(entry) = CtxEntry::get(&[entry_name.clone()], entry_type) {
112                entries.insert(entry_name, entry);
113            };
114        }
115
116        entries
117    }
118
119    fn create(
120        name_path: &[String],
121        entry_type: &ActivationType,
122        opts: &EntryOptions,
123    ) -> io::Result<CtxEntry> {
124        let path_str = get_full_path(entry_type, name_path);
125        let (_, disp) = HKCR.create_subkey(path_str)?;
126
127        if disp == REG_OPENED_EXISTING_KEY {
128            return Err(io::Error::from(ErrorKind::AlreadyExists));
129        }
130
131        let mut entry = CtxEntry {
132            name_path: name_path.to_vec(),
133            entry_type: entry_type.clone(),
134        };
135
136        entry.set_command(opts.command.as_deref())?;
137        entry.set_icon(opts.icon.as_deref())?;
138        entry.set_position(opts.position.clone())?;
139        entry.set_extended(opts.extended)?;
140
141        Ok(entry)
142    }
143
144    /// Creates a new top-level entry with the given entry type.
145    /// The resulting entry will appear in the context menu but will do
146    /// nothing until modified.
147    ///
148    /// # Examples
149    ///
150    /// ```no_run
151    /// let mut entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
152    /// ```
153    pub fn new(name: &str, entry_type: &ActivationType) -> io::Result<CtxEntry> {
154        CtxEntry::new_with_options(
155            name,
156            entry_type,
157            &EntryOptions {
158                command: None,
159                icon: None,
160                position: None,
161                separator: None,
162                extended: false,
163            },
164        )
165    }
166
167    /// Creates a new top-level entry under the given `entry_type`.
168    ///
169    /// # Examples
170    ///
171    /// ```no_run
172    /// let entry = CtxEntry::new(
173    ///     "Open in terminal",
174    ///     &ActivationType::Folder,
175    ///     &EntryOptions {
176    ///         // This command opens the target directory in cmd.
177    ///         command: Some("cmd /s /k pushd \"%V\""),
178    ///         icon: Some("C:\\Windows\\System32\\cmd.exe"),
179    ///         position: None,
180    ///         extended: false,
181    ///     }
182    /// )?;
183    /// ```
184    pub fn new_with_options(
185        name: &str,
186        entry_type: &ActivationType,
187        opts: &EntryOptions,
188    ) -> io::Result<CtxEntry> {
189        let name_path = [name.to_string()];
190        CtxEntry::create(&name_path, entry_type, opts)
191    }
192
193    /// Deletes the entry and any children.
194    ///
195    /// # Examples
196    ///
197    /// ```no_run
198    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
199    /// entry.delete()?;
200    /// ```
201    pub fn delete(self) -> io::Result<()> {
202        HKCR.delete_subkey_all(self.path())
203    }
204
205    /// Gets the entry's current name.
206    ///
207    /// # Examples
208    ///
209    /// ```no_run
210    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
211    /// let name = entry.name()?;
212    /// ```
213    pub fn name(&self) -> io::Result<String> {
214        let _ = self.key()?;
215        Ok(self.name_path.last().unwrap().to_owned())
216    }
217
218    /// Renames the entry.
219    ///
220    /// # Examples
221    ///
222    /// ```no_run
223    /// let mut entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
224    /// entry.rename("Renamed entry")?;
225    /// ```
226    pub fn rename(&mut self, new_name: &str) -> io::Result<()> {
227        if new_name.is_empty() {
228            return Err(io::Error::new(
229                ErrorKind::InvalidInput,
230                "Name cannot be empty",
231            ));
232        }
233
234        let old_name = self.name()?;
235
236        let parent_name_path = &self.name_path[..self.name_path.len() - 1];
237        let parent_path_str = get_full_path(&self.entry_type, parent_name_path);
238        let parent_key = HKCR.open_subkey(parent_path_str)?;
239        let res = parent_key.rename_subkey(old_name, new_name);
240
241        let path_len = self.name_path.len();
242        self.name_path[path_len - 1] = new_name.to_string();
243
244        res
245    }
246
247    /// Gets the entry's command, if any.
248    ///
249    /// # Examples
250    ///
251    /// ```no_run
252    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
253    /// let command = entry.command()?;
254    /// ```
255    pub fn command(&self) -> io::Result<Option<String>> {
256        let path = format!(r"{}\command", self.path());
257        let key = get_key(&path)?;
258        Ok(key.get_value::<String, _>("").ok())
259    }
260
261    /// Sets the entry's command.
262    ///
263    /// # Examples
264    ///
265    /// ```no_run
266    /// let mut entry = CtxEntry::new("Basic entry", ActivationType::Folder)?;
267    /// // This command opens the target directory in Powershell.
268    /// entry.set_command(Some("powershell.exe -noexit -command Set-Location -literalPath '%V'"))?;
269    /// ```
270    pub fn set_command(&mut self, command: Option<&str>) -> io::Result<()> {
271        let key = self.key()?;
272        match command {
273            Some(c) => {
274                let (command_key, _) = key.create_subkey("command")?;
275                command_key.set_value("", &c)
276            }
277            None => match key.delete_subkey("command") {
278                Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
279                Err(e) => Err(e),
280                Ok(_) => Ok(()),
281            },
282        }
283    }
284
285    /// Gets the entry's icon, if any.
286    ///
287    /// # Examples
288    ///
289    /// ```no_run
290    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
291    /// let icon = entry.icon()?;
292    /// ```
293    pub fn icon(&self) -> io::Result<Option<String>> {
294        let key = self.key()?;
295        Ok(key.get_value::<String, _>("Icon").ok())
296    }
297
298    /// Sets the entry's icon.
299    ///
300    /// # Examples
301    ///
302    /// ```no_run
303    /// let mut entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
304    /// entry.set_icon(Some("C:\\Windows\\System32\\control.exe"))?;
305    /// ```
306    pub fn set_icon(&mut self, icon: Option<&str>) -> io::Result<()> {
307        let key = self.key()?;
308        match icon {
309            Some(icon) => key.set_value("Icon", &icon),
310            None => self.safe_delete_value("Icon"),
311        }
312    }
313
314    /// Gets the entry's position, if any.
315    ///
316    /// # Examples
317    ///
318    /// ```no_run
319    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
320    /// let position = entry.position()?;
321    /// ```
322    pub fn position(&self) -> io::Result<Option<MenuPosition>> {
323        let key = self.key()?;
324        let val = match key.get_value::<String, _>("Position") {
325            Ok(v) if v == "Top" => Some(MenuPosition::Top),
326            Ok(v) if v == "Bottom" => Some(MenuPosition::Bottom),
327            _ => None,
328        };
329
330        Ok(val)
331    }
332
333    /// Sets the entry's menu position. By default, new root entries are
334    /// positioned at the top. Does not affect child entries.
335    ///
336    /// # Examples
337    ///
338    /// ```no_run
339    /// let mut entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
340    /// entry.set_position(Some(MenuPosition::Bottom))?;
341    /// ```
342    pub fn set_position(&mut self, position: Option<MenuPosition>) -> io::Result<()> {
343        if position.is_none() {
344            return self.safe_delete_value("Position");
345        }
346
347        let position_str = match position {
348            Some(MenuPosition::Top) => "Top",
349            Some(MenuPosition::Bottom) => "Bottom",
350            None => "",
351        };
352
353        self.key()?.set_value("Position", &position_str)
354    }
355
356    /// Gets whether the entry appears with Shift+RClick.
357    ///
358    /// # Examples
359    ///
360    /// ```no_run
361    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
362    /// let is_extended = entry.extended()?;
363    /// ```
364    pub fn extended(&self) -> io::Result<bool> {
365        let key = self.key()?;
366        Ok(key.get_value::<String, _>("Extended").ok().is_some())
367    }
368
369    /// Sets whether the entry should only appear with Shift+RClick.
370    ///
371    /// # Examples
372    ///
373    /// ```no_run
374    /// let mut entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
375    /// entry.set_extended(true)?;
376    /// ```
377    pub fn set_extended(&mut self, extended: bool) -> io::Result<()> {
378        if extended {
379            self.key()?.set_value("Extended", &"")
380        } else {
381            self.safe_delete_value("Extended")
382        }
383    }
384
385    /// Gets the entry's separator(s), if any.
386    ///
387    /// # Examples
388    ///
389    /// ```no_run
390    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
391    /// let separator = entry.separator()?;
392    /// ```
393    pub fn separator(&self) -> io::Result<Option<Separator>> {
394        let key = self.key()?;
395        let sep_before = key.get_value::<String, _>("SeparatorBefore");
396        let sep_after = key.get_value::<String, _>("SeparatorAfter");
397
398        Ok(match (sep_before, sep_after) {
399            (Ok(_), Ok(_)) => Some(Separator::Both),
400            (Ok(_), Err(_)) => Some(Separator::Before),
401            (Err(_), Ok(_)) => Some(Separator::After),
402            _ => None,
403        })
404    }
405
406    /// Sets the entry's separator(s).
407    ///
408    /// # Examples
409    ///
410    /// ```no_run
411    /// let mut entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
412    /// entry.set_separator(Some(Separator::After))?;
413    /// ```
414    pub fn set_separator(&mut self, separator: Option<Separator>) -> io::Result<()> {
415        let key = self.key()?;
416        match separator {
417            Some(Separator::Before) => {
418                key.set_value("SeparatorBefore", &"")?;
419                self.safe_delete_value("SeparatorAfter")?;
420                Ok(())
421            }
422            Some(Separator::After) => {
423                key.set_value("SeparatorAfter", &"")?;
424                self.safe_delete_value("SeparatorBefore")?;
425                Ok(())
426            }
427            Some(Separator::Both) => {
428                key.set_value("SeparatorBefore", &"")?;
429                key.set_value("SeparatorAfter", &"")?;
430                Ok(())
431            }
432            None => {
433                self.safe_delete_value("SeparatorBefore")?;
434                self.safe_delete_value("SeparatorAfter")?;
435                Ok(())
436            }
437        }
438    }
439
440    /// Gets the entry's parent, if any.
441    ///
442    /// # Examples
443    ///
444    /// ```no_run
445    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
446    /// let child = entry.new_child("Basic child entry")?;
447    /// let parent = child.parent()?;
448    /// assert_eq!(entry.name().unwrap(), parent.name().unwrap());
449    /// ```
450    pub fn parent(&self) -> Option<CtxEntry> {
451        if self.name_path.len() <= 1 {
452            return None;
453        }
454
455        let parent_path = &self.name_path[..self.name_path.len() - 1];
456        CtxEntry::get(parent_path, &self.entry_type)
457    }
458
459    /// Gets one of the entry's children, if any.
460    ///
461    /// # Examples
462    ///
463    /// ```no_run
464    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
465    /// let created_child = entry.new_child("Basic child entry")?;
466    /// let retrieved_child = entry.child("Basic child entry")?;
467    /// assert_eq!(created_child.name().unwrap(), retrieved_child.name().unwrap());
468    /// ```
469    pub fn child(&self, name: &str) -> io::Result<Option<CtxEntry>> {
470        let mut name_path = self.name_path.clone();
471        name_path.push(name.to_string());
472        let path_str = get_full_path(&self.entry_type, &name_path);
473
474        match get_key(&path_str) {
475            Ok(_) => Ok(Some(CtxEntry {
476                name_path,
477                entry_type: self.entry_type.clone(),
478            })),
479            Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
480            Err(e) => Err(e),
481        }
482    }
483
484    /// Gets the entry's children, if any.
485    ///
486    /// # Examples
487    ///
488    /// ```no_run
489    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
490    /// let child_1 = entry.new_child("Child 1")?;
491    /// let child_2 = entry.new_child("Child 2")?;
492    /// let children = entry.children()?;
493    /// ```
494    pub fn children(&self) -> io::Result<Vec<CtxEntry>> {
495        let path = format!("{}\\shell", self.path());
496        let key = get_key(&path);
497
498        if key.is_err() {
499            return Ok(Vec::new());
500        }
501
502        let mut children = Vec::new();
503
504        for name in key.unwrap().enum_keys().map(|x| x.unwrap()) {
505            if let Some(child) = self.child(&name).unwrap() {
506                children.push(child)
507            }
508        }
509
510        Ok(children)
511    }
512
513    /// Creates a new child entry under the entry. The resulting entry
514    /// will appear in the context menu but will do nothing until modified.
515    ///
516    /// # Examples
517    ///
518    /// ```no_run
519    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
520    /// let child = entry.new_child("Basic child entry")?;
521    /// ```
522    pub fn new_child(&self, name: &str) -> io::Result<CtxEntry> {
523        self.new_child_with_options(
524            name,
525            &EntryOptions {
526                command: None,
527                icon: None,
528                position: None,
529                separator: None,
530                extended: false,
531            },
532        )
533    }
534
535    /// Creates a new child entry under the entry.
536    ///
537    /// # Examples
538    ///
539    /// ```no_run
540    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
541    /// let child = entry.new_child_with_options(
542    ///     "Basic child entry",
543    ///     &EntryOptions {
544    ///         // This command opens the target directory in cmd.
545    ///         command: Some("cmd /s /k pushd \"%V\""),
546    ///         icon: Some("C:\\Windows\\System32\\cmd.exe"),
547    ///         position: None,
548    ///         extended: false,
549    ///     }
550    /// )?;
551    /// ```
552    pub fn new_child_with_options(&self, name: &str, opts: &EntryOptions) -> io::Result<CtxEntry> {
553        let key = self.key()?;
554        key.set_value("Subcommands", &"")?;
555
556        let mut path = self.name_path.clone();
557        path.push(name.to_string());
558
559        CtxEntry::create(path.as_slice(), &self.entry_type, opts)
560    }
561
562    /// Gets the full path to the entry's registry key.
563    ///
564    /// # Examples
565    ///
566    /// ```no_run
567    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
568    /// let path = entry.path();
569    /// ```
570    pub fn path(&self) -> String {
571        get_full_path(&self.entry_type, &self.name_path)
572    }
573
574    // Shortcut to get the entry's registry key.
575    // Should be checked before every operation.
576    fn key(&self) -> io::Result<RegKey> {
577        get_key(&self.path())
578    }
579
580    // Delete value without erroring if nonexistent.
581    fn safe_delete_value(&self, value: &str) -> io::Result<()> {
582        let key = self.key()?;
583        match key.delete_value(value) {
584            Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
585            Err(e) => Err(e),
586            Ok(_) => Ok(()),
587        }
588    }
589}
590
591fn get_key(path: &str) -> io::Result<RegKey> {
592    match HKCR.open_subkey_with_flags(path, KEY_ALL_ACCESS) {
593        Err(e) if e.kind() == ErrorKind::NotFound => Err(io::Error::new(
594            ErrorKind::NotFound,
595            "Registry key does not exist",
596        )),
597        Err(e) => Err(e),
598        Ok(key) => Ok(key),
599    }
600}