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 (only applies at top level)
21#[derive(Clone)]
22pub enum MenuPosition {
23    Top,
24    Bottom,
25}
26
27pub struct CtxEntry {
28    /// The path to the entry as a list of entry names
29    pub path: Vec<String>,
30    pub entry_type: ActivationType,
31    key: RegKey,
32}
33
34/// Options for further customizing an entry
35#[derive(Clone)]
36pub struct EntryOptions {
37    /// The command to run when the entry is selected
38    pub command: Option<String>,
39    /// The icon to display beside the entry
40    pub icon: Option<String>,
41    /// The location of the entry in the context menu (if top-level)
42    pub position: Option<MenuPosition>,
43    /// Whether the entry should only appear with Shift+RClick
44    pub extended: bool,
45}
46
47impl CtxEntry {
48    /// Gets an existing entry at the given name path. The last name
49    /// corresponds to the returned entry.
50    ///
51    /// # Examples
52    ///
53    /// ```no_run
54    /// let name_path = &["Root entry", "Sub entry", "Sub sub entry"];
55    /// let entry = CtxEntry::get(name_path, &ActivationType::Folder)?;
56    /// ``````
57    pub fn get<N: AsRef<str>>(name_path: &[N], entry_type: &ActivationType) -> Option<CtxEntry> {
58        if name_path.len() == 0 {
59            return None;
60        }
61
62        let mut str_path = get_base_path(&entry_type);
63
64        for entry_name in name_path.iter().map(|x| x.as_ref()) {
65            str_path.push_str(&format!("\\shell\\{entry_name}"));
66        }
67
68        let key = get_key(&str_path);
69
70        if key
71            .as_ref()
72            .err()
73            .map_or(false, |e| e.kind() == ErrorKind::NotFound)
74        {
75            return None;
76        }
77
78        Some(CtxEntry {
79            path: name_path.iter().map(|x| x.as_ref().to_string()).collect(),
80            entry_type: entry_type.clone(),
81            key: key.unwrap(),
82        })
83    }
84
85    fn create(
86        name_path: &[String],
87        entry_type: &ActivationType,
88        opts: &EntryOptions,
89    ) -> io::Result<CtxEntry> {
90        let path_str = get_full_path(entry_type, name_path);
91        let (key, disp) = HKCR.create_subkey(path_str)?;
92
93        if disp == REG_OPENED_EXISTING_KEY {
94            return Err(io::Error::from(ErrorKind::AlreadyExists));
95        }
96
97        let mut entry = CtxEntry {
98            path: name_path.to_vec(),
99            entry_type: entry_type.clone(),
100            key,
101        };
102
103        if let Some(command) = &opts.command {
104            entry.set_command(&command)?;
105        }
106
107        if let Some(icon) = &opts.icon {
108            entry.set_icon(&icon)?;
109        }
110
111        if let Some(position) = &opts.position {
112            entry.set_position(Some(position.clone()))?;
113        }
114
115        entry.set_extended(opts.extended)?;
116
117        Ok(entry)
118    }
119
120    /// Creates a new top-level entry under the given `entry_type`.
121    /// The resulting entry will appear in the context menu but will do
122    /// nothing until modified.
123    ///
124    /// # Examples
125    ///
126    /// ```no_run
127    /// let mut entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
128    /// ```
129    pub fn new(name: &str, entry_type: &ActivationType) -> io::Result<CtxEntry> {
130        CtxEntry::new_with_options(
131            name,
132            entry_type,
133            &EntryOptions {
134                command: None,
135                icon: None,
136                position: None,
137                extended: false,
138            },
139        )
140    }
141
142    /// Creates a new top-level entry under the given `entry_type`.
143    ///
144    /// # Examples
145    ///
146    /// ```no_run
147    /// let entry = CtxEntry::new(
148    ///     "Open in terminal",
149    ///     &ActivationType::Folder,
150    ///     &EntryOptions {
151    ///         // This command opens the target directory in cmd.
152    ///         command: Some("cmd /s /k pushd \"%V\""),
153    ///         icon: Some("C:\\Windows\\System32\\cmd.exe"),
154    ///         position: None,
155    ///         extended: false,
156    ///     }
157    /// )?;
158    /// ```
159    pub fn new_with_options(
160        name: &str,
161        entry_type: &ActivationType,
162        opts: &EntryOptions,
163    ) -> io::Result<CtxEntry> {
164        let name_path = [name.to_string()];
165        CtxEntry::create(&name_path, entry_type, opts)
166    }
167
168    /// Deletes the entry and any children.
169    ///
170    /// # Examples
171    ///
172    /// ```no_run
173    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
174    /// entry.delete();
175    /// ```
176    pub fn delete(self) -> io::Result<()> {
177        HKCR.delete_subkey_all(&self.get_full_path())
178    }
179
180    /// Gets the entry's current name.
181    ///
182    /// # Examples
183    ///
184    /// ```no_run
185    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
186    /// let name = entry.name();
187    /// ```
188    pub fn name(&self) -> String {
189        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, name: &str) -> io::Result<()> {
201        let parent_name_path = &self.path[..self.path.len() - 1];
202        let parent_path_str = get_full_path(&self.entry_type, parent_name_path);
203        let parent_key = HKCR.open_subkey(parent_path_str)?;
204
205        let old_name = self.name();
206        let rename_res = parent_key.rename_subkey(old_name, name);
207
208        let path_len = self.path.len();
209        self.path[path_len - 1] = name.to_string();
210
211        rename_res
212    }
213
214    /// Gets the entry's command, if any.
215    ///
216    /// # Examples
217    ///
218    /// ```no_run
219    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
220    /// let command = entry.command()?;
221    /// ```
222    pub fn command(&self) -> Option<String> {
223        let path = format!(r"{}\command", self.get_full_path());
224        let key = get_key(&path);
225
226        match key {
227            Ok(k) => k.get_value::<String, _>("").ok(),
228            Err(_) => None,
229        }
230    }
231
232    /// Sets the entry's command.
233    ///
234    /// # Examples
235    ///
236    /// ```no_run
237    /// let mut entry = CtxEntry::new("Basic entry", ActivationType::Folder)?;
238    /// // This command opens the target directory in Powershell.
239    /// entry.set_command(Some("powershell.exe -noexit -command Set-Location -literalPath '%V'"));
240    /// ```
241    pub fn set_command(&mut self, command: &str) -> io::Result<()> {
242        let (key, _) = self.key.create_subkey("command")?;
243        key.set_value("", &command)
244    }
245
246    /// Gets the entry's icon, if any.
247    ///
248    /// # Examples
249    ///
250    /// ```no_run
251    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
252    /// let icon = entry.icon()?;
253    /// ```
254    pub fn icon(&self) -> Option<String> {
255        match get_key(&self.get_full_path()) {
256            Ok(k) => k.get_value::<String, _>("Icon").ok(),
257            Err(_) => None,
258        }
259    }
260
261    /// Sets the entry's icon.
262    ///
263    /// # Examples
264    ///
265    /// ```no_run
266    /// let mut entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
267    /// entry.set_icon(Some("C:\\Windows\\System32\\control.exe"));
268    /// ```
269    pub fn set_icon(&mut self, icon: &str) -> io::Result<()> {
270        self.key.set_value("Icon", &icon)
271    }
272
273    /// Gets the entry's position, if any.
274    ///
275    /// # Examples
276    ///
277    /// ```no_run
278    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
279    /// let position = entry.position()?;
280    /// ```
281    pub fn position(&self) -> Option<MenuPosition> {
282        match get_key(&self.get_full_path()) {
283            Ok(k) => match k.get_value::<String, _>("Position") {
284                Ok(v) if v == "Top" => Some(MenuPosition::Top),
285                Ok(v) if v == "Bottom" => Some(MenuPosition::Bottom),
286                _ => None,
287            },
288            Err(_) => None,
289        }
290    }
291
292    /// Sets the entry's menu position. By default, new root entries are
293    /// positioned at the top. Does not affect child entries.
294    ///
295    /// # Examples
296    ///
297    /// ```no_run
298    /// let mut entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
299    /// entry.set_position(Some(MenuPosition::Bottom));
300    /// ```
301    pub fn set_position(&mut self, position: Option<MenuPosition>) -> io::Result<()> {
302        if position.is_none() {
303            return match self.key.delete_value("Position") {
304                Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
305                Err(e) => Err(e),
306                Ok(_) => Ok(()),
307            };
308        }
309
310        let position_str = match position {
311            Some(MenuPosition::Top) => "Top",
312            Some(MenuPosition::Bottom) => "Bottom",
313            None => "",
314        };
315
316        self.key.set_value("Position", &position_str)
317    }
318
319    /// Gets whether the entry appears with Shift+RClick.
320    ///
321    /// # Examples
322    ///
323    /// ```no_run
324    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
325    /// let is_extended = entry.extended();
326    /// ```
327    pub fn extended(&self) -> bool {
328        match get_key(&self.get_full_path()) {
329            Ok(k) => k.get_value::<String, _>("Extended").ok().is_some(),
330            Err(_) => false,
331        }
332    }
333
334    /// Sets whether the entry should only appear with Shift+RClick.
335    ///
336    /// # Examples
337    ///
338    /// ```no_run
339    /// let mut entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
340    /// entry.set_extended(true);
341    /// ```
342    pub fn set_extended(&mut self, extended: bool) -> io::Result<()> {
343        if extended {
344            self.key.set_value("Extended", &"")
345        } else {
346            match self.key.delete_value("Extended") {
347                Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
348                Err(e) => Err(e),
349                Ok(_) => Ok(()),
350            }
351        }
352    }
353
354    /// Gets the entry's parent, if any.
355    ///
356    /// # Examples
357    ///
358    /// ```no_run
359    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
360    /// let child = entry.new_child("Basic child entry")?;
361    /// let parent = child.parent()?;
362    /// assert_eq!(entry.name(), parent.name());
363    /// ```
364    pub fn parent(&self) -> Option<CtxEntry> {
365        if self.path.len() <= 1 {
366            return None;
367        }
368
369        let parent_path = &self.path[..self.path.len() - 1];
370        CtxEntry::get(parent_path, &self.entry_type)
371    }
372
373    /// Gets one of the entry's children, if any.
374    ///
375    /// # Examples
376    ///
377    /// ```no_run
378    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
379    /// let created_child = entry.new_child("Basic child entry")?;
380    /// let retrieved_child = entry.child("Basic child entry")?;
381    /// assert_eq!(created_child.name(), retrieved_child.name());
382    /// ```
383    pub fn child(&self, name: &str) -> Option<CtxEntry> {
384        let mut name_path = self.path.clone();
385        name_path.push(name.to_string());
386        let path_str = get_full_path(&self.entry_type, &name_path);
387
388        match get_key(&path_str) {
389            Ok(key) => Some(CtxEntry {
390                path: name_path,
391                entry_type: self.entry_type.clone(),
392                key,
393            }),
394            Err(_) => None,
395        }
396    }
397
398    /// Gets the entry's children, if any.
399    ///
400    /// # Examples
401    ///
402    /// ```no_run
403    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
404    /// let child_1 = entry.new_child("Child 1")?;
405    /// let child_2 = entry.new_child("Child 2")?;
406    /// let children = entry.children();
407    /// ```
408    pub fn children(&self) -> Vec<CtxEntry> {
409        let mut children = Vec::new();
410
411        for name in self.key.enum_keys().map(|x| x.unwrap()) {
412            let child = self.child(&name).unwrap();
413            children.push(child);
414        }
415
416        children
417    }
418
419    /// Creates a new child entry under the entry. The resulting entry
420    /// will appear in the context menu but will do nothing until modified.
421    ///
422    /// # Examples
423    ///
424    /// ```no_run
425    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
426    /// let child = entry.new_child("Basic child entry")?;
427    /// ```
428    pub fn new_child(&self, name: &str) -> io::Result<CtxEntry> {
429        self.new_child_with_options(
430            name,
431            &EntryOptions {
432                command: None,
433                icon: None,
434                position: None,
435                extended: false,
436            },
437        )
438    }
439
440    /// Creates a new child entry under the entry.
441    ///
442    /// # Examples
443    ///
444    /// ```no_run
445    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
446    /// let child = entry.new_child_with_options(
447    ///     "Basic child entry",
448    ///     &EntryOptions {
449    ///         // This command opens the target directory in cmd.
450    ///         command: Some("cmd /s /k pushd \"%V\""),
451    ///         icon: Some("C:\\Windows\\System32\\cmd.exe"),
452    ///         position: None,
453    ///         extended: false,
454    ///     }
455    /// )?;
456    /// ```
457    pub fn new_child_with_options(&self, name: &str, opts: &EntryOptions) -> io::Result<CtxEntry> {
458        self.key.set_value("Subcommands", &"")?;
459
460        let mut path = self.path.clone();
461        path.push(name.to_string());
462
463        CtxEntry::create(path.as_slice(), &self.entry_type, &opts)
464    }
465
466    /// Gets the full path to the entry's registry key.
467    ///
468    /// # Examples
469    ///
470    /// ```no_run
471    /// let entry = CtxEntry::new("Basic entry", ActivationType::Background)?;
472    /// let path = entry.get_full_path();
473    /// ```
474    pub fn get_full_path(&self) -> String {
475        get_full_path(&self.entry_type, &self.path)
476    }
477}
478
479fn get_key(path: &str) -> io::Result<RegKey> {
480    HKCR.open_subkey_with_flags(path, KEY_ALL_ACCESS)
481}