worm_hole 1.3.0

CLI tool to easily jump between directories
use crate::{db::Database, error::WHResult, path::FilePath};
use clap::Parser;
use std::str::FromStr;
use string_builder::Builder as StringBuilder;

#[derive(Parser, Debug)]
pub struct Init {
	/// The shell to generate the init code for
	shell: Shell,

	// All these flags will be used to set the commands to write in the console to call worm_hole
	// Only the worm_hole and cd command has a default value
	/// The command to use to call worm_hole
	#[clap(long, default_value = "wh")]
	worm_hole: String,
	/// The command to use to change directory with worm_hole
	#[clap(long, default_value = "whcd")]
	cd: String,
	/// The command to use to add a new path to worm_hole
	#[clap(long)]
	add: Option<String>,
	/// The command to use to remove a path from worm_hole
	#[clap(long)]
	remove: Option<String>,
	/// The command to use to list all the paths in worm_hole
	#[clap(long)]
	list: Option<String>,
	/// The command to use to search for all paths containing a given pattern in worm_hole
	#[clap(long)]
	search: Option<String>,
	/// The command to use to query a path in worm_hole
	#[clap(long)]
	query: Option<String>,
	/// The command to use to edit a path in worm_hole
	#[clap(long)]
	edit: Option<String>,
	/// The command to use to rename an alias in worm_hole
	#[clap(long)]
	rename: Option<String>,
}

impl Init {
	pub fn run(&self, database: &Database, db_path: &str) -> WHResult<()> {
		database.init();
		println!("{}", self.shell.get_init_code(self, db_path));
		Ok(())
	}
}

#[derive(clap::ValueEnum, Debug, Clone)]
enum Shell {
	Bash,
	Fish,
	Zsh,
	Nu,
}
impl Shell {
	/// Add an alias to the string builder that will output the init code
	fn new_alias(&self, builder: &mut StringBuilder, alias: &str, command: &str) {
		builder.append(match self {
			Shell::Bash | Shell::Zsh | Shell::Fish => format!("alias {}='{}'\n", alias, command),
			Shell::Nu => format!("alias {} = {}\n", alias, command),
		});
	}

	/// Get the init code for the shell
	/// The init code can be eval to create aliases and functions required to use worm_hole
	/// Works for bash, zsh and fish
	pub fn get_init_code(&self, aliases: &Init, db_path: &str) -> String {
		let mut builder = StringBuilder::default();
		self.new_alias(
			&mut builder,
			&aliases.worm_hole,
			&format!("worm_hole --db-path {}", FilePath::from_str(db_path).unwrap().str()),
		);
		builder.append(self.get_cd_function(aliases.worm_hole.as_str()));
		self.new_alias(&mut builder, &aliases.cd, "__worm_hole_cd");
		if let Some(add) = &aliases.add {
			self.new_alias(&mut builder, add, &format!("{} add", aliases.worm_hole));
		}
		if let Some(remove) = &aliases.remove {
			self.new_alias(&mut builder, remove, &format!("{} remove", aliases.worm_hole));
		}
		if let Some(list) = &aliases.list {
			self.new_alias(&mut builder, list, &format!("{} list", aliases.worm_hole));
		}
		if let Some(search) = &aliases.search {
			self.new_alias(&mut builder, search, &format!("{} search", aliases.worm_hole));
		}
		if let Some(query) = &aliases.query {
			self.new_alias(&mut builder, query, &format!("{} query", aliases.worm_hole));
		}
		if let Some(edit) = &aliases.edit {
			self.new_alias(&mut builder, edit, &format!("{} edit", aliases.worm_hole));
		}
		if let Some(rename) = &aliases.rename {
			self.new_alias(&mut builder, rename, &format!("{} rename", aliases.worm_hole));
		}
		builder.append(match self {
			Shell::Bash | Shell::Zsh => format!("export WH_ALIAS=\"{}\"\n", aliases.worm_hole),
			Shell::Fish => format!("set -gx WH_ALIAS \"{}\"\n", aliases.worm_hole),
			Shell::Nu => format!("$env.WH_ALIAS = '{}'\n", aliases.worm_hole),
		});
		builder.string().unwrap()
	}

