From c069982ed9fa985d00c6a0acc4287c90de3534f2 Mon Sep 17 00:00:00 2001 From: Mark Poliakov Date: Sat, 1 Mar 2025 18:40:24 +0200 Subject: [PATCH] sysutils: ls colors --- userspace/sysutils/src/ls.rs | 135 +++++++++++++++++++++++++++++++++-- 1 file changed, 129 insertions(+), 6 deletions(-) diff --git a/userspace/sysutils/src/ls.rs b/userspace/sysutils/src/ls.rs index b23ad3ff..86ecad39 100644 --- a/userspace/sysutils/src/ls.rs +++ b/userspace/sysutils/src/ls.rs @@ -6,7 +6,7 @@ use std::{ ffi::OsString, fmt, fs::{read_dir, FileType, Metadata}, - io, + io::{self, stdout, IsTerminal}, path::{Path, PathBuf}, process::ExitCode, time::SystemTime, @@ -24,6 +24,8 @@ use libutil::fmt::FormatSize; #[derive(Parser)] #[clap(disable_help_flag = true)] pub struct Args { + #[arg(long, default_value_t = true)] + color: bool, #[arg(short)] long: bool, #[arg(short)] @@ -64,6 +66,12 @@ trait MetadataImpl { fn mtime(&self) -> SystemTime; } +trait FileTypeImpl { + fn is_directory(&self) -> bool; + fn is_block_device(&self) -> bool; + fn is_char_device(&self) -> bool; +} + impl DisplaySizeBit for u64 { fn display_size_bit(self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result { if opts.human_readable { @@ -106,7 +114,11 @@ struct Entry { impl DisplayBit for Option { fn display_bit(&self, _opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(ty) = self { - if ty.is_dir() { + if ty.is_char_device() { + f.write_str("c") + } else if ty.is_block_device() { + f.write_str("b") + } else if ty.is_directory() { f.write_str("d") } else if ty.is_symlink() { f.write_str("l") @@ -119,6 +131,40 @@ impl DisplayBit for Option { } } +#[cfg(any(target_os = "yggdrasil", rust_analyzer))] +impl FileTypeImpl for FileType { + fn is_directory(&self) -> bool { + FileType::is_dir(self) + } + + fn is_char_device(&self) -> bool { + use std::os::yggdrasil::fs::FileTypeExt; + FileTypeExt::is_char_device(self) + } + + fn is_block_device(&self) -> bool { + use std::os::yggdrasil::fs::FileTypeExt; + FileTypeExt::is_block_device(self) + } +} + +#[cfg(any(unix, rust_analyzer))] +impl FileTypeImpl for FileType { + fn is_directory(&self) -> bool { + FileType::is_dir(self) + } + + fn is_char_device(&self) -> bool { + use std::os::unix::fs::FileTypeExt; + FileTypeExt::is_char_device(self) + } + + fn is_block_device(&self) -> bool { + use std::os::unix::fs::FileTypeExt; + FileTypeExt::is_block_device(self) + } +} + #[cfg(any(target_os = "yggdrasil", rust_analyzer))] impl DisplayBit for std::os::yggdrasil::io::RawFileMode { fn display_bit(&self, _opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -176,6 +222,47 @@ impl DisplayBit for UnixFileMode { } } +impl Entry { + pub fn is_symlink(&self) -> bool { + self.ty.map_or(false, |d| d.is_symlink()) + } + + pub fn is_directory(&self) -> bool { + self.ty.map_or(false, |d| d.is_dir()) + } +} + +#[cfg(any(unix, rust_analyzer))] +impl Entry { + pub fn is_device(&self) -> bool { + self.ty + .map_or(false, |d| d.is_block_device() || d.is_char_device()) + } + + pub fn is_executable(&self) -> bool { + self.attrs + .as_ref() + .map(MetadataImpl::mode) + .map_or(false, |d| d.0 & 0o100 != 0) + } +} + +#[cfg(any(target_os = "yggdrasil", rust_analyzer))] +impl Entry { + pub fn is_device(&self) -> bool { + self.ty.map_or(false, |d| d.is_block_device() || d.is_char_device()) + } + + pub fn is_executable(&self) -> bool { + self.attrs + .as_ref() + .map(MetadataImpl::mode) + .map_or(false, |d| { + d.contains(std::os::yggdrasil::io::RawFileMode::USER_EXEC) + }) + } +} + #[cfg(any(unix, rust_analyzer))] impl MetadataImpl for Metadata { type Mode = UnixFileMode; @@ -272,6 +359,34 @@ impl DisplayBit for Option { } } +fn display_filename(f: &mut fmt::Formatter<'_>, entry: &Entry, color: bool) -> fmt::Result { + if !color { + return f.write_str(&entry.name); + } + + let color = if entry.is_device() { + // Yellow + Some(3) + } else if entry.is_symlink() { + // Purple + Some(5) + } else if entry.is_directory() { + // Cyan + Some(6) + } else if entry.is_executable() { + // Green + Some(2) + } else { + None + }; + + if let Some(color) = color { + write!(f, "\x1B[3{color}m{}\x1B[0m", &entry.name) + } else { + f.write_str(&entry.name) + } +} + impl DisplayBit for Entry { fn display_bit(&self, opts: &Args, f: &mut fmt::Formatter<'_>) -> fmt::Result { // let ino = self.attrs.as_ref().and_then(::inode); @@ -279,12 +394,13 @@ impl DisplayBit for Entry { if opts.long { write!( f, - "{}{} {}", + "{}{} ", self.ty.display_with(opts), self.attrs.display_with(opts), - self.name )?; + display_filename(f, self, opts.color)?; + if let Some(target) = self.target.as_ref() { write!(f, " -> {}", target)?; } @@ -302,7 +418,10 @@ impl DisplayBit for Entry { write!(f, "{:<8} ", "---")?; } } - f.write_str(&self.name) + + display_filename(f, self, opts.color)?; + + Ok(()) } } } @@ -431,7 +550,11 @@ fn run(opts: &Args) -> Vec> { } pub fn main() -> ExitCode { - let args = Args::parse(); + let mut args = Args::parse(); + + if !stdout().is_terminal() { + args.color = false; + } let results = run(&args); let code = match results.iter().any(|e| e.is_err()) {