Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions src/blocks/cpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@

use std::str::FromStr as _;

use tokio::fs::File;
use tokio::io::{AsyncBufReadExt as _, BufReader};

use super::prelude::*;
use crate::formatting::unit::Unit;
use crate::util::read_file;
use tokio::fs::File;
use tokio::io::BufReader;

const CPU_BOOST_PATH: &str = "/sys/devices/system/cpu/cpufreq/boost";
const CPU_NO_TURBO_PATH: &str = "/sys/devices/system/cpu/intel_pstate/no_turbo";
Expand Down Expand Up @@ -105,13 +105,6 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
}
cputime = new_cputime;

// Create barchart indicating per-core utilization
let mut barchart = String::new();
const BOXCHARS: &[char] = &['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
for utilization in &utilizations {
barchart.push(BOXCHARS[(7.5 * utilization) as usize]);
}

// Read boost state on intel CPUs
let boost = boost_status().await.map(|status| match status {
true => "cpu_boost_on",
Expand All @@ -120,7 +113,7 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {

let mut values = map!(
"icon" => Value::icon_progression("cpu", utilization_avg),
"barchart" => Value::text(barchart),
"barchart" => Value::numbers(utilizations.iter().map(|u| u * 100.0).collect::<Vec<_>>(), Unit::Percents),
"utilization" => Value::percents(utilization_avg * 100.),
[if !freqs.is_empty()] "frequency" => Value::hertz(freqs.iter().sum::<f64>() / (freqs.len() as f64)),
[if !freqs.is_empty()] "max_frequency" => Value::hertz(freqs.iter().copied().max_by(f64::total_cmp).unwrap()),
Expand Down
6 changes: 3 additions & 3 deletions src/blocks/net.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@
//! - `net_down`

use super::prelude::*;
use crate::formatting::unit::Unit;
use crate::netlink::NetDevice;
use crate::util;
use itertools::Itertools as _;
use regex::Regex;
use std::time::Instant;
Expand Down Expand Up @@ -153,8 +153,8 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
"icon" => icon,
"speed_down" => Value::bytes(speed_down),
"speed_up" => Value::bytes(speed_up),
"graph_down" => Value::text(util::format_bar_graph(&rx_hist)),
"graph_up" => Value::text(util::format_bar_graph(&tx_hist)),
"graph_down" => Value::numbers(rx_hist, Unit::Bytes),
"graph_up" => Value::numbers(tx_hist, Unit::Bytes),
[if let Some(v) = device.ip] "ip" => Value::text(v.to_string()),
[if let Some(v) = device.ipv6] "ipv6" => Value::text(v.to_string()),
[if let Some(v) = device.ssid()] "ssid" => Value::text(v),
Expand Down
5 changes: 5 additions & 0 deletions src/formatting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
//! --------------------------|------------------
//! Text | `str`
//! Number | `eng`
//! Numbers | `eng`
//! Datetime | `datetime`
//! Duration | `duration`
//! [Flag](#how-to-use-flags) | N/A
Expand Down Expand Up @@ -60,6 +61,8 @@
//! `range` | a range of allowed values, in the format `<start>..<end>`, inclusive. Both start and end are optional. Can be used to, for example, hide the block when the value is not in a given range. | `..`
//! `show` | show this value. Can be used with `range` for conditional formatting | `true`
//!
//! Note: For [`Value::Numbers`] only the last value will be used for formatting.
//!
//! ## `bar` - Display numbers as progress bars
//!
//! Argument | Description |Default value
Expand All @@ -68,6 +71,8 @@
//! `max_value` | which value is treated as "full". For example, for battery level `100` is full. | `100`
//! `vertical` or `v` | whether to render the bar vertically or not | `false`
//!
//! Note: For [`Value::Numbers`] the result will be a bar chart: if vertical, each inner value will be a single bar; if horizontal, only the last value will be displayed as a single bar.
//!
//! ## `tally` - Display numbers as tally marks
//!
//! Argument | Description |Default value
Expand Down
186 changes: 167 additions & 19 deletions src/formatting/formatter/bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,34 +42,70 @@ impl BarFormatter {
vertical,
})
}
}

const HORIZONTAL_BAR_CHARS: [char; 9] = [
' ', '\u{258f}', '\u{258e}', '\u{258d}', '\u{258c}', '\u{258b}', '\u{258a}', '\u{2589}',
'\u{2588}',
];
#[inline]
fn norm(&self, val: f64, max_value: f64) -> f64 {
// NOTE: This has the drawback of potentially ignoring the max_value set by the user.
Some(val / max_value)
.filter(|v| v.is_finite())
.unwrap_or(0.0)
.clamp(0.0, 1.0)
}

#[inline]
fn format_single_vertical(&self, val: f64, max_value: f64) -> char {
let val = self.norm(val, max_value);
VERTICAL_BAR_CHARS[(val * 8.0).ceil() as usize]
}

#[inline]
fn format_horizontal_cell(&self, val: f64, max_value: f64, i: usize) -> char {
let val = self.norm(val, max_value);
let chars_to_fill = val * self.width as f64;
HORIZONTAL_BAR_CHARS[((chars_to_fill - i as f64).clamp(0.0, 1.0) * 8.0).ceil() as usize]
}

#[inline]
fn format_horizontal_bar(&self, val: f64, max_value: f64) -> String {
(0..self.width)
.map(|i| self.format_horizontal_cell(val, max_value, i))
.collect()
}
}

const VERTICAL_BAR_CHARS: [char; 9] = [
' ', '\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2585}', '\u{2586}', '\u{2587}',
'\u{2588}',
];
const HORIZONTAL_BAR_CHARS: [char; 9] = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
const VERTICAL_BAR_CHARS: [char; 9] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];

