1use crate::bookmark::Handler;
2use crate::bookmark::HandlerClass;
3use crate::bookmark::Location;
4use crate::bookmark::LocationType;
5use crate::environment::LinkVariable;
6use bookmark::Bookmark;
7use clap::{ArgGroup, Parser};
8use edit::edit;
9use environment::{Category, Configurations};
10use prompted::input;
11use std::{collections::HashSet, fs};
12
13mod bookmark;
14mod data_provider;
15mod environment;
16
17#[derive(Parser)]
18#[command(version, about)]
19#[clap(group(ArgGroup::new("info").required(false).args(&["keyglobal", "keycategory", "name", "edit", "summary", "config", "delete", "rename"])))]
20struct Cli {
21 name: Option<Vec<String>>,
23
24 #[arg(short, long, group = "cats")]
26 category: Option<String>,
27
28 #[arg(short = '0', long = "summary", default_value_t = false)]
30 summary: bool,
31
32 #[arg(short, long = "edit-bookmarks", default_value_t = false)]
34 edit: bool,
35
36 #[arg(short = 'E', long = "edit-configuration", default_value_t = false)]
38 config: bool,
39
40 #[arg(short = 'K', long = "list-global-keys", default_value_t = false)]
42 keyglobal: bool,
43
44 #[arg(short, long = "list-category-keys", default_value_t = false)]
46 keycategory: bool,
47
48 #[arg(short, long = "new-category", group = "cats", value_name = "CATEGORY")]
50 new: Option<String>,
51
52 #[arg(
54 short,
55 long = "delete-category",
56 group = "cats",
57 value_name = "CATEGORY"
58 )]
59 delete: Option<String>,
60
61 #[arg(
63 short,
64 long = "rename-category",
65 value_delimiter = ' ',
66 num_args = 2,
67 group = "cats",
68 value_names = ["OLD_NAME", "NEW_NAME"]
69 )]
70 rename: Option<Vec<String>>,
71}
72
73#[derive(Clone)]
74enum PadType {
75 ByKey(char),
76 ByName(String),
77 BySearch(SearchService),
78 BySelector(Selector),
79}
80
81#[derive(Clone)]
82struct CategoryManager {
83 root: String,
84 name: String,
85 categories: Vec<String>,
86}
87
88impl CategoryManager {
89 fn new() -> Self {
90 let name = Configurations::get_setting()
91 .default_category
92 .unwrap_or(Configurations::default_settings().default_category.unwrap());
93 let root = Configurations::category_home();
94 let f_name = format!("{root}/{name}");
95 if !Configurations::check_path(&f_name) {
96 eprintln!(
98 "Hi! It seems the set default-category '{name}' in your configuration file is missing."
99 );
100 eprintln!(
101 "Please correct this by either changing the default-category to some other available category or re-create the '{name}' category and run this program again."
102 );
103 eprintln!(
104 "This message does not appear for a new installation, which means you might have deleted the default bookmark file and forgot to change the default category in the process."
105 );
106 eprintln!(
107 "It also means yu do not have 'create-missing-category' set to true, which prevents hlg from recreating the default bookmark file."
108 );
109 std::process::exit(22);
110 }
111 let categories = Self::get_category_list();
112 Self {
113 name,
114 categories,
115 root,
116 }
117 }
118
119 fn set_global(&self) -> Category {
121 Category::new(Scope::Global)
122 }
123
124 pub fn by_name<T: AsRef<str> + ToString>(mut self, category_name: T) -> Self {
126 self.name = category_name.to_string();
127 self
128 }
129
130 fn refresh_category_headers() {
131 let checkfile = format!("{}/categories", Configurations::get_records());
132 if Configurations::check_path(&checkfile) {
133 fs::remove_file(checkfile).expect("Could not remove the old category file");
134 }
135 Self::create_category_file();
136 }
137
138 fn create_new(&self, category_name: String) -> std::io::Result<()> {
139 if self.categories.contains(&category_name) {
140 eprintln!(
141 "Error: {category_name} category already exists in your bookmarks database. Choose another name, or simply use the {category_name} category to perform any bookmark management you wished to perform with this new category."
142 );
143 std::process::exit(25);
144 } else {
145 let data = format!(
146 r"# This is the category header for the {category_name} category.
147* <@access-computing.com/>"
148 );
149 let newcategory = format!("{}/{category_name}", self.root);
150 fs::write(newcategory, data.as_bytes())?;
151 println!(
152 "{category_name} was successfully created and added to database. Feel fre to add bookmarks to it, or even set it as your default category in the configuration file."
153 );
154
155 Self::refresh_category_headers();
156 }
157 Ok(())
158 }
159
160 fn safe_category(&self) -> String {
161 let mut home = Configurations::get_homepage();
162 home = if home.contains('.') {
163 let pos = home.find('.').unwrap();
164 let (dish, _) = home.split_at(pos);
165 dish.to_string()
166 } else {
167 self.name.clone()
168 };
169 home
170 }
171
172 fn delete_category(&self, name: &String) -> std::io::Result<()> {
173 let safe_category = self.safe_category();
174 if *name == self.name || *name == safe_category {
175 eprintln!(
176 "Error: cannot delete default or home-linked category. This option can onnly be called with the name of the category to be deleted, other than the default or home-linked category."
177 );
178 std::process::exit(20);
179 }
180 if !self.categories.contains(name) {
181 eprintln!("Error: No category '{name}' found in database.");
182 std::process::exit(21);
183 }
184 let filename = format!("{}/{name}", self.root);
185 println!(
186 "are you sure that you would like to delete {name} from the bookmarks database? This action cannot be undone."
187 );
188 let confirm = input!("Enter y to confirm:");
189 if confirm.trim().to_lowercase() == "y" {
190 fs::remove_file(filename)?;
191 println!(
192 "{name} category deleted successfully. You will no longer be able to perform any operation that involves this category unless you recreate it"
193 );
194 Self::refresh_category_headers();
195 } else {
196 println!(
197 "{name} category not deleted, and is still available for any category management operations. Bye!"
198 );
199 }
200
201 Ok(())
202 }
203
204 fn rename_category(&self, old_name: &String, new_name: &String) -> std::io::Result<()> {
205 if !self.categories.contains(&old_name) {
207 eprintln!("Sorry, this name {old_name} not found in database.");
208 eprintln!("You can simply create a new category with the name, '{new_name}' instead.");
209 std::process::exit(21);
210 }
211 let safe_category = self.safe_category();
213 if *old_name == safe_category || *old_name == self.name {
214 eprintln!(
215 "Hi, what are you trying to do? You cannot rename a default category or a home-linked category to a new name while they are still in use."
216 );
217 eprintln!(
218 "Consider changing the default category or a category linked by the main home bookmark in the configuration file before attempting this operation."
219 );
220 std::process::exit(20);
221 }
222 if self.categories.contains(&new_name) {
224 eprintln!("Error: This category '{new_name}' is already in the bookmarks database.");
225 eprintln!("Just use it instead.");
226 std::process::exit(25);
227 }
228 let old_file = format!("{}/{old_name}", self.root);
230 let new_file = format!("{}/{new_name}", self.root);
231 println!(
232 "Are you sure that you would like to rename the category '{old_name}' to '{new_name}'?"
233 );
234 let confirm = input!("Enter Y to confirm:");
235 if confirm.trim().to_lowercase() == "y" {
236 fs::rename(old_file, new_file)?;
237 Self::refresh_category_headers();
238 println!("Success! The category {old_name} was renamed to {new_name}.");
239 println!(
240 "Remember to use this name in any category management activities rather than '{old_name}'."
241 );
242 } else {
243 println!(
244 "Okay, the rename operation was cancelled. Your category is still referred to as '{old_name}' and not '{new_name}'."
245 );
246 }
247 Ok(())
248 }
249
250 fn check_name_availability(&self) {
251 let create_missing_category = Configurations::get_setting()
252 .create_missing_category
253 .unwrap_or(
254 Configurations::default_settings()
255 .create_missing_category
256 .unwrap(),
257 );
258 if !self.categories.contains(&self.name) && !create_missing_category {
259 eprintln!("Error: the {} category could not be found.", self.name);
260 eprintln!(
261 "As your default setting is not to create missing categories on the fly, consider creating a file called {} with your bookmarks and place it in the 'bookmarks' directory.",
262 self.name
263 );
264 eprintln!(
265 "Otherwise, just change the create-missing-category option to 'yes' in your configuration file. Now exiting.."
266 );
267 std::process::exit(26);
268 }
269 }
270
271 fn load_categories() -> Vec<(String, String)> {
272 let mut loader = Vec::new();
273 let category_file = format!("{}/categories", Configurations::get_records());
274 if !Configurations::check_path(&category_file) {
275 Self::create_category_file();
276 }
277 let list =
278 fs::read_to_string(category_file).expect("Could not load category file for reading");
279 Self::shortcut_keycheck(&list);
280 for line in list.lines() {
281 if line.contains('(') {
282 let pos = line.find('(').unwrap();
283 let (category_name, comment) = line.split_at(pos);
284 loader.push((format!("{category_name}"), format!("{}", &comment[1..])));
285 } else {
286 let tokens: Vec<&str> = line.split_whitespace().collect();
287 loader.push((
288 format!("{}", tokens.get(0).unwrap()),
289 format!("<NONE> {}", &tokens[1..].join(" ")),
290 ));
291 }
292 }
293 loader
294 }
295
296 fn create_category_file() {
297 let filelist = Self::file_list();
298 if filelist.len() > 0 {
299 let mut raw_string = String::new();
300 for file in filelist {
301 let fname = format!("{}/bookmarks/{file}", Configurations::get_app_home());
302 let header = Self::find_category_line(&fname);
303 if header.is_some() {
304 raw_string = format!("{raw_string}{file}{}\n", header.unwrap());
305 } else {
306 raw_string = format!("{raw_string}{file}\n");
307 }
308 }
309 if !Configurations::check_path(&Configurations::get_records()) {
310 fs::create_dir(Configurations::get_records())
311 .expect("Could not create the records directory");
312 }
313 let category_file = format!("{}/categories", Configurations::get_records());
314 fs::write(category_file.as_str(), raw_string.as_bytes())
315 .expect("Could not save category details");
316 } else {
317 eprintln!("Error: no categories were found in the database.");
318 eprintln!("Please add one and try again.");
319 std::process::exit(36);
320 }
321 }
322
323 fn get_category_list() -> Vec<String> {
324 let categories = Self::load_categories();
325 let mut names = Vec::new();
326 for name in categories {
327 names.push(name.0.to_string());
328 }
329 names
330 }
331
332 fn get_name_by_key(key: char) -> Option<String> {
334 let mut category_name = None;
335 let category_list = Self::load_categories();
336 for candidate in category_list {
337 let name = candidate.0;
338 let header = candidate.1;
339 let words: Vec<&str> = header.split_whitespace().collect();
340 if words[0] != "<NONE>" {
341 let letter = words.get(0).unwrap();
342 let psb = letter.chars().nth(0).unwrap();
343 if psb == key {
344 category_name = Some(name.to_string())
345 }
346 }
347 }
348 category_name
349 }
350
351 fn file_list() -> Vec<String> {
352 let working_dir = Configurations::category_home();
353 let mut categories = Vec::new();
354 if Configurations::check_path(&working_dir) {
355 for entry in fs::read_dir(&working_dir)
356 .unwrap()
357 .into_iter()
358 .filter_map(Result::ok)
359 {
360 let f_name = String::from(entry.file_name().to_string_lossy());
361 categories.push(f_name);
362 }
363 categories.sort();
364 categories.dedup();
365 }
366 categories
367 }
368
369 fn find_category_line(fs_name: &String) -> Option<String> {
371 let lines = fs::read_to_string(fs_name).expect("Could not load category header line");
372 let lines: Vec<&str> = lines.lines().collect();
373 let lines = lines
374 .into_iter()
375 .filter(|line| !line.is_empty())
376 .map(|ln| ln.to_string())
377 .collect::<Vec<String>>();
378 let header = lines.get(0).unwrap();
379 if header.starts_with("#!") {
380 let header = &header[2..];
381 Self::validate_header(header, fs_name);
382 let header_fields: Vec<&str> = header.split(';').collect();
383 if header_fields.len() > 1 {
384 let shortcut = &header_fields[1].trim();
385 let comment = if header_fields.len() > 2 {
386 &header_fields[2].trim()
387 } else {
388 ""
389 };
390 if shortcut.is_empty() {
391 Some(format!(" {comment}"))
392 } else {
393 Some(format!("({shortcut}) {comment}"))
394 }
395 } else {
396 None
397 }
398 } else {
399 None
400 }
401 }
402
403 fn shortcut_keycheck(rawlist: &String) {
404 let category_list: Vec<&str> = rawlist.lines().collect();
405 let mut keys = Vec::new();
406 let mut names = Vec::new();
407 for category in category_list {
408 if category.contains('(') {
409 let divider = category.find('(').unwrap();
410 let (_, key) = category.split_at(divider);
411 let key = key.chars().nth(1).unwrap();
412 keys.push(key);
413 names.push(category.to_string());
414 }
415 }
416 for key in keys {
417 let mut list = Vec::new();
418 for name in &names {
419 let pos = name.find('(').unwrap();
420 let (category, sym) = name.split_at(pos);
421 let sym = sym.chars().nth(1).unwrap();
422 if sym == key {
423 list.push(category.to_string());
424 }
425 }
426 if list.len() > 1 {
427 eprintln!("Error: Category key shortcut duplication detected!");
428 eprintln!(
429 "This shortcut, '{key}', is assigned to {} categories, namely {} and {}",
430 list.len(),
431 &list[..list.len() - 1].join(", "),
432 &list[list.len() - 1]
433 );
434 eprintln!(
435 "To fix this error, just change the shortcut keys of the other categories and use {key} on only one category."
436 );
437 std::process::exit(19);
438 }
439 }
440 }
441
442 fn validate_header(header: &str, fname: &String) {
443 let fname = {
444 let pos = fname.rfind('/').unwrap();
445 let (_, name) = fname.split_at(pos);
446 name.strip_prefix('/').unwrap()
447 };
448 let tokens: Vec<&str> = header.split(';').collect();
449 if tokens.len() > 3 {
450 eprintln!(
451 "Error: A category header cannot have more than three fields: the bookmark field, the shortcut key field and the descripption field."
452 );
453 eprintln!(
454 "Please remove extra fields in the category header of the {fname} category to fix this error."
455 );
456 eprintln!(
457 "Fields are created by semicolons, so remove any extra semicolon characters after the description field."
458 );
459 std::process::exit(19);
460 }
461 if tokens.len() == 1 {
462 let home = tokens.get(0).unwrap().trim();
464 Configurations::check_header_bookmark(home, fname);
465 } else if tokens.len() == 2 {
466 let home = tokens.get(0).unwrap().trim();
467 let token = tokens.get(1).unwrap().trim();
468 Configurations::check_header_bookmark(home, fname);
469 if !token.is_empty() {
470 Configurations::check_header_key(&token, fname);
471 }
472 if home.is_empty() && token.is_empty() {
473 eprintln!("Error: you cannot have an empty category header.");
474 eprintln!(
475 "This category header of the {fname} category has got two optional fields, but both of them are empty."
476 );
477 eprintln!(
478 "If you do not like a category header, you can just leave it out of the bookmark file altogether."
479 );
480 std::process::exit(19);
481 }
482 } else {
483 let bookmark = tokens.get(0).unwrap().trim();
484 let key = tokens.get(1).unwrap().trim();
485 let comment = tokens.get(2).unwrap().trim();
486 if bookmark.is_empty() && key.is_empty() && comment.is_empty() {
487 eprintln!("Error: empty category header in the category {fname}!");
488 eprintln!(
489 "You must not have an empty category header. If you do not like a category header altogether, just do not add a line that starts with '#!' in the bookmark file."
490 );
491 eprintln!(
492 "Once you add a category header, you have to add either a bookmark field, a shortcut key, a description or all of these fields, but not to leave them out altogether."
493 );
494 std::process::exit(19);
495 }
496 if !bookmark.is_empty() {
497 Configurations::check_header_bookmark(bookmark, fname);
498 }
499 if !key.is_empty() {
500 Configurations::check_header_key(key, fname);
501 }
502 }
503 }
504
505 fn validate_category(&self) {
506 if self.name.contains(" ") {
507 eprintln!(
508 "Error: A Category name must not have embedded spaces in it. Use dashes or underscores to separate words"
509 );
510 std::process::exit(27);
511 }
512 if self.name.contains('.') {
513 eprintln!("Error: Hlg only supports one level of category at this time.");
514 eprintln!(
515 "Remove all period characters and replace them with either dashes or underscores to separate words."
516 );
517 std::process::exit(27);
518 }
519 }
520
521 fn preferred_category(self) -> Category {
522 self.validate_category();
523 self.check_name_availability();
524 Category::new(Scope::Local(self.name.clone()))
525 }
526}
527
528struct LaunchPad {
529 category: Category,
530 pad_type: PadType,
531 bookmark: Bookmark,
532}
533
534impl LaunchPad {
535 fn new(target: Target) -> Self {
536 let pad_type = target.get_pad();
537 let category = target.scope.apply();
538 let bookmark = Bookmark::default();
539 Self {
540 pad_type,
541 category,
542 bookmark,
543 }
544 }
545
546 fn get_starred(&mut self) {
547 let mut starred_bookmarks = Vec::new();
548 for chosen in self.category.entries() {
549 if chosen.info.is_favorite {
550 starred_bookmarks.push(chosen.clone());
551 }
552 }
553 let category = if self.category.name == "<global>" {
554 "the global scope".to_string()
555 } else {
556 format!("the {} category", self.category.name)
557 };
558 if starred_bookmarks.len() > 0 {
559 let autoselect = Configurations::get_setting()
560 .autoselect
561 .unwrap_or(Configurations::default_settings().autoselect.unwrap());
562 if starred_bookmarks.len() == 1 && autoselect {
563 self.bookmark = starred_bookmarks.get(0).unwrap().clone();
564 } else {
565 println!("Starred bookmarks in {category}");
566 println!("* * * * * * *");
567 println!("[Menu position]\tBookmark name:\t\tDescription");
568 starred_bookmarks.sort();
569 starred_bookmarks.dedup();
570 self.bookmark = Self::make_choice(&starred_bookmarks);
571 }
572 } else {
573 eprintln!("Error: You have no starred bookmarks in {category}.");
574 eprintln!(
575 "To star a bookmark, just add a trailing asterisk or star symbol, '*' on the bookmark name."
576 );
577 std::process::exit(30);
578 }
579 }
580
581 fn which_key(&self, key: char) -> Option<Bookmark> {
582 let mut shortcut = None;
583 for fast_one in self.category.entries() {
584 if fast_one.info.key.is_some() {
585 if fast_one.info.get_key() == key {
587 shortcut = Some(fast_one.clone());
588 break;
589 }
590 }
591 }
592 shortcut
593 }
594
595 fn get_by_key(&mut self, key: char) {
596 self.bookmark = if let Some(thing) = self.which_key(key) {
597 thing
598 } else {
599 let scope = self.category.name.clone();
600 let scope = if scope == "<global>" {
601 format!("the global scope")
602 } else {
603 format!("the {scope} category")
604 };
605 eprintln!(
606 "Error: this shortcut key '{key}' is not yet assigned for any bookmark in {scope}."
607 );
608 eprintln!(
609 "Please check the case of your shortcuts when invoking hlg, as the case used is the same you used during saving the appropriate bookmark."
610 );
611 std::process::exit(35);
612 };
613 }
614
615 fn by_alias(&mut self, phrase: String) {
616 let key = phrase.to_lowercase();
617 let mut found = Vec::new();
618 for candidate in &self.category.entries() {
619 if candidate.info.tags.is_some() {
620 for marble in candidate.info.get_tags() {
621 if marble.to_lowercase() == key {
622 found.push(candidate.clone());
623 }
624 }
625 }
626 }
627 if found.len() > 0 {
628 let autoselect = Configurations::get_setting()
629 .autoselect
630 .unwrap_or(Configurations::default_settings().autoselect.unwrap());
631 if found.len() == 1 && autoselect {
632 self.bookmark = found.get(0).unwrap().clone();
633 } else {
634 let number = if found.len() == 1 {
635 "is only one"
636 } else {
637 "are more than one"
638 };
639 println!("There {number} bookmark with the alias '{phrase}'");
640 println!("Please select one from the list below:");
641 println!("[Choice]\tBookmark\t\tDescription");
642 found.sort();
643 found.dedup();
644 self.bookmark = Self::make_choice(&found);
645 }
646 } else {
647 let cat = if self.category.name == "<global>" {
648 "global scope".to_string()
649 } else {
650 format!("{} category", self.category.name)
651 };
652 eprintln!("Sorry, no bookmark with the alias {phrase} was found in the {cat}.");
653 eprintln!(
654 "Just add an alias to a bookmark by enclosing its name inside square brackets on the line before the bookmark entry"
655 );
656 std::process::exit(34);
657 }
658 }
659
660 fn select(&mut self, selector: Selector) {
661 match selector {
662 Selector::Starred => self.get_starred(),
663 Selector::Aliases(alias) => self.by_alias(alias),
664 }
665 }
666
667 fn web_search(&mut self, service: SearchService) {
668 let key;
669 let address: String;
670 match service {
671 SearchService::Engine(ref engine, ref search_phrase) => {
672 address = format!("search?q={search_phrase}");
673 key = engine;
674 }
675 SearchService::Wiki(ref site, ref topic) => {
676 address = format!("{topic}");
677 key = site;
678 }
679 }
680 let binding = self.category.entries();
681 let provisional_bookmark = if key.len() == 1 {
682 self.which_key(key.chars().nth(0).unwrap())
683 } else {
684 binding.get(key.as_str()).cloned()
685 };
686 let bookmark = if provisional_bookmark.is_some() {
687 let chosen = provisional_bookmark.unwrap();
688 if !chosen.info.is_search && service.is_search() {
689 eprintln!(
690 "Error: while this bookmark {chosen} is a valid entry in the database, it has no search flag set on it."
691 );
692 eprintln!(
693 "To set a search flag, just add a trailing question mark on {chosen} like this: '{chosen}?'"
694 );
695 std::process::exit(65);
696 }
697 if !chosen.info.is_wiki && service.is_wiki() {
698 eprintln!("Error: this bookmark '{chosen}' has no wiki flag set on it.");
699 eprintln!(
700 "If {chosen} points to a wiki site, flag it by adding a trailing plus sign like this: '{chosen}+'"
701 );
702 std::process::exit(65);
703 }
704 let location = chosen.location.clone();
705 let path = LocationType::Link(format!("{}/{address}", location.get()));
706 let location = Location { path, ..location };
707 Bookmark { location, ..chosen }
708 } else {
709 eprintln!("Error: the {key} service bookmark could not be found in database.");
710 eprintln!(
711 "If you are sure that it exists, consider placing an appropriate search or wiki flag on {key} and try again."
712 );
713 std::process::exit(65);
714 };
715 self.bookmark = bookmark;
716 }
717
718 fn get_by_name(&mut self, name: String) {
719 let start = name.chars().nth(0).unwrap();
720 let end = name.chars().nth(name.len() - 1).unwrap();
721 if !start.is_alphanumeric() || !end.is_alphanumeric() {
722 eprintln!(
723 "Error: there is an error with this name, '{name}' as it either starts or ends with a non-alphanumeric character."
724 );
725 eprintln!(
726 "Symbols used to flag a bookmark name in the database do not form part of the bookmark name. So try to remove the invalid characters and try again."
727 );
728 std::process::exit(34);
729 }
730 let bookmark = if let Some(chawada) = self.category.entries().get(name.as_str()) {
731 chawada.clone()
732 } else if name == "CATEGORY_HOME" {
733 self.category
734 .entries()
735 .get(
736 Configurations::default_settings()
737 .special_pages
738 .unwrap()
739 .home
740 .unwrap()
741 .as_str(),
742 )
743 .unwrap()
744 .clone()
745 } else {
746 let ref_category = if self.category.name == "<global>" {
747 "global scope".to_string()
748 } else {
749 format!("{} category", self.category.name)
750 };
751 eprintln!(
753 "Error, {name} bookmark could not be found in the {ref_category}. Could it be that {name} is an alias, or in another category, or is meant to be used as a search phrase?"
754 );
755 eprintln!("If this is an alias, add a leading forward slash to {name}");
756 eprintln!(
757 "Otherwise, if you meant it to be a search phrase, then you have to add a leading question mark for a search engine like this, '?{name}', or as a topic for a wiki site like this '+{name}'"
758 );
759 eprintln!(
760 "You can only leave out the leading symbol when you are passing more than one argument to hlg. A single argument you passed in is always taken for a bookmark name."
761 );
762 eprintln!("Bye!");
763 std::process::exit(35);
764 };
765 self.bookmark = bookmark;
766 }
767
768 fn start(&mut self) {
769 match &self.pad_type {
770 PadType::ByKey(letter) => self.get_by_key(*letter),
771 PadType::ByName(bkname) => self.get_by_name(bkname.clone()),
772 PadType::BySelector(selector) => self.select(selector.clone()),
773 PadType::BySearch(service) => self.web_search(service.clone()),
774 }
775 self.run();
776 }
777
778 fn link_resolver(&self) -> Bookmark {
779 let info = self.bookmark.info.clone();
780 let link = LinkVariable::new(
781 self.bookmark.location.get(),
782 self.category.clone(),
783 self.bookmark.info.name.clone(),
784 )
785 .expand();
786 let path = LocationType::new(link);
787 let handler = if self.bookmark.location.handler.name.contains(":hapana:") {
788 Handler::new().name_from_protocol(HandlerClass::new(&path))
789 } else {
790 self.bookmark.location.handler.clone()
791 };
792 Bookmark::new(
793 info,
794 Location {
795 path,
796 handler,
797 has_variable: false,
798 },
799 )
800 }
801
802 fn run(&self) {
803 let bookmark = self.bookmark.clone();
805 let bookmark = if bookmark.location.has_variable {
806 self.link_resolver()
807 } else {
808 bookmark
809 };
810 if bookmark.info.is_deactivated {
811 eprintln!(
812 "Unfortunately, the {} bookmark is deactivated so cannot be launched. Please reactivate it by removing the trailing dash after its name and try again. Now exiting.",
813 self.bookmark
814 );
815 std::process::exit(36);
816 } else {
817 Configurations::record(format!("{}.{}", self.category.name, bookmark));
818 bookmark.launch();
819 }
820 }
821 fn make_choice(found: &Vec<Bookmark>) -> Bookmark {
822 for (number, bookmark) in found.iter().enumerate() {
823 println!(
824 "[{}]\t{}\t\t{}",
825 number + 1,
826 bookmark,
827 if bookmark.info.comment.is_some() {
828 bookmark.info.get_comment()
829 } else {
830 String::from("(No description)")
831 }
832 );
833 }
834 let number = input!("Your choice:");
835 let number: usize = number
836 .trim()
837 .parse()
838 .unwrap_or_else(|_| found.len() - found.len());
839 let number = if number < 1 {
840 println!("{number} below the minimum entry. Changing it to 1");
841 1
842 } else if number > found.len() {
843 println!(
844 "{number} out of bounds for these choices. Changing it to {}",
845 found.len()
846 );
847 found.len()
848 } else {
849 number
850 };
851 found.get(number - 1).unwrap().clone()
852 }
853}
854
855#[derive(Clone, Copy)]
856pub enum Statistics {
857 Keys,
858 Summary,
859}
860
861#[derive(Clone)]
862pub enum DoWhat {
863 Add(String),
864 Remove(String),
865 Rename(Vec<String>),
866}
867
868#[derive(Clone)]
869enum TargetAction<'a> {
870 Activity(DoWhat),
871 File(&'a str),
872}
873
874#[derive(Clone)]
875pub struct Editor<'a> {
876 manager: CategoryManager,
877 target_action: TargetAction<'a>,
878}
879
880impl<'a> Editor<'a> {
881 fn new(target_action: TargetAction<'a>) -> Self {
882 let manager = CategoryManager::new();
883 Self {
884 manager,
885 target_action,
886 }
887 }
888
889 fn with_scope(mut self, scope: Scope) -> Self {
890 let scope = if scope.get_name() == "<global>" {
891 Scope::Local(CategoryManager::new().name)
892 } else {
893 scope
894 };
895 self.manager = scope.get_manager();
896 self
897 }
898 fn edit(&self) {
899 match &self.target_action {
900 TargetAction::Activity(action) => self.modify_category(action.clone()),
901 TargetAction::File(filename) => self
902 .edit_config_file(filename)
903 .expect("Could not edit configuration file"),
904 }
905 }
906
907 fn modify_category(&self, action: DoWhat) {
908 match action {
909 DoWhat::Add(category_name) => self
910 .manager
911 .create_new(category_name)
912 .expect("Could not create new category"),
913 DoWhat::Remove(fname) => self
914 .manager
915 .delete_category(&fname)
916 .expect("Could not delete category from database"),
917 DoWhat::Rename(names) => {
918 let old_name = names.get(0).unwrap();
919 let new_name = names.get(1).unwrap();
920 self.manager
921 .rename_category(old_name, new_name)
922 .expect("Could not rename category");
923 }
924 }
925 }
926
927 fn edit_config_file(&self, config_file: &'a str) -> std::io::Result<()> {
928 let mut needs_refresh = false;
929 let mut category_name = String::new();
930 let filename = match config_file {
931 "bookmarks" => {
932 let chosen_category = if Cli::parse().category.is_some() {
933 Cli::parse().category.unwrap()
934 } else {
935 self.manager.name.clone()
936 };
937 category_name = chosen_category.clone();
938 let filename = format!("{}/{chosen_category}", self.manager.root);
939 if !Configurations::check_path(&filename) {
940 eprintln!(
941 "Error: the category which you want to edit, '{chosen_category}', cannot be found in database.",
942 );
943 eprintln!(
944 "Consider creating it with 'hlg --new-category {chosen_category}' before performing any editing operations on it."
945 );
946 std::process::exit(24);
947 }
948 needs_refresh = true;
949 filename
950 }
951 "config" => Configurations::config_file(),
952 _ => {
953 eprintln!("No file with that name exists here");
954 std::process::exit(1);
955 }
956 };
957 let template = fs::read_to_string(&filename)?;
958 let edited = edit(template)?;
959 if needs_refresh {
960 let lines: Vec<&str> = edited.lines().collect();
962 let header = lines.get(0).unwrap();
963 if header.starts_with("#!") {
964 CategoryManager::validate_header(&header[2..], &filename);
965 let fields: Vec<&str> = header.split(';').collect();
967 if fields.len() > 1 {
968 let key = &fields[1].trim();
969 if !key.is_empty()
970 && CategoryManager::get_name_by_key(key.chars().nth(0).unwrap()).is_some()
971 {
972 let key = key.chars().nth(0).unwrap();
973 let refcat = CategoryManager::get_name_by_key(key).unwrap();
974 if refcat != category_name {
975 eprintln!(
976 "Error: this key '{key}' you are trying to assign to the {category_name} category is already assigned to the {refcat} category."
977 );
978 eprintln!(
979 "No two categories in the database can use one shortcut key."
980 );
981 std::process::exit(19);
982 }
983 }
984 }
985 }
986 let save_with_shortcuts = Configurations::get_setting().save_with_shortcuts.unwrap_or(
987 Configurations::default_settings()
988 .save_with_shortcuts
989 .unwrap(),
990 );
991 let edited = if save_with_shortcuts {
992 let mut bookmarks = String::new();
993 for line in lines {
994 let newline = if line.contains("<http") || line.contains("<file:") {
995 Self::use_link_shortcuts(line)
996 } else {
997 line.to_string()
998 };
999 bookmarks = format!("{bookmarks}{newline}\n");
1000 }
1001 bookmarks
1002 } else {
1003 edited
1004 };
1005 fs::write(&filename, edited)?;
1006 CategoryManager::refresh_category_headers();
1007 } else {
1008 fs::write(&filename, edited)?;
1009 }
1010 Ok(())
1011 }
1012
1013 fn use_link_shortcuts(line: &str) -> String {
1014 line.replace("www.", "=")
1015 .replace("http://localhost:", "@#")
1016 .replace("http://", "@!")
1017 .replace("https://", "@")
1018 .replace("file://", "@+")
1019 }
1020}
1021
1022pub enum Action<'a> {
1023 Launch(Target),
1024 Listing(Statistics),
1025 Edit(Editor<'a>),
1026}
1027
1028impl Action<'_> {
1029 pub fn new() -> Self {
1030 Self::check_program_configs();
1031 let choice = Cli::parse();
1032 if choice.edit
1033 || choice.config
1034 || choice.rename.is_some()
1035 || choice.delete.is_some()
1036 || choice.new.is_some()
1037 {
1038 let target = {
1039 if choice.edit {
1040 TargetAction::File("bookmarks")
1041 } else if choice.config {
1042 TargetAction::File("config")
1043 } else {
1044 let activity = if choice.delete.is_some() {
1045 DoWhat::Remove(choice.delete.unwrap())
1046 } else if choice.new.is_some() {
1047 DoWhat::Add(choice.new.unwrap().clone())
1048 } else {
1049 DoWhat::Rename(choice.rename.unwrap().clone())
1050 };
1051 TargetAction::Activity(activity)
1052 }
1053 };
1054 let editor = Editor::new(target);
1055 Self::Edit(editor)
1056 } else if choice.keycategory || choice.keyglobal || choice.summary {
1057 let lister = if choice.keycategory || choice.keyglobal {
1058 Statistics::Keys
1059 } else {
1060 Statistics::Summary
1061 };
1062 Self::Listing(lister)
1063 } else {
1064 Self::Launch(Target::new(choice))
1065 }
1066 }
1067
1068 fn check_program_configs() {
1070 if !Configurations::check_path(&Configurations::get_app_home()) {
1071 Configurations::initialize_defaults().expect(
1072 "Could not create program configuration files. Please check file permissions",
1073 );
1074 }
1075 }
1076
1077 pub fn on(&self) {
1078 let mut scope = Scope::Local(
1079 Configurations::get_setting()
1080 .default_category
1081 .unwrap_or(Configurations::default_settings().default_category.unwrap()),
1082 );
1083 match self {
1084 Self::Edit(editor) => editor.clone().with_scope(scope).edit(),
1085 Self::Launch(target) => LaunchPad::new(target.clone()).start(),
1086 Self::Listing(lister) => {
1087 scope = if Cli::parse().keyglobal {
1088 Scope::Global
1089 } else {
1090 scope
1091 };
1092 scope.get_stats(*lister);
1093 }
1094 }
1095 }
1096}
1097
1098#[derive(Clone)]
1099pub enum Scope {
1100 Local(String),
1101 Global,
1102}
1103
1104impl Scope {
1105 fn get_manager(&self) -> CategoryManager {
1106 if self.is_local() {
1107 CategoryManager::new().by_name(self.get_name().clone())
1108 } else {
1109 CategoryManager::new()
1110 }
1111 }
1112
1113 pub fn get_name(&self) -> String {
1114 match self {
1115 Self::Local(category_name) => category_name.to_string(),
1116 _ => "<global>".to_string(),
1117 }
1118 }
1119
1120 pub fn is_local(&self) -> bool {
1121 match self {
1122 Self::Global => false,
1123 _ => true,
1124 }
1125 }
1126
1127 fn apply(&self) -> Category {
1128 let manager = CategoryManager::new();
1129 match self {
1130 Self::Global => manager.set_global(),
1131 Self::Local(category_name) => manager.by_name(category_name).preferred_category(),
1132 }
1133 }
1134
1135 fn list_keys(&self) {
1137 let category = self.apply();
1138 let mut keymap: Vec<Bookmark> = category
1139 .entries()
1140 .into_iter()
1141 .filter(|bookmark| bookmark.info.key.is_some())
1142 .collect();
1143 let display_text = match self {
1144 Self::Global => "global scope".to_string(),
1145 Self::Local(name) => "local category ".to_string() + name,
1146 };
1147 if keymap.len() > 0 {
1148 println!("Key Listing for the {display_text}");
1149 println!("* * * * * *");
1150 println!(
1151 "There is a total of {} bookmarks in the scope, of which {} of them have got shortcuts.",
1152 category.entries().len(),
1153 keymap.len()
1154 );
1155 println!("These are:");
1156 println!("Key:\tBookmark");
1157 keymap.sort_by(|a, b| {
1158 a.info
1159 .get_key()
1160 .to_lowercase()
1161 .cmp(b.info.get_key().to_lowercase())
1162 });
1163 for bookmark in keymap {
1164 println!("{}:\t{}", bookmark.info.get_key(), bookmark);
1165 }
1166 } else {
1167 println!(
1168 "Hi, seems you currently have no {display_text} shortcut keys assigned to your bookmarks."
1169 );
1170 println!(
1171 "Assign shortcut keys by either placing an underscore before the bookmark name or else append a pair of parentheses to the bookmark name and place the desired shortcut key inside the parentheses."
1172 );
1173 println!("Bye!");
1174 }
1175 }
1176
1177 fn summary(&self) {
1178 if Configurations::check_path(&Configurations::config_file()) {
1179 println!("hlg Special Bookmark Summary:");
1180 println!("* * * * * * * *");
1181 println!(
1182 "1. Your default category is currently set to '{}'",
1183 Configurations::get_setting()
1184 .default_category
1185 .unwrap_or(Configurations::default_settings().default_category.unwrap())
1186 );
1187 let home = Configurations::get_homepage();
1188 let home = if home.contains(".") {
1189 let (category, bookmark) = home.split_at(home.find('.').unwrap());
1190 let category = if category.is_empty() {
1191 "default"
1192 } else {
1193 category
1194 };
1195 let bookmark = if bookmark.is_empty() {
1196 "category home"
1197 } else {
1198 bookmark
1199 };
1200 format!(
1201 "the '{}' bookmark which is in the {category} category",
1202 bookmark.strip_prefix('.').unwrap()
1203 )
1204 } else {
1205 format!(" the {home} bookmark in the global scope")
1206 };
1207 println!("2. Your main home bookmark is set to {home}");
1208
1209 let last_visited = format!("{}/last", Configurations::get_records());
1210 if Configurations::check_path(&last_visited) {
1211 let last_bookmark = Configurations::get_last_visit();
1212 let position = last_bookmark.find('.').unwrap();
1213 let (category, name) = last_bookmark.split_at(position);
1214 let category = if category == "<global>" {
1215 "the global scope".to_string()
1216 } else {
1217 format!("the {category} category")
1218 };
1219 println!(
1220 "3. Your last visited bookmark is '{}' which is in {category}",
1221 name.strip_prefix('.').unwrap()
1222 );
1223 } else {
1224 println!(
1225 "3. Currently we have no record of your last visited bookmak. Perhaps this is the first time running hlg on this computer."
1226 );
1227 println!(
1228 "Run this program after making some visits to get the record of your last visited bookmark."
1229 );
1230 }
1231 } else {
1232 println!(
1233 "Hi, your configuration files are not yet set. Could this be your first run of this program on this computer?"
1234 );
1235 println!(
1236 "hlg can only provide a quick summary of your activities based on the setting in your configuration file."
1237 );
1238 println!("Bye.");
1239 }
1240 }
1241
1242 fn get_stats(&self, lister: Statistics) {
1243 match lister {
1244 Statistics::Keys => self.list_keys(),
1245 Statistics::Summary => self.summary(),
1246 }
1247 }
1248
1249 pub fn extend_with(category: &str) -> HashSet<Bookmark> {
1251 match category {
1252 "<global>" => CategoryManager::new().set_global().entries(),
1253 _ => CategoryManager::new()
1254 .by_name(category)
1255 .preferred_category()
1256 .entries(),
1257 }
1258 }
1259}
1260
1261#[derive(Clone)]
1262enum Selector {
1263 Aliases(String),
1264 Starred,
1265}
1266
1267#[derive(Clone)]
1268enum SearchService {
1269 Engine(String, String),
1270 Wiki(String, String),
1271}
1272
1273impl SearchService {
1274 fn new(candidate: &str) -> Self {
1275 let (engine, phrase) = candidate.split_at(candidate.find(')').unwrap());
1276 let engine = engine.strip_prefix('(').unwrap().to_string();
1277 let phrase = phrase.strip_prefix(')').unwrap();
1278 if phrase.starts_with('+') {
1279 let topic = phrase.strip_prefix('+').unwrap();
1280 let topic = topic.replace(" ", "_");
1281 SearchService::Wiki(engine, topic)
1282 } else {
1283 let keywords = phrase.strip_prefix('?').unwrap();
1284 let keywords = keywords.replace(" ", "+");
1285 SearchService::Engine(engine, keywords)
1286 }
1287 }
1288
1289 fn is_wiki(&self) -> bool {
1290 match self {
1291 Self::Wiki(_, _) => true,
1292 _ => false,
1293 }
1294 }
1295
1296 fn is_search(&self) -> bool {
1297 match self {
1298 Self::Engine(_, _) => true,
1299 _ => false,
1300 }
1301 }
1302}
1303
1304#[derive(Clone)]
1305pub struct Target {
1306 name: String,
1307 scope: Scope,
1308}
1309
1310impl Target {
1311 fn new(choice: Cli) -> Self {
1312 let candidate = if choice.category.is_none() && choice.name.is_none() {
1313 let home = Configurations::get_homepage();
1314 if !home.contains('.') {
1315 format!("<global>.{home}")
1316 } else {
1317 home
1318 }
1319 } else if choice.name.is_none() && choice.category.is_some() {
1320 format!("{}.CATEGORY_HOME", choice.category.unwrap())
1321 } else if choice.name.is_some() && choice.category.is_none() {
1322 let values = choice.name.clone().unwrap();
1323 let tg = values.get(0).unwrap();
1324 if (tg.starts_with(':') && values.len() > 1)
1325 || tg.starts_with(":?")
1326 || tg.starts_with(":+")
1327 {
1328 eprintln!(
1329 "Error: Already searches are done in the global scope. So there is no need to qualify an expression with the global scope indicator."
1330 );
1331 eprintln!("Simply remove the leading colon to fix this error.");
1332 std::process::exit(54);
1333 }
1334 if tg.starts_with('=') && values.len() > 1 {
1335 eprintln!(
1336 "Error: The equality trigger is only used with category and bookmark names in the database and cannot be used in search phrases."
1337 );
1338 std::process::exit(52);
1339 }
1340 let key = tg.chars().nth(0).unwrap();
1341 if tg.len() == 1 && values.len() == 1 && !key.is_alphanumeric() {
1342 let key = tg.chars().nth(0).unwrap();
1343 if key == '+' {
1344 format!(
1345 "{}./*",
1346 Configurations::get_setting().default_category.unwrap_or(
1347 Configurations::default_settings().default_category.unwrap()
1348 )
1349 )
1350 } else {
1351 Self::special_keys(key)
1352 }
1353 } else if (tg.starts_with('?') || tg.starts_with('+') || tg.ends_with(':'))
1354 || values.len() > 1
1355 {
1356 Self::process_search(choice)
1357 } else if tg.starts_with('@') {
1358 Self::process_category_shortcuts(tg)
1359 } else if tg.starts_with('=') {
1360 Self::equality_argument_processor(tg)
1361 } else {
1362 Self::process_bookmark(tg)
1363 }
1364 } else {
1365 let values = choice.name.unwrap();
1367 let word = values.get(0).unwrap();
1368 if word.starts_with('?')
1370 || word.starts_with('+')
1371 || word.starts_with(':')
1372 || word.starts_with('@')
1373 {
1374 eprintln!(
1375 "Error: you cannot set the '--category' option while the global scope switch is oactive"
1376 );
1377 eprintln!(
1378 "Searches and the colon symbol automatically switch to the global scope. So remove these if you want to use the category option."
1379 );
1380 eprintln!(
1381 "Additionally, the at magic symbol is a complete instruction on its own which renders the category option unnecessary."
1382 );
1383 std::process::exit(28);
1384 }
1385 if values.len() > 1 {
1386 eprintln!(
1387 "Error: Automatic searching for phrases only happens in the global scope. As you have selected the category '{}', the search facility is no longer available.",
1388 choice.category.unwrap()
1389 );
1390 eprintln!(
1391 "Any argument passed to hlg should either be a bookmark name or an alias. So remove the extra arguments or the category option to fix this error."
1392 );
1393 std::process::exit(49);
1394 }
1395 if word.contains('.') {
1396 eprintln!(
1397 "Error: category is set in two different places: the '--category' option has the argument of {} and the prefix to the {word} argument both set the category to be used.",
1398 choice.category.unwrap()
1399 );
1400 eprintln!("To fix this error, just set the category in one option and try again.");
1401 std::process::exit(28);
1402 }
1403 let text = if word == "+" {
1404 "/*".to_string()
1405 } else {
1406 word.to_string()
1407 };
1408 let category = choice.category.unwrap();
1409 Self::check_spurious_qualifications(&category);
1410 format!("{category}.{text}")
1411 };
1412 let position = candidate.find('.').unwrap();
1413 let (category, name) = candidate.split_at(position);
1414 let scope = if category == "<global>" {
1415 Scope::Global
1416 } else {
1417 Scope::Local(category.to_string())
1418 };
1419 let name = format!("{}", &name[1..]);
1420 Self { scope, name }
1421 }
1422
1423 fn process_search(choice: Cli) -> String {
1424 let phrases = choice.name.unwrap().clone();
1425 let length = phrases.len();
1426 let bookmark_candidate = phrases.get(0).unwrap();
1427 let mut first_word_is_service = false;
1428 let search_phrase = if (bookmark_candidate.starts_with('?')
1429 || bookmark_candidate.starts_with('+'))
1430 && length == 1
1431 {
1432 bookmark_candidate.clone()
1433 } else if (bookmark_candidate.starts_with('?') || bookmark_candidate.starts_with('+'))
1434 && length > 1
1435 {
1436 phrases.join(" ")
1437 } else if length > 1 {
1438 let terms = &phrases[1..];
1439 let guide = terms.get(0).unwrap();
1440 first_word_is_service = bookmark_candidate.ends_with(':');
1441 if first_word_is_service {
1442 if guide.starts_with('?') || guide.starts_with('+') {
1443 terms.join(" ")
1444 } else {
1445 format!("?{}", terms.join(" "))
1446 }
1447 } else {
1448 format!("?{}", phrases.join(" "))
1449 }
1450 } else {
1451 format!("?{}", phrases.join(" "))
1452 };
1453 let service = if first_word_is_service {
1454 format!("{}", bookmark_candidate.strip_suffix(':').unwrap())
1455 } else if search_phrase.starts_with('?') {
1456 Self::get_default_service("search")
1457 } else {
1458 Self::get_default_service("wiki")
1459 };
1460 format!("<global>.({service}){search_phrase}")
1461 }
1462
1463 fn check_spurious_qualifications(category: &String) {
1464 let default_category = Configurations::get_setting()
1465 .default_category
1466 .unwrap_or(Configurations::default_settings().default_category.unwrap());
1467 if default_category == *category {
1468 println!("WARNING: spurious bookmark name qualification.");
1469 println!(
1470 "Your default category is already set to {category}, yet you are invoking its bookmark with the category name supplied."
1471 );
1472 println!(
1473 "Just call bookmarks in the default category without passing in the category name, whether as a shortcut or as a fully-qualified bookmark name to silence this warning."
1474 );
1475 }
1476 }
1477
1478 fn process_category_shortcuts(token: &String) -> String {
1479 let key = token.chars().nth(1).unwrap();
1480 let target = &token[2..];
1481 let possible_category = CategoryManager::get_name_by_key(key);
1482 if possible_category.is_none() {
1483 eprintln!("Error: this shortcut key, '{key}' is not assigned to any category.");
1484 eprintln!(
1485 "If you want to assign it, create a category header which uses semicolon field separators, then in the second field add the key like this: ';{key};' followed by any description of the category."
1486 );
1487 std::process::exit(37);
1488 }
1489 let category = possible_category.unwrap();
1490 if target.contains('.') {
1491 eprintln!("Error: Invalid syntax in calling category via a shortcut.");
1492 eprintln!(
1493 "A period is used to qualify a bookmark name so that it references its category. However, with a shortcut key, the period becomes unnecessary as you are fetching an identified category with that shortcut."
1494 );
1495 eprintln!(
1496 "Just pass in the name of your bookmark or the bookmark shortcut to activate a target."
1497 );
1498 std::process::exit(28);
1499 }
1500 let target = if target.is_empty() {
1501 "CATEGORY_HOME"
1502 } else if target == "+" {
1503 "/*"
1504 } else {
1505 target
1506 };
1507 Self::check_spurious_qualifications(&category);
1508 format!("{category}.{target}")
1509 }
1510
1511 fn process_bookmark(target: &String) -> String {
1512 let category = Configurations::get_setting()
1513 .default_category
1514 .unwrap_or(Configurations::default_settings().default_category.unwrap());
1515 if target.starts_with(':') {
1516 format!("<global>.{}", target.strip_prefix(':').unwrap())
1517 } else if target.starts_with(".") {
1518 format!("{category}{target}")
1519 } else if target.ends_with(".") {
1520 Self::check_spurious_qualifications(&target.strip_suffix('.').unwrap().to_string());
1521 format!("{target}CATEGORY_HOME")
1522 } else if target.ends_with(".+") {
1523 Self::check_spurious_qualifications(&format!("{}", &target[..target.len() - 1]));
1524 format!("{}/*", &target[..target.len() - 1])
1525 } else if !target.contains(".") {
1526 format!("{category}.{target}")
1527 } else {
1528 let (cat, _) = target.split_at(target.find('.').unwrap());
1529 Self::check_spurious_qualifications(&cat.to_string());
1530 target.to_string()
1531 }
1532 }
1533
1534 fn get_default_service(service_name: &str) -> String {
1535 let pages = Configurations::get_setting().special_pages;
1536 if pages.is_none() {
1537 if service_name == "search" {
1538 return "HLG_SEARCH".to_string();
1539 } else if service_name == "wiki" {
1540 return "HLG_WIKI".to_string();
1541 } else {
1542 eprintln!(
1543 "Error: the 'special-pages' section is missing in your configuration file, so you do not have the '{service_name}' option entry set."
1544 );
1545 eprintln!(
1546 "Please create this section and add the 'search' option along with a special bookmark with the search flag, before running this option."
1547 );
1548 std::process::exit(63);
1549 }
1550 }
1551 let page = pages.unwrap();
1552 match service_name {
1553 "search" => {
1554 if page.search.is_some() {
1555 page.search.unwrap()
1556 } else {
1557 String::from("HLG_SEARCH")
1558 }
1559 }
1560 "wiki" => {
1561 if page.wiki.is_some() {
1562 page.wiki.unwrap()
1563 } else {
1564 String::from("HLG_WIKI")
1565 }
1566 }
1567 _ => {
1568 eprintln!("Error: search service not recognized.");
1569 std::process::exit(1);
1570 }
1571 }
1572 }
1573
1574 fn get_pad(&self) -> PadType {
1575 if self.name.len() == 1 {
1576 let char = self.name.chars().nth(0).unwrap();
1577 if !char.is_alphanumeric() {
1578 eprintln!(
1579 "Error: only use alphanumeric characters to launch bookmarks with shortcut keys."
1580 );
1581 eprintln!(
1582 "Only special bookmarks can be launched by symbols, of which {char} is not one of them."
1583 );
1584 std::process::exit(32);
1585 }
1586 PadType::ByKey(char)
1587 } else if self.name.starts_with('/') {
1588 let phrase = &self.name.strip_prefix('/').unwrap();
1589 let selector = if phrase == &"*" {
1590 Selector::Starred
1591 } else {
1592 Selector::Aliases(phrase.to_string())
1593 };
1594 PadType::BySelector(selector)
1595 } else if self.name.starts_with('(') {
1596 let service = SearchService::new(&self.name);
1597 PadType::BySearch(service)
1598 } else {
1599 PadType::ByName(self.name.clone())
1600 }
1601 }
1602
1603 fn special_keys(key: char) -> String {
1604 let category = Configurations::get_setting()
1605 .default_category
1606 .unwrap_or(Configurations::default_settings().default_category.unwrap());
1607 match key {
1608 '-' => Configurations::get_last_visit(),
1609 '.' => format!("{category}.CATEGORY_HOME"),
1610 '?' => format!("<global>.HLG_HELP"),
1611 _ => {
1612 eprintln!("Error: Unrecognized symbol to hlg.");
1613 eprintln!("This key, {key}, is not a special symbol for hlg.");
1614 std::process::exit(32);
1615 }
1616 }
1617 }
1618
1619 fn equality_argument_processor(candidate: &String) -> String {
1620 if candidate.contains('.') {
1621 eprintln!(
1622 "Error: An equality trigger is already a fully-qualified name with both the category and the bookmark name using the same name."
1623 );
1624 eprintln!(
1625 "It is not necessary to add another qualifier with the period as you did here."
1626 );
1627 std::process::exit(52);
1628 }
1629 let candidate = candidate.strip_prefix('=').unwrap();
1630 if candidate.len() == 1 {
1631 Self::process_category_shortcuts(&format!("@{candidate}{candidate}"))
1632 } else {
1633 Self::process_bookmark(&format!("{candidate}.{candidate}"))
1634 }
1635 }
1636}