tload: Basic implementation of tload (#362)

* tload: add basic tui layout of modern look

* tload: set x-axis bound from 0 to the width of terminal

* tload: fix mismatched returning type

* tload: add `#[allow(clippy::cognitive_complexity)]`

* tload: add by-utils test

* tload: tweaks for max height of chart

* tload: bump version of `ratatui` from `0.28` to `0.29`

* tload: fix typo

* tload: set exit code to 130

* tload: fix typo

* tload: add license header for `tui.rs`
This commit is contained in:
Krysztal Huang
2025-03-25 23:58:35 +08:00
committed by GitHub
parent 7158c33864
commit 1d83d7a8a3
10 changed files with 704 additions and 8 deletions
Generated
+358 -5
View File
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -30,13 +30,14 @@ feat_common_core = [
"pgrep",
"pidof",
"pidwait",
"pkill",
"pmap",
"ps",
"pwdx",
"slabtop",
"snice",
"pkill",
"sysctl",
"tload",
"top",
"w",
"watch",
@@ -48,12 +49,14 @@ chrono = { version = "0.4.38", default-features = false, features = ["clock"] }
clap = { version = "4.5.4", features = ["wrap_help", "cargo"] }
clap_complete = "4.5.2"
clap_mangen = "0.2.20"
crossterm = "0.28.1"
libc = "0.2.154"
nix = { version = "0.29", default-features = false, features = ["process"] }
phf = "0.11.2"
phf_codegen = "0.11.2"
prettytable-rs = "0.10.0"
rand = { version = "0.9.0", features = ["small_rng"] }
ratatui = "0.29.0"
regex = "1.10.4"
sysinfo = "0.33.0"
tempfile = "3.10.1"
@@ -80,13 +83,14 @@ free = { optional = true, version = "0.0.1", package = "uu_free", path = "src/uu
pgrep = { optional = true, version = "0.0.1", package = "uu_pgrep", path = "src/uu/pgrep" }
pidof = { optional = true, version = "0.0.1", package = "uu_pidof", path = "src/uu/pidof" }
pidwait = { optional = true, version = "0.0.1", package = "uu_pidwait", path = "src/uu/pidwait" }
pkill = { optional = true, version = "0.0.1", package = "uu_pkill", path = "src/uu/pkill" }
pmap = { optional = true, version = "0.0.1", package = "uu_pmap", path = "src/uu/pmap" }
ps = { optional = true, version = "0.0.1", package = "uu_ps", path = "src/uu/ps" }
pwdx = { optional = true, version = "0.0.1", package = "uu_pwdx", path = "src/uu/pwdx" }
slabtop = { optional = true, version = "0.0.1", package = "uu_slabtop", path = "src/uu/slabtop" }
snice = { optional = true, version = "0.0.1", package = "uu_snice", path = "src/uu/snice" }
pkill = { optional = true, version = "0.0.1", package = "uu_pkill", path = "src/uu/pkill" }
sysctl = { optional = true, version = "0.0.1", package = "uu_sysctl", path = "src/uu/sysctl" }
tload = { optional = true, version = "0.0.1", package = "uu_tload", path = "src/uu/tload" }
top = { optional = true, version = "0.0.1", package = "uu_top", path = "src/uu/top" }
w = { optional = true, version = "0.0.1", package = "uu_w", path = "src/uu/w" }
watch = { optional = true, version = "0.0.1", package = "uu_watch", path = "src/uu/watch" }
+1 -1
View File
@@ -23,6 +23,7 @@ Ongoing:
* `slabtop`: Displays detailed kernel slab cache information in real time.
* `snice`: Changes the scheduling priority of a running process.
* `sysctl`: Read or write kernel parameters at run-time.
* `tload`: Prints a graphical representation of system load average to the terminal.
* `top`: Displays real-time information about system processes.
* `w`: Shows who is logged on and what they are doing.
* `watch`: Executes a program periodically, showing output fullscreen.
@@ -30,7 +31,6 @@ Ongoing:
TODO:
* `hugetop`: Report hugepage usage of processes and the system as a whole.
* `skill`: Sends a signal to processes based on criteria like user, terminal, etc.
* `tload`: Prints a graphical representation of system load average to the terminal.
* `vmstat`: Reports information about processes, memory, paging, block IO, traps, and CPU activity.
Elsewhere:
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "uu_tload"
version = "0.0.1"
edition = "2021"
authors = ["uutils developers"]
license = "MIT"
description = "tload ~ (uutils) graphic representation of system load average"
homepage = "https://github.com/uutils/procps"
repository = "https://github.com/uutils/procps/tree/main/src/uu/tload"
keywords = ["acl", "uutils", "cross-platform", "cli", "utility"]
categories = ["command-line-utilities"]
[dependencies]
clap = { workspace = true }
crossterm = { workspace = true }
ratatui = { workspace = true }
uucore = { workspace = true }
[lib]
path = "src/tload.rs"
[[bin]]
name = "tload"
path = "src/main.rs"
+1
View File
@@ -0,0 +1 @@
uucore::bin!(uu_tload);
+172
View File
@@ -0,0 +1,172 @@
// This file is part of the uutils procps package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use std::collections::VecDeque;
use std::sync::{Arc, RwLock};
use std::thread::{self, sleep};
use std::time::Duration;
use clap::{arg, crate_version, value_parser, ArgAction, ArgMatches, Command};
use crossterm::event::{self, KeyCode, KeyEvent, KeyModifiers};
use tui::{LegacyTui, ModernTui};
use uucore::{error::UResult, format_usage, help_about, help_usage};
const ABOUT: &str = help_about!("tload.md");
const USAGE: &str = help_usage!("tload.md");
mod tui;
#[derive(Debug, Default, Clone)]
struct SystemLoadAvg {
pub(crate) last_1: f32,
pub(crate) last_5: f32,
pub(crate) last_10: f32,
}
impl SystemLoadAvg {
#[cfg(target_os = "linux")]
fn new() -> UResult<SystemLoadAvg> {
use std::fs;
use uucore::error::USimpleError;
let result = fs::read_to_string("/proc/loadavg")?;
let split = result.split(" ").collect::<Vec<_>>();
// Helper function to keep code clean
fn f(s: &str) -> UResult<f32> {
s.parse::<f32>()
.map_err(|e| USimpleError::new(1, e.to_string()))
}
Ok(SystemLoadAvg {
last_1: f(split[0])?,
last_5: f(split[1])?,
last_10: f(split[2])?,
})
}
#[cfg(not(target_os = "linux"))]
fn new() -> UResult<SystemLoadAvg> {
Ok(SystemLoadAvg::default())
}
}
#[allow(unused)]
#[derive(Debug)]
struct Settings {
delay: u64,
scale: usize, // Not used
is_modern: bool, // For modern display
}
impl Settings {
fn new(matches: &ArgMatches) -> Settings {
Settings {
delay: matches.get_one("delay").cloned().unwrap(),
scale: matches.get_one("scale").cloned().unwrap(),
is_modern: matches.get_flag("modern"),
}
}
}
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app().try_get_matches_from(args)?;
let settings = Settings::new(&matches);
let mut terminal = ratatui::init();
let data = {
// Why 10240?
//
// Emm, maybe there will be some terminal can display more than 10000 char?
let data = Arc::new(RwLock::new(VecDeque::with_capacity(10240)));
data.write()
.unwrap()
.push_back(SystemLoadAvg::new().unwrap());
data
};
let cloned_data = data.clone();
thread::spawn(move || loop {
sleep(Duration::from_secs(settings.delay));
let mut data = cloned_data.write().unwrap();
if data.iter().len() >= 10240 {
// Keep this VecDeque smaller than 10240
data.pop_front();
}
data.push_back(SystemLoadAvg::new().unwrap());
});
loop {
// Now only accept `Ctrl+C` for compatibility with the original implementation
//
// Use `event::poll` for non-blocking event reading
if let Ok(true) = event::poll(Duration::from_millis(10)) {
// If event available, break this loop
if let Ok(event::Event::Key(KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
..
})) = event::read()
{
// compatibility with the original implementation
uucore::error::set_exit_code(130);
break;
}
}
terminal.draw(|frame| {
let data = &data.read().unwrap();
let data = data.iter().cloned().collect::<Vec<_>>();
frame.render_widget(
if settings.is_modern {
ModernTui::new(&data)
} else {
LegacyTui::new(&data)
},
frame.area(),
);
})?;
std::thread::sleep(Duration::from_millis(10));
}
ratatui::restore();
Ok(())
}
#[allow(clippy::cognitive_complexity)]
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.version(crate_version!())
.about(ABOUT)
.override_usage(format_usage(USAGE))
.infer_long_args(true)
.args([
arg!(-d --delay <secs> "update delay in seconds")
.value_parser(value_parser!(u64))
.default_value("5")
.hide_default_value(true),
arg!(-m --modern "modern look").action(ArgAction::SetTrue),
// TODO: Implement this arg
arg!(-s --scale <num> "vertical scale")
.value_parser(value_parser!(usize))
.default_value("5")
.hide_default_value(true),
])
}
#[cfg(test)]
mod tests {
use super::*;
// It's just a test to make sure if can parsing correctly.
#[test]
fn test_system_load_avg() {
let _ = SystemLoadAvg::new().expect("SystemLoadAvg::new");
}
}
+119
View File
@@ -0,0 +1,119 @@
// This file is part of the uutils procps package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use ratatui::{
buffer::Buffer,
layout::{Constraint, Direction, Layout, Rect},
style::{Style, Stylize},
symbols::Marker,
text::{Line, Text},
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType, Paragraph, Widget},
};
use crate::SystemLoadAvg;
pub(crate) struct ModernTui<'a>(&'a [SystemLoadAvg]);
impl ModernTui<'_> {
fn render_header(&self, area: Rect, buf: &mut Buffer) {
let text = Text::from(vec![
Line::from(format!(
"Last 1 min load: {:>5}",
self.0.last().unwrap().last_1
)),
Line::from(format!(
"Last 5 min load: {:>5}",
self.0.last().unwrap().last_5
)),
Line::from(format!(
"Last 10 min load: {:>5}",
self.0.last().unwrap().last_10
)),
]);
Paragraph::new(text)
.style(Style::default().bold().italic())
.block(
Block::new()
.borders(Borders::ALL)
.title("System load history"),
)
.render(area, buf);
}
fn render_chart(&self, area: Rect, buf: &mut Buffer) {
let result = &self.0[self.0.len().saturating_sub(area.width.into())..]
.iter()
.enumerate()
.map(|(index, load)| (index as f64, load.last_1 as f64))
.collect::<Vec<_>>();
let data = Dataset::default()
.graph_type(GraphType::Line)
.marker(Marker::Braille)
.data(result);
let x_axis = {
let start = Line::from("0");
let middle = Line::from((area.width / 2).to_string());
let end = Line::from(area.width.to_string());
Axis::default()
.title("Time(per delay)")
.bounds([0.0, area.width.into()])
.labels(vec![start, middle, end])
};
// Why this tweak?
//
// Sometime the chart cannot display all the line because of max height are equals the max
// load of system in the history, so I add 0.2*{max_load} to the height of chart make it
// display beautiful
let y_axis_upper_bound = result.iter().map(|it| it.1).reduce(f64::max).unwrap_or(0.0);
let y_axis_upper_bound = y_axis_upper_bound + y_axis_upper_bound * 0.2;
let label = {
let min = "0.0".to_owned();
let mid = format!("{:.1}", y_axis_upper_bound / 2.0);
let max = format!("{:.1}", y_axis_upper_bound);
vec![min, mid, max]
};
let y_axis = Axis::default()
.bounds([0.0, y_axis_upper_bound])
.labels(label)
.title("System Load");
Chart::new(vec![data])
.x_axis(x_axis)
.y_axis(y_axis)
.render(area, buf);
}
}
impl ModernTui<'_> {
pub(crate) fn new(input: &[SystemLoadAvg]) -> ModernTui<'_> {
ModernTui(input)
}
}
impl Widget for ModernTui<'_> {
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
where
Self: Sized,
{
let layout = Layout::new(
Direction::Vertical,
[Constraint::Length(5), Constraint::Min(0)],
)
.split(area);
let header = layout[0];
let chart = layout[1];
self.render_header(header, buf);
self.render_chart(chart, buf);
}
}
// TODO: Implemented LegacyTui
pub(crate) type LegacyTui<'a> = ModernTui<'a>;
+7
View File
@@ -0,0 +1,7 @@
# tload
```
tload [options] [tty]
```
tload prints a graph of the current system load average to the specified tty (or the tty of the tload process if none is specified).
+11
View File
@@ -0,0 +1,11 @@
// This file is part of the uutils procps package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use crate::common::util::TestScenario;
#[test]
fn test_invalid_arg() {
new_ucmd!().arg("--definitely-invalid").fails().code_is(1);
}
+4
View File
@@ -60,3 +60,7 @@ mod test_pkill;
#[cfg(feature = "sysctl")]
#[path = "by-util/test_sysctl.rs"]
mod test_sysctl;
#[cfg(feature = "tload")]
#[path = "by-util/test_tload.rs"]
mod test_tload;