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}