mirror of
https://github.com/uutils/procps.git
synced 2026-06-10 16:14:00 -07:00
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:
Generated
+358
-5
File diff suppressed because it is too large
Load Diff
+6
-2
@@ -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" }
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
uucore::bin!(uu_tload);
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
@@ -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).
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user