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