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}