impl Formatter for BarFormatter {
fn format(&self, val: &Value, _config: &SharedConfig) -> Result<String, FormatError> {
match val {
&Value::Number { mut val, .. } => {
val = (val / self.max_value).clamp(0., 1.);
Value::Number { val, .. } => {
if self.vertical {
let vert_char = VERTICAL_BAR_CHARS[(val * 8.) as usize];
Ok((0..self.width).map(|_| vert_char).collect())
let c = self.format_single_vertical(*val, self.max_value.max(*val));
Ok(std::iter::repeat_n(c, self.width).collect())
} else {
let chars_to_fill = val * self.width as f64;
Ok((0..self.width)
.map(|i| {
HORIZONTAL_BAR_CHARS
[((chars_to_fill - i as f64).clamp(0., 1.) * 8.) as usize]
})
Ok(self.format_horizontal_bar(*val, self.max_value.max(*val)))
}
}
Value::Numbers { vals, .. } => {
if self.vertical {
// NOTE: print at most `width` values as a windowed chart
let start = vals.len().saturating_sub(self.width);
let shown = vals.len() - start;
let max = vals
.iter()
.copied()
.max_by(f64::total_cmp)
.unwrap_or(self.max_value);

Ok(std::iter::repeat_n(0.0, self.width - shown)
.chain(vals[start..].iter().copied())
.map(|val| self.format_single_vertical(val, self.max_value.max(max)))
.collect())
} else {
// NOTE: print the last value as a horizontal bar
let last = vals.last().copied().unwrap_or(0.0);
Ok(self.format_horizontal_bar(last, self.max_value.max(last)))
}
}
other => Err(FormatError::IncompatibleFormatter {
Expand All @@ -79,3 +115,115 @@ impl Formatter for BarFormatter {
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::formatting::unit::Unit;

fn fmt_horiz(width: usize, max_value: f64) -> BarFormatter {
BarFormatter {
width,
max_value,
vertical: false,
}
}

fn fmt_vert(width: usize, max_value: f64) -> BarFormatter {
BarFormatter {
width,
max_value,
vertical: true,
}
}

fn number(val: f64) -> Value {
Value::Number {
val,
unit: Unit::None,
}
}

fn numbers(vals: &[f64]) -> Value {
Value::Numbers {
vals: vals.to_vec(),
unit: Unit::None,
}
}

#[test]
fn single_number_horizontal_basic() {
let fmt = fmt_horiz(8, 8.0);
let out = fmt.format(&number(4.0), &Default::default()).unwrap();
assert_eq!(out, "████ ");
}

#[test]
fn single_number_horizontal_fractional() {
let fmt = fmt_horiz(8, 8.0);
let out = fmt.format(&number(3.5), &Default::default()).unwrap();
assert_eq!(out, "███▌ ");
}

#[test]
fn single_number_vertical_basic() {
let fmt = fmt_vert(5, 8.0);
let out = fmt.format(&number(4.0), &Default::default()).unwrap();
assert_eq!(out, "▄▄▄▄▄");
}

#[test]
fn single_number_vertical_clamps() {
let fmt = fmt_vert(3, 8.0);
let out = fmt
.format(&number(999.0), &SharedConfig::default())
.unwrap();
assert_eq!(out, "███");

let fmt = fmt_vert(3, 8.0);
let out = fmt.format(&number(-1.0), &Default::default()).unwrap();
assert_eq!(out, " ");
}

#[test]
fn multiple_values_horizontal_uses_last_value() {
let fmt = fmt_horiz(8, 8.0);
let out = fmt
.format(&numbers(&[1.0, 2.0, 4.0]), &Default::default())
.unwrap();
assert_eq!(out, "████ ");
}

#[test]
fn multiple_values_vertical_graph() {
let fmt = fmt_vert(9, 8.0);
let out = fmt
.format(
&numbers(&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]),
&SharedConfig::default(),
)
.unwrap();
assert_eq!(out, " ▁▂▃▄▅▆▇█");
}

#[test]
fn multiple_values_vertical_pads_when_short() {
let fmt = fmt_vert(5, 8.0);
let out = fmt
.format(&numbers(&[4.0, 8.0]), &Default::default())
.unwrap();
assert_eq!(out, " ▄█");
}

#[test]
fn multiple_values_vertical_truncates_when_long() {
let fmt = fmt_vert(4, 8.0);
let out = fmt
.format(
&numbers(&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]),
&Default::default(),
)
.unwrap();
assert_eq!(out, "▅▆▇█");
}
}
10 changes: 10 additions & 0 deletions src/formatting/formatter/eng.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,16 @@ impl Formatter for EngFormatter {

Ok(retval)
}
&Value::Numbers { ref vals, unit } => {
// NOTE: by default, Value::Numbers will be formatted as the last value in the series
Ok(self.format(
&Value::Number {
val: vals.last().copied().unwrap_or(0.0),
unit,
},
_config,
)?)
}
other => Err(FormatError::IncompatibleFormatter {
ty: other.type_name(),
fmt: "eng",
Expand Down
17 changes: 16 additions & 1 deletion src/formatting/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub enum ValueInner {
Text(String),
Icon(Cow<'static, str>, Option<f64>),
Number { val: f64, unit: Unit },
Numbers { vals: Vec<f64>, unit: Unit },
Datetime(DateTime<Utc>, Option<Tz>),
Duration(Duration),
Flag,
Expand All @@ -29,6 +30,7 @@ impl ValueInner {
ValueInner::Text(..) => "Text",
ValueInner::Icon(..) => "Icon",
ValueInner::Number { .. } => "Number",
ValueInner::Numbers { .. } => "Numbers",
ValueInner::Datetime(..) => "Datetime",
ValueInner::Duration(..) => "Duration",
ValueInner::Flag => "Flag",
Expand Down Expand Up @@ -129,6 +131,17 @@ impl Value {
pub fn number(val: impl IntoF64) -> Self {
Self::number_unit(val, Unit::None)
}

pub fn numbers<T, V>(vals: T, unit: Unit) -> Self
where
T: IntoIterator<Item = V>,
V: IntoF64,
{
Self::new(ValueInner::Numbers {
vals: vals.into_iter().map(IntoF64::into_f64).collect(),
unit,
})
}
}

/// Set options
Expand All @@ -151,7 +164,9 @@ impl Value {
pub fn default_formatter(&self) -> &'static dyn formatter::Formatter {
match &self.inner {
ValueInner::Text(_) | ValueInner::Icon(..) => &formatter::DEFAULT_STRING_FORMATTER,
ValueInner::Number { .. } => &formatter::DEFAULT_NUMBER_FORMATTER,
ValueInner::Number { .. } | ValueInner::Numbers { .. } => {
&formatter::DEFAULT_NUMBER_FORMATTER
}
ValueInner::Datetime { .. } => &*formatter::DEFAULT_DATETIME_FORMATTER,
ValueInner::Duration { .. } => &formatter::DEFAULT_DURATION_FORMATTER,
ValueInner::Flag => &formatter::DEFAULT_FLAG_FORMATTER,
Expand Down