hlg/
lib.rs

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    /// The bookmark name to launch
22    name: Option<Vec<String>>,
23
24    /// The category to use for this session
25    #[arg(short, long, group = "cats")]
26    category: Option<String>,
27
28    ///  Get a quick summary of hlg special bookmark
29    #[arg(short = '0', long = "summary", default_value_t = false)]
30    summary: bool,
31
32    /// Edit the bookmark file
33    #[arg(short, long = "edit-bookmarks", default_value_t = false)]
34    edit: bool,
35
36    /// Edit the configuration file
37    #[arg(short = 'E', long = "edit-configuration", default_value_t = false)]
38    config: bool,
39
40    /// List global shortcut keys
41    #[arg(short = 'K', long = "list-global-keys", default_value_t = false)]
42    keyglobal: bool,
43
44    /// List category keys
45    #[arg(short, long = "list-category-keys", default_value_t = false)]
46    keycategory: bool,
47
48    /// Add new category
49    #[arg(short, long = "new-category", group = "cats", value_name = "CATEGORY")]
50    new: Option<String>,
51
52    /// Delete category from database
53    #[arg(
54        short,
55        long = "delete-category",
56        group = "cats",
57        value_name = "CATEGORY"
58    )]
59    delete: Option<String>,
60
61    /// Rename category
62    #[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            // The default category might have been manually deleted, so tell the user and quit:
97            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    //Set category to global:
120    fn set_global(&self) -> Category {
121        Category::new(Scope::Global)
122    }
123
124    // Set the category from the name of the passed in argument:
125    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        // Do some validation checks first:
206        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        // Protect safe categories:
212        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        // Done with old_name checks, what about the proposed name?
223        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        // Okay at this point, let us rename:
229        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    // Find the category with this assigned key:
333    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    /// Is this category tagged?
370    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            // We only have the bookmark field:
463            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                // Check to see if it corresponds to the one we got:
586                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            // We have no bookmark by that name:
752            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        // If this method runs, we are nearly done:
804        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            // Then this is a category file, so:
961            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                // Okay, the header is fine but what about key allocation?
966                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    // Set program defaults:
1069    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    /// List available shortcut keys:
1136    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    /// Extend bookmarks available in a scope by requesting:
1250    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            // Both are set:
1366            let values = choice.name.unwrap();
1367            let word = values.get(0).unwrap();
1368            // Do initial checks before processing:
1369            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}