	/// Get the function to change directory with worm_hole
	/// The function will call worm_hole query to get the path of the alias and cd to it
	/// Works for bash, zsh and fish
	/// The function is named __worm_hole_cd
	fn get_cd_function(&self, worm_hole_command: &str) -> String {
		let mut builder = StringBuilder::default();
		match self {
			Shell::Bash | Shell::Zsh => {
				builder.append("__worm_hole_cd() {\n");
				builder.append("    if [ -z \"$1\" ]\n");
				builder.append("    then\n");
				builder.append("        cd $HOME\n");
				builder.append("    else\n");
				builder.append(format!(
					"        CD=$({} query \"$1\") && \\builtin cd \"$CD\"\n",
					worm_hole_command
				));
				builder.append("    fi\n");
				builder.append("}\n");
			}
			Shell::Fish => {
				builder.append("function __worm_hole_cd\n");
				builder.append("    if test -z $argv\n");
				builder.append("        cd $HOME\n");
				builder.append("    else\n");
				builder.append(format!("        set -l CD ({} query \"$argv\")\n", worm_hole_command));
				builder.append("        if test -n $CD\n");
				builder.append("            builtin cd \"$CD\"\n");
				builder.append("        end\n");
				builder.append("    end\n");
				builder.append("end\n");
			}
			Shell::Nu => {
				builder.append("def --env __worm_hole_cd [alias?] {\n");
				builder.append("    if $alias == null {\n");
				builder.append("        cd $env.HOME\n");
				builder.append("    } else {\n");
				builder.append(format!("        let CD = {} query $alias\n", worm_hole_command));
				builder.append("        if $CD != null {\n");
				builder.append("            cd $CD\n");
				builder.append("        }\n");
				builder.append("    }\n");
				builder.append("}\n");
			}
		};
		builder.string().unwrap()
	}
}

#[cfg(test)]
mod test {
	use super::*;

	fn has_bin_index(bin: usize, index: usize) -> bool {
		(bin >> index) & 1 == 1
	}
	#[rustfmt::skip]
	fn get_init_struct(shell: Shell, data: usize) -> Init {
		Init {
			shell,
			worm_hole: if has_bin_index(data, 0) { "custom_wh".to_string() } else { "wh".to_string() },
			cd: if has_bin_index(data, 1) { "custom_whcd".to_string() } else { "whcd".to_string() },
			add: if has_bin_index(data, 2) { Some("custom_add".to_string()) } else { None },
			remove: if has_bin_index(data, 3) { Some("custom_remove".to_string()) } else { None },
			list: if has_bin_index(data, 4) { Some("custom_list".to_string()) } else { None },
			search: if has_bin_index(data, 5) { Some("custom_search".to_string()) } else { None },
			query: if has_bin_index(data, 6) { Some("custom_query".to_string()) } else { None },
			edit: if has_bin_index(data, 7) { Some("custom_edit".to_string()) } else { None },
			rename: if has_bin_index(data, 8) { Some("custom_rename".to_string()) } else { None },
		}
	}

	const DB_PATH: &str = "/init";

