Skip to content

Commit 1d83d7a

Browse files
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`
1 parent 7158c33 commit 1d83d7a

File tree

10 files changed

+703
-7
lines changed

10 files changed

+703
-7
lines changed

Cargo.lock

Lines changed: 357 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,14 @@ feat_common_core = [
3030
"pgrep",
3131
"pidof",
3232
"pidwait",
33+
"pkill",
3334
"pmap",
3435
"ps",
3536
"pwdx",
3637
"slabtop",
3738
"snice",
38-
"pkill",
3939
"sysctl",
40+
"tload",
4041
"top",
4142
"w",
4243
"watch",
@@ -48,12 +49,14 @@ chrono = { version = "0.4.38", default-features = false, features = ["clock"] }
4849
clap = { version = "4.5.4", features = ["wrap_help", "cargo"] }
4950
clap_complete = "4.5.2"
5051
clap_mangen = "0.2.20"
52+
crossterm = "0.28.1"
5153
libc = "0.2.154"
5254
nix = { version = "0.29", default-features = false, features = ["process"] }
5355
phf = "0.11.2"
5456
phf_codegen = "0.11.2"
5557
prettytable-rs = "0.10.0"
5658
rand = { version = "0.9.0", features = ["small_rng"] }
59+
ratatui = "0.29.0"
5760
regex = "1.10.4"
5861
sysinfo = "0.33.0"
5962
tempfile = "3.10.1"
@@ -80,13 +83,14 @@ free = { optional = true, version = "0.0.1", package = "uu_free", path = "src/uu
8083
pgrep = { optional = true, version = "0.0.1", package = "uu_pgrep", path = "src/uu/pgrep" }
8184
pidof = { optional = true, version = "0.0.1", package = "uu_pidof", path = "src/uu/pidof" }
8285
pidwait = { optional = true, version = "0.0.1", package = "uu_pidwait", path = "src/uu/pidwait" }
86+
pkill = { optional = true, version = "0.0.1", package = "uu_pkill", path = "src/uu/pkill" }
8387
pmap = { optional = true, version = "0.0.1", package = "uu_pmap", path = "src/uu/pmap" }
8488
ps = { optional = true, version = "0.0.1", package = "uu_ps", path = "src/uu/ps" }
8589
pwdx = { optional = true, version = "0.0.1", package = "uu_pwdx", path = "src/uu/pwdx" }
8690
slabtop = { optional = true, version = "0.0.1", package = "uu_slabtop", path = "src/uu/slabtop" }
8791
snice = { optional = true, version = "0.0.1", package = "uu_snice", path = "src/uu/snice" }
88-
pkill = { optional = true, version = "0.0.1", package = "uu_pkill", path = "src/uu/pkill" }
8992
sysctl = { optional = true, version = "0.0.1", package = "uu_sysctl", path = "src/uu/sysctl" }
93+
tload = { optional = true, version = "0.0.1", package = "uu_tload", path = "src/uu/tload" }
9094
top = { optional = true, version = "0.0.1", package = "uu_top", path = "src/uu/top" }
9195
w = { optional = true, version = "0.0.1", package = "uu_w", path = "src/uu/w" }
9296
watch = { optional = true, version = "0.0.1", package = "uu_watch", path = "src/uu/watch" }

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ Ongoing:
2323
* `slabtop`: Displays detailed kernel slab cache information in real time.
2424
* `snice`: Changes the scheduling priority of a running process.
2525
* `sysctl`: Read or write kernel parameters at run-time.
26+
* `tload`: Prints a graphical representation of system load average to the terminal.
2627
* `top`: Displays real-time information about system processes.
2728
* `w`: Shows who is logged on and what they are doing.
2829
* `watch`: Executes a program periodically, showing output fullscreen.
2930

3031
TODO:
3132
* `hugetop`: Report hugepage usage of processes and the system as a whole.
3233
* `skill`: Sends a signal to processes based on criteria like user, terminal, etc.
33-
* `tload`: Prints a graphical representation of system load average to the terminal.
3434
* `vmstat`: Reports information about processes, memory, paging, block IO, traps, and CPU activity.
3535

3636
Elsewhere:

src/uu/tload/Cargo.toml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[package]
2+
name = "uu_tload"
3+
version = "0.0.1"
4+
edition = "2021"
5+
authors = ["uutils developers"]
6+
license = "MIT"
7+
description = "tload ~ (uutils) graphic representation of system load average"
8+
9+
homepage = "https://github.com/uutils/procps"
10+
repository = "https://github.com/uutils/procps/tree/main/src/uu/tload"
11+
keywords = ["acl", "uutils", "cross-platform", "cli", "utility"]
12+
categories = ["command-line-utilities"]
13+
14+
[dependencies]
15+
clap = { workspace = true }
16+
crossterm = { workspace = true }
17+
ratatui = { workspace = true }
18+
uucore = { workspace = true }
19+
20+
[lib]
21+
path = "src/tload.rs"
22+
23+
[[bin]]
24+
name = "tload"
25+
path = "src/main.rs"

src/uu/tload/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uucore::bin!(uu_tload);

src/uu/tload/src/tload.rs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
// This file is part of the uutils procps package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// file that was distributed with this source code.
5+
6+
use std::collections::VecDeque;
7+
use std::sync::{Arc, RwLock};
8+
use std::thread::{self, sleep};
9+
use std::time::Duration;
10+
11+
use clap::{arg, crate_version, value_parser, ArgAction, ArgMatches, Command};
12+
use crossterm::event::{self, KeyCode, KeyEvent, KeyModifiers};
13+
use tui::{LegacyTui, ModernTui};
14+
use uucore::{error::UResult, format_usage, help_about, help_usage};
15+
16+
const ABOUT: &str = help_about!("tload.md");
17+
const USAGE: &str = help_usage!("tload.md");
18+
19+
mod tui;
20+
21+
#[derive(Debug, Default, Clone)]
22+
struct SystemLoadAvg {
23+
pub(crate) last_1: f32,
24+
pub(crate) last_5: f32,
25+
pub(crate) last_10: f32,
26+
}
27+
28+
impl SystemLoadAvg {
29+
#[cfg(target_os = "linux")]
30+
fn new() -> UResult<SystemLoadAvg> {
31+
use std::fs;
32+
use uucore::error::USimpleError;
33+
34+
let result = fs::read_to_string("/proc/loadavg")?;
35+
let split = result.split(" ").collect::<Vec<_>>();
36+
37+
// Helper function to keep code clean
38+
fn f(s: &str) -> UResult<f32> {
39+
s.parse::<f32>()
40+
.map_err(|e| USimpleError::new(1, e.to_string()))
41+
}
42+
43+
Ok(SystemLoadAvg {
44+
last_1: f(split[0])?,
45+
last_5: f(split[1])?,
46+
last_10: f(split[2])?,
47+
})
48+
}
49+
50+
#[cfg(not(target_os = "linux"))]
51+
fn new() -> UResult<SystemLoadAvg> {
52+
Ok(SystemLoadAvg::default())
53+
}
54+
}
55+
56+
#[allow(unused)]
57+
#[derive(Debug)]
58+
struct Settings {
59+
delay: u64,
60+
scale: usize, // Not used
61+
62+
is_modern: bool, // For modern display
63+
}
64+
65+
impl Settings {
66+
fn new(matches: &ArgMatches) -> Settings {
67+
Settings {
68+
delay: matches.get_one("delay").cloned().unwrap(),
69+
scale: matches.get_one("scale").cloned().unwrap(),
70+
is_modern: matches.get_flag("modern"),
71+
}
72+
}
73+
}
74+
75+
#[uucore::main]
76+
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
77+
let matches = uu_app().try_get_matches_from(args)?;
78+
let settings = Settings::new(&matches);
79+
80+
let mut terminal = ratatui::init();
81+
82+
let data = {
83+
// Why 10240?
84+
//
85+
// Emm, maybe there will be some terminal can display more than 10000 char?
86+
let data = Arc::new(RwLock::new(VecDeque::with_capacity(10240)));
87+
data.write()
88+
.unwrap()
89+
.push_back(SystemLoadAvg::new().unwrap());
90+
data
91+
};
92+
let cloned_data = data.clone();
93+
thread::spawn(move || loop {
94+
sleep(Duration::from_secs(settings.delay));
95+
96+
let mut data = cloned_data.write().unwrap();
97+
if data.iter().len() >= 10240 {
98+
// Keep this VecDeque smaller than 10240
99+
data.pop_front();
100+
}
101+
data.push_back(SystemLoadAvg::new().unwrap());
102+
});
103+
104+
loop {
105+
// Now only accept `Ctrl+C` for compatibility with the original implementation
106+
//
107+
// Use `event::poll` for non-blocking event reading
108+
if let Ok(true) = event::poll(Duration::from_millis(10)) {
109+
// If event available, break this loop
110+
if let Ok(event::Event::Key(KeyEvent {
111+
code: KeyCode::Char('c'),
112+
modifiers: KeyModifiers::CONTROL,
113+
..
114+
})) = event::read()
115+
{
116+
// compatibility with the original implementation
117+
uucore::error::set_exit_code(130);
118+
break;
119+
}
120+
}
121+
122+
terminal.draw(|frame| {
123+
let data = &data.read().unwrap();
124+
let data = data.iter().cloned().collect::<Vec<_>>();
125+
frame.render_widget(
126+
if settings.is_modern {
127+
ModernTui::new(&data)
128+
} else {
129+
LegacyTui::new(&data)
130+
},
131+
frame.area(),
132+
);
133+
})?;
134+
135+
std::thread::sleep(Duration::from_millis(10));
136+
}
137+
138+
ratatui::restore();
139+
Ok(())
140+
}
141+
142+
#[allow(clippy::cognitive_complexity)]
143+
pub fn uu_app() -> Command {
144+
Command::new(uucore::util_name())
145+
.version(crate_version!())
146+
.about(ABOUT)
147+
.override_usage(format_usage(USAGE))
148+
.infer_long_args(true)
149+
.args([
150+
arg!(-d --delay <secs> "update delay in seconds")
151+
.value_parser(value_parser!(u64))
152+
.default_value("5")
153+
.hide_default_value(true),
154+
arg!(-m --modern "modern look").action(ArgAction::SetTrue),
155+
// TODO: Implement this arg
156+
arg!(-s --scale <num> "vertical scale")
157+
.value_parser(value_parser!(usize))
158+
.default_value("5")
159+
.hide_default_value(true),
160+
])
161+
}
162+
163+
#[cfg(test)]
164+
mod tests {
165+
use super::*;
166+
167+
// It's just a test to make sure if can parsing correctly.
168+
#[test]
169+
fn test_system_load_avg() {
170+
let _ = SystemLoadAvg::new().expect("SystemLoadAvg::new");
171+
}
172+
}

src/uu/tload/src/tui.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// This file is part of the uutils procps package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// file that was distributed with this source code.
5+
6+
use ratatui::{
7+
buffer::Buffer,
8+
layout::{Constraint, Direction, Layout, Rect},
9+
style::{Style, Stylize},
10+
symbols::Marker,
11+
text::{Line, Text},
12+
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType, Paragraph, Widget},
13+
};
14+
15+
use crate::SystemLoadAvg;
16+
17+
pub(crate) struct ModernTui<'a>(&'a [SystemLoadAvg]);
18+
19+
impl ModernTui<'_> {
20+
fn render_header(&self, area: Rect, buf: &mut Buffer) {
21+
let text = Text::from(vec![
22+
Line::from(format!(
23+
"Last 1 min load: {:>5}",
24+
self.0.last().unwrap().last_1
25+
)),
26+
Line::from(format!(
27+
"Last 5 min load: {:>5}",
28+
self.0.last().unwrap().last_5
29+
)),
30+
Line::from(format!(
31+
"Last 10 min load: {:>5}",
32+
self.0.last().unwrap().last_10
33+
)),
34+
]);
35+
36+
Paragraph::new(text)
37+
.style(Style::default().bold().italic())
38+
.block(
39+
Block::new()
40+
.borders(Borders::ALL)
41+
.title("System load history"),
42+
)
43+
.render(area, buf);
44+
}
45+
46+
fn render_chart(&self, area: Rect, buf: &mut Buffer) {
47+
let result = &self.0[self.0.len().saturating_sub(area.width.into())..]
48+
.iter()
49+
.enumerate()
50+
.map(|(index, load)| (index as f64, load.last_1 as f64))
51+
.collect::<Vec<_>>();
52+
53+
let data = Dataset::default()
54+
.graph_type(GraphType::Line)
55+
.marker(Marker::Braille)
56+
.data(result);
57+
58+
let x_axis = {
59+
let start = Line::from("0");
60+
let middle = Line::from((area.width / 2).to_string());
61+
let end = Line::from(area.width.to_string());
62+
Axis::default()
63+
.title("Time(per delay)")
64+
.bounds([0.0, area.width.into()])
65+
.labels(vec![start, middle, end])
66+
};
67+
68+
// Why this tweak?
69+
//
70+
// Sometime the chart cannot display all the line because of max height are equals the max
71+
// load of system in the history, so I add 0.2*{max_load} to the height of chart make it
72+
// display beautiful
73+
let y_axis_upper_bound = result.iter().map(|it| it.1).reduce(f64::max).unwrap_or(0.0);
74+
let y_axis_upper_bound = y_axis_upper_bound + y_axis_upper_bound * 0.2;
75+
let label = {
76+
let min = "0.0".to_owned();
77+
let mid = format!("{:.1}", y_axis_upper_bound / 2.0);
78+
let max = format!("{:.1}", y_axis_upper_bound);
79+
vec![min, mid, max]
80+
};
81+
let y_axis = Axis::default()
82+
.bounds([0.0, y_axis_upper_bound])
83+
.labels(label)
84+
.title("System Load");
85+
86+
Chart::new(vec![data])
87+
.x_axis(x_axis)
88+
.y_axis(y_axis)
89+
.render(area, buf);
90+
}
91+
}
92+
93+
impl ModernTui<'_> {
94+
pub(crate) fn new(input: &[SystemLoadAvg]) -> ModernTui<'_> {
95+
ModernTui(input)
96+
}
97+
}
98+
99+
impl Widget for ModernTui<'_> {
100+
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
101+
where
102+
Self: Sized,
103+
{
104+
let layout = Layout::new(
105+
Direction::Vertical,
106+
[Constraint::Length(5), Constraint::Min(0)],
107+
)
108+
.split(area);
109+
110+
let header = layout[0];
111+
let chart = layout[1];
112+
113+
self.render_header(header, buf);
114+
self.render_chart(chart, buf);
115+
}
116+
}
117+
118+
// TODO: Implemented LegacyTui
119+
pub(crate) type LegacyTui<'a> = ModernTui<'a>;

src/uu/tload/tload.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# tload
2+
3+
```
4+
tload [options] [tty]
5+
```
6+
7+
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).

0 commit comments

Comments
 (0)