use async_trait::async_trait;
use sysinfo::Disks;
use crate::payload::{BarsData, Body, Payload, RatioData, TextData};
use crate::render::Shape;
use crate::samples;
use super::super::{FetchContext, FetchError, Fetcher, Safety};
use super::{disk_bars, disk_label, payload, primary_disk, ratio_of};
pub struct SystemMonitorDisk;
#[async_trait]
impl Fetcher for SystemMonitorDisk {
fn name(&self) -> &str {
"system_monitor_disk"
}
fn safety(&self) -> Safety {
Safety::Safe
}
fn description(&self) -> &'static str {
"Disk usage for the largest mounted volume. `Ratio` drives gauges with the used/total fraction; `Text` formats it as `\"45% of 500 GB\"`; `Bars` lists every mount with used bytes for chart_bar."
}
fn refresh_interval(&self) -> u64 {
60 * 5
}
fn shapes(&self) -> &[Shape] {
&[Shape::Ratio, Shape::Text, Shape::Bars]
}
fn sample_body(&self, shape: Shape) -> Option<Body> {
Some(match shape {
Shape::Ratio => samples::ratio(0.58, "disk"),
Shape::Text => samples::text("58% of 400 GB"),
Shape::Bars => samples::bars(&[("/", 42), ("/home", 110), ("/data", 200)]),
_ => return None,
})
}
async fn fetch(&self, ctx: &FetchContext) -> Result<Payload, FetchError> {
let disks = Disks::new_with_refreshed_list();
let payload = match ctx.shape.unwrap_or(Shape::Ratio) {
Shape::Bars => payload(Body::Bars(BarsData {
bars: disk_bars(&disks),
})),
Shape::Text => payload(Body::Text(TextData {
value: primary_disk(&disks)
.map(|(t, a)| disk_label(t, a))
.unwrap_or_else(|| "no disks".into()),
})),
_ => primary_disk(&disks)
.map(|(total, available)| {
let used = total.saturating_sub(available);
payload(Body::Ratio(RatioData {
value: ratio_of(used, total),
label: Some(disk_label(total, available)),
denominator: None,
}))
})
.unwrap_or_else(|| {
payload(Body::Ratio(RatioData {
value: 0.0,
label: Some("no disks".into()),
denominator: None,
}))
}),
};
Ok(payload)
}
}
#[cfg(test)]
mod tests {
use super::super::test_helpers::ctx_with_shape;
use super::*;
#[test]
fn sample_body_matches_documented_shapes() {
assert!(matches!(
SystemMonitorDisk.sample_body(Shape::Ratio),
Some(Body::Ratio(ratio))
if ratio.label.as_deref() == Some("disk") && ratio.value == 0.58
));
assert!(matches!(
SystemMonitorDisk.sample_body(Shape::Text),
Some(Body::Text(text)) if text.value == "58% of 400 GB"
));
assert!(matches!(
SystemMonitorDisk.sample_body(Shape::Bars),
Some(Body::Bars(bars))
if bars.bars.len() == 3
&& bars.bars[0].label == "/"
&& bars.bars[2].value == 200
));
}
#[tokio::test]
async fn defaults_to_ratio_or_text_fallback() {
let ctx = ctx_with_shape(None);
let p = SystemMonitorDisk.fetch(&ctx).await.unwrap();
assert!(matches!(p.body, Body::Ratio(_) | Body::Text(_)));
}
#[tokio::test]
async fn bars_shape_emits_bars_body() {
let ctx = ctx_with_shape(Some(Shape::Bars));
let p = SystemMonitorDisk.fetch(&ctx).await.unwrap();
assert!(matches!(p.body, Body::Bars(_)));
}
#[tokio::test]
async fn text_shape_emits_text_body() {
let ctx = ctx_with_shape(Some(Shape::Text));
let p = SystemMonitorDisk.fetch(&ctx).await.unwrap();
let Body::Text(t) = p.body else {
panic!("expected Body::Text");
};
assert!(!t.value.is_empty());
}
}