diff --git a/Cargo.lock b/Cargo.lock index 77767c8..af0c84c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,6 +66,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6d640bee2da49f60a4068a7fae53acde8982514ab7bae8b8cea9e88cbcfd799" +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "bitflags" version = "1.3.2" @@ -87,6 +93,18 @@ dependencies = [ "libc", ] +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "4.3.19" @@ -152,8 +170,59 @@ dependencies = [ "ecoji", "indoc", "itertools", + "sysinfo", + "which", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg 1.1.0", + "cfg-if 1.0.0", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if 1.0.0", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "ecoji" version = "1.0.0" @@ -247,6 +316,34 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg 1.1.0", +] + +[[package]] +name = "ntapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +dependencies = [ + "winapi", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "once_cell" version = "1.18.0" @@ -315,7 +412,7 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" dependencies = [ - "autocfg", + "autocfg 0.1.2", "libc", "rand_chacha", "rand_core 0.4.2", @@ -334,7 +431,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" dependencies = [ - "autocfg", + "autocfg 0.1.2", "rand_core 0.3.1", ] @@ -402,7 +499,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" dependencies = [ - "autocfg", + "autocfg 0.1.2", "rand_core 0.4.2", ] @@ -415,6 +512,28 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "rdrand" version = "0.4.0" @@ -437,6 +556,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "siphasher" version = "0.2.3" @@ -460,6 +585,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sysinfo" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac193374347e7c263c5f547524f36ff8ec6702d56c8799c8331d26dffe8c1e" +dependencies = [ + "cfg-if 0.1.10", + "doc-comment", + "libc", + "ntapi", + "once_cell", + "rayon", + "winapi", +] + [[package]] name = "unicode-ident" version = "1.0.11" @@ -472,11 +612,20 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "libc", +] + [[package]] name = "winapi" -version = "0.3.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", diff --git a/Cargo.toml b/Cargo.toml index 6d084c7..b8061ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,10 @@ clap = { version = "4.0", features = ["derive"] } ecoji = "1.0.0" itertools = "0.10.5" +[target.'cfg(unix)'.dependencies] +sysinfo = "0.13.1" +which = { version = "3.1.1", default-features = false } + [dev-dependencies] indoc = "2.0.3" diff --git a/Readme.md b/Readme.md index cf9da23..aad84af 100644 --- a/Readme.md +++ b/Readme.md @@ -25,3 +25,27 @@ md5sum [file] | coloursum ``` Coloursum also prints full usage information if you run `coloursum --help`. + +### Shell Integration + +You can also integrate coloursum into your shell, to output colourful checksums by default! + +By default, it will search for known checksum commands' presence, and generate shell functions for those which are found. + +If this behaviour is not acceptable, or your checksum command is not in the list, you can optionally specify a checksum command as the last argument to `coloursum shell-setup` to generate a shell function just for it. + +#### bash, zsh, and other similar shells + +Add this line to your ~/.bash_profile, ~/.zshrc or equivalent file: + +```sh +eval "$(coloursum --mode=1password shell-setup)" +``` + +#### fish shell + +Add this line to ~/.config/fish/config.fish: + +```fish +status --is-interactive; and coloursum --mode=ecoji shell-setup | source +``` diff --git a/src/ansi_coloured_line.rs b/src/ansi_coloured_line.rs index 4271b3c..10c5a16 100644 --- a/src/ansi_coloured_line.rs +++ b/src/ansi_coloured_line.rs @@ -56,6 +56,16 @@ impl Line for ANSIColouredLine { #[cfg(test)] mod tests { + #[test] + fn display_works() { + use super::ANSIColouredLine; + + assert_eq!( + format!("{}", ANSIColouredLine::from("MD5 (./src/main.rs) = b7527e0e28c09f6f62dd2d4197d5d225".to_string())), + "MD5 (./src/main.rs) = \u{1b}[38;5;183mb7\u{1b}[0m\u{1b}[38;5;82m52\u{1b}[0m\u{1b}[38;5;126m7e\u{1b}[0m\u{1b}[38;5;14m0e\u{1b}[0m\u{1b}[38;5;40m28\u{1b}[0m\u{1b}[38;5;192mc0\u{1b}[0m\u{1b}[38;5;159m9f\u{1b}[0m\u{1b}[38;5;111m6f\u{1b}[0m\u{1b}[38;5;98m62\u{1b}[0m\u{1b}[38;5;221mdd\u{1b}[0m\u{1b}[38;5;45m2d\u{1b}[0m\u{1b}[38;5;65m41\u{1b}[0m\u{1b}[38;5;151m97\u{1b}[0m\u{1b}[38;5;213md5\u{1b}[0m\u{1b}[38;5;210md2\u{1b}[0m\u{1b}[38;5;37m25\u{1b}[0m" + ) + } + #[test] fn format_hash_works() { use super::ANSIColouredLine; diff --git a/src/base_line.rs b/src/base_line.rs index 16a811b..71c236e 100644 --- a/src/base_line.rs +++ b/src/base_line.rs @@ -92,6 +92,18 @@ fn find_sum_prefixed_line(line: &str) -> Option { #[cfg(test)] mod tests { + #[test] + fn from_string_works() { + use super::FormattableLine; + + let string = "MD5 (./src/main.rs) = b7527e0e28c09f6f62dd2d4197d5d225".to_string(); + let line = FormattableLine::from(string.clone()); + + assert_eq!(line.contents, string); + assert_eq!(line.formattable_start, Some(22)); + assert_eq!(line.formattable_end, None); + } + #[test] fn find_bsd_tag_line_works() { use super::find_bsd_tag_line; diff --git a/src/ecoji_line.rs b/src/ecoji_line.rs index 1ff25e6..c229b27 100644 --- a/src/ecoji_line.rs +++ b/src/ecoji_line.rs @@ -56,6 +56,21 @@ impl Line for EcojiLine { #[cfg(test)] mod tests { + #[test] + fn display_works() { + use super::EcojiLine; + + assert_eq!( + format!( + "{}", + EcojiLine::from( + "MD5 (./src/main.rs) = b7527e0e28c09f6f62dd2d4197d5d225".to_string() + ) + ), + "MD5 (./src/main.rs) = 😨🏸🤰📺🙎📇🦎😨🍽🇮📆💣🍜☕☕☕" + ) + } + #[test] fn format_hash_works() { use super::EcojiLine; diff --git a/src/main.rs b/src/main.rs index 66adedf..0f9a5b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,9 +11,29 @@ enum FormattingMode { OnePassword, } +impl ToString for FormattingMode { + fn to_string(&self) -> String { + match self { + FormattingMode::ANSIColours => "ansi-colours", + FormattingMode::Ecoji => "ecoji", + FormattingMode::OnePassword => "1password", + } + .to_string() + } +} + #[derive(Parser, Debug)] #[clap(version)] struct Options { + #[clap(flatten)] + main_options: MainOptions, + + #[clap(subcommand)] + cmd: Option, +} + +#[derive(Parser, Debug)] +struct MainOptions { /// What sort of formatting to use for checksum values. #[clap( short, @@ -25,9 +45,44 @@ struct Options { mode: FormattingMode, } -fn main() -> io::Result<()> { - let options = Options::parse(); +#[derive(Parser, Debug)] +#[clap(rename_all = "kebab-case")] +enum Subcommand { + /// Configure the current shell environment to use coloursum + /// + /// Automatically detects the currently used shell, and configures + /// it to use coloursum with the specified options + /// + /// For example, to configure your zsh shell to use 1Password-style + /// formatting, add the line + /// `eval "$(coloursum --mode 1password shell-setup)"` to your ~/.zshrc + /// file + /// + /// Or, to configure your fish shell to use ecoji formatting, but only + /// for `sha256sum`, add the line + /// `status --is-interactive; and coloursum --mode ecoji shell-setup sha256sum | source` + /// to your ~/.config/fish/config.fish file + #[cfg(unix)] + #[clap(override_usage = r#" + # for bash, zsh, and other similar shells + eval "$(coloursum [OPTIONS] shell-setup [command])" + # for fish + status --is-interactive; and coloursum [OPTIONS] shell-setup [command] | source"#)] + ShellSetup(ShellSetupOptions), +} + +#[derive(Parser, Debug)] +struct ShellSetupOptions { + /// Checksum command to set up shell integration for. If omitted, coloursum + /// will attempt to automatically detect installed checksum commands. + /// If specified, coloursum will configure only that checksum command. + /// You may call shell-setup repeatedly to configure integration for + /// multiple manually-configured checksum commands. + command: Option, +} + +fn coloursum(options: &MainOptions) -> io::Result<()> { let stdin = io::stdin(); let locked_stdin = stdin.lock(); @@ -40,3 +95,117 @@ fn main() -> io::Result<()> { FormattingMode::OnePassword => OnePasswordLine::coloursum(locked_stdin, locked_stdout), } } + +#[cfg(unix)] +static SUM_EXECNAMES: &[&str] = &[ + "md5", + "md5sum", + "gmd5sum", + "shasum", + "sha1sum", + "gsha1sum", + "sha2", + "sha256sum", + "gsha256sum", + "sha224sum", + "gsha224sum", + "sha384sum", + "gsha384sum", + "sha512sum", + "gsha512sum", +]; + +#[cfg(unix)] +fn shell_setup( + options: &MainOptions, + shell_setup_options: &ShellSetupOptions, +) -> Result<(), std::io::Error> { + // detect the calling shell's name + let shell_name = match get_shell_name() { + Some(shell_name) => shell_name, + // fall back to "sh" if not detectable/something weird happens + None => "sh".to_string(), + }; + + println!("# coloursum: generated setup for `{}`", shell_name); + + if let Some(command) = &shell_setup_options.command { + print_shell_function(options, shell_name.as_ref(), command.to_string()); + } else { + for executable in SUM_EXECNAMES { + if let Ok(_path) = which::which(executable) { + print_shell_function(options, shell_name.as_ref(), (*executable).to_string()) + } + } + } + + Ok(()) +} + +#[cfg(unix)] +fn get_shell_name() -> Option { + use sysinfo::{ProcessExt, System, SystemExt}; + + let system = System::new_with_specifics(sysinfo::RefreshKind::new().with_processes()); + + // find the current process by pid, fall back to 0 + // (which is invalid/reserved, so will return None early) + let process = system.get_process(sysinfo::get_current_pid().unwrap_or(0))?; + + // find the parent process via the current process's parent ID + let parent = system.get_process(process.parent()?)?; + + // and if we make it all the way here, return some name + Some(parent.name().to_string()) +} + +#[cfg(unix)] +fn print_shell_function(options: &MainOptions, shell_name: &str, command: String) { + // TODO: work out how to print this losslessly + let exe_name = match std::env::current_exe() { + Ok(path) => path.to_string_lossy().into_owned(), + Err(_) => "coloursum".to_string(), + }; + + match shell_name { + "fish" => println!( + "function {0}\n\ + \tcommand {0} $argv | {1} --mode {2}\n\ + end", + command, + exe_name, + options.mode.to_string() + ), + "ksh" => println!( + "function {0} {{\n\ + \tcommand {0} \"$@\" | {1} --mode {2}\n\ + }}", + command, + exe_name, + options.mode.to_string() + ), + _ => println!( + "function {0}() {{\n\ + \tcommand {0} \"$@\" | {1} --mode {2}\n\ + }}", + command, + exe_name, + options.mode.to_string() + ), + } +} + +fn main() -> Result<(), std::io::Error> { + let options = Options::parse(); + + if let Some(command) = options.cmd { + match command { + #[cfg(unix)] + Subcommand::ShellSetup(shell_setup_options) => { + shell_setup(&options.main_options, &shell_setup_options) + } + } + } else { + coloursum(&options.main_options) + } +} diff --git a/src/onepassword_line.rs b/src/onepassword_line.rs index 3e92289..eab6069 100644 --- a/src/onepassword_line.rs +++ b/src/onepassword_line.rs @@ -45,6 +45,16 @@ impl Line for OnePasswordLine { #[cfg(test)] mod tests { + #[test] + fn display_works() { + use super::OnePasswordLine; + + assert_eq!( + format!("{}", OnePasswordLine::from("MD5 (./src/main.rs) = b7527e0e28c09f6f62dd2d4197d5d225".to_string())), + "MD5 (./src/main.rs) = b\u{1b}[38;5;4m7\u{1b}[0m\u{1b}[38;5;4m5\u{1b}[0m\u{1b}[38;5;4m2\u{1b}[0m\u{1b}[38;5;4m7\u{1b}[0me\u{1b}[38;5;4m0\u{1b}[0me\u{1b}[38;5;4m2\u{1b}[0m\u{1b}[38;5;4m8\u{1b}[0mc\u{1b}[38;5;4m0\u{1b}[0m\u{1b}[38;5;4m9\u{1b}[0mf\u{1b}[38;5;4m6\u{1b}[0mf\u{1b}[38;5;4m6\u{1b}[0m\u{1b}[38;5;4m2\u{1b}[0mdd\u{1b}[38;5;4m2\u{1b}[0md\u{1b}[38;5;4m4\u{1b}[0m\u{1b}[38;5;4m1\u{1b}[0m\u{1b}[38;5;4m9\u{1b}[0m\u{1b}[38;5;4m7\u{1b}[0md\u{1b}[38;5;4m5\u{1b}[0md\u{1b}[38;5;4m2\u{1b}[0m\u{1b}[38;5;4m2\u{1b}[0m\u{1b}[38;5;4m5\u{1b}[0m" + ) + } + #[test] fn format_hash_works() { use super::OnePasswordLine;