use std::borrow::Borrow;
use std::cmp::Ordering;
use std::fmt;
use std::fmt::Display;
use std::fmt::Formatter;
use std::hash::Hash;
use std::hash::Hasher;
use std::os::unix::process::CommandExt;
use std::process::Command;
use crate::data_provider::Format;
use crate::environment::Configurations;
use crate::environment::LinkVariable;
#[derive(Debug, Clone)]
pub struct Bookmark {
pub info: Info,
pub location: Location,
}
impl Display for Bookmark {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.info.name)
}
}
impl Ord for Bookmark {
fn cmp(&self, other: &Self) -> Ordering {
self.info.name.cmp(&other.info.name)
}
}
impl PartialOrd for Bookmark {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq<Bookmark> for Bookmark {
fn eq(&self, other: &Bookmark) -> bool {
self.info.name == other.info.name
}
}
impl Eq for Bookmark {}
impl Hash for Bookmark {
fn hash<H: Hasher>(&self, hasher: &mut H) {
self.info.name.hash(hasher);
}
}
impl Borrow<str> for Bookmark {
fn borrow(&self) -> &str {
&self.info.name
}
}
impl Default for Bookmark {
fn default() -> Self {
let info = Info::new("HLG_HELP");
let help_file = "docs/guide.html";
let address = format!("@+{}/{help_file}", Configurations::get_app_home());
let path = LocationType::new(address);
let handler = Handler::new().name_from_protocol(HandlerClass::new(&path));
let location = Location {
path,
handler,
has_variable: false,
};
Self { info, location }
}
}
impl Bookmark {
pub fn new(info: Info, location: Location) -> Self {
Self { info, location }
}
pub fn launch(self) {
self.location.open();
}
}
enum TextDescriber {
Comment(String),
Tags(Vec<String>),
Both(String, Vec<String>),
}
impl TextDescriber {
fn new(text: &str) -> Self {
let candidate = text.trim();
if candidate.is_empty() {
eprintln!(
"Error: You cannot have empty hashmark lines in your bookmark file. Please add either a comment, some tags in square brackets, or both after a line starting with the # sign."
);
std::process::exit(19);
}
if candidate.starts_with('[') {
Self::Tags(Self::process_tags(candidate))
} else if !candidate.contains('[') {
Self::Comment(String::from(candidate))
} else {
let position = candidate.find('[').unwrap();
let (description, tag_finder) = candidate.split_at(position);
let list = Self::process_tags(tag_finder);
let comment = description.to_string();
Self::Both(comment, list)
}
}
fn process_tags(data: &str) -> Vec<String> {
let data = data.trim().to_lowercase();
if !data.ends_with(']') {
eprintln!("Error: unclosed tag block. Please fix this and try again.");
eprintln!(
"Remember that tags cannot be followed by a comment. So if this is the case, just place your comment before the tag list and run this program again."
);
std::process::exit(19);
}
let newtext = data.replace("[", "").replace("]", "");
let newtext = newtext.trim();
if newtext.is_empty() {
eprintln!(
"Error: empty tag block. Please make sure that you have tags inside square brackets or else just remove them to avoid this error."
);
std::process::exit(19);
}
newtext
.split_whitespace()
.map(|tag| String::from(tag))
.collect::<Vec<String>>()
}
}
#[derive(Debug, Clone)]
pub struct Info {
pub name: String,
pub key: Option<char>,
pub is_favorite: bool,
pub home_flag: bool,
pub is_deactivated: bool,
pub is_global: bool,
pub is_wiki: bool,
pub is_search: bool,
pub comment: Option<String>,
pub tags: Option<Vec<String>>,
}
impl Info {
pub fn new(mut text: &str) -> Self {
let mut comment = None;
let mut tags = None;
if text.contains("#") {
let tag_position = text.find('#').unwrap();
let (actual_text, extra_data) = text.split_at(tag_position);
text = actual_text;
let info = TextDescriber::new(extra_data.strip_prefix('#').unwrap());
match info {
TextDescriber::Comment(remark) => comment = Some(remark),
TextDescriber::Tags(bag) => tags = Some(bag),
TextDescriber::Both(cmt, list) => {
comment = Some(cmt);
tags = Some(list);
}
}
}
let is_deactivated = text.ends_with("-");
if is_deactivated {
text = Self::trimmed_bookmark_name(text);
}
let home_flag = text == "CATEGORY_HOME";
let is_favorite = text.ends_with('*');
let is_search = text.ends_with('?');
let is_wiki = text.ends_with('+');
let is_global = text.ends_with(':') || is_wiki || is_search;
text = if is_favorite || is_global || is_wiki || is_search {
Self::trimmed_bookmark_name(text)
} else {
text
};
if is_deactivated && is_favorite {
eprintln!(
"Error: you cannot deactivate a starred bookmark. Please fix this error by removing either of these flags on the {text} bookmark."
);
std::process::exit(20);
}
let key = {
if text.starts_with('_') && text.contains('(') {
eprintln!(
"Error: this bookmark entry {text} is assigned keys in two different places."
);
eprintln!(
"First, is the underscore at the start of the name, and the second, is the key assignment block as indicated by brackets."
);
eprintln!(
"Please fix this error by just assigning to only one place and run this program again."
);
std::process::exit(19);
}
if text.contains('(') {
let pos = text.find('(').unwrap();
let (wortext, bracket) = text.split_at(pos);
if bracket.find(')').is_none() {
eprintln!(
"Error: The key assignment brackets for the bookmark '{wortext}' are not properly closed."
);
eprintln!("Please add the missing ')' to close them.");
std::process::exit(19);
}
if bracket.contains(" ") {
eprintln!(
"Error: The key assignment brackets for the bookmark '{wortext}' have embedded spaces in them."
);
eprintln!("Please strip all spaces inside the brackets to fix this error.");
std::process::exit(19);
}
if bracket.len() != 3 {
eprintln!(
"Error: The key assignment brackets for the bookmark '{wortext}' are malformed."
);
eprintln!(
"Either there are no characters inside or there are extra characters inside them."
);
eprintln!(
"Only place one character inside the brackets which you want to be the shortcut key for '{wortext}' to fix this error."
);
std::process::exit(19);
}
let chosen_char = Self::allocate_key(bracket);
if !chosen_char.is_alphanumeric() {
eprintln!("Error: Only alphanumeric characters can be used as shortcut keys.");
eprintln!(
"This bookmark, '{wortext}', is trying to allocate a {chosen_char} as its shortcut key."
);
eprintln!("Please allocate a valid character to fix this error.");
std::process::exit(19);
}
text = wortext;
Some(chosen_char)
} else if text.starts_with('_') {
let vara = Self::allocate_key(text);
text = text.strip_prefix('_').unwrap();
Some(vara)
} else {
None
}
};
let name = Self::name_check(text);
Self {
name,
home_flag,
key,
is_favorite,
is_deactivated,
is_global,
is_search,
is_wiki,
comment,
tags,
}
}
pub fn get_comment(&self) -> String {
self.comment.clone().unwrap()
}
pub fn get_tags(&self) -> Vec<String> {
self.tags.clone().unwrap()
}
fn name_check(working_name: &str) -> String {
if working_name.contains(" ") {
eprintln!("This bookmark name {working_name} has embedded spaces in it.");
eprintln!(
"Please remove them. Consider using dashes or underscores in place of spaces."
);
std::process::exit(19);
}
if working_name.len() == 1 {
eprintln!("Error: a bookmark name must be at least two characters long.");
eprintln!("A single character is reserved for shortcut key assignments.");
eprintln!(
"Rename this bookmark, '{working_name}' to something longer than one character to fix this error."
);
std::process::exit(19);
}
if Self::contains_illegal_characters(working_name) {
eprintln!("Error: your bookmark '{working_name}' has invalid characters in it.");
eprintln!(
"You cannot have a colon, a forward slash, a star, a question mark, a plus sign or a period as part of a bookmark name as they are used by hlg to resolve bookmark names."
);
std::process::exit(19);
}
let first_char = working_name.chars().nth(0).unwrap();
let last_char = working_name.chars().nth(working_name.len() - 1).unwrap();
if !first_char.is_alphanumeric() || !last_char.is_alphanumeric() {
eprintln!(
"Error: Bookmark '{working_name}' does not start or end with an alphanumeric character. A bookmark name must only start and end with an alphanumeric character."
);
std::process::exit(19);
}
working_name.to_string()
}
fn contains_illegal_characters(str: &str) -> bool {
str.chars().any(|c| match c {
'/' | '.' | ':' | '?' | '+' | '*' => true,
_ => false,
})
}
fn allocate_key(letters: &str) -> char {
letters.chars().nth(1).unwrap()
}
pub fn get_key(&self) -> char {
self.key.unwrap()
}
fn trimmed_bookmark_name<'a>(rawname: &'a str) -> &'a str {
let character = rawname.chars().nth(rawname.len() - 1).unwrap();
rawname.strip_suffix(character).unwrap()
}
}
#[derive(Debug, Clone)]
pub struct Location {
pub path: LocationType,
pub handler: Handler,
pub has_variable: bool,
}
impl Location {
pub fn get(&self) -> String {
self.path.resolve()
}
pub fn is_a_command(&self) -> bool {
self.path.is_a_command()
}
fn open(&self) {
self.handler.clone().handle(&self.get());
}
pub fn uses_shortcut(&self) -> bool {
self.get().starts_with('@') && !self.get().contains("{")
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum LocationType {
Command(String, bool),
Link(String),
}
impl LocationType {
pub fn new(address: String) -> Self {
if address.starts_with(":") {
let is_script = address.chars().nth(1).unwrap() == '!';
let cmd = address.strip_prefix(':').unwrap();
if cmd.contains(" ") && !is_script {
eprintln!("Error: Only an inline command can have spaces.");
eprintln!(
"As '{cmd}' is meant to be a script file, consider replacing the spaces with dashes or underscores."
);
std::process::exit(19);
}
Self::Command(cmd.to_string(), is_script)
} else {
if address.contains(" ") {
eprintln!("Error: a link location cannot have embedded spaces in it.");
eprintln!(
"Please remove all spaces in this address '{address}' before running this program again."
);
std::process::exit(19);
}
let address = if address.starts_with('@') && !address.contains('{') {
LinkVariable::shortcut_processor(address.strip_prefix('@').unwrap())
} else {
address
};
Self::Link(address)
}
}
fn resolve(&self) -> String {
match self {
Self::Link(path) => {
let path = if path.starts_with("~/") || path.starts_with("$HOME") {
String::from(shellexpand::full(path).unwrap())
} else {
String::from(path)
};
path
}
Self::Command(script, is_inline) => {
if *is_inline {
format!("{}", &script[1..])
} else {
format!(
"{}/{}",
shellexpand::full(&Configurations::script_dir()).unwrap(),
script
)
}
}
}
}
fn is_a_command(&self) -> bool {
match self {
Self::Command(_, _) => true,
_ => false,
}
}
}
#[derive(Clone, PartialEq)]
pub enum HandlerClass {
Browser,
FileManager,
DataManager(String),
Shell,
}
impl HandlerClass {
pub fn new(location_type: &LocationType) -> Self {
match location_type {
LocationType::Command(_, _) => Self::Shell,
LocationType::Link(item) => {
if item.contains(":/") || item.starts_with("www.") {
Self::Browser
} else if item.contains(".") {
let pos = item.find('.').unwrap();
let (_, extension) = item.split_at(pos);
Self::DataManager(String::from(extension))
} else {
Self::FileManager
}
}
}
}
fn get_name(&self) -> String {
let handlers = Configurations::get_setting()
.handlers
.unwrap_or(Configurations::default_settings().handlers.unwrap());
match self {
Self::Browser => handlers.browser.unwrap_or(
Configurations::default_settings()
.handlers
.unwrap()
.browser
.unwrap(),
),
Self::FileManager => handlers.file_manager.unwrap_or(
Configurations::default_settings()
.handlers
.unwrap()
.file_manager
.unwrap(),
),
Self::Shell => handlers.command.unwrap_or(
Configurations::default_settings()
.handlers
.unwrap()
.command
.unwrap(),
),
Self::DataManager(fmt) => self.which_program(
Configurations::get_setting()
.formats
.unwrap_or(Configurations::default_settings().formats.unwrap())
.clone(),
fmt,
),
}
}
fn which_program(&self, handler_list: Vec<Format>, key: &String) -> String {
let key = format!("{}", key.strip_prefix('.').unwrap());
let mut program = String::new();
for handler in handler_list {
if handler.extensions.contains(&key) {
program = handler.name.clone();
break;
}
}
if program.is_empty() {
eprintln!(
"Error: the filetype '{key}' has no default handler provided for it in the configuration file."
);
eprintln!(
"To fix this error, please always supply a program in the bookmark entry's handler field for '{key}' or else enter a program in the configuration file in the 'formats' secction."
);
std::process::exit(30);
}
program
}
}
#[derive(Debug, Clone)]
pub struct Handler {
pub name: String,
pub option: String,
}
impl Display for Handler {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
impl Handler {
pub fn new() -> Self {
Self {
name: String::new(),
option: String::new(),
}
}
pub fn name(mut self, name: String) -> Self {
self.name = name;
self
}
pub fn name_from_protocol(mut self, handler_class: HandlerClass) -> Self {
self.name = handler_class.get_name();
self
}
pub fn options(mut self, argument: String) -> Self {
self.option = argument;
self
}
fn handle(self, location: &str) {
let mut cmd_options: Vec<&str> = self.option.split_whitespace().collect();
cmd_options.push(location);
match Command::new(self.name.as_str()).args(&cmd_options).exec() {
_ => {
eprintln!(
"{} not found in your PATH environment. If it is installed, please provide full path to its binary or else install it. You can also use the default handler for this location by simply deleting {} from the handler field.",
self.name, self.name
);
std::process::exit(10);
}
}
}
}