	#[test]
	fn bash() {
		const SHELL: Shell = Shell::Bash;
		let normal = "alias wh='worm_hole --db-path ".to_owned() + DB_PATH + "'\n__worm_hole_cd() {\n    if [ -z \"$1\" ]\n    then\n        cd $HOME\n    else\n        CD=$(wh query \"$1\") && \\builtin cd \"$CD\"\n    fi\n}\nalias whcd='__worm_hole_cd'\n";
		vec![
			(0b000000000, normal.clone() + "export WH_ALIAS=\"wh\"\n"),
			(0b000000001, "alias custom_wh='worm_hole --db-path ".to_owned() + DB_PATH + "'\n__worm_hole_cd() {\n    if [ -z \"$1\" ]\n    then\n        cd $HOME\n    else\n        CD=$(custom_wh query \"$1\") && \\builtin cd \"$CD\"\n    fi\n}\nalias whcd='__worm_hole_cd'\nexport WH_ALIAS=\"custom_wh\"\n"),
			(0b000000010, "alias wh='worm_hole --db-path ".to_owned() + DB_PATH + "'\n__worm_hole_cd() {\n    if [ -z \"$1\" ]\n    then\n        cd $HOME\n    else\n        CD=$(wh query \"$1\") && \\builtin cd \"$CD\"\n    fi\n}\nalias custom_whcd='__worm_hole_cd'\nexport WH_ALIAS=\"wh\"\n"),
			(0b000000100, normal.clone() + "alias custom_add='wh add'\nexport WH_ALIAS=\"wh\"\n"),
			(0b000001000, normal.clone() + "alias custom_remove='wh remove'\nexport WH_ALIAS=\"wh\"\n"),
			(0b000010000, normal.clone() + "alias custom_list='wh list'\nexport WH_ALIAS=\"wh\"\n"),
			(0b000100000, normal.clone() + "alias custom_search='wh search'\nexport WH_ALIAS=\"wh\"\n"),
			(0b001000000, normal.clone() + "alias custom_query='wh query'\nexport WH_ALIAS=\"wh\"\n"),
			(0b010000000, normal.clone() + "alias custom_edit='wh edit'\nexport WH_ALIAS=\"wh\"\n"),
			(0b100000000, normal.clone() + "alias custom_rename='wh rename'\nexport WH_ALIAS=\"wh\"\n"),
			(0b111111111, "alias custom_wh='worm_hole --db-path ".to_owned() + DB_PATH + "'\n__worm_hole_cd() {\n    if [ -z \"$1\" ]\n    then\n        cd $HOME\n    else\n        CD=$(custom_wh query \"$1\") && \\builtin cd \"$CD\"\n    fi\n}\nalias custom_whcd='__worm_hole_cd'\nalias custom_add='custom_wh add'\nalias custom_remove='custom_wh remove'\nalias custom_list='custom_wh list'\nalias custom_search='custom_wh search'\nalias custom_query='custom_wh query'\nalias custom_edit='custom_wh edit'\nalias custom_rename='custom_wh rename'\nexport WH_ALIAS=\"custom_wh\"\n"),
		].into_iter().for_each(|(data, expected)| {
			println!("testing: {:#011b}", data);
			assert_eq!(SHELL.get_init_code(&get_init_struct(SHELL, data), DB_PATH), expected);
		});
	}

	#[test]
	fn zsh() {
		const SHELL: Shell = Shell::Zsh;
		let normal = "alias wh='worm_hole --db-path ".to_owned() + DB_PATH + "'\n__worm_hole_cd() {\n    if [ -z \"$1\" ]\n    then\n        cd $HOME\n    else\n        CD=$(wh query \"$1\") && \\builtin cd \"$CD\"\n    fi\n}\nalias whcd='__worm_hole_cd'\n";
		vec![
			(0b000000000, normal.clone() + "export WH_ALIAS=\"wh\"\n"),
			(0b000000001, "alias custom_wh='worm_hole --db-path ".to_owned() + DB_PATH + "'\n__worm_hole_cd() {\n    if [ -z \"$1\" ]\n    then\n        cd $HOME\n    else\n        CD=$(custom_wh query \"$1\") && \\builtin cd \"$CD\"\n    fi\n}\nalias whcd='__worm_hole_cd'\nexport WH_ALIAS=\"custom_wh\"\n"),
			(0b000000010, "alias wh='worm_hole --db-path ".to_owned() + DB_PATH + "'\n__worm_hole_cd() {\n    if [ -z \"$1\" ]\n    then\n        cd $HOME\n    else\n        CD=$(wh query \"$1\") && \\builtin cd \"$CD\"\n    fi\n}\nalias custom_whcd='__worm_hole_cd'\nexport WH_ALIAS=\"wh\"\n"),
			(0b000000100, normal.clone() + "alias custom_add='wh add'\nexport WH_ALIAS=\"wh\"\n"),
			(0b000001000, normal.clone() + "alias custom_remove='wh remove'\nexport WH_ALIAS=\"wh\"\n"),
			(0b000010000, normal.clone() + "alias custom_list='wh list'\nexport WH_ALIAS=\"wh\"\n"),
			(0b000100000, normal.clone() + "alias custom_search='wh search'\nexport WH_ALIAS=\"wh\"\n"),
			(0b001000000, normal.clone() + "alias custom_query='wh query'\nexport WH_ALIAS=\"wh\"\n"),
			(0b010000000, normal.clone() + "alias custom_edit='wh edit'\nexport WH_ALIAS=\"wh\"\n"),
			(0b100000000, normal.clone() + "alias custom_rename='wh rename'\nexport WH_ALIAS=\"wh\"\n"),
			(0b111111111, "alias custom_wh='worm_hole --db-path ".to_owned() + DB_PATH + "'\n__worm_hole_cd() {\n    if [ -z \"$1\" ]\n    then\n        cd $HOME\n    else\n        CD=$(custom_wh query \"$1\") && \\builtin cd \"$CD\"\n    fi\n}\nalias custom_whcd='__worm_hole_cd'\nalias custom_add='custom_wh add'\nalias custom_remove='custom_wh remove'\nalias custom_list='custom_wh list'\nalias custom_search='custom_wh search'\nalias custom_query='custom_wh query'\nalias custom_edit='custom_wh edit'\nalias custom_rename='custom_wh rename'\nexport WH_ALIAS=\"custom_wh\"\n"),
		].into_iter().for_each(|(data, expected)| {
			println!("testing: {:#011b}", data);
			assert_eq!(SHELL.get_init_code(&get_init_struct(SHELL, data), DB_PATH), expected);
		});
	}

