use std::ffi::CStr;
use std::fmt::Write as FmtWrite;
use std::path::PathBuf;
use crate::who;
#[derive(Clone, Debug)]
pub struct PinkyConfig {
pub long_format: bool,
pub omit_home_shell: bool,
pub omit_project: bool,
pub omit_plan: bool,
pub short_format: bool,
pub omit_heading: bool,
pub omit_fullname: bool,
pub omit_fullname_host: bool,
pub omit_fullname_host_idle: bool,
pub users: Vec<String>,
}
impl Default for PinkyConfig {
fn default() -> Self {
Self {
long_format: false,
omit_home_shell: false,
omit_project: false,
omit_plan: false,
short_format: true,
omit_heading: false,
omit_fullname: false,
omit_fullname_host: false,
omit_fullname_host_idle: false,
users: Vec::new(),
}
}
}
#[derive(Clone, Debug)]
pub struct UserInfo {
pub login: String,
pub fullname: String,
pub home_dir: String,
pub shell: String,
}
pub fn get_user_info(username: &str) -> Option<UserInfo> {
let c_name = std::ffi::CString::new(username).ok()?;
unsafe {
let pw = libc::getpwnam(c_name.as_ptr());
if pw.is_null() {
return None;
}
let pw = &*pw;
let login = CStr::from_ptr(pw.pw_name).to_string_lossy().into_owned();
let gecos = if pw.pw_gecos.is_null() {
String::new()
} else {
CStr::from_ptr(pw.pw_gecos).to_string_lossy().into_owned()
};
let fullname = gecos.split(',').next().unwrap_or("").to_string();
let home_dir = CStr::from_ptr(pw.pw_dir).to_string_lossy().into_owned();
let shell = CStr::from_ptr(pw.pw_shell).to_string_lossy().into_owned();
Some(UserInfo {
login,
fullname,
home_dir,
shell,
})
}
}
fn idle_str(line: &str) -> String {
if line.is_empty() {
return "?????".to_string();
}
let dev_path = if line.starts_with('/') {
line.to_string()
} else if let Some(idx) = line.find("pts/") {
format!("/dev/{}", &line[idx..])
} else if let Some(idx) = line.find("tty") {
format!("/dev/{}", &line[idx..])
} else {
format!("/dev/{}", line)
};
let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
let c_path = std::ffi::CString::new(dev_path).unwrap_or_default();
let rc = unsafe { libc::stat(c_path.as_ptr(), &mut stat_buf) };
if rc != 0 {
return "?????".to_string();
}
let now = unsafe { libc::time(std::ptr::null_mut()) };
let atime = stat_buf.st_atime;
let idle_secs = now - atime;
if idle_secs < 60 {
String::new()
} else {
let hours = idle_secs / 3600;
let mins = (idle_secs % 3600) / 60;
format!("{:02}:{:02}", hours, mins)
}
}
fn format_time_short(tv_sec: i64) -> String {
if tv_sec == 0 {
return String::new();
}
let t = tv_sec as libc::time_t;
let tm = unsafe {
let mut tm: libc::tm = std::mem::zeroed();
libc::localtime_r(&t, &mut tm);
tm
};
format!(
"{:04}-{:02}-{:02} {:02}:{:02}",
tm.tm_year + 1900,
tm.tm_mon + 1,
tm.tm_mday,
tm.tm_hour,
tm.tm_min
)
}
fn read_first_line(path: &PathBuf) -> String {
match std::fs::read_to_string(path) {
Ok(contents) => contents.lines().next().unwrap_or("").to_string(),
Err(_) => String::new(),
}
}
fn read_file_contents(path: &PathBuf) -> String {
std::fs::read_to_string(path).unwrap_or_default()
}
pub fn format_short_heading(config: &PinkyConfig) -> String {
let mut out = String::new();
let _ = write!(out, "{:<8}", "Login");
if !config.omit_fullname && !config.omit_fullname_host && !config.omit_fullname_host_idle {
let _ = write!(out, " {:<19}", "Name");
}
let _ = write!(out, " {:<9}", " TTY");
if !config.omit_fullname_host_idle {
let _ = write!(out, " {:<6}", "Idle");
}
let _ = write!(out, " {:<16}", "When");
if !config.omit_fullname_host && !config.omit_fullname_host_idle {
let _ = write!(out, " {}", "Where");
}
out
}
fn pinky_mesg_status(line: &str) -> char {
let dev_part = if let Some(space_idx) = line.find(' ') {
&line[space_idx + 1..]
} else {
line
};
if dev_part.is_empty() {
return '?';
}
let dev_path = if dev_part.starts_with('/') {
dev_part.to_string()
} else {
format!("/dev/{}", dev_part)
};
let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
let c_path = std::ffi::CString::new(dev_path).unwrap_or_default();
let rc = unsafe { libc::stat(c_path.as_ptr(), &mut stat_buf) };
if rc != 0 {
return '?';
}
if stat_buf.st_mode & libc::S_IWGRP != 0 {
' '
} else {
'*'
}
}
pub fn format_short_entry(entry: &who::UtmpxEntry, config: &PinkyConfig) -> String {
let mut out = String::new();
let user = &entry.ut_user;
if user.len() < 8 {
let _ = write!(out, "{:<8}", user);
} else {
let _ = write!(out, "{}", user);
}
if !config.omit_fullname && !config.omit_fullname_host && !config.omit_fullname_host_idle {
let fullname = get_user_info(&entry.ut_user)
.map(|u| u.fullname)
.unwrap_or_default();
let display_name: String = fullname.chars().take(19).collect();
let _ = write!(out, " {:<19}", display_name);
}
let mesg = pinky_mesg_status(&entry.ut_line);
let _ = write!(out, " {}", mesg);
let line = &entry.ut_line;
if line.len() < 8 {
let _ = write!(out, "{:<8}", line);
} else {
let _ = write!(out, "{}", line);
}
if !config.omit_fullname_host_idle {
let idle = idle_str(&entry.ut_line);
let _ = write!(out, " {:<6}", idle);
}
let time_str = format_time_short(entry.ut_tv_sec);
let _ = write!(out, " {}", time_str);
if !config.omit_fullname_host && !config.omit_fullname_host_idle {
if !entry.ut_host.is_empty() {
let _ = write!(out, " {}", entry.ut_host);
}
}
out
}
pub fn format_long_entry(username: &str, config: &PinkyConfig) -> String {
let mut out = String::new();
let info = get_user_info(username);
let _ = write!(out, "Login name: {:<28}", username);
if info.is_none() {
let _ = writeln!(out, "In real life: ???");
return out;
}
let info = info.unwrap();
let _ = writeln!(out, "In real life: {}", info.fullname);
if !config.omit_home_shell {
let _ = write!(out, "Directory: {:<29}", info.home_dir);
let _ = writeln!(out, "Shell: {}", info.shell);
}
if !config.omit_project {
let project_path = PathBuf::from(&info.home_dir).join(".project");
if project_path.exists() {
let project = read_first_line(&project_path);
if !project.is_empty() {
let _ = writeln!(out, "Project: {}", project);
}
}
}
if !config.omit_plan {
let plan_path = PathBuf::from(&info.home_dir).join(".plan");
if plan_path.exists() {
let plan = read_file_contents(&plan_path);
if !plan.is_empty() {
let _ = writeln!(out, "Plan:");
let _ = write!(out, "{}", plan);
if !plan.ends_with('\n') {
let _ = writeln!(out);
}
}
}
}
let _ = writeln!(out);
out
}
pub fn run_pinky(config: &PinkyConfig) -> String {
let mut output = String::new();
if config.long_format {
let users = if config.users.is_empty() {
let entries = who::read_utmpx_with_systemd_fallback_no_pid_check();
let mut names: Vec<String> = entries
.iter()
.filter(|e| e.ut_type == 7) .map(|e| e.ut_user.clone())
.collect();
names.sort();
names.dedup();
names
} else {
config.users.clone()
};
for user in users.iter() {
let _ = write!(output, "{}", format_long_entry(user, config));
}
} else {
let entries = who::read_utmpx_with_systemd_fallback_no_pid_check();
if !config.omit_heading {
let _ = writeln!(output, "{}", format_short_heading(config));
}
let mut user_entries: Vec<&who::UtmpxEntry> = entries
.iter()
.filter(|e| e.ut_type == 7) .filter(|e| {
if config.users.is_empty() {
true
} else {
config.users.iter().any(|u| u == &e.ut_user)
}
})
.collect();
user_entries.sort_by(|a, b| b.ut_tv_sec.cmp(&a.ut_tv_sec));
for entry in &user_entries {
let _ = writeln!(output, "{}", format_short_entry(entry, config));
}
}
if output.ends_with('\n') {
output.pop();
}
output
}