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}