use std::io::{self, Write};
use crate::strutil::truncate_max_bytes;
use crate::types::*;
pub type DeltaFn<'a> = Option<&'a dyn Fn(i32, &str, &str) -> DeltaStatus>;
pub struct Theme {
pub is_tty: bool,
}
impl Theme {
pub fn new(is_tty: bool) -> Self {
Self { is_tty }
}
pub fn reset(&self) -> &str {
if self.is_tty { "\x1b[0m" } else { "" }
}
pub fn cyan(&self) -> &str {
if self.is_tty { "\x1b[1;96m" } else { "" }
}
pub fn magenta(&self) -> &str {
if self.is_tty { "\x1b[1;95m" } else { "" }
}
pub fn green(&self) -> &str {
if self.is_tty { "\x1b[1;92m" } else { "" }
}
pub fn yellow(&self) -> &str {
if self.is_tty { "\x1b[1;93m" } else { "" }
}
pub fn red(&self) -> &str {
if self.is_tty { "\x1b[1;91m" } else { "" }
}
pub fn blue(&self) -> &str {
if self.is_tty { "\x1b[1;94m" } else { "" }
}
pub fn dim(&self) -> &str {
if self.is_tty { "\x1b[2m" } else { "" }
}
pub fn bold(&self) -> &str {
if self.is_tty { "\x1b[1m" } else { "" }
}
pub fn hdr_bg(&self) -> &str {
if self.is_tty { "\x1b[48;5;234m" } else { "" }
}
pub fn row_alt(&self) -> &str {
if self.is_tty { "\x1b[48;5;233m" } else { "" }
}
pub fn cmd_title(&self) -> &str {
if self.is_tty { "PROCESS" } else { "COMMAND" }
}
pub fn dev_title(&self) -> &str {
if self.is_tty { "DEV/ICE" } else { "DEVICE" }
}
pub fn fd_title(&self) -> &str {
"FD"
}
pub fn name_title(&self) -> &str {
if self.is_tty { "T4RGET" } else { "NAME" }
}
pub fn node_title(&self) -> &str {
if self.is_tty { "N0DE" } else { "NODE" }
}
pub fn pid_title(&self) -> &str {
if self.is_tty { "PRC" } else { "PID" }
}
pub fn size_off_title(&self) -> &str {
if self.is_tty { "BYT3/0FF" } else { "SIZE/OFF" }
}
pub fn type_title(&self) -> &str {
if self.is_tty { "CL4SS" } else { "TYPE" }
}
pub fn user_title(&self) -> &str {
if self.is_tty { "H4XOR" } else { "USER" }
}
pub fn pgid_title(&self) -> &str {
"PGID"
}
pub fn ppid_title(&self) -> &str {
if self.is_tty { "PPRC" } else { "PPID" }
}
}
#[derive(Default, Clone)]
pub struct DisplayOpts {
pub show_nlink: bool,
pub numeric_uid: bool,
pub offset_always: bool,
pub size_always: bool,
pub offset_digits: Option<usize>,
pub tcp_info: bool,
pub command_width: Option<usize>,
}
impl DisplayOpts {
pub fn from_args(args: &crate::cli::Args) -> Self {
Self {
show_nlink: args.list_nlink,
numeric_uid: args.numeric_uid,
offset_always: args.offset_always,
size_always: args.size_always,
offset_digits: args.offset_digits,
tcp_info: args.tcp_info,
command_width: args.command_width,
}
}
fn cmd_cap(&self) -> usize {
match self.command_width {
Some(0) => usize::MAX,
Some(w) => w,
None => 15,
}
}
fn size_off_cell(&self, f: &OpenFile) -> String {
if self.offset_always
&& let Some(off) = f.offset
{
return match self.offset_digits {
Some(d) => format!("0t{off:0width$}", width = d),
None => format!("0t{off}"),
};
}
if self.size_always
&& let Some(sz) = f.size
{
return format!("{sz}");
}
f.size_or_offset_str()
}
fn user_cell(&self, p: &Process) -> String {
if self.numeric_uid {
p.uid.to_string()
} else {
truncate_max_bytes(p.username(), 8).to_string()
}
}
}
struct ColWidths {
cmd: usize,
pid: usize,
user: usize,
fd: usize,
type_: usize,
device: usize,
size_off: usize,
node: usize,
nlink: usize,
pgid: usize,
ppid: usize,
}
impl ColWidths {
fn compute(procs: &[Process], show_pgid: bool, show_ppid: bool, disp: &DisplayOpts) -> Self {
let mut w = ColWidths {
cmd: 7, pid: 3, user: 4, fd: 2, type_: 4, device: 6, size_off: 8, node: 4, nlink: 5, pgid: 4, ppid: 4, };
let cmd_cap = disp.cmd_cap();
for p in procs {
w.cmd = w.cmd.max(p.command.len().min(cmd_cap));
w.pid = w.pid.max(p.pid.to_string().len());
w.user = w.user.max(disp.user_cell(p).len());
if show_pgid {
w.pgid = w.pgid.max(p.pgid.to_string().len());
}
if show_ppid {
w.ppid = w.ppid.max(p.ppid.to_string().len());
}
for f in &p.files {
let fd_str = f.fd.with_access(f.access);
w.fd = w.fd.max(fd_str.len());
w.type_ = w.type_.max(f.file_type.as_str().len());
w.device = w.device.max(f.device_str().len());
w.size_off = w.size_off.max(disp.size_off_cell(f).len());
w.node = w.node.max(f.node_str().len());
if disp.show_nlink {
w.nlink = w.nlink.max(f.nlink_str().len());
}
}
}
w
}
}
pub fn print_processes(
procs: &[Process],
theme: &Theme,
show_pgid: bool,
show_ppid: bool,
disp: &DisplayOpts,
delta_status: DeltaFn<'_>,
) {
let show_nlink = disp.show_nlink;
let w = ColWidths::compute(procs, show_pgid, show_ppid, disp);
let out = io::stdout();
let mut out = out.lock();
let _ = write!(
out,
"{bg}{bold}{cmd:<cw$} {pid:>pw$} ",
bg = theme.hdr_bg(),
bold = theme.bold(),
cmd = theme.cmd_title(),
cw = w.cmd,
pid = theme.pid_title(),
pw = w.pid,
);
if show_pgid {
let _ = write!(out, "{:>gw$} ", theme.pgid_title(), gw = w.pgid);
}
if show_ppid {
let _ = write!(out, "{:>rw$} ", theme.ppid_title(), rw = w.ppid);
}
let size_off_title = match (disp.offset_always, disp.size_always) {
(true, false) => "OFFSET",
(false, true) => "SIZE",
_ => theme.size_off_title(),
};
let _ = write!(
out,
"{user:<uw$} {fd:<fw$} {type_:<tw$} {dev:<dw$} {szoff:>sw$} {node:<nw$} ",
user = theme.user_title(),
uw = w.user,
fd = theme.fd_title(),
fw = w.fd,
type_ = theme.type_title(),
tw = w.type_,
dev = theme.dev_title(),
dw = w.device,
szoff = size_off_title,
sw = w.size_off,
node = theme.node_title(),
nw = w.node,
);
if show_nlink {
let _ = write!(out, "{:>lw$} ", "NLINK", lw = w.nlink);
}
let _ = writeln!(
out,
"{name}{reset}",
name = theme.name_title(),
reset = theme.reset(),
);
let mut row = 0usize;
for p in procs {
let user_display = disp.user_cell(p);
let cmd_display = truncate_max_bytes(&p.command, disp.cmd_cap());
let mut first = true;
for f in &p.files {
let alt = if row % 2 == 1 { theme.row_alt() } else { "" };
let fd_str = f.fd.with_access(f.access);
let type_str = f.file_type.as_str();
let dev_str = f.device_str();
let szoff_str = disp.size_off_cell(f);
let node_str = f.node_str();
let name_str = f.display_name();
let (prefix, suffix) = if let Some(ref classify) = delta_status {
let ds = classify(p.pid, &fd_str, &f.name);
match ds {
DeltaStatus::New => (theme.green(), theme.reset()),
DeltaStatus::Gone => (theme.red(), theme.reset()),
DeltaStatus::Unchanged => ("", ""),
}
} else {
("", "")
};
let _ = write!(out, "{prefix}{alt}");
if first {
let _ = write!(
out,
"{cyan}{cmd:<cw$}{reset} {mag}{pid:>pw$}{reset} ",
cyan = theme.cyan(),
cmd = cmd_display,
cw = w.cmd,
reset = theme.reset(),
mag = theme.magenta(),
pid = p.pid,
pw = w.pid,
);
if show_pgid {
let _ = write!(out, "{:>gw$} ", p.pgid, gw = w.pgid);
}
if show_ppid {
let _ = write!(out, "{:>rw$} ", p.ppid, rw = w.ppid);
}
let _ = write!(
out,
"{yellow}{user:<uw$}{reset} ",
yellow = theme.yellow(),
user = user_display,
uw = w.user,
reset = theme.reset(),
);
first = false;
} else {
let _ = write!(out, "{:<cw$} {:>pw$} ", "", "", cw = w.cmd, pw = w.pid,);
if show_pgid {
let _ = write!(out, "{:>gw$} ", "", gw = w.pgid);
}
if show_ppid {
let _ = write!(out, "{:>rw$} ", "", rw = w.ppid);
}
let _ = write!(out, "{:<uw$} ", "", uw = w.user);
}
let _ = write!(
out,
"{green}{fd:<fw$}{reset} {blue}{type_:<tw$}{reset} {dim}{dev:<dw$}{reset} {szoff:>sw$} {node:<nw$} ",
green = theme.green(),
fd = fd_str,
fw = w.fd,
reset = theme.reset(),
blue = theme.blue(),
type_ = type_str,
tw = w.type_,
dim = theme.dim(),
dev = dev_str,
dw = w.device,
szoff = szoff_str,
sw = w.size_off,
node = node_str,
nw = w.node,
);
if show_nlink {
let _ = write!(out, "{:>lw$} ", f.nlink_str(), lw = w.nlink);
}
let _ = writeln!(
out,
"{name}{suffix}{reset}",
name = name_str,
suffix = suffix,
reset = theme.reset(),
);
row += 1;
}
}
}
pub fn print_terse(procs: &[Process]) {
let out = io::stdout();
let mut out = out.lock();
for p in procs {
let _ = writeln!(out, "{}", p.pid);
}
}
pub fn print_field_output(procs: &[Process], fields: &str, terminator: char) {
let out = io::stdout();
write_field_output(&mut out.lock(), procs, fields, terminator);
}
pub fn write_field_output<W: io::Write>(
out: &mut W,
procs: &[Process],
fields: &str,
terminator: char,
) {
let wanted: Vec<char> = if fields.is_empty() {
ALL_FIELDS.chars().collect()
} else {
fields.chars().collect()
};
let on = |c: char| wanted.contains(&c);
for p in procs {
let _ = write!(out, "p{}{}", p.pid, terminator);
for fc in PROCESS_FIELD_ORDER.chars().filter(|&c| on(c)) {
let _ = match fc {
'g' => write!(out, "g{}{}", p.pgid, terminator),
'R' => write!(out, "R{}{}", p.ppid, terminator),
'c' => write!(out, "c{}{}", p.command, terminator),
'u' => write!(out, "u{}{}", p.uid, terminator),
'L' => write!(out, "L{}{}", p.username(), terminator),
_ => Ok(()),
};
}
for f in &p.files {
let _ = write!(out, "f{}{}", f.fd.as_display(), terminator);
for fc in FILE_FIELD_ORDER.chars().filter(|&c| on(c)) {
match fc {
'a' => {
let _ = write!(out, "a{}{}", f.access.as_char(), terminator);
}
'l' => {
let _ = write!(out, "l{}{}", f.lock, terminator);
}
't' if !f.file_type.as_str().is_empty() => {
let _ = write!(out, "t{}{}", f.file_type.as_str(), terminator);
}
'G' => {
if let Some(flags) = f.file_flags {
let _ = write!(
out,
"G0x{flags:x};0x{:x}{terminator}",
f.file_status.unwrap_or(0)
);
}
}
'D' => {
if let Some(dev) = f.raw_device() {
let _ = write!(out, "D0x{dev:x}{terminator}");
}
}
'd' => {
if f.device.is_none()
&& let Some(label) = &f.device_label
{
let _ = write!(out, "d{label}{terminator}");
}
}
'r' => {
if let Some(rdev) = f.raw_rdev() {
let _ = write!(out, "r0x{rdev:x}{terminator}");
}
}
's' => {
if let Some(sz) = f.size {
let _ = write!(out, "s{sz}{terminator}");
}
}
'o' => {
if let Some(off) = f.offset {
let _ = write!(out, "o0t{off}{terminator}");
}
}
'i' => {
if let Some(ino) = f.inode {
let _ = write!(out, "i{ino}{terminator}");
}
}
'k' => {
if let Some(n) = f.nlink {
let _ = write!(out, "k{n}{terminator}");
}
}
'n' => {
let _ = write!(out, "n{}{}", f.full_name(), terminator);
}
'P' => {
if let Some(ref si) = f.socket_info
&& !si.protocol.is_empty()
{
let _ = write!(out, "P{}{}", si.protocol, terminator);
}
}
'T' => {
if let Some(ref si) = f.socket_info
&& let Some(ref state) = si.tcp_state
{
let _ = write!(out, "TST={state}{terminator}");
if let Some(q) = si.recv_queue {
let _ = write!(out, "TQR={q}{terminator}");
}
if let Some(q) = si.send_queue {
let _ = write!(out, "TQS={q}{terminator}");
}
}
}
_ => {}
}
}
}
if terminator == '\0' {
let _ = writeln!(out);
}
}
}
const PROCESS_FIELD_ORDER: &str = "gRcuL";
const FILE_FIELD_ORDER: &str = "altGDdsoikPnT";
pub const ALL_FIELDS: &str = "acCdDfGgiklLnNoPrRsStTu";
pub const FIELD_HELP: &[(char, &str)] = &[
('a', "access: r = read; w = write; u = read/write"),
('c', "command name"),
('d', "device character code"),
('D', "major/minor device number as 0x<hex>"),
('f', "file descriptor (always selected)"),
('G', "file flaGs"),
('g', "process group ID (PGID)"),
('i', "inode number"),
('k', "link count"),
('l', "lock: r/R = read; w/W = write; u = read/write"),
('L', "login name"),
('n', "comment, name, Internet addresses"),
('o', "file offset as 0t<dec> or 0x<hex>"),
('p', "process ID (PID)"),
('P', "protocol name"),
('r', "raw device number as 0x<hex>"),
('R', "paRent PID"),
('s', "file size"),
('t', "file type"),
('T', "TCP/TPI info"),
('u', "user ID (UID)"),
('0', "(zero) use NUL field terminator instead of NL"),
];
pub fn print_field_help() {
println!("lsofrs:\tID field description");
for (id, desc) in FIELD_HELP {
println!("\t {id} {desc}");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn theme_tty_has_ansi_codes() {
let t = Theme::new(true);
assert!(t.reset().contains("\x1b["));
assert!(t.cyan().contains("\x1b["));
assert!(t.magenta().contains("\x1b["));
assert!(t.green().contains("\x1b["));
assert!(t.yellow().contains("\x1b["));
assert!(t.red().contains("\x1b["));
assert!(t.blue().contains("\x1b["));
assert!(t.dim().contains("\x1b["));
assert!(t.bold().contains("\x1b["));
assert!(t.hdr_bg().contains("\x1b["));
assert!(t.row_alt().contains("\x1b["));
}
#[test]
fn theme_no_tty_empty_strings() {
let t = Theme::new(false);
assert_eq!(t.reset(), "");
assert_eq!(t.cyan(), "");
assert_eq!(t.magenta(), "");
assert_eq!(t.green(), "");
assert_eq!(t.yellow(), "");
assert_eq!(t.red(), "");
assert_eq!(t.blue(), "");
assert_eq!(t.dim(), "");
assert_eq!(t.bold(), "");
assert_eq!(t.hdr_bg(), "");
assert_eq!(t.row_alt(), "");
}
#[test]
fn theme_tty_cyberpunk_titles() {
let t = Theme::new(true);
assert_eq!(t.cmd_title(), "PROCESS");
assert_eq!(t.pid_title(), "PRC");
assert_eq!(t.user_title(), "H4XOR");
assert_eq!(t.type_title(), "CL4SS");
assert_eq!(t.dev_title(), "DEV/ICE");
assert_eq!(t.size_off_title(), "BYT3/0FF");
assert_eq!(t.node_title(), "N0DE");
assert_eq!(t.name_title(), "T4RGET");
assert_eq!(t.ppid_title(), "PPRC");
}
#[test]
fn theme_pipe_plain_titles() {
let t = Theme::new(false);
assert_eq!(t.cmd_title(), "COMMAND");
assert_eq!(t.pid_title(), "PID");
assert_eq!(t.user_title(), "USER");
assert_eq!(t.type_title(), "TYPE");
assert_eq!(t.dev_title(), "DEVICE");
assert_eq!(t.size_off_title(), "SIZE/OFF");
assert_eq!(t.node_title(), "NODE");
assert_eq!(t.name_title(), "NAME");
assert_eq!(t.ppid_title(), "PPID");
}
#[test]
fn theme_fd_and_pgid_titles_same() {
let tty = Theme::new(true);
let pipe = Theme::new(false);
assert_eq!(tty.fd_title(), "FD");
assert_eq!(pipe.fd_title(), "FD");
assert_eq!(tty.pgid_title(), "PGID");
assert_eq!(pipe.pgid_title(), "PGID");
}
fn make_proc(pid: i32, cmd: &str, files: Vec<OpenFile>) -> Process {
Process::new(pid, 1, pid, 0, cmd.to_string(), files)
}
fn make_file(fd: i32, ft: FileType, name: &str) -> OpenFile {
OpenFile {
fd: FdName::Number(fd),
access: Access::ReadWrite,
file_type: ft,
name: name.to_string(),
..Default::default()
}
}
#[test]
fn col_widths_defaults_on_empty() {
let w = ColWidths::compute(&[], false, false, &DisplayOpts::default());
assert!(w.cmd >= 7);
assert!(w.pid >= 3);
}
#[test]
fn col_widths_grows_for_long_pid() {
let p = make_proc(1234567, "test", vec![make_file(3, FileType::Reg, "/x")]);
let w = ColWidths::compute(&[p], false, false, &DisplayOpts::default());
assert!(w.pid >= 7); }
#[test]
fn col_widths_cmd_capped_at_15() {
let p = make_proc(1, "a_very_long_command_name_here", vec![]);
let w = ColWidths::compute(&[p], false, false, &DisplayOpts::default());
assert!(w.cmd <= 15);
}
#[test]
fn col_widths_pgid_only_with_flag() {
let p = make_proc(1, "test", vec![]);
let w_no = ColWidths::compute(
std::slice::from_ref(&p),
false,
false,
&DisplayOpts::default(),
);
let w_yes = ColWidths::compute(
std::slice::from_ref(&p),
true,
false,
&DisplayOpts::default(),
);
assert_eq!(w_no.pgid, 4); assert!(w_yes.pgid >= 1);
}
#[test]
fn col_widths_ppid_only_with_flag() {
let p = Process::new(1, 123456, 1, 0, "x".to_string(), vec![]);
let w_no = ColWidths::compute(
std::slice::from_ref(&p),
false,
false,
&DisplayOpts::default(),
);
let w_yes = ColWidths::compute(
std::slice::from_ref(&p),
false,
true,
&DisplayOpts::default(),
);
assert_eq!(w_no.ppid, 4);
assert!(w_yes.ppid >= 6);
}
#[test]
fn col_widths_fd_grows_for_cwd_token() {
let f = OpenFile {
fd: FdName::Cwd,
access: Access::ReadWrite,
file_type: FileType::Dir,
name: "/".to_string(),
..Default::default()
};
let p = make_proc(1, "x", vec![f]);
let w = ColWidths::compute(
std::slice::from_ref(&p),
false,
false,
&DisplayOpts::default(),
);
assert!(w.fd >= "cwd".len());
}
#[test]
fn col_widths_type_grows_for_unknown_long_label() {
let f = OpenFile {
fd: FdName::Number(0),
access: Access::Read,
file_type: FileType::Unknown("VERYLONGTYPE".to_string()),
name: "/x".to_string(),
..Default::default()
};
let p = make_proc(1, "x", vec![f]);
let w = ColWidths::compute(
std::slice::from_ref(&p),
false,
false,
&DisplayOpts::default(),
);
assert!(w.type_ >= "VERYLONGTYPE".len());
}
#[test]
fn col_widths_size_off_includes_0t_prefix_for_offset() {
let mut f = make_file(1, FileType::Reg, "/x");
f.offset = Some(4096);
let p = make_proc(1, "x", vec![f]);
let w = ColWidths::compute(
std::slice::from_ref(&p),
false,
false,
&DisplayOpts::default(),
);
assert!(w.size_off >= "0t4096".len());
}
#[test]
fn col_widths_node_from_protocol_when_no_inode() {
let mut f = make_file(5, FileType::IPv4, "*:80");
f.socket_info = Some(crate::types::SocketInfo {
protocol: "TCP".to_string(),
..Default::default()
});
let p = make_proc(1, "x", vec![f]);
let w = ColWidths::compute(
std::slice::from_ref(&p),
false,
false,
&DisplayOpts::default(),
);
assert!(w.node >= "TCP".len());
}
#[test]
fn print_processes_empty_no_panic() {
let theme = Theme::new(false);
print_processes(&[], &theme, false, false, &DisplayOpts::default(), None);
}
#[test]
fn col_widths_nlink_only_with_flag() {
let mut f = make_file(3, FileType::Reg, "/x");
f.nlink = Some(123456);
let p = make_proc(1, "x", vec![f]);
let w_no = ColWidths::compute(
std::slice::from_ref(&p),
false,
false,
&DisplayOpts::default(),
);
let w_yes = ColWidths::compute(
std::slice::from_ref(&p),
false,
false,
&DisplayOpts {
show_nlink: true,
..Default::default()
},
);
assert_eq!(w_no.nlink, 5); assert!(w_yes.nlink >= "123456".len());
}
#[test]
fn display_opts_size_off_cell_offset_always() {
let mut f = make_file(3, FileType::Reg, "/x");
f.size = Some(4096);
f.offset = Some(128);
let disp = DisplayOpts {
offset_always: true,
..Default::default()
};
assert_eq!(disp.size_off_cell(&f), "0t128");
let padded = DisplayOpts {
offset_always: true,
offset_digits: Some(9),
..Default::default()
};
assert_eq!(padded.size_off_cell(&f), "0t000000128");
}
#[test]
fn display_opts_size_off_cell_size_always() {
let mut f = make_file(3, FileType::Reg, "/x");
f.size = Some(4096);
f.offset = Some(128);
let disp = DisplayOpts {
size_always: true,
..Default::default()
};
assert_eq!(disp.size_off_cell(&f), "4096");
}
#[test]
fn display_opts_user_cell_numeric_uid() {
let p = Process::new(10, 1, 10, 501, "x".to_string(), vec![]);
let numeric = DisplayOpts {
numeric_uid: true,
..Default::default()
};
assert_eq!(numeric.user_cell(&p), "501");
let named = DisplayOpts::default();
assert!(!named.user_cell(&p).is_empty());
}
#[test]
fn display_opts_offset_always_falls_back_when_no_offset() {
let mut f = make_file(3, FileType::Reg, "/x");
f.size = Some(512);
f.offset = None;
let disp = DisplayOpts {
offset_always: true,
..Default::default()
};
assert_eq!(disp.size_off_cell(&f), "512");
}
#[test]
fn display_opts_size_always_falls_back_when_no_size() {
let mut f = make_file(3, FileType::Reg, "/x");
f.size = None;
f.offset = Some(64);
let disp = DisplayOpts {
size_always: true,
..Default::default()
};
assert_eq!(disp.size_off_cell(&f), "0t64");
}
#[test]
fn display_opts_from_args_maps_all_flags() {
let args = crate::cli::Args::parse_from(["lsofrs", "+L", "-l", "-o", "-s", "-T", "+c0"]);
let disp = DisplayOpts::from_args(&args);
assert!(disp.show_nlink);
assert!(disp.numeric_uid);
assert!(disp.offset_always);
assert!(disp.size_always);
assert!(disp.tcp_info);
assert_eq!(disp.command_width, Some(0));
}
#[test]
fn display_opts_cmd_cap_unlimited_and_default() {
assert_eq!(DisplayOpts::default().cmd_cap(), 15);
let unlimited = DisplayOpts {
command_width: Some(0),
..Default::default()
};
assert_eq!(unlimited.cmd_cap(), usize::MAX);
let fixed = DisplayOpts {
command_width: Some(30),
..Default::default()
};
assert_eq!(fixed.cmd_cap(), 30);
}
#[test]
fn print_processes_with_nlink_column_no_panic() {
let theme = Theme::new(false);
let mut f = make_file(3, FileType::Reg, "/tmp/x");
f.nlink = Some(0);
let procs = vec![make_proc(42, "test", vec![f])];
print_processes(
&procs,
&theme,
false,
false,
&DisplayOpts {
show_nlink: true,
..Default::default()
},
None,
);
}
#[test]
fn print_processes_with_data_no_panic() {
let theme = Theme::new(false);
let procs = vec![make_proc(
42,
"test",
vec![make_file(3, FileType::Reg, "/tmp/x")],
)];
print_processes(&procs, &theme, false, false, &DisplayOpts::default(), None);
}
#[test]
fn print_processes_tty_theme_no_panic() {
let theme = Theme::new(true);
let procs = vec![make_proc(
42,
"test",
vec![make_file(3, FileType::Reg, "/tmp/x")],
)];
print_processes(&procs, &theme, false, false, &DisplayOpts::default(), None);
}
#[test]
fn print_processes_with_pgid_ppid_no_panic() {
let theme = Theme::new(false);
let procs = vec![make_proc(
42,
"test",
vec![make_file(3, FileType::Reg, "/tmp/x")],
)];
print_processes(&procs, &theme, true, true, &DisplayOpts::default(), None);
}
#[test]
fn print_processes_with_delta_no_panic() {
let theme = Theme::new(false);
let procs = vec![make_proc(
42,
"test",
vec![make_file(3, FileType::Reg, "/tmp/x")],
)];
let delta = |_pid: i32, _fd: &str, _name: &str| DeltaStatus::New;
print_processes(
&procs,
&theme,
false,
false,
&DisplayOpts::default(),
Some(&delta),
);
}
#[test]
fn print_processes_with_delta_gone_no_panic() {
let theme = Theme::new(false);
let procs = vec![make_proc(
42,
"test",
vec![make_file(3, FileType::Reg, "/tmp/x")],
)];
let delta = |_pid: i32, _fd: &str, _name: &str| DeltaStatus::Gone;
print_processes(
&procs,
&theme,
false,
false,
&DisplayOpts::default(),
Some(&delta),
);
}
#[test]
fn print_processes_with_delta_unchanged_no_panic() {
let theme = Theme::new(false);
let procs = vec![make_proc(
42,
"test",
vec![make_file(3, FileType::Reg, "/tmp/x")],
)];
let delta = |_pid: i32, _fd: &str, _name: &str| DeltaStatus::Unchanged;
print_processes(
&procs,
&theme,
false,
false,
&DisplayOpts::default(),
Some(&delta),
);
}
#[test]
fn print_terse_no_panic() {
let procs = vec![make_proc(1, "a", vec![]), make_proc(2, "b", vec![])];
print_terse(&procs);
}
#[test]
fn print_terse_duplicate_pid_no_panic() {
let procs = vec![make_proc(42, "x", vec![]), make_proc(42, "y", vec![])];
print_terse(&procs);
}
fn capture_field_output(procs: &[Process], fields: &str, terminator: char) -> String {
let mut buf: Vec<u8> = Vec::new();
write_field_output(&mut buf, procs, fields, terminator);
String::from_utf8(buf).expect("field output is UTF-8")
}
#[test]
fn field_output_always_emits_pid_and_bare_fd() {
let mut f = OpenFile {
fd: FdName::Number(7),
access: Access::Write,
file_type: FileType::Reg,
name: "/tmp/x".to_string(),
..Default::default()
};
f.device = Some((1, 13));
let p = Process::new(42, 1, 1, 501, "cmd".into(), vec![f]);
let out = capture_field_output(std::slice::from_ref(&p), "n", '\n');
assert!(out.starts_with("p42\n"), "pid field missing: {out:?}");
assert!(out.contains("\nf7\n"), "bare fd field missing: {out:?}");
assert!(
!out.contains("f7w"),
"access must not ride on the fd: {out:?}"
);
let with_access = capture_field_output(&[p], "a", '\n');
assert!(with_access.contains("\naw\n"), "{with_access:?}");
}
#[test]
fn field_output_device_is_the_raw_dev_t() {
let f = OpenFile {
fd: FdName::Number(3),
file_type: FileType::Reg,
device: Some((1, 13)),
..Default::default()
};
let p = Process::new(42, 1, 1, 501, "cmd".into(), vec![f]);
let out = capture_field_output(&[p], "D", '\n');
let expected = format!("D0x{:x}", crate::types::make_dev(1, 13));
assert!(out.contains(&expected), "{out:?} lacks {expected}");
}
#[test]
fn field_output_keeps_tcp_state_out_of_the_name() {
let f = OpenFile {
fd: FdName::Number(3),
file_type: FileType::IPv4,
name: "1.2.3.4:80".to_string(),
socket_info: Some(crate::types::SocketInfo {
protocol: "TCP".to_string(),
tcp_state: Some(TcpState::Listen),
..Default::default()
}),
..Default::default()
};
assert_eq!(f.full_name(), "1.2.3.4:80");
assert_eq!(f.display_name(), "1.2.3.4:80 (LISTEN)");
let p = Process::new(42, 1, 1, 501, "cmd".into(), vec![f]);
let out = capture_field_output(&[p], "nT", '\n');
assert!(out.contains("n1.2.3.4:80\n"), "{out:?}");
assert!(out.contains("TST=LISTEN\n"), "{out:?}");
}
#[test]
fn print_field_output_no_panic() {
let procs = vec![make_proc(
42,
"test",
vec![make_file(3, FileType::Reg, "/tmp/x")],
)];
print_field_output(&procs, "pcfnta", '\n');
}
#[test]
fn print_field_output_empty_fields_uses_defaults() {
let procs = vec![make_proc(
42,
"test",
vec![make_file(3, FileType::Reg, "/tmp/x")],
)];
print_field_output(&procs, "", '\n');
}
#[test]
fn print_field_output_all_fields_no_panic() {
let mut f = make_file(3, FileType::Reg, "/tmp/x");
f.device = Some((1, 16));
f.size = Some(4096);
f.offset = Some(0);
f.inode = Some(12345);
f.socket_info = Some(crate::types::SocketInfo {
protocol: "TCP".to_string(),
tcp_state: Some(TcpState::Established),
..Default::default()
});
let procs = vec![make_proc(42, "test", vec![f])];
print_field_output(&procs, "pcfntaDsoiPTguRL", '\n');
}
#[test]
fn print_field_output_nul_terminator_no_panic() {
let procs = vec![make_proc(
42,
"test",
vec![make_file(3, FileType::Reg, "/tmp/x")],
)];
print_field_output(&procs, "pfn", '\0');
}
#[test]
fn print_field_output_unknown_chars_ignored_no_panic() {
let procs = vec![make_proc(
42,
"test",
vec![make_file(3, FileType::Reg, "/tmp/x")],
)];
print_field_output(&procs, "pfx!z", '\n');
}
#[test]
fn print_field_output_pgid_ppid_username_no_panic() {
let mut p = make_proc(7, "cmd", vec![make_file(3, FileType::Reg, "/x")]);
p.pgid = 99;
p.ppid = 100;
p.uid = 501;
print_field_output(std::slice::from_ref(&p), "pgRuL", '\n');
}
#[test]
fn theme_plain_column_titles_when_not_tty() {
let t = Theme::new(false);
assert_eq!(t.cmd_title(), "COMMAND");
assert_eq!(t.name_title(), "NAME");
assert_eq!(t.pid_title(), "PID");
}
#[test]
fn theme_tty_column_titles() {
let t = Theme::new(true);
assert_eq!(t.cmd_title(), "PROCESS");
assert_eq!(t.name_title(), "T4RGET");
assert_eq!(t.pid_title(), "PRC");
}
#[test]
fn theme_hdr_bg_empty_when_not_tty() {
let t = Theme::new(false);
assert_eq!(t.hdr_bg(), "");
}
#[test]
fn theme_row_alt_empty_when_not_tty() {
let t = Theme::new(false);
assert_eq!(t.row_alt(), "");
}
#[test]
fn theme_color_codes_empty_when_not_tty() {
let t = Theme::new(false);
assert_eq!(t.reset(), "");
assert_eq!(t.cyan(), "");
assert_eq!(t.magenta(), "");
assert_eq!(t.green(), "");
assert_eq!(t.yellow(), "");
assert_eq!(t.red(), "");
assert_eq!(t.blue(), "");
assert_eq!(t.dim(), "");
assert_eq!(t.bold(), "");
}
}