diff --git a/src/uu/who/src/platform/mod.rs b/src/uu/who/src/platform/mod.rs new file mode 100644 index 000000000..e0e87dca1 --- /dev/null +++ b/src/uu/who/src/platform/mod.rs @@ -0,0 +1,14 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +#[cfg(not(target_os = "openbsd"))] +mod unix; +#[cfg(not(target_os = "openbsd"))] +pub use self::unix::*; + +#[cfg(target_os = "openbsd")] +mod openbsd; +#[cfg(target_os = "openbsd")] +pub use self::openbsd::*; diff --git a/src/uu/who/src/platform/openbsd.rs b/src/uu/who/src/platform/openbsd.rs new file mode 100644 index 000000000..7e6970c1f --- /dev/null +++ b/src/uu/who/src/platform/openbsd.rs @@ -0,0 +1,17 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// Specific implementation for OpenBSD: tool unsupported (utmpx not supported) + +use crate::uu_app; + +use uucore::error::UResult; + +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let _matches = uu_app().try_get_matches_from(args)?; + + println!("unsupported command on OpenBSD"); + Ok(()) +} diff --git a/src/uu/who/src/platform/unix.rs b/src/uu/who/src/platform/unix.rs new file mode 100644 index 000000000..17eaa9af9 --- /dev/null +++ b/src/uu/who/src/platform/unix.rs @@ -0,0 +1,432 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore (ToDO) ttyname hostnames runlevel mesg wtmp statted boottime deadprocs initspawn clockchange curr runlvline pidstr exitstr hoststr + +use crate::options; +use crate::uu_app; + +use uucore::display::Quotable; +use uucore::error::{FromIo, UResult}; +use uucore::libc::{ttyname, STDIN_FILENO, S_IWGRP}; +use uucore::utmpx::{self, time, Utmpx}; + +use std::borrow::Cow; +use std::ffi::CStr; +use std::fmt::Write; +use std::os::unix::fs::MetadataExt; +use std::path::PathBuf; + +fn get_long_usage() -> String { + format!( + "If FILE is not specified, use {}. /var/log/wtmp as FILE is common.\n\ + If ARG1 ARG2 given, -m presumed: 'am i' or 'mom likes' are usual.", + utmpx::DEFAULT_FILE, + ) +} + +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let args = args.collect_ignore(); + + let matches = uu_app() + .after_help(get_long_usage()) + .try_get_matches_from(args)?; + + let files: Vec = matches + .get_many::(options::FILE) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); + + // If true, attempt to canonicalize hostnames via a DNS lookup. + let do_lookup = matches.get_flag(options::LOOKUP); + + // If true, display only a list of usernames and count of + // the users logged on. + // Ignored for 'who am i'. + let short_list = matches.get_flag(options::COUNT); + + let all = matches.get_flag(options::ALL); + + // If true, display a line at the top describing each field. + let include_heading = matches.get_flag(options::HEADING); + + // If true, display a '+' for each user if mesg y, a '-' if mesg n, + // or a '?' if their tty cannot be statted. + let include_mesg = all || matches.get_flag(options::MESG); + + // If true, display the last boot time. + let need_boottime = all || matches.get_flag(options::BOOT); + + // If true, display dead processes. + let need_deadprocs = all || matches.get_flag(options::DEAD); + + // If true, display processes waiting for user login. + let need_login = all || matches.get_flag(options::LOGIN); + + // If true, display processes started by init. + let need_initspawn = all || matches.get_flag(options::PROCESS); + + // If true, display the last clock change. + let need_clockchange = all || matches.get_flag(options::TIME); + + // If true, display the current runlevel. + let need_runlevel = all || matches.get_flag(options::RUNLEVEL); + + let use_defaults = !(all + || need_boottime + || need_deadprocs + || need_login + || need_initspawn + || need_runlevel + || need_clockchange + || matches.get_flag(options::USERS)); + + // If true, display user processes. + let need_users = all || matches.get_flag(options::USERS) || use_defaults; + + // If true, display the hours:minutes since each user has touched + // the keyboard, or "." if within the last minute, or "old" if + // not within the last day. + let include_idle = need_deadprocs || need_login || need_runlevel || need_users; + + // If true, display process termination & exit status. + let include_exit = need_deadprocs; + + // If true, display only name, line, and time fields. + let short_output = !include_exit && use_defaults; + + // If true, display info only for the controlling tty. + let my_line_only = matches.get_flag(options::ONLY_HOSTNAME_USER) || files.len() == 2; + + let mut who = Who { + do_lookup, + short_list, + short_output, + include_idle, + include_heading, + include_mesg, + include_exit, + need_boottime, + need_deadprocs, + need_login, + need_initspawn, + need_clockchange, + need_runlevel, + need_users, + my_line_only, + args: files, + }; + + who.exec() +} + +struct Who { + do_lookup: bool, + short_list: bool, + short_output: bool, + include_idle: bool, + include_heading: bool, + include_mesg: bool, + include_exit: bool, + need_boottime: bool, + need_deadprocs: bool, + need_login: bool, + need_initspawn: bool, + need_clockchange: bool, + need_runlevel: bool, + need_users: bool, + my_line_only: bool, + args: Vec, +} + +fn idle_string<'a>(when: i64, boottime: i64) -> Cow<'a, str> { + thread_local! { + static NOW: time::OffsetDateTime = time::OffsetDateTime::now_local().unwrap(); + } + NOW.with(|n| { + let now = n.unix_timestamp(); + if boottime < when && now - 24 * 3600 < when && when <= now { + let seconds_idle = now - when; + if seconds_idle < 60 { + " . ".into() + } else { + format!( + "{:02}:{:02}", + seconds_idle / 3600, + (seconds_idle % 3600) / 60 + ) + .into() + } + } else { + " old ".into() + } + }) +} + +fn time_string(ut: &Utmpx) -> String { + // "%b %e %H:%M" + let time_format: Vec = + time::format_description::parse("[month repr:short] [day padding:space] [hour]:[minute]") + .unwrap(); + ut.login_time().format(&time_format).unwrap() // LC_ALL=C +} + +#[inline] +fn current_tty() -> String { + unsafe { + let res = ttyname(STDIN_FILENO); + if res.is_null() { + String::new() + } else { + CStr::from_ptr(res as *const _) + .to_string_lossy() + .trim_start_matches("/dev/") + .to_owned() + } + } +} + +impl Who { + #[allow(clippy::cognitive_complexity)] + fn exec(&mut self) -> UResult<()> { + let run_level_chk = |_record: i16| { + #[cfg(not(target_os = "linux"))] + return false; + + #[cfg(target_os = "linux")] + return _record == utmpx::RUN_LVL; + }; + + let f = if self.args.len() == 1 { + self.args[0].as_ref() + } else { + utmpx::DEFAULT_FILE + }; + if self.short_list { + let users = Utmpx::iter_all_records_from(f) + .filter(Utmpx::is_user_process) + .map(|ut| ut.user()) + .collect::>(); + println!("{}", users.join(" ")); + println!("# users={}", users.len()); + } else { + let records = Utmpx::iter_all_records_from(f); + + if self.include_heading { + self.print_heading(); + } + let cur_tty = if self.my_line_only { + current_tty() + } else { + String::new() + }; + + for ut in records { + if !self.my_line_only || cur_tty == ut.tty_device() { + if self.need_users && ut.is_user_process() { + self.print_user(&ut)?; + } else if self.need_runlevel && run_level_chk(ut.record_type()) { + if cfg!(target_os = "linux") { + self.print_runlevel(&ut); + } + } else if self.need_boottime && ut.record_type() == utmpx::BOOT_TIME { + self.print_boottime(&ut); + } else if self.need_clockchange && ut.record_type() == utmpx::NEW_TIME { + self.print_clockchange(&ut); + } else if self.need_initspawn && ut.record_type() == utmpx::INIT_PROCESS { + self.print_initspawn(&ut); + } else if self.need_login && ut.record_type() == utmpx::LOGIN_PROCESS { + self.print_login(&ut); + } else if self.need_deadprocs && ut.record_type() == utmpx::DEAD_PROCESS { + self.print_deadprocs(&ut); + } + } + + if ut.record_type() == utmpx::BOOT_TIME {} + } + } + Ok(()) + } + + #[inline] + fn print_runlevel(&self, ut: &Utmpx) { + let last = (ut.pid() / 256) as u8 as char; + let curr = (ut.pid() % 256) as u8 as char; + let runlvline = format!("run-level {curr}"); + let comment = format!("last={}", if last == 'N' { 'S' } else { 'N' }); + + self.print_line( + "", + ' ', + &runlvline, + &time_string(ut), + "", + "", + if last.is_control() { "" } else { &comment }, + "", + ); + } + + #[inline] + fn print_clockchange(&self, ut: &Utmpx) { + self.print_line("", ' ', "clock change", &time_string(ut), "", "", "", ""); + } + + #[inline] + fn print_login(&self, ut: &Utmpx) { + let comment = format!("id={}", ut.terminal_suffix()); + let pidstr = format!("{}", ut.pid()); + self.print_line( + "LOGIN", + ' ', + &ut.tty_device(), + &time_string(ut), + "", + &pidstr, + &comment, + "", + ); + } + + #[inline] + fn print_deadprocs(&self, ut: &Utmpx) { + let comment = format!("id={}", ut.terminal_suffix()); + let pidstr = format!("{}", ut.pid()); + let e = ut.exit_status(); + let exitstr = format!("term={} exit={}", e.0, e.1); + self.print_line( + "", + ' ', + &ut.tty_device(), + &time_string(ut), + "", + &pidstr, + &comment, + &exitstr, + ); + } + + #[inline] + fn print_initspawn(&self, ut: &Utmpx) { + let comment = format!("id={}", ut.terminal_suffix()); + let pidstr = format!("{}", ut.pid()); + self.print_line( + "", + ' ', + &ut.tty_device(), + &time_string(ut), + "", + &pidstr, + &comment, + "", + ); + } + + #[inline] + fn print_boottime(&self, ut: &Utmpx) { + self.print_line("", ' ', "system boot", &time_string(ut), "", "", "", ""); + } + + fn print_user(&self, ut: &Utmpx) -> UResult<()> { + let mut p = PathBuf::from("/dev"); + p.push(ut.tty_device().as_str()); + let mesg; + let last_change; + match p.metadata() { + Ok(meta) => { + #[cfg(all( + not(target_os = "android"), + not(target_os = "freebsd"), + not(target_vendor = "apple") + ))] + let iwgrp = S_IWGRP; + #[cfg(any(target_os = "android", target_os = "freebsd", target_vendor = "apple"))] + let iwgrp = S_IWGRP as u32; + mesg = if meta.mode() & iwgrp == 0 { '-' } else { '+' }; + last_change = meta.atime(); + } + _ => { + mesg = '?'; + last_change = 0; + } + } + + let idle = if last_change == 0 { + " ?".into() + } else { + idle_string(last_change, 0) + }; + + let s = if self.do_lookup { + ut.canon_host().map_err_context(|| { + let host = ut.host(); + format!( + "failed to canonicalize {}", + host.split(':').next().unwrap_or(&host).quote() + ) + })? + } else { + ut.host() + }; + let hoststr = if s.is_empty() { s } else { format!("({s})") }; + + self.print_line( + ut.user().as_ref(), + mesg, + ut.tty_device().as_ref(), + time_string(ut).as_str(), + idle.as_ref(), + format!("{}", ut.pid()).as_str(), + hoststr.as_str(), + "", + ); + + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn print_line( + &self, + user: &str, + state: char, + line: &str, + time: &str, + idle: &str, + pid: &str, + comment: &str, + exit: &str, + ) { + let mut buf = String::with_capacity(64); + let msg = vec![' ', state].into_iter().collect::(); + + write!(buf, "{user:<8}").unwrap(); + if self.include_mesg { + buf.push_str(&msg); + } + write!(buf, " {line:<12}").unwrap(); + // "%b %e %H:%M" (LC_ALL=C) + let time_size = 3 + 2 + 2 + 1 + 2; + write!(buf, " {time:10}").unwrap(); + } + write!(buf, " {comment:<8}").unwrap(); + if self.include_exit { + write!(buf, " {exit:<12}").unwrap(); + } + println!("{}", buf.trim_end()); + } + + #[inline] + fn print_heading(&self) { + self.print_line( + "NAME", ' ', "LINE", "TIME", "IDLE", "PID", "COMMENT", "EXIT", + ); + } +} diff --git a/src/uu/who/src/who.rs b/src/uu/who/src/who.rs index 5d952efff..1eb28e874 100644 --- a/src/uu/who/src/who.rs +++ b/src/uu/who/src/who.rs @@ -5,19 +5,11 @@ // spell-checker:ignore (ToDO) ttyname hostnames runlevel mesg wtmp statted boottime deadprocs initspawn clockchange curr runlvline pidstr exitstr hoststr -use uucore::display::Quotable; -use uucore::error::{FromIo, UResult}; -use uucore::libc::{ttyname, STDIN_FILENO, S_IWGRP}; -use uucore::utmpx::{self, time, Utmpx}; - use clap::{crate_version, Arg, ArgAction, Command}; -use std::borrow::Cow; -use std::ffi::CStr; -use std::fmt::Write; -use std::os::unix::fs::MetadataExt; -use std::path::PathBuf; use uucore::{format_usage, help_about, help_usage}; +mod platform; + mod options { pub const ALL: &str = "all"; pub const BOOT: &str = "boot"; @@ -44,109 +36,8 @@ static RUNLEVEL_HELP: &str = "print current runlevel"; #[cfg(not(target_os = "linux"))] static RUNLEVEL_HELP: &str = "print current runlevel (This is meaningless on non Linux)"; -fn get_long_usage() -> String { - format!( - "If FILE is not specified, use {}. /var/log/wtmp as FILE is common.\n\ - If ARG1 ARG2 given, -m presumed: 'am i' or 'mom likes' are usual.", - utmpx::DEFAULT_FILE, - ) -} - #[uucore::main] -pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let args = args.collect_ignore(); - - let matches = uu_app() - .after_help(get_long_usage()) - .try_get_matches_from(args)?; - - let files: Vec = matches - .get_many::(options::FILE) - .map(|v| v.map(ToString::to_string).collect()) - .unwrap_or_default(); - - // If true, attempt to canonicalize hostnames via a DNS lookup. - let do_lookup = matches.get_flag(options::LOOKUP); - - // If true, display only a list of usernames and count of - // the users logged on. - // Ignored for 'who am i'. - let short_list = matches.get_flag(options::COUNT); - - let all = matches.get_flag(options::ALL); - - // If true, display a line at the top describing each field. - let include_heading = matches.get_flag(options::HEADING); - - // If true, display a '+' for each user if mesg y, a '-' if mesg n, - // or a '?' if their tty cannot be statted. - let include_mesg = all || matches.get_flag(options::MESG); - - // If true, display the last boot time. - let need_boottime = all || matches.get_flag(options::BOOT); - - // If true, display dead processes. - let need_deadprocs = all || matches.get_flag(options::DEAD); - - // If true, display processes waiting for user login. - let need_login = all || matches.get_flag(options::LOGIN); - - // If true, display processes started by init. - let need_initspawn = all || matches.get_flag(options::PROCESS); - - // If true, display the last clock change. - let need_clockchange = all || matches.get_flag(options::TIME); - - // If true, display the current runlevel. - let need_runlevel = all || matches.get_flag(options::RUNLEVEL); - - let use_defaults = !(all - || need_boottime - || need_deadprocs - || need_login - || need_initspawn - || need_runlevel - || need_clockchange - || matches.get_flag(options::USERS)); - - // If true, display user processes. - let need_users = all || matches.get_flag(options::USERS) || use_defaults; - - // If true, display the hours:minutes since each user has touched - // the keyboard, or "." if within the last minute, or "old" if - // not within the last day. - let include_idle = need_deadprocs || need_login || need_runlevel || need_users; - - // If true, display process termination & exit status. - let include_exit = need_deadprocs; - - // If true, display only name, line, and time fields. - let short_output = !include_exit && use_defaults; - - // If true, display info only for the controlling tty. - let my_line_only = matches.get_flag(options::ONLY_HOSTNAME_USER) || files.len() == 2; - - let mut who = Who { - do_lookup, - short_list, - short_output, - include_idle, - include_heading, - include_mesg, - include_exit, - need_boottime, - need_deadprocs, - need_login, - need_initspawn, - need_clockchange, - need_runlevel, - need_users, - my_line_only, - args: files, - }; - - who.exec() -} +use platform::uumain; pub fn uu_app() -> Command { Command::new(uucore::util_name()) @@ -258,312 +149,3 @@ pub fn uu_app() -> Command { .value_hint(clap::ValueHint::FilePath), ) } - -struct Who { - do_lookup: bool, - short_list: bool, - short_output: bool, - include_idle: bool, - include_heading: bool, - include_mesg: bool, - include_exit: bool, - need_boottime: bool, - need_deadprocs: bool, - need_login: bool, - need_initspawn: bool, - need_clockchange: bool, - need_runlevel: bool, - need_users: bool, - my_line_only: bool, - args: Vec, -} - -fn idle_string<'a>(when: i64, boottime: i64) -> Cow<'a, str> { - thread_local! { - static NOW: time::OffsetDateTime = time::OffsetDateTime::now_local().unwrap(); - } - NOW.with(|n| { - let now = n.unix_timestamp(); - if boottime < when && now - 24 * 3600 < when && when <= now { - let seconds_idle = now - when; - if seconds_idle < 60 { - " . ".into() - } else { - format!( - "{:02}:{:02}", - seconds_idle / 3600, - (seconds_idle % 3600) / 60 - ) - .into() - } - } else { - " old ".into() - } - }) -} - -fn time_string(ut: &Utmpx) -> String { - // "%b %e %H:%M" - let time_format: Vec = - time::format_description::parse("[month repr:short] [day padding:space] [hour]:[minute]") - .unwrap(); - ut.login_time().format(&time_format).unwrap() // LC_ALL=C -} - -#[inline] -fn current_tty() -> String { - unsafe { - let res = ttyname(STDIN_FILENO); - if res.is_null() { - String::new() - } else { - CStr::from_ptr(res as *const _) - .to_string_lossy() - .trim_start_matches("/dev/") - .to_owned() - } - } -} - -impl Who { - #[allow(clippy::cognitive_complexity)] - fn exec(&mut self) -> UResult<()> { - let run_level_chk = |_record: i16| { - #[cfg(not(target_os = "linux"))] - return false; - - #[cfg(target_os = "linux")] - return _record == utmpx::RUN_LVL; - }; - - let f = if self.args.len() == 1 { - self.args[0].as_ref() - } else { - utmpx::DEFAULT_FILE - }; - if self.short_list { - let users = Utmpx::iter_all_records_from(f) - .filter(Utmpx::is_user_process) - .map(|ut| ut.user()) - .collect::>(); - println!("{}", users.join(" ")); - println!("# users={}", users.len()); - } else { - let records = Utmpx::iter_all_records_from(f); - - if self.include_heading { - self.print_heading(); - } - let cur_tty = if self.my_line_only { - current_tty() - } else { - String::new() - }; - - for ut in records { - if !self.my_line_only || cur_tty == ut.tty_device() { - if self.need_users && ut.is_user_process() { - self.print_user(&ut)?; - } else if self.need_runlevel && run_level_chk(ut.record_type()) { - if cfg!(target_os = "linux") { - self.print_runlevel(&ut); - } - } else if self.need_boottime && ut.record_type() == utmpx::BOOT_TIME { - self.print_boottime(&ut); - } else if self.need_clockchange && ut.record_type() == utmpx::NEW_TIME { - self.print_clockchange(&ut); - } else if self.need_initspawn && ut.record_type() == utmpx::INIT_PROCESS { - self.print_initspawn(&ut); - } else if self.need_login && ut.record_type() == utmpx::LOGIN_PROCESS { - self.print_login(&ut); - } else if self.need_deadprocs && ut.record_type() == utmpx::DEAD_PROCESS { - self.print_deadprocs(&ut); - } - } - - if ut.record_type() == utmpx::BOOT_TIME {} - } - } - Ok(()) - } - - #[inline] - fn print_runlevel(&self, ut: &Utmpx) { - let last = (ut.pid() / 256) as u8 as char; - let curr = (ut.pid() % 256) as u8 as char; - let runlvline = format!("run-level {curr}"); - let comment = format!("last={}", if last == 'N' { 'S' } else { 'N' }); - - self.print_line( - "", - ' ', - &runlvline, - &time_string(ut), - "", - "", - if last.is_control() { "" } else { &comment }, - "", - ); - } - - #[inline] - fn print_clockchange(&self, ut: &Utmpx) { - self.print_line("", ' ', "clock change", &time_string(ut), "", "", "", ""); - } - - #[inline] - fn print_login(&self, ut: &Utmpx) { - let comment = format!("id={}", ut.terminal_suffix()); - let pidstr = format!("{}", ut.pid()); - self.print_line( - "LOGIN", - ' ', - &ut.tty_device(), - &time_string(ut), - "", - &pidstr, - &comment, - "", - ); - } - - #[inline] - fn print_deadprocs(&self, ut: &Utmpx) { - let comment = format!("id={}", ut.terminal_suffix()); - let pidstr = format!("{}", ut.pid()); - let e = ut.exit_status(); - let exitstr = format!("term={} exit={}", e.0, e.1); - self.print_line( - "", - ' ', - &ut.tty_device(), - &time_string(ut), - "", - &pidstr, - &comment, - &exitstr, - ); - } - - #[inline] - fn print_initspawn(&self, ut: &Utmpx) { - let comment = format!("id={}", ut.terminal_suffix()); - let pidstr = format!("{}", ut.pid()); - self.print_line( - "", - ' ', - &ut.tty_device(), - &time_string(ut), - "", - &pidstr, - &comment, - "", - ); - } - - #[inline] - fn print_boottime(&self, ut: &Utmpx) { - self.print_line("", ' ', "system boot", &time_string(ut), "", "", "", ""); - } - - fn print_user(&self, ut: &Utmpx) -> UResult<()> { - let mut p = PathBuf::from("/dev"); - p.push(ut.tty_device().as_str()); - let mesg; - let last_change; - match p.metadata() { - Ok(meta) => { - #[cfg(all( - not(target_os = "android"), - not(target_os = "freebsd"), - not(target_vendor = "apple") - ))] - let iwgrp = S_IWGRP; - #[cfg(any(target_os = "android", target_os = "freebsd", target_vendor = "apple"))] - let iwgrp = S_IWGRP as u32; - mesg = if meta.mode() & iwgrp == 0 { '-' } else { '+' }; - last_change = meta.atime(); - } - _ => { - mesg = '?'; - last_change = 0; - } - } - - let idle = if last_change == 0 { - " ?".into() - } else { - idle_string(last_change, 0) - }; - - let s = if self.do_lookup { - ut.canon_host().map_err_context(|| { - let host = ut.host(); - format!( - "failed to canonicalize {}", - host.split(':').next().unwrap_or(&host).quote() - ) - })? - } else { - ut.host() - }; - let hoststr = if s.is_empty() { s } else { format!("({s})") }; - - self.print_line( - ut.user().as_ref(), - mesg, - ut.tty_device().as_ref(), - time_string(ut).as_str(), - idle.as_ref(), - format!("{}", ut.pid()).as_str(), - hoststr.as_str(), - "", - ); - - Ok(()) - } - - #[allow(clippy::too_many_arguments)] - fn print_line( - &self, - user: &str, - state: char, - line: &str, - time: &str, - idle: &str, - pid: &str, - comment: &str, - exit: &str, - ) { - let mut buf = String::with_capacity(64); - let msg = vec![' ', state].into_iter().collect::(); - - write!(buf, "{user:<8}").unwrap(); - if self.include_mesg { - buf.push_str(&msg); - } - write!(buf, " {line:<12}").unwrap(); - // "%b %e %H:%M" (LC_ALL=C) - let time_size = 3 + 2 + 2 + 1 + 2; - write!(buf, " {time:10}").unwrap(); - } - write!(buf, " {comment:<8}").unwrap(); - if self.include_exit { - write!(buf, " {exit:<12}").unwrap(); - } - println!("{}", buf.trim_end()); - } - - #[inline] - fn print_heading(&self) { - self.print_line( - "NAME", ' ', "LINE", "TIME", "IDLE", "PID", "COMMENT", "EXIT", - ); - } -}