Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use std::io::{self, IsTerminal};
use std::process;

use crate::constants::{APP_NAME, SEP, VERSION};
use crate::presentation::colors::Colors;
use crate::slot::{ServiceSlot, SlotFilter};
use crate::{APP_NAME, SEP, VERSION};

/// `Opts` contains the parsed command-line options for the utility
pub struct Opts {
Expand All @@ -14,6 +14,7 @@ pub struct Opts {
pub slot_filter: SlotFilter, // Service selection and ordering
}

// ... [Rest of cli.rs remains exactly the same] ...
/// `Opts` implements the command-line options parser
impl Opts {
/// `from_args()` parses `argv` into [`Opts`], printing usage and exiting on any error
Expand Down
41 changes: 41 additions & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/// Application name displayed in the header
pub const APP_NAME: &str = "Rust-CLI-SysInfo";

/// Horizontal separator line used for visual sectioning
pub const SEP: &str =
"────────────────────────────────────────────────────────────────────────────────";

/// Application version, sourced from Cargo package version
pub const VERSION: &str = env!("CARGO_PKG_VERSION");

/// Clears the entire terminal screen and resets cursor to top-left
pub const CLEAR_SCREEN: &str = "\x1bc";

/// Clears the current line and returns cursor to its start
pub const CLEAR_LINE: &str = "\r\x1b[2K";

// ANSI escape sequences for terminal color output
pub const RED: &str = "\x1b[0;31m";
pub const GREEN: &str = "\x1b[0;32m";
pub const YELLOW: &str = "\x1b[0;33m";
pub const CYAN: &str = "\x1b[0;36m";
pub const BOLD: &str = "\x1b[1m";
pub const RESET: &str = "\x1b[0m";

// Time conversions for `format_uptime()`
pub const SECS_PER_MIN: u64 = 60;
pub const SECS_PER_HOUR: u64 = 3_600;
pub const SECS_PER_DAY: u64 = 86_400;

// Size conversions for `format_size()`
pub const KB_PER_MB: f64 = 1_024.0;
pub const KB_PER_GB: f64 = 1_048_576.0;
pub const KB_PER_TB: f64 = 1_073_741_824.0;

// Warning/Critical thresholds percentages
pub const CPU_WARN_PCT: f64 = 70.0;
pub const CPU_CRIT_PCT: f64 = 90.0;
pub const MEM_WARN_PCT: f64 = 75.0;
pub const MEM_CRIT_PCT: f64 = 90.0;
pub const DISK_WARN_PCT: f64 = 80.0;
pub const DISK_CRIT_PCT: f64 = 95.0;
13 changes: 5 additions & 8 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//! Displays metrics natively from Linux-based system calls

mod cli;
mod constants;
mod core;
mod presentation;
mod services;
Expand All @@ -12,6 +13,7 @@ use std::collections::HashMap;
use std::io::{self, Write};

use crate::cli::Opts;
use crate::constants::{APP_NAME, CLEAR_LINE, CLEAR_SCREEN, SEP};
use crate::core::error::AppError;
use crate::presentation::colors::{Colors, Threshold};
use crate::presentation::format::print_row;
Expand All @@ -29,11 +31,6 @@ use crate::services::uptime::UptimeService;
use crate::services::users::UsersService;
use crate::slot::{ServiceSlot, SlotFilter};

pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const APP_NAME: &str = "Rust-CLI-SysInfo";
pub const SEP: &str =
"────────────────────────────────────────────────────────────────────────────────";

/// `CollectResult` is the type-erased result of a single service's `collect()` call
type CollectResult = Result<Box<dyn Any>, AppError>;

Expand Down Expand Up @@ -227,7 +224,7 @@ fn main() {
};

if opts.clear {
print!("\x1bc");
print!("{CLEAR_SCREEN}");
}

print!(
Expand All @@ -240,9 +237,9 @@ fn main() {
let collected = collect_services(&active_slots, &registry);

if opts.clear {
println!("\x1bc");
println!("{CLEAR_SCREEN}");
} else {
print!("\r\x1b[2K");
print!("{CLEAR_LINE}");
}

render_services(&active_slots, &registry, &collected, &colors);
Expand Down
15 changes: 9 additions & 6 deletions src/presentation/colors.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::constants::{BOLD, CYAN, GREEN, RED, RESET, YELLOW};

/// ANSI escape sequences for terminal color output
///
pub struct Colors {
Expand All @@ -16,15 +18,16 @@ impl Colors {
/// `new` creates a `Colors` instance with ANSI codes when `enabled` is `true`, or empty
/// strings otherwise
///
#[must_use]
pub const fn new(enabled: bool) -> Self {
if enabled {
Self {
red: "\x1b[0;31m",
green: "\x1b[0;32m",
yellow: "\x1b[0;33m",
cyan: "\x1b[0;36m",
bold: "\x1b[1m",
reset: "\x1b[0m",
red: RED,
green: GREEN,
yellow: YELLOW,
cyan: CYAN,
bold: BOLD,
reset: RESET,
}
} else {
Self {
Expand Down
41 changes: 20 additions & 21 deletions src/presentation/format.rs
Original file line number Diff line number Diff line change
@@ -1,57 +1,56 @@
use super::colors::{Colors, Threshold};
use crate::constants::{
KB_PER_GB, KB_PER_MB, KB_PER_TB, SECS_PER_DAY, SECS_PER_HOUR, SECS_PER_MIN,
};

/// `print_row()` prints a left-aligned label/value row, coloring the value
///
pub fn print_row(label: &str, value: &str, threshold: &Threshold, c: &Colors) {
let color = match threshold {
Threshold::Check {
value: v,
warn: _,
crit,
} if v >= crit => c.red,
Threshold::Check {
value: v,
warn,
crit: _,
} if v >= warn => c.yellow,
Threshold::Check { value: v, crit, .. } if v >= crit => c.red,
Threshold::Check { value: v, warn, .. } if v >= warn => c.yellow,
Threshold::Check { .. } => c.green,
Threshold::None => c.reset,
};

println!("{:<16} {}{}{}", label, color, value, c.reset);
println!("{label:<16} {color}{value}{}", c.reset);
}

/// `format_uptime()` formats a duration in seconds as `DDDd:HHh:MMm:SSs`
///
#[must_use]
pub fn format_uptime(seconds: u64) -> String {
let days = seconds / 86400;
let hours = (seconds % 86400) / 3600;
let mins = (seconds % 3600) / 60;
let secs = seconds % 60;
let days = seconds / SECS_PER_DAY;
let hours = (seconds % SECS_PER_DAY) / SECS_PER_HOUR;
let mins = (seconds % SECS_PER_HOUR) / SECS_PER_MIN;
let secs = seconds % SECS_PER_MIN;

format!("{days:03}d:{hours:02}h:{mins:02}m:{secs:02}s")
}

/// `format_size()` formats a size given in kilobytes as a human-readable string with a T/G/M/K
/// suffix
///
#[must_use]
pub fn format_size(kb: u64) -> String {
// Divisors are powers of 1024 (all expressed in KB)

#[allow(clippy::cast_precision_loss)]
let k = kb as f64;

if k >= 1_073_741_824.0 {
format!("{:.1}T", k / 1_073_741_824.0)
} else if k >= 1_048_576.0 {
format!("{:.1}G", k / 1_048_576.0)
} else if k >= 1_024.0 {
format!("{:.1}M", k / 1_024.0)
if k >= KB_PER_TB {
format!("{:.1}T", k / KB_PER_TB)
} else if k >= KB_PER_GB {
format!("{:.1}G", k / KB_PER_GB)
} else if k >= KB_PER_MB {
format!("{:.1}M", k / KB_PER_MB)
} else {
format!("{kb}K")
}
}

// ... [Tests remain exactly the same] ...

#[cfg(test)]
mod tests {
use super::*;
Expand Down
1 change: 1 addition & 0 deletions src/services/cpu_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ fn read_cpu_model() -> Option<String> {
let file = File::open("/proc/cpuinfo").ok()?;

for line in BufReader::new(file).lines().map_while(Result::ok) {
// Note: `let`-chains require Rust edition 2024
if line.starts_with("model name")
&& let Some((_, val)) = line.split_once(':')
{
Expand Down
5 changes: 3 additions & 2 deletions src/services/cpu_usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::thread;
use std::time::Duration;

use super::prelude::*;
use crate::constants::{CPU_CRIT_PCT, CPU_WARN_PCT};
use crate::core::utils::read_first_line;

/// `CpuUsageInfo` contains CPU utilization sampled over a configurable sampling period
Expand Down Expand Up @@ -42,8 +43,8 @@ impl Service for CpuUsageService {
format!("{v:.1}%"),
Threshold::Check {
value: v,
warn: 70.0,
crit: 90.0,
warn: CPU_WARN_PCT,
crit: CPU_CRIT_PCT,
},
)
},
Expand Down
6 changes: 3 additions & 3 deletions src/services/disk.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::process;

use super::prelude::*;
use crate::constants::{DISK_CRIT_PCT, DISK_WARN_PCT};
use crate::presentation::format::format_size;

/// `DiskInfo` is a struct containing disk usage metrics for a single mount point
Expand Down Expand Up @@ -72,7 +73,6 @@ impl Service for DiskService {
pct,
})
}

/// `render()` renders disk usage as a percentage with used/total sizes and threshold-based
/// color coding
///
Expand All @@ -91,8 +91,8 @@ impl Service for DiskService {
text,
Threshold::Check {
value: disk.pct,
warn: 80.0,
crit: 95.0,
warn: DISK_WARN_PCT,
crit: DISK_CRIT_PCT,
},
)
};
Expand Down
5 changes: 3 additions & 2 deletions src/services/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::fs;
use std::io::{self, BufRead};

use super::prelude::*;
use crate::constants::{MEM_CRIT_PCT, MEM_WARN_PCT};

/// `MemInfo` contains memory usage metrics parsed from `/proc/meminfo`
pub struct MemInfo {
Expand Down Expand Up @@ -92,8 +93,8 @@ impl Service for MemoryService {
&mem_str,
&Threshold::Check {
value: mem.pct,
warn: 75.0,
crit: 90.0,
warn: MEM_WARN_PCT,
crit: MEM_CRIT_PCT,
},
c,
);
Expand Down
9 changes: 9 additions & 0 deletions src/slot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,21 @@ impl ServiceSlot {

/// `token()` returns the token string for this slot (used in `-s` output and parsing)
///
#[must_use]
pub fn token(self) -> &'static str {
self.meta().token
}

/// `description()` returns a description shown next to the token in `-s` labeled output
///
#[must_use]
pub fn description(self) -> &'static str {
self.meta().description
}

/// `all()` returns the default ordered list of all slots, defining the standard output layout
///
#[must_use]
pub fn all() -> Vec<Self> {
SLOT_TABLE.iter().map(|m| m.slot).collect()
}
Expand All @@ -84,6 +87,10 @@ impl ServiceSlot {
/// ordered `Vec<ServiceSlot>`
///
pub fn parse_list(input: &str) -> Result<Vec<Self>, String> {
if input.trim().is_empty() {
return Err("service token list cannot be empty".to_string());
}

input
.split('-')
.map(|raw| {
Expand Down Expand Up @@ -283,6 +290,7 @@ mod tests {
fn parse_list_empty_string_returns_err() {
let result = ServiceSlot::parse_list("");
assert!(result.is_err(), "empty input must not silently succeed");
assert_eq!(result.unwrap_err(), "service token list cannot be empty");
}

#[test]
Expand All @@ -292,6 +300,7 @@ mod tests {
fn parse_list_whitespace_only_returns_err() {
let result = ServiceSlot::parse_list(" ");
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "service token list cannot be empty");
}

#[test]
Expand Down