Support fastfetch backend

This commit is contained in:
Teoh Han Hui 2024-07-11 17:43:59 +08:00
parent 3cc26d7dd9
commit 965b009bc7
No known key found for this signature in database
GPG key ID: D43E2BABAF97DCAE
5 changed files with 279 additions and 113 deletions

View file

@ -64,7 +64,8 @@ fn main() -> Result<()> {
println!();
if !june_path.is_file() {
fs::create_dir_all(cache_path).context("failed to create cache dir")?;
fs::create_dir_all(&cache_path)
.with_context(|| format!("failed to create cache dir {cache_path:?}"))?;
File::create(&june_path)
.with_context(|| format!("failed to create file {june_path:?}"))?;
}
@ -116,7 +117,7 @@ fn main() -> Result<()> {
let asc = color_align
.recolor_ascii(asc, color_profile, color_mode, config.light_dark)
.context("failed to recolor ascii")?;
neofetch_util::run(asc, backend, args).context("failed to run")?;
neofetch_util::run(asc, backend, args)?;
if options.ask_exit {
print!("Press any key to exit...");

View file

@ -248,7 +248,7 @@ where
let start = m.end();
let end = msg[start..]
.find(')')
.ok_or_else(|| anyhow!("missing closing brace for color code"));
.context("missing closing brace for color code");
let end = match end {
Ok(end) => end,
Err(err) => {
@ -306,7 +306,7 @@ where
true
});
if let Some(err) = ret_err {
Err(err)?;
return Err(err);
}
Ok(dst)

View file

@ -1,10 +1,8 @@
use std::borrow::Cow;
use std::ffi::OsStr;
use std::io::Write;
#[cfg(unix)]
use std::os::unix::process::ExitStatusExt as _;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus};
use std::process::Command;
use std::sync::OnceLock;
use std::{env, fmt};
@ -24,6 +22,7 @@ use crate::color_util::{
use crate::distros::Distro;
use crate::presets::ColorProfile;
use crate::types::{AnsiMode, Backend, LightDark};
use crate::utils::{find_file, find_in_path, process_command_status};
const NEOFETCH_COLOR_PATTERNS: [&str; 6] = ["${c1}", "${c2}", "${c3}", "${c4}", "${c5}", "${c6}"];
static NEOFETCH_COLORS_AC: OnceLock<AhoCorasick> = OnceLock::new();
@ -287,51 +286,59 @@ impl ColorAlignment {
}
/// Gets the absolute path of the neofetch command.
pub fn get_command_path() -> Result<PathBuf> {
pub fn neofetch_path() -> Result<Option<PathBuf>> {
if let Some(workspace_dir) = env::var_os("CARGO_WORKSPACE_DIR") {
let path = Path::new(&workspace_dir);
if path.exists() {
let path = path.join("neofetch");
match path.try_exists() {
Ok(true) => {
#[cfg(not(windows))]
return path.canonicalize().context("failed to canonicalize path");
#[cfg(windows)]
return path
.normalize()
.map(|p| p.into())
.context("failed to normalize 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:?}"))?;
},
}
}
debug!(
?workspace_dir,
"CARGO_WORKSPACE_DIR env var is set; using neofetch from project directory"
);
let workspace_path = Path::new(&workspace_dir);
let workspace_path = match workspace_path.try_exists() {
Ok(true) => workspace_path,
Ok(false) => {
return Err(anyhow!(
"{workspace_path:?} does not exist or is not readable"
));
},
Err(err) => {
return Err(err).with_context(|| {
format!("failed to check for existence of {workspace_path:?}")
});
},
};
let neofetch_path = workspace_path.join("neofetch");
return find_file(&neofetch_path)
.with_context(|| format!("failed to check existence of file {neofetch_path:?}"));
}
let Some(path_env) = env::var_os("PATH") else {
return Err(anyhow!("`PATH` env var is not set or invalid"));
let neowofetch_path = find_in_path("neowofetch")
.context("failed to check existence of `neowofetch` in `PATH`")?;
// Fall back to `neowofetch` in directory of current executable
let neowofetch_path = if neowofetch_path.is_some() {
neowofetch_path
} else {
let current_exe_path = env::current_exe()
.and_then(|p| {
#[cfg(not(windows))]
{
p.canonicalize()
}
#[cfg(windows)]
{
p.normalize().map(|p| p.into())
}
})
.context("failed to get path of current running executable")?;
let neowofetch_path = current_exe_path
.parent()
.expect("parent should not be `None`")
.join("neowofetch");
find_file(&neowofetch_path)
.with_context(|| format!("failed to check existence of file {neowofetch_path:?}"))?
};
for search_path in env::split_paths(&path_env) {
let path = search_path.join("neowofetch");
if !path.is_file() {
continue;
}
#[cfg(not(windows))]
return path.canonicalize().context("failed to canonicalize path");
#[cfg(windows)]
return path
.normalize()
.map(|p| p.into())
.context("failed to normalize path");
}
Err(anyhow!("neofetch command not found"))
Ok(neowofetch_path)
}
/// Ensures git bash installation for Windows.
@ -342,16 +349,12 @@ pub fn ensure_git_bash() -> Result<PathBuf> {
let git_bash_path = {
// Bundled git bash
let current_exe_path = env::current_exe()
.and_then(|p| {
#[cfg(not(windows))]
{
p.canonicalize()
}
#[cfg(windows)]
p.normalize().map(|p| p.into())
})
.and_then(|p| p.normalize().map(|p| p.into()))
.context("failed to get path of current running executable")?;
let bash_path = current_exe_path.join("git/bin/bash.exe");
let bash_path = current_exe_path
.parent()
.expect("parent should not be `None`")
.join("git/bin/bash.exe");
if bash_path.is_file() {
Some(bash_path)
} else {
@ -377,9 +380,7 @@ pub fn ensure_git_bash() -> Result<PathBuf> {
}
});
let Some(git_bash_path) = git_bash_path else {
return Err(anyhow!("failed to find git bash executable"));
};
let git_bash_path = git_bash_path.context("failed to find git bash executable")?;
Ok(git_bash_path)
}
@ -421,17 +422,16 @@ where
Ok((normalize_ascii(asc), None))
}
#[tracing::instrument(level = "debug", skip(asc))]
pub fn run(asc: String, backend: Backend, args: Option<&Vec<String>>) -> Result<()> {
match backend {
Backend::Neofetch => {
run_neofetch(asc, args).context("failed to run neofetch")?;
},
Backend::Fastfetch => {
todo!();
run_fastfetch(asc, args, false).context("failed to run fastfetch")?;
},
Backend::FastfetchOld => {
todo!();
run_fastfetch(asc, args, true).context("failed to run fastfetch")?;
},
Backend::Qwqfetch => {
todo!();
@ -512,9 +512,9 @@ where
// line starts with neofetch color code, do nothing
},
_ => {
new.push_str(last.ok_or_else(|| {
anyhow!("failed to find neofetch color code from a previous line")
})?);
new.push_str(
last.context("failed to find neofetch color code from a previous line")?,
);
},
}
new.push_str(line);
@ -529,29 +529,12 @@ where
Ok(new)
}
/// Runs neofetch command.
#[tracing::instrument(level = "debug")]
fn run_neofetch_command<S>(args: &[S]) -> Result<()>
where
S: AsRef<OsStr> + fmt::Debug,
{
let mut command = make_neofetch_command(args).context("failed to make neofetch command")?;
let status = command
.status()
.context("failed to execute neofetch command as child process")?;
process_command_status(&status).context("neofetch command exited with error")?;
Ok(())
}
/// Runs neofetch command, returning the piped stdout output.
#[tracing::instrument(level = "debug")]
fn run_neofetch_command_piped<S>(args: &[S]) -> Result<String>
where
S: AsRef<OsStr> + fmt::Debug,
{
let mut command = make_neofetch_command(args).context("failed to make neofetch command")?;
let mut command = make_neofetch_command(args)?;
let output = command
.output()
@ -570,7 +553,8 @@ fn make_neofetch_command<S>(args: &[S]) -> Result<Command>
where
S: AsRef<OsStr>,
{
let neofetch_path = get_command_path().context("failed to get neofetch command path")?;
let neofetch_path = neofetch_path().context("failed to get neofetch path")?;
let neofetch_path = neofetch_path.context("neofetch command not found")?;
debug!(?neofetch_path, "neofetch path");
@ -591,29 +575,6 @@ where
}
}
fn process_command_status(status: &ExitStatus) -> Result<()> {
if status.success() {
return Ok(());
}
let err = if let Some(code) = status.code() {
anyhow!("child process exited with status code: {code}")
} else {
#[cfg(unix)]
{
anyhow!(
"child process terminated by signal: {}",
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)
}
#[tracing::instrument(level = "debug")]
fn get_distro_name() -> Result<String> {
run_neofetch_command_piped(&["ascii_distro_name"])
@ -651,7 +612,111 @@ fn run_neofetch(asc: String, args: Option<&Vec<String>>) -> Result<()> {
}
v
};
run_neofetch_command(&args).context("failed to run neofetch command")?;
let mut command = make_neofetch_command(&args)?;
debug!(?command, "neofetch command");
let status = command
.status()
.context("failed to execute neofetch command as child process")?;
process_command_status(&status).context("neofetch command exited with error")?;
Ok(())
}
fn fastfetch_path() -> Result<Option<PathBuf>> {
let fastfetch_path =
find_in_path("fastfetch").context("failed to check existence of `fastfetch` in `PATH`")?;
// Fall back to `fastfetch` in directory of current executable
let current_exe_path = env::current_exe()
.and_then(|p| {
#[cfg(not(windows))]
{
p.canonicalize()
}
#[cfg(windows)]
{
p.normalize().map(|p| p.into())
}
})
.context("failed to get path of current running executable")?;
let current_exe_dir_path = current_exe_path
.parent()
.expect("parent should not be `None`");
let fastfetch_path = if fastfetch_path.is_some() {
fastfetch_path
} else {
let fastfetch_path = current_exe_dir_path.join("fastfetch");
find_file(&fastfetch_path)
.with_context(|| format!("failed to check existence of file {fastfetch_path:?}"))?
};
// Bundled fastfetch
#[cfg(unix)]
let fastfetch_path = if fastfetch_path.is_some() {
fastfetch_path
} else {
let fastfetch_path = current_exe_dir_path.join("fastfetch/usr/bin/fastfetch");
find_file(&fastfetch_path)
.with_context(|| format!("failed to check existence of file {fastfetch_path:?}"))?
};
let fastfetch_path = if fastfetch_path.is_some() {
fastfetch_path
} else {
let fastfetch_path = current_exe_dir_path.join("fastfetch/fastfetch");
find_file(&fastfetch_path)
.with_context(|| format!("failed to check existence of file {fastfetch_path:?}"))?
};
#[cfg(windows)]
let fastfetch_path = if fastfetch_path.is_some() {
fastfetch_path
} else {
let fastfetch_path = current_exe_dir_path.join("fastfetch/fastfetch.exe");
find_file(&fastfetch_path)
.with_context(|| format!("failed to check existence of file {fastfetch_path:?}"))?
};
Ok(fastfetch_path)
}
/// Runs fastfetch with colors.
#[tracing::instrument(level = "debug", skip(asc))]
fn run_fastfetch(asc: String, args: Option<&Vec<String>>, legacy: bool) -> Result<()> {
// Find fastfetch binary
let fastfetch_path = fastfetch_path().context("failed to get fastfetch path")?;
let fastfetch_path = fastfetch_path.context("fastfetch command not found")?;
debug!(?fastfetch_path, "fastfetch path");
// Write temp file
let mut temp_file =
NamedTempFile::with_prefix("ascii.txt").context("failed to create temp file for ascii")?;
temp_file
.write_all(asc.as_bytes())
.context("failed to write ascii to temp file")?;
// Call fastfetch with the temp file
let temp_file_path = temp_file.into_temp_path();
let mut command = Command::new(fastfetch_path);
command.arg(if legacy { "--raw" } else { "--file-raw" });
command.arg(&temp_file_path);
if let Some(args) = args {
command.args(args);
}
debug!(?command, "fastfetch command");
let status = command
.status()
.context("failed to execute fastfetch command as child process")?;
if status.code() == Some(144) {
eprintln!(
"exit code 144 detected; please upgrade fastfetch to >=1.8.0 or use the \
'fastfetch-old' backend"
);
}
process_command_status(&status).context("fastfetch command exited with error")?;
Ok(())
}

View file

@ -7,6 +7,7 @@ use palette::num::ClampAssign;
use palette::{Hsl, IntoColorMut, LinSrgb, Srgb};
use serde::{Deserialize, Serialize};
use strum::{EnumString, VariantNames};
use tracing::debug;
use unicode_segmentation::UnicodeSegmentation;
use crate::color_util::{ForegroundBackground, Lightness, ToAnsiString};
@ -413,9 +414,10 @@ impl ColorProfile {
/// `colors[i]` appears)
pub fn with_weights(&self, weights: Vec<u8>) -> Result<Self> {
if weights.len() != self.colors.len() {
Err(anyhow!(
debug!(?weights, ?self.colors, "length mismatch between `weights` and `colors`");
return Err(anyhow!(
"`weights` should have the same number of elements as `colors`"
))?;
));
}
let mut weighted_colors = vec![];
@ -488,7 +490,7 @@ impl ColorProfile {
let length = txt.len();
let length: u8 = length.try_into().expect("`length` should fit in `u8`");
self.with_length(length)
.context("failed to spread color profile to length")?
.with_context(|| format!("failed to spread color profile to length {length}"))?
};
let mut buf = String::new();

View file

@ -1,7 +1,14 @@
use std::path::PathBuf;
#[cfg(unix)]
use std::os::unix::process::ExitStatusExt as _;
use std::path::{Path, PathBuf};
use std::process::ExitStatus;
use std::{env, fs, io};
use anyhow::{Context, Result};
use anyhow::{anyhow, Context, Result};
use directories::ProjectDirs;
#[cfg(windows)]
use normpath::PathExt as _;
use tracing::debug;
pub fn get_cache_path() -> Result<PathBuf> {
let path = ProjectDirs::from("", "", "hyfetch")
@ -11,6 +18,97 @@ pub fn get_cache_path() -> Result<PathBuf> {
Ok(path)
}
/// Finds a command in `PATH`.
///
/// Returns the canonicalized / normalized absolute path to the command.
pub fn find_in_path<P>(program: P) -> Result<Option<PathBuf>>
where
P: AsRef<Path>,
{
let program = program.as_ref();
// Only accept program name, i.e. a relative path with one component
if program.parent() != Some(Path::new("")) {
return Err(anyhow!("invalid command name {program:?}"));
};
let path_env = env::var_os("PATH").context("`PATH` env var is not set or invalid")?;
for search_path in env::split_paths(&path_env) {
let path = search_path.join(program);
let path = find_file(&path)
.with_context(|| format!("failed to check existence of file {path:?}"))?;
if path.is_some() {
return Ok(path);
}
}
Ok(None)
}
/// Finds a file.
///
/// Returns the canonicalized / normalized absolute path to the file.
pub fn find_file<P>(path: P) -> Result<Option<PathBuf>>
where
P: AsRef<Path>,
{
let path = path.as_ref();
let metadata = match fs::metadata(path) {
Ok(metadata) => metadata,
Err(err) if err.kind() == io::ErrorKind::NotFound => {
return Ok(None);
},
Err(err) => {
return Err(err).with_context(|| format!("failed to get metadata for {path:?}"));
},
};
if !metadata.is_file() {
debug!(?path, "path exists but is not a file");
return Ok(None);
}
#[cfg(not(windows))]
{
path.canonicalize()
.with_context(|| format!("failed to canonicalize path {path:?}"))
.map(Some)
}
#[cfg(windows)]
{
path.normalize()
.with_context(|| format!("failed to normalize path {path:?}"))
.map(|p| Some(p.into()))
}
}
pub fn process_command_status(status: &ExitStatus) -> Result<()> {
if status.success() {
return Ok(());
}
let err = if let Some(code) = status.code() {
anyhow!("child process exited with status code: {code}")
} else {
#[cfg(unix)]
{
anyhow!(
"child process terminated by signal: {}",
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)
}
pub(crate) mod index_map_serde {
use std::fmt;
use std::fmt::Display;