use super::common::{SystemCommand, format_duration, is_completing_arg};
use crate::cli::{SysAction, SysArgs};
use crate::os::{Domain, ExecutableCommand, OutputFormat, SysInfoData, SysManager, SysTimeData};
use anyhow::Result;
use clap::{ArgMatches, Args, Command as ClapCommand, FromArgMatches};
use std::process::Command;
use sysinfo::System;
pub struct StandardSys;
impl Domain for StandardSys {
fn name(&self) -> &'static str {
"sys"
}
fn command(&self) -> ClapCommand {
SysArgs::augment_args(
ClapCommand::new("sys").about("Manage core system (updates, power, time)"),
)
}
fn execute(
&self,
matches: &ArgMatches,
_app: &ClapCommand,
) -> Result<Box<dyn ExecutableCommand>> {
let args = SysArgs::from_arg_matches(matches)?;
match &args.action {
Some(SysAction::Info { format }) => self.info(*format),
Some(SysAction::Power { state, now, force }) => self.power(state, *now, *force),
Some(SysAction::Time {
action,
value,
format,
}) => self.time(action, value.as_deref(), *format),
None => self.info(OutputFormat::Table),
}
}
fn complete(
&self,
_line: &str,
words: &[&str],
last_word_complete: bool,
) -> Result<Vec<String>> {
if is_completing_arg(words, &["ao", "sys", "power"], 1, last_word_complete) {
return Ok(vec![
"reboot".to_string(),
"shutdown".to_string(),
"suspend".to_string(),
"hibernate".to_string(),
]);
}
if is_completing_arg(words, &["ao", "sys", "time"], 1, last_word_complete) {
return Ok(vec![
"status".to_string(),
"set".to_string(),
"sync".to_string(),
]);
}
Ok(vec![])
}
}
impl SysManager for StandardSys {
fn info(&self, format: OutputFormat) -> Result<Box<dyn ExecutableCommand>> {
if matches!(format, OutputFormat::Original) {
let cmd = crate::os::linux_generic::common::CompoundCommand::new(vec![
Box::new(SystemCommand::new("uname").arg("-a")),
Box::new(SystemCommand::new("uptime")),
Box::new(SystemCommand::new("cat").arg("/etc/os-release")),
]);
return Ok(Box::new(cmd));
}
Ok(Box::new(SysInfoCommand { format }))
}
fn power(&self, state: &str, now: bool, force: bool) -> Result<Box<dyn ExecutableCommand>> {
let mut cmd = SystemCommand::new("systemctl");
match state {
"reboot" => cmd = cmd.arg("reboot"),
"shutdown" => cmd = cmd.arg("poweroff"),
"suspend" => cmd = cmd.arg("suspend"),
"hibernate" => cmd = cmd.arg("hibernate"),
_ => anyhow::bail!("Unsupported power state: {}", state),
}
if now {
cmd = cmd.arg("--now");
}
if force {
cmd = cmd.arg("--force");
}
Ok(Box::new(cmd))
}
fn time(
&self,
action: &str,
value: Option<&str>,
format: OutputFormat,
) -> Result<Box<dyn ExecutableCommand>> {
if action == "status" {
if matches!(format, OutputFormat::Original) {
return Ok(Box::new(SystemCommand::new("timedatectl").arg("status")));
}
return Ok(Box::new(SysTimeCommand { format }));
}
let mut cmd = SystemCommand::new("timedatectl");
match action {
"set" => {
if let Some(v) = value {
cmd = cmd.arg("set-timezone").arg("--").arg(v);
} else {
anyhow::bail!("Timezone value required for set action");
}
}
"sync" => cmd = cmd.arg("set-ntp").arg("true"),
_ => anyhow::bail!("Unsupported time action: {}", action),
}
Ok(Box::new(cmd))
}
}
pub struct SysInfoCommand {
pub format: OutputFormat,
}
impl ExecutableCommand for SysInfoCommand {
fn execute(&self) -> Result<()> {
let mut sys = System::new_all();
sys.refresh_all();
let total_memory_gb = sys.total_memory() as f64 / 1_073_741_824.0;
let used_memory_gb = sys.used_memory() as f64 / 1_073_741_824.0;
let total_memory_readable = format!("{:.2} GB", total_memory_gb);
let used_memory_readable = format!("{:.2} GB", used_memory_gb);
let cpu_model = sys
.cpus()
.first()
.map(|c| c.brand().to_string())
.unwrap_or_else(|| "Unknown".to_string());
let physical_drives = Command::new("lsblk")
.arg("-d")
.arg("-n")
.arg("-o")
.arg("TYPE")
.output()
.map(|o| {
String::from_utf8_lossy(&o.stdout)
.lines()
.filter(|l| l.trim() == "disk")
.count()
})
.unwrap_or(0);
let mut lan_adapters = Vec::new();
let mut wifi_adapters = Vec::new();
if let Ok(entries) = std::fs::read_dir("/sys/class/net") {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name == "lo" {
continue;
}
let path = entry.path();
if !path.join("device").exists() {
continue;
}
let is_wifi = path.join("wireless").exists() || path.join("phy80211").exists();
if is_wifi {
wifi_adapters.push(name);
} else {
lan_adapters.push(name);
}
}
}
let mut bt_adapters = Vec::new();
if let Ok(output) = Command::new("hciconfig").output() {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.contains("hci")
&& line.contains(':')
&& let Some(name) = line.split(':').next()
{
bt_adapters.push(name.trim().to_string());
}
}
}
if bt_adapters.is_empty()
&& let Ok(output) = Command::new("bluetoothctl").arg("list").output()
{
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if let Some(name) = line.split_whitespace().nth(1) {
bt_adapters.push(name.to_string());
}
}
}
let mut monitors = Vec::new();
let output = Command::new("xrandr")
.arg("--query")
.output()
.or_else(|_| Command::new("wlr-randr").output());
if let Ok(output) = output {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.contains(" connected") {
let parts: Vec<&str> = line.split_whitespace().collect();
if !parts.is_empty() {
let name = parts[0].to_string();
monitors.push(name);
}
}
}
}
let mut system_users_count = 0;
let mut common_users_count = 0;
if let Ok(content) = std::fs::read_to_string("/etc/passwd") {
for line in content.lines() {
let parts: Vec<&str> = line.split(':').collect();
if parts.len() >= 3
&& let Ok(uid) = parts[2].parse::<u32>()
{
if uid < 1000 {
system_users_count += 1;
} else {
common_users_count += 1;
}
}
}
}
let ram_type = "Unknown".to_string();
let ram_model = "Unknown".to_string();
let data = SysInfoData {
hostname: System::host_name().unwrap_or_default(),
os: System::long_os_version().unwrap_or_default(),
kernel: System::kernel_version().unwrap_or_default(),
architecture: System::cpu_arch(),
uptime: format_duration(System::uptime()),
cpu_count: sys.cpus().len(),
cpu_model,
total_memory: sys.total_memory(),
used_memory: sys.used_memory(),
total_memory_readable,
used_memory_readable,
ram_type,
ram_model,
physical_drives,
lan_adapters,
wifi_adapters,
bt_adapters,
monitors,
system_users_count,
common_users_count,
};
match self.format {
OutputFormat::Table => {
let mut table = comfy_table::Table::new();
table.set_header(vec!["Property", "Value"]);
table.add_row(vec!["Hostname", &data.hostname]);
table.add_row(vec!["OS", &data.os]);
table.add_row(vec!["Kernel", &data.kernel]);
table.add_row(vec!["Architecture", &data.architecture]);
table.add_row(vec!["Uptime", &data.uptime]);
table.add_row(vec!["CPU Model", &data.cpu_model]);
table.add_row(vec!["CPU Count", &data.cpu_count.to_string()]);
table.add_row(vec!["Total Memory", &data.total_memory_readable]);
table.add_row(vec!["Used Memory", &data.used_memory_readable]);
table.add_row(vec!["RAM Type", &data.ram_type]);
table.add_row(vec!["RAM Model", &data.ram_model]);
table.add_row(vec!["Physical Drives", &data.physical_drives.to_string()]);
table.add_row(vec!["LAN Adapters", &data.lan_adapters.join(", ")]);
table.add_row(vec!["WiFi Adapters", &data.wifi_adapters.join(", ")]);
table.add_row(vec!["BT Adapters", &data.bt_adapters.join(", ")]);
table.add_row(vec!["Monitors", &data.monitors.join(", ")]);
table.add_row(vec!["System Users", &data.system_users_count.to_string()]);
table.add_row(vec!["Common Users", &data.common_users_count.to_string()]);
println!("{}", table);
}
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&data)?);
}
OutputFormat::Yaml => {
println!("{}", serde_yaml::to_string(&data)?);
}
OutputFormat::Original => unreachable!(),
}
Ok(())
}
fn as_string(&self) -> String {
"sysinfo (Rust library)".to_string()
}
fn is_structured(&self) -> bool {
matches!(
self.format,
OutputFormat::Json | OutputFormat::Yaml | OutputFormat::Original
)
}
}
pub struct SysTimeCommand {
pub format: OutputFormat,
}
impl ExecutableCommand for SysTimeCommand {
fn execute(&self) -> Result<()> {
let output = Command::new("timedatectl").output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
let mut data = SysTimeData {
local_time: String::new(),
universal_time: String::new(),
rtc_time: String::new(),
time_zone: String::new(),
system_clock_synchronized: String::new(),
ntp_service: String::new(),
rtc_in_local_tz: String::new(),
};
for line in stdout.lines() {
let line = line.trim();
if let Some(val) = line.strip_prefix("Local time:") {
data.local_time = val.trim().to_string();
} else if let Some(val) = line.strip_prefix("Universal time:") {
data.universal_time = val.trim().to_string();
} else if let Some(val) = line.strip_prefix("RTC time:") {
data.rtc_time = val.trim().to_string();
} else if let Some(val) = line.strip_prefix("Time zone:") {
data.time_zone = val.trim().to_string();
} else if let Some(val) = line.strip_prefix("System clock synchronized:") {
data.system_clock_synchronized = val.trim().to_string();
} else if let Some(val) = line.strip_prefix("NTP service:") {
data.ntp_service = val.trim().to_string();
} else if let Some(val) = line.strip_prefix("RTC in local TZ:") {
data.rtc_in_local_tz = val.trim().to_string();
}
}
match self.format {
OutputFormat::Table => {
let mut table = comfy_table::Table::new();
table.set_header(vec!["Property", "Value"]);
table.add_row(vec!["Local Time", &data.local_time]);
table.add_row(vec!["Universal Time", &data.universal_time]);
table.add_row(vec!["RTC Time", &data.rtc_time]);
table.add_row(vec!["Time Zone", &data.time_zone]);
table.add_row(vec!["Clock Synced", &data.system_clock_synchronized]);
table.add_row(vec!["NTP Service", &data.ntp_service]);
table.add_row(vec!["RTC in Local TZ", &data.rtc_in_local_tz]);
println!("{}", table);
}
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&data)?);
}
OutputFormat::Yaml => {
println!("{}", serde_yaml::to_string(&data)?);
}
OutputFormat::Original => unreachable!(),
}
Ok(())
}
fn as_string(&self) -> String {
"timedatectl status".to_string()
}
fn is_structured(&self) -> bool {
matches!(
self.format,
OutputFormat::Json | OutputFormat::Yaml | OutputFormat::Original
)
}
}