win_ctx/
entry.rs

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