	#[test]
	fn fish() {
		const SHELL: Shell = Shell::Fish;
		let normal = "alias wh='worm_hole --db-path ".to_owned() + DB_PATH + "'\nfunction __worm_hole_cd\n    if test -z $argv\n        cd $HOME\n    else\n        set -l CD (wh query \"$argv\")\n        if test -n $CD\n            builtin cd \"$CD\"\n        end\n    end\nend\nalias whcd='__worm_hole_cd'\n";
		vec![
			(0b000000000, normal.clone() + "set -gx WH_ALIAS \"wh\"\n"),
			(0b000000001, "alias custom_wh='worm_hole --db-path ".to_owned() + DB_PATH + "'\nfunction __worm_hole_cd\n    if test -z $argv\n        cd $HOME\n    else\n        set -l CD (custom_wh query \"$argv\")\n        if test -n $CD\n            builtin cd \"$CD\"\n        end\n    end\nend\nalias whcd='__worm_hole_cd'\nset -gx WH_ALIAS \"custom_wh\"\n"),
			(0b000000010, "alias wh='worm_hole --db-path ".to_owned() + DB_PATH + "'\nfunction __worm_hole_cd\n    if test -z $argv\n        cd $HOME\n    else\n        set -l CD (wh query \"$argv\")\n        if test -n $CD\n            builtin cd \"$CD\"\n        end\n    end\nend\nalias custom_whcd='__worm_hole_cd'\nset -gx WH_ALIAS \"wh\"\n"),
			(0b000000100, normal.clone() + "alias custom_add='wh add'\nset -gx WH_ALIAS \"wh\"\n"),
			(0b000001000, normal.clone() + "alias custom_remove='wh remove'\nset -gx WH_ALIAS \"wh\"\n"),
			(0b000010000, normal.clone() + "alias custom_list='wh list'\nset -gx WH_ALIAS \"wh\"\n"),
			(0b000100000, normal.clone() + "alias custom_search='wh search'\nset -gx WH_ALIAS \"wh\"\n"),
			(0b001000000, normal.clone() + "alias custom_query='wh query'\nset -gx WH_ALIAS \"wh\"\n"),
			(0b010000000, normal.clone() + "alias custom_edit='wh edit'\nset -gx WH_ALIAS \"wh\"\n"),
			(0b100000000, normal.clone() + "alias custom_rename='wh rename'\nset -gx WH_ALIAS \"wh\"\n"),
			(0b111111111, "alias custom_wh='worm_hole --db-path ".to_owned() + DB_PATH + "'\nfunction __worm_hole_cd\n    if test -z $argv\n        cd $HOME\n    else\n        set -l CD (custom_wh query \"$argv\")\n        if test -n $CD\n            builtin cd \"$CD\"\n        end\n    end\nend\nalias custom_whcd='__worm_hole_cd'\nalias custom_add='custom_wh add'\nalias custom_remove='custom_wh remove'\nalias custom_list='custom_wh list'\nalias custom_search='custom_wh search'\nalias custom_query='custom_wh query'\nalias custom_edit='custom_wh edit'\nalias custom_rename='custom_wh rename'\nset -gx WH_ALIAS \"custom_wh\"\n"),
		].into_iter().for_each(|(data, expected)| {
			println!("testing: {:#011b}", data);
			assert_eq!(SHELL.get_init_code(&get_init_struct(SHELL, data), DB_PATH), expected);
		});
	}

	#[test]
	fn nu() {
		const SHELL: Shell = Shell::Nu;
		let normal = "alias wh = worm_hole --db-path ".to_owned() + DB_PATH + "\ndef --env __worm_hole_cd [alias?] {\n    if $alias == null {\n        cd $env.HOME\n    } else {\n        let CD = wh query $alias\n        if $CD != null {\n            cd $CD\n        }\n    }\n}\nalias whcd = __worm_hole_cd\n";
		vec![
			(0b000000000, normal.clone() + "$env.WH_ALIAS = 'wh'\n"),
			(0b000000001, "alias custom_wh = worm_hole --db-path ".to_owned() + DB_PATH + "\ndef --env __worm_hole_cd [alias?] {\n    if $alias == null {\n        cd $env.HOME\n    } else {\n        let CD = custom_wh query $alias\n        if $CD != null {\n            cd $CD\n        }\n    }\n}\nalias whcd = __worm_hole_cd\n$env.WH_ALIAS = 'custom_wh'\n"),
			(0b000000010, "alias wh = worm_hole --db-path ".to_owned() + DB_PATH + "\ndef --env __worm_hole_cd [alias?] {\n    if $alias == null {\n        cd $env.HOME\n    } else {\n        let CD = wh query $alias\n        if $CD != null {\n            cd $CD\n        }\n    }\n}\nalias custom_whcd = __worm_hole_cd\n$env.WH_ALIAS = 'wh'\n"),
			(0b000000100, normal.clone() + "alias custom_add = wh add\n$env.WH_ALIAS = 'wh'\n"),
			(0b000001000, normal.clone() + "alias custom_remove = wh remove\n$env.WH_ALIAS = 'wh'\n"),
			(0b000010000, normal.clone() + "alias custom_list = wh list\n$env.WH_ALIAS = 'wh'\n"),
			(0b000100000, normal.clone() + "alias custom_search = wh search\n$env.WH_ALIAS = 'wh'\n"),
			(0b001000000, normal.clone() + "alias custom_query = wh query\n$env.WH_ALIAS = 'wh'\n"),
			(0b010000000, normal.clone() + "alias custom_edit = wh edit\n$env.WH_ALIAS = 'wh'\n"),
			(0b100000000, normal.clone() + "alias custom_rename = wh rename\n$env.WH_ALIAS = 'wh'\n"),
			(0b111111111, "alias custom_wh = worm_hole --db-path ".to_owned() + DB_PATH + "\ndef --env __worm_hole_cd [alias?] {\n    if $alias == null {\n        cd $env.HOME\n    } else {\n        let CD = custom_wh query $alias\n        if $CD != null {\n            cd $CD\n        }\n    }\n}\nalias custom_whcd = __worm_hole_cd\nalias custom_add = custom_wh add\nalias custom_remove = custom_wh remove\nalias custom_list = custom_wh list\nalias custom_search = custom_wh search\nalias custom_query = custom_wh query\nalias custom_edit = custom_wh edit\nalias custom_rename = custom_wh rename\n$env.WH_ALIAS = 'custom_wh'\n"),
		].into_iter().for_each(|(data, expected)| {
			println!("testing: {:#011b}", data);
			assert_eq!(SHELL.get_init_code(&get_init_struct(SHELL, data), DB_PATH), expected);
		});
	}
}