diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..47684146 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[env] +CARGO_WORKSPACE_DIR = { value = "", relative = true } diff --git a/Cargo.lock b/Cargo.lock index 1a93ddc3..615b85e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,6 +125,7 @@ dependencies = [ "indexmap", "log", "rgb", + "shell-words", "strum", ] @@ -180,6 +181,12 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "strum" version = "0.26.3" diff --git a/Cargo.toml b/Cargo.toml index 3b9e382c..19d11df3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,4 +20,5 @@ env_logger = { version = "0.11.3", default-features = false } indexmap = { version = "2.2.6", default-features = false } log = { version = "0.4.21", default-features = false } rgb = { version = "0.8.37", default-features = false } +shell-words = { version = "1.1.0", default-features = false } strum = { version = "0.26.3", default-features = false } diff --git a/crates/hyfetch/Cargo.toml b/crates/hyfetch/Cargo.toml index 63147028..0773edc0 100644 --- a/crates/hyfetch/Cargo.toml +++ b/crates/hyfetch/Cargo.toml @@ -17,6 +17,7 @@ env_logger = { workspace = true, features = ["auto-color", "humantime", "unstabl indexmap = { workspace = true, features = ["std"] } log = { workspace = true, features = ["kv"] } rgb = { workspace = true, features = [] } +shell-words = { workspace = true, features = ["std"] } strum = { workspace = true, features = ["derive", "std"] } [features] diff --git a/crates/hyfetch/src/bin/hyfetch.rs b/crates/hyfetch/src/bin/hyfetch.rs index aec64cfa..34b7b842 100644 --- a/crates/hyfetch/src/bin/hyfetch.rs +++ b/crates/hyfetch/src/bin/hyfetch.rs @@ -1,5 +1,6 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use hyfetch::cli_options::options; +use hyfetch::neofetch_util::get_distro_ascii; use log::debug; fn main() -> Result<()> { @@ -10,5 +11,15 @@ fn main() -> Result<()> { // TODO + if options.test_print { + println!( + "{}", + get_distro_ascii(None).context("Failed to get distro ascii")? + ); + return Ok(()); + } + + // TODO + Ok(()) } diff --git a/crates/hyfetch/src/cli_options.rs b/crates/hyfetch/src/cli_options.rs index 11441dd3..9910bcd0 100644 --- a/crates/hyfetch/src/cli_options.rs +++ b/crates/hyfetch/src/cli_options.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use std::str::FromStr; +use anyhow::Context; #[cfg(feature = "autocomplete")] use bpaf::ShellComp; use bpaf::{construct, long, OptionParser, Parser}; @@ -16,7 +17,7 @@ pub struct Options { pub preset: Option, pub mode: Option, pub backend: Option, - pub backend_args: Option, + pub backend_args: Vec, pub colors_scale: Option, pub colors_set_lightness: Option, pub colors_use_overlay: bool, @@ -47,7 +48,9 @@ PRESET={{{}}}", .argument("PRESET"); #[cfg(feature = "autocomplete")] let preset = preset.complete(complete_preset); - let preset = preset.parse(|s| Preset::from_str(&s)).optional(); + let preset = preset + .parse(|s| Preset::from_str(&s).with_context(|| format!("Failed to parse preset `{s}`"))) + .optional(); let mode = long("mode") .short('m') .help(&*format!( @@ -58,7 +61,9 @@ MODE={{{}}}", .argument("MODE"); #[cfg(feature = "autocomplete")] let mode = mode.complete(complete_mode); - let mode = mode.parse(|s| AnsiMode::from_str(&s)).optional(); + let mode = mode + .parse(|s| AnsiMode::from_str(&s).with_context(|| format!("Failed to parse mode `{s}`"))) + .optional(); let backend = long("backend") .short('b') .help(&*format!( @@ -69,11 +74,14 @@ BACKEND={{{}}}", .argument("BACKEND"); #[cfg(feature = "autocomplete")] let backend = backend.complete(complete_backend); - let backend = backend.parse(|s| Backend::from_str(&s)).optional(); + let backend = backend + .parse(|s| Backend::from_str(&s).with_context(|| format!("Failed to parse backend `{s}`"))) + .optional(); let backend_args = long("args") .help("Additional arguments pass-through to backend") - .argument("ARGS") - .optional(); + .argument::("ARGS") + .parse(|s| shell_words::split(&s).context("Failed to split args for shell")) + .fallback(vec![]); let colors_scale = long("c-scale") .help("Lighten colors by a multiplier") .argument("SCALE") diff --git a/crates/hyfetch/src/lib.rs b/crates/hyfetch/src/lib.rs index a4bd0bfb..579ff006 100644 --- a/crates/hyfetch/src/lib.rs +++ b/crates/hyfetch/src/lib.rs @@ -1,4 +1,5 @@ pub mod cli_options; pub mod color_util; +pub mod neofetch_util; pub mod presets; pub mod types; diff --git a/crates/hyfetch/src/neofetch_util.rs b/crates/hyfetch/src/neofetch_util.rs new file mode 100644 index 00000000..540452b5 --- /dev/null +++ b/crates/hyfetch/src/neofetch_util.rs @@ -0,0 +1,119 @@ +use std::ffi::OsStr; +#[cfg(unix)] +use std::os::unix::process::ExitStatusExt as _; +use std::path::PathBuf; +use std::process::Command; +use std::{env, fmt}; + +use anyhow::{anyhow, Context, Result}; +use log::debug; + +/// Gets the absolute path of the neofetch command. +pub fn get_command_path() -> Result { + if let Some(workspace_dir) = option_env!("CARGO_WORKSPACE_DIR") { + let path = PathBuf::from(workspace_dir); + if path.exists() { + let path = path.join("neofetch"); + match path.try_exists() { + Ok(true) => { + return path.canonicalize().context("Failed to canonicalize path"); + }, + Ok(false) => { + Err(anyhow!("{path:?} does not exist or is not readable"))?; + }, + Err(err) => { + Err(err) + .with_context(|| format!("Failed to check for existence of {path:?}"))?; + }, + } + } + } + + let Ok(path_env) = env::var("PATH") else { + return Err(anyhow!("`PATH` env var is not set or invalid")); + }; + + for search_path in env::split_paths(&path_env) { + let path = search_path.join("neowofetch"); + if !path.is_file() { + continue; + } + return path.canonicalize().context("Failed to canonicalize path"); + } + + Err(anyhow!("neofetch command not found")) +} + +pub fn get_distro_ascii(distro: Option) -> Result { + // TODO + + let distro = if let Some(distro) = distro { + distro + } else { + get_distro_name().context("Failed to get distro name")? + }; + debug!(distro:% = distro; "resolved distro name"); + + todo!() +} + +/// Runs neofetch command, returning the piped stdout output. +fn run_neofetch_command_piped(args: &[S]) -> Result +where + S: AsRef + fmt::Debug, +{ + let mut command = make_neofetch_command(args).context("Failed to make neofetch command")?; + + let output = command + .output() + .context("Failed to execute neofetch as child process")?; + debug!(output:?, args:?; "neofetch output"); + + if !output.status.success() { + let err = if let Some(code) = output.status.code() { + anyhow!("neofetch process exited with status code: {code}") + } else { + #[cfg(unix)] + { + anyhow!( + "neofetch process terminated by signal: {}", + output + .status + .signal() + .expect("either one of status code or signal should be set") + ) + } + #[cfg(not(unix))] + unimplemented!("status code not expected to be `None` on non-Unix platforms") + }; + Err(err)?; + } + + let out = String::from_utf8(output.stdout) + .context("Failed to process neofetch output as it contains invalid UTF-8")? + .trim() + .to_owned(); + Ok(out) +} + +fn make_neofetch_command(args: &[S]) -> Result +where + S: AsRef, +{ + #[cfg(not(windows))] + { + let mut command = Command::new("bash"); + command.arg(get_command_path().context("Failed to get neofetch command path")?); + command.args(args); + Ok(command) + } + #[cfg(windows)] + { + todo!() + } +} + +fn get_distro_name() -> Result { + run_neofetch_command_piped(&["ascii_distro_name"]) + .context("Failed to get distro name from neofetch") +}