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}