use shep_core::protocol::HostUsage;
use crate::lookout::view::cell;
use crate::output::human_bytes;
use crate::output::width::char_columns;
use crate::style::Presentation;
use crate::vocabulary::Role;
const GAUGE_CELLS: usize = 10;
const SEPARATOR: &str = " ";
pub(crate) fn strip(usage: Option<HostUsage>, style: Presentation, width: usize) -> String {
let drawn = style.level.boxes();
let runs = runs(usage, drawn);
if drawn {
return fit(&runs, style, width);
}
runs.iter().map(|run| run.text.as_str()).collect()
}
struct Run {
text: String,
ink: Ink,
}
#[derive(Clone, Copy)]
enum Ink {
Muted,
Cpu,
Mem,
}
impl Ink {
fn role(self, style: Presentation) -> Option<Role> {
if !style.colour {
return None;
}
Some(match self {
Ink::Muted => Role::Ink3,
Ink::Cpu => Role::Butter,
Ink::Mem => Role::Sky,
})
}
fn paint(self, text: &str, style: Presentation) -> String {
match self.role(style) {
None => text.to_owned(),
Some(role) => {
let painted = crate::output::paint::style_for(role, style.deep_colour);
format!("{painted}{text}{painted:#}")
}
}
}
}
fn fit(runs: &[Run], style: Presentation, width: usize) -> String {
let total: usize = runs
.iter()
.flat_map(|run| run.text.chars())
.map(char_columns)
.sum();
if total <= width {
return runs
.iter()
.map(|run| run.ink.paint(&run.text, style))
.collect();
}
if width == 0 {
return String::new();
}
let budget = width - 1;
let mut out = String::new();
let mut used = 0;
for run in runs {
if used >= budget {
break;
}
let mut kept = String::new();
for c in run.text.chars() {
let columns = char_columns(c);
if used + columns > budget {
break;
}
kept.push(c);
used += columns;
}
if !kept.is_empty() {
out.push_str(&run.ink.paint(&kept, style));
}
}
out.push_str(&Ink::Muted.paint("…", style));
out
}
fn cpu_gauge(percent: Option<f32>) -> String {
match percent {
Some(percent) => cell::gauge(
percent.clamp(0.0, 100.0).round() as u64,
Some(100),
GAUGE_CELLS,
),
None => cell::gauge(0, None, GAUGE_CELLS),
}
}
fn rate(bytes_per_second: Option<u64>) -> String {
match bytes_per_second {
Some(bytes) => format!("{}/s", human_bytes(bytes)),
None => "-".to_owned(),
}
}
fn runs(usage: Option<HostUsage>, gauges: bool) -> Vec<Run> {
let muted = |text: String| Run {
text,
ink: Ink::Muted,
};
let Some(usage) = usage else {
return vec![muted(
"host usage is not available on this platform".to_owned(),
)];
};
let (read, written) = usage
.disk_bytes_per_second
.map_or((None, None), |(read, written)| (Some(read), Some(written)));
let (received, transmitted) = usage
.network_bytes_per_second
.map_or((None, None), |(received, transmitted)| {
(Some(received), Some(transmitted))
});
let cpu = match usage.cpu_percent {
Some(percent) => format!("{percent:.0}%"),
None => "-".to_owned(),
};
let mut out = vec![
muted("host cpu ".to_owned()),
];
if gauges {
out.push(Run {
text: cpu_gauge(usage.cpu_percent),
ink: Ink::Cpu,
});
out.push(muted(" ".to_owned()));
}
out.push(muted(cpu));
out.push(muted(format!("{SEPARATOR}mem ")));
if gauges {
out.push(Run {
text: cell::gauge(
usage.memory_used_bytes,
Some(usage.memory_total_bytes),
GAUGE_CELLS,
),
ink: Ink::Mem,
});
out.push(muted(" ".to_owned()));
}
out.extend([
muted(format!(
"{} / {}",
human_bytes(usage.memory_used_bytes),
human_bytes(usage.memory_total_bytes)
)),
muted(format!(
"{SEPARATOR}disk r {} w {}",
rate(read),
rate(written)
)),
muted(format!(
"{SEPARATOR}net rx {} tx {}",
rate(received),
rate(transmitted)
)),
]);
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::StyleLevel;
fn usage(rates: bool) -> HostUsage {
HostUsage {
cpu_percent: rates.then_some(11.459_433),
memory_used_bytes: 39_963_869_184,
memory_total_bytes: 51_539_607_552,
disk_bytes_per_second: rates.then_some((1_258_291, 491_520)),
network_bytes_per_second: rates.then_some((24_594, 9_260)),
}
}
fn drawn() -> Presentation {
Presentation {
level: StyleLevel::Plain,
colour: false,
deep_colour: false,
width: 200,
}
}
fn coloured() -> Presentation {
Presentation::new(StyleLevel::Full, None, None, None, 200)
}
#[test]
fn a_measured_reading_names_all_four() {
assert_eq!(
strip(Some(usage(true)), drawn(), 200),
"host cpu █░░░░░░░░░ 11% mem ████████░░ 37.2G / 48.0G \
disk r 1.2M/s w 480.0K/s net rx 24.0K/s tx 9.0K/s"
);
}
#[test]
fn a_plain_strip_drops_both_bars_and_is_never_cut() {
let plain = strip(Some(usage(true)), Presentation::BARE, 80);
assert_eq!(
plain,
"host cpu 11% mem 37.2G / 48.0G \
disk r 1.2M/s w 480.0K/s net rx 24.0K/s tx 9.0K/s"
);
assert!(!plain.contains('█') && !plain.contains('░'));
assert!(!plain.contains('…'), "80 columns is a guess, not a window");
}
#[test]
fn a_reading_with_no_window_dashes_every_rate_and_still_prints_memory() {
assert_eq!(
strip(Some(usage(false)), drawn(), 200),
"host cpu ░░░░░░░░░░ - mem ████████░░ 37.2G / 48.0G \
disk r - w - net rx - tx -"
);
}
#[test]
fn a_platform_that_cannot_be_read_says_so() {
assert_eq!(
strip(None, Presentation::BARE, 200),
"host usage is not available on this platform"
);
}
#[test]
fn a_narrow_strip_truncates_visibly_and_keeps_the_gauges() {
let narrow = strip(Some(usage(true)), drawn(), 40);
assert!(narrow.starts_with("host cpu "), "got {narrow:?}");
assert!(
narrow.ends_with('…'),
"a truncation the operator can see: {narrow:?}"
);
assert!(!narrow.contains("net rx"), "net is the first thing off");
assert_eq!(
narrow.chars().map(char_columns).sum::<usize>(),
40,
"the cut lands on the budget, ellipsis included"
);
let full = strip(Some(usage(true)), drawn(), 200);
assert!(!full.contains('…'));
assert!(full.contains("net rx 24.0K/s"));
}
#[test]
fn a_strip_with_no_columns_is_empty_rather_than_a_panic() {
assert_eq!(strip(Some(usage(true)), drawn(), 0), "");
assert_eq!(strip(Some(usage(true)), drawn(), 1), "…");
}
#[test]
fn only_the_two_gauges_wear_a_colour_of_their_own() {
let style = coloured();
let painted = strip(Some(usage(true)), style, 200);
let span = |role| {
format!(
"{}",
crate::output::paint::style_for(role, style.deep_colour)
)
};
assert_eq!(
painted.matches(&span(Role::Butter)).count(),
1,
"the CPU bar and nothing else: {painted:?}"
);
assert_eq!(
painted.matches(&span(Role::Sky)).count(),
1,
"the memory bar and nothing else: {painted:?}"
);
assert!(painted.contains(&format!("{}█░░░░░░░░░", span(Role::Butter))));
assert!(painted.contains(&format!("{}████████░░", span(Role::Sky))));
}
#[test]
fn a_bare_strip_carries_no_escapes() {
let bare = strip(Some(usage(true)), Presentation::BARE, 200);
assert!(!bare.contains('\u{1b}'), "got {bare:?}");
}
#[test]
fn a_truncated_coloured_strip_closes_every_span_it_opens() {
let painted = strip(Some(usage(true)), coloured(), 25);
let opens = painted.matches("\u{1b}[").count();
let closes = painted.matches("\u{1b}[0m").count();
assert_eq!(
opens,
closes * 2,
"one open and one reset per run: {painted:?}"
);
}
}