worm_hole 4.1.1

CLI tool to easily jump between directories
// Copyright (C) 2026 Rignchen
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

use crate::{
	db::Database,
	error::{WHError, WHResult},
	path::FilePath,
};
use clap::Parser;
use std::{env::current_exe, fmt::Display, str::FromStr};

#[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 used to call worm_hole
	#[clap(long, default_value = "wh")]
	worm_hole: String,
	/// The command used to change directory
	#[clap(long, default_value = "whcd")]
	cd: String,
	/// The command to add a new alias-path pairs
	#[clap(long)]
	add: Option<String>,
	/// The command to remove an alias-path pair
	#[clap(long)]
	remove: Option<String>,
	/// The command to list all alias-path pairs
	#[clap(long)]
	list: Option<String>,
	/// The command to search for an alias from its path
	#[clap(long)]
	search: Option<String>,
	/// The command to query a path from an alias
	#[clap(long)]
	query: Option<String>,
	/// The command to edit the path of an alias-path pair
	#[clap(long)]
	edit: Option<String>,
	/// The command to rename an alias without changing the path
	#[clap(long)]
	rename: Option<String>,
}

impl Init {
	pub fn run(&self, database: &Database, db_path: &str) -> WHResult<()> {
		database.init();
		print!("{}", 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, alias: &str, command: &str) -> WHResult<String> {
		Ok(match self {
			Shell::Bash | Shell::Zsh | Shell::Fish => {
				if alias.contains(' ') {
					return Err(WHError::Err(format!("Aliases cannot contain spaces in {self:?}"), 1));
				}
				format!("alias {}='{}'", alias, command)
			}
			Shell::Nu => format!("export alias '{}' = {}", alias, command),
		})
	}

	/// Turn some text into a comment for the current shell
	fn comment(&self, text: impl Display) -> String {
		format!("# {}", text)
	}

	/// 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) -> WHResult<String> {
		let mut builder = vec![self.comment(format!("This was generated using worm hole {}\n", env!("CARGO_PKG_VERSION")))];
		let wh = &aliases.worm_hole;
		builder.push(self.new_alias(
			&aliases.worm_hole,
			&format!(
				"{} --db-path {}",
				current_exe().unwrap().into_os_string().into_string().unwrap(),
				FilePath::from_str(db_path).unwrap().str()
			),
		)?);
		builder.push(self.get_cd_function(aliases));
		if let Some(add) = &aliases.add {
			builder.push(match self {
				Shell::Nu => format!("#Add an alias-path pair to worm hole\nexport def '{add}' [alias: string path?: string # The real path to the location\n]: nothing -> nothing {{ {wh} 'add' ...([$alias $path] | where $it != null) }}"),
				_ => self.new_alias(add, &format!("{wh} add"))?,
			});
		}
		if let Some(remove) = &aliases.remove {
			builder.push(match self {
				Shell::Nu => format!("#Remove an alias-path pair from worm hole\nexport def '{remove}' [alias: string@__worm_hole_list]: nothing -> nothing {{ {wh} 'rm' $alias }}"),
				_ => self.new_alias(remove, &format!("{wh} rm"))?,
			});
		}
		if let Some(list) = &aliases.list {
			builder.push(match self {
				Shell::Nu => format!("#List all aliases and the corresponding path in worm hole\nexport def '{list}' []: nothing -> table<alias: string, path: string> {{ {wh} 'ls' | parse --regex '(?P<alias>.+?)\\s+-> (?P<path>.+)' }}"),
				_ => self.new_alias(list, &format!("{wh} ls"))?
			});
		}
		if let Some(search) = &aliases.search {
			builder.push(match self {
				Shell::Nu => format!("#Search for worm hole aliases with path matching a pattern\nexport def '{search}' [pattern?: string # The pattern to search for in the aliases's paths, search for aliases in the current directory if not provided\n]: nothing -> table<alias: string, path: string> {{ {wh} 'search' ...([$pattern] | where $it != null) | parse --regex '(?P<alias>.+?)\\s+-> (?P<path>.+)' }}"),
				_ => self.new_alias(search, &format!("{wh} search"))?
			});
		}
		if let Some(query) = &aliases.query {
			builder.push(match self {
				Shell::Nu => format!("#Get the path for the given worm hole alias\nexport def '{query}' [alias: string@__worm_hole_list]: nothing -> nothing {{ {wh} query $alias }}"),
				_ => self.new_alias(query, &format!("{wh} query"))?,
			});
		}
		if let Some(edit) = &aliases.edit {
			builder.push(match self {
				Shell::Nu => format!("#Edit the path of a worm hole alias\nexport def '{edit}' [alias: string@__worm_hole_list path?: string # The new path to the location\n]: nothing -> nothing {{ {wh} edit ...([$alias $path] | where $it != null) }}"),
				_ => self.new_alias(edit, &format!("{wh} edit"))?,
			});
		}
		if let Some(rename) = &aliases.rename {
			builder.push(match self {
				Shell::Nu => format!("#Change an alias in worm hole without changing the path\nexport def '{rename}' [old_alias: string@__worm_hole_list new_alias: string]: nothing -> nothing {{ {wh} rename $old_alias $new_alias }}"),
				_ => self.new_alias(rename, &format!("{wh} rename"))?,
			});
		}
		builder.push(match self {
			Shell::Bash | Shell::Zsh => format!("export WH_ALIAS=\"{wh}\""),
			Shell::Fish => format!("set -gx WH_ALIAS \"{wh}\""),
			Shell::Nu => format!("export-env {{ $env.WH_ALIAS = '{wh}' }}"),
		});
		Ok(builder.join("\n"))
	}

	/// 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
	/// The function is named whcd by default but can be changed with the --cd flag
	#[rustfmt::skip]
	fn get_cd_function(&self, aliases: &Init) -> String {
		let cd = &aliases.cd;
		let wh = &aliases.worm_hole;
		match self {
			Shell::Bash | Shell::Zsh => vec![
				     format!("{cd}() {{"),
				String::from("	if [ -z \"$1\" ]"),
				String::from("	then"),
				String::from("		cd $HOME"),
				String::from("	else"),
				     format!("		CD=$({wh} query \"$1\") && \\builtin cd \"$CD\""),
				String::from("	fi"),
				String::from("}"),
			],
			Shell::Fish => vec![
				     format!("function {cd}"),
				String::from("	if test -z $argv"),
				String::from("		cd $HOME"),
				String::from("	else"),
				     format!("		set -l CD ({wh} query \"$argv\")"),
				String::from("		if test -n $CD"),
				String::from("			builtin cd \"$CD\""),
				String::from("		end"),
				String::from("	end"),
				String::from("end"),
			],
			Shell::Nu => vec![
				if aliases.list == Some(format!("{wh} ls")) {
				     format!("def __worm_hole_list [] {{ ({wh} ls | rename -c {{ alias: value path: description }}) }}")
				} else {
				     format!("def __worm_hole_list [] {{ ({wh} ls | parse --regex '(?P<value>.+?)\\s+-> (?P<description>.+)') }}")
				},
				     format!("#Change the current working directory using worm hole's aliases\nexport def --env {cd} [alias?: string@__worm_hole_list] {{"),
				String::from("	if $alias == null {"),
				String::from("		cd $env.HOME"),
				String::from("	} else {"),
				     format!("		let CD = {wh} query $alias"),
				String::from("		cd $CD"),
				String::from("	}"),
				String::from("}"),
			],
		}.join("\n")
	}
}