Compare commits

..

9 Commits

Author SHA1 Message Date
bee2189ae5 work: add Amsterdam time 2025-03-05 17:20:08 -08:00
3b2c74c01e Add UTC time to work clock 2025-01-23 12:30:43 -08:00
b21a06b622 Address lint 2024-08-27 11:43:46 -07:00
4fcbf23210 Show current volume level, disable London time on work 2024-08-27 11:42:24 -07:00
b124148b0b Only show power levels for battery powered things 2024-08-27 11:31:33 -07:00
b9935c991f Make network device filtering a regex.
Work and home have different naming schemes.
2023-07-31 07:53:11 -07:00
f51bbf62d9 Merge branch 'master' of https://git.z.xinu.tv/wathiede/i3xs 2023-07-30 14:06:55 -07:00
376db0eeb4 Add disk R/W stats 2023-07-30 14:06:39 -07:00
ee79a10968 Add disk R/W stats 2023-07-30 14:06:06 -07:00
8 changed files with 236 additions and 13 deletions

1
Cargo.lock generated
View File

@@ -208,6 +208,7 @@ dependencies = [
"glob",
"i3monkit",
"num_cpus",
"regex",
"structopt",
"thiserror",
]

View File

@@ -27,3 +27,4 @@ chrono = "0.4"
chrono-tz = "0.5"
glob = "0.3.1"
thiserror = "1.0.40"
regex = "1.9.1"

View File

@@ -1,8 +1,9 @@
use chrono::NaiveTime;
use i3monkit::{ColorRGB, Header, I3Protocol, WidgetCollection};
use i3monkit::{widgets::VolumeWidget, ColorRGB, Header, I3Protocol, WidgetCollection};
use i3xs::widgets::{
cpu::CpuWidget,
datetime::{DateTimeWidget, TimeColor},
disk::AllDiskSpeedWidget,
network::AllNetworkSpeedWidget,
power::PowerSupply,
};
@@ -20,9 +21,14 @@ fn main() {
bar.push(PowerSupply::default());
bar.push(CpuWidget::default());
// Realtime upload/download rate for a interface
// Realtime upload/download rate for a any active NICs
bar.push(AllNetworkSpeedWidget::new(6));
// Realtime read/write rate for a any active disks
bar.push(AllDiskSpeedWidget::new(6));
bar.push(VolumeWidget::new("default", "Master", 0));
let mut dt = DateTimeWidget::new("%m/%d %H:%M");
dt.set_colors(vec![
TimeColor {

View File

@@ -1,8 +1,9 @@
use chrono::NaiveTime;
use i3monkit::{ColorRGB, Header, I3Protocol, WidgetCollection};
use i3monkit::{widgets::VolumeWidget, ColorRGB, Header, I3Protocol, WidgetCollection};
use i3xs::widgets::{
cpu::CpuWidget,
datetime::{DateTimeWidget, TimeColor},
disk::AllDiskSpeedWidget,
network::AllNetworkSpeedWidget,
power::PowerSupply,
};
@@ -23,7 +24,11 @@ fn main() {
// Realtime upload/download rate for a interface
bar.push(AllNetworkSpeedWidget::new(6));
let mut dt = DateTimeWidget::tz("%H:%M %Z", chrono_tz::Europe::London);
// Realtime read/write rate for a any active disks
bar.push(AllDiskSpeedWidget::new(6));
// Setup time in Amsterdam widget with end of day warnings
let mut dt = DateTimeWidget::tz("%H:%M %Z", chrono_tz::Europe::Amsterdam);
dt.set_colors(vec![
TimeColor {
start: NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
@@ -44,6 +49,8 @@ fn main() {
]);
bar.push(dt);
bar.push(VolumeWidget::new("default", "Master", 0));
let mut dt = DateTimeWidget::new("%m/%d %H:%M");
dt.set_colors(vec![
TimeColor {
@@ -64,6 +71,8 @@ fn main() {
},
]);
bar.push(dt);
let dt_utc = DateTimeWidget::tz("UTC %H:%M", chrono::offset::Utc);
bar.push(dt_utc);
// Then start updating the status bar
bar.update_loop(I3Protocol::new(Header::new(1), std::io::stdout()));

199
src/widgets/disk.rs Normal file
View File

@@ -0,0 +1,199 @@
use std::{collections::vec_deque::VecDeque, io::Result, time::SystemTime};
use i3monkit::{Block, Widget, WidgetUpdate};
use crate::spark;
const DISK_STATS_PAT: &str = "/sys/class/block/*/*/stat";
struct TransferStat {
rx: u64,
tx: u64,
ts: SystemTime,
}
impl TransferStat {
fn read_stat(stat_path: &str) -> Result<Self> {
// File format documented at https://www.kernel.org/doc/Documentation/block/stat.txt
let s = std::fs::read_to_string(stat_path)?;
let parts: Vec<_> = s.split(" ").filter(|s| !s.is_empty()).collect();
let rx = parts[2].parse().unwrap_or(0) * 512;
let tx = parts[6].parse().unwrap_or(0) * 512;
let ts = SystemTime::now();
Ok(Self { rx, tx, ts })
}
fn duration(&self, earlier: &Self) -> f64 {
let duration = self.ts.duration_since(earlier.ts).unwrap();
duration.as_secs() as f64 + duration.subsec_nanos() as f64 / 1_000_000_000.0
}
fn rx_rate(&self, earlier: &Self) -> f64 {
let duration = self.duration(earlier);
if duration < 1e-5 {
return std::f64::NAN;
}
(self.rx - earlier.rx) as f64 / duration
}
fn tx_rate(&self, earlier: &Self) -> f64 {
let duration = self.duration(earlier);
if duration < 1e-5 {
return std::f64::NAN;
}
(self.tx - earlier.tx) as f64 / duration
}
}
/// A widget that shows the disk R/W realtimely
pub struct DiskSpeedWidget {
device: String,
last_stat: TransferStat,
rx_history: VecDeque<f32>,
tx_history: VecDeque<f32>,
}
impl DiskSpeedWidget {
/// Create the widget, for given device.
///
/// **device** The interface to monitor
pub fn new(device: &str, num_samples: usize) -> Self {
let last_stat = TransferStat::read_stat(device).unwrap();
let device = device.to_string();
let rx_history: VecDeque<f32> = (0..num_samples).map(|_| 1.).collect();
let tx_history: VecDeque<f32> = (0..num_samples).map(|_| 1.).collect();
Self {
last_stat,
device,
rx_history,
tx_history,
}
}
fn is_active(&self) -> bool {
(self.rx_history.iter().sum::<f32>() + self.tx_history.iter().sum::<f32>()) > 0.
}
fn format_rate(rate: f64) -> String {
if rate.is_nan() {
return "N/A".to_string();
}
const UNIT_NAME: [&str; 6] = [" B/s", "KB/s", "MB/s", "GB/s", "TB/s", "PB/s"];
let mut best_unit = UNIT_NAME[0];
let mut best_multiplier = 1.0;
for unit in UNIT_NAME[1..].iter() {
if best_multiplier > rate / 1024.0 {
break;
}
best_unit = unit;
best_multiplier *= 1024.0
}
format!("{:6.1}{}", rate / best_multiplier, best_unit)
}
fn get_human_readable_stat(&mut self) -> Result<(String, String)> {
let cur_stat = TransferStat::read_stat(&self.device)?;
let rx_rate = cur_stat.rx_rate(&self.last_stat);
let tx_rate = cur_stat.tx_rate(&self.last_stat);
self.rx_history.push_back(rx_rate as f32);
self.rx_history.pop_front();
self.tx_history.push_back(tx_rate as f32);
self.tx_history.pop_front();
self.last_stat = cur_stat;
Ok((Self::format_rate(rx_rate), Self::format_rate(tx_rate)))
}
fn render(&mut self) -> Option<String> {
if let Ok((rx, tx)) = self.get_human_readable_stat() {
let (rx_history, tx_history) = if self.rx_history.len() > 1 {
let g = spark::Graph {
min: None,
max: None,
};
(
g.render(&self.rx_history.iter().cloned().collect::<Vec<f32>>()),
g.render(&self.tx_history.iter().cloned().collect::<Vec<f32>>()),
)
} else {
("".to_string(), "".to_string())
};
return Some(format!(
"R:<tt>{} {}</tt> W:<tt>{} {}</tt> ",
rx, rx_history, tx, tx_history
));
}
None
}
}
impl Widget for DiskSpeedWidget {
fn update(&mut self) -> Option<WidgetUpdate> {
if let Some(text) = self.render() {
let mut data = Block::new();
data.use_pango();
data.append_full_text(&text);
return Some(WidgetUpdate {
refresh_interval: std::time::Duration::new(1, 0),
data: Some(data),
});
}
None
}
}
pub struct AllDiskSpeedWidget {
disks: Vec<DiskSpeedWidget>,
}
impl AllDiskSpeedWidget {
pub fn new(num_samples: usize) -> Self {
let disks = glob::glob(DISK_STATS_PAT)
.expect("couldn't find glob for disks")
.map(|path| {
let p = path.unwrap().display().to_string();
DiskSpeedWidget::new(&p, num_samples)
})
.collect();
AllDiskSpeedWidget { disks }
}
}
impl Widget for AllDiskSpeedWidget {
fn update(&mut self) -> Option<WidgetUpdate> {
let disks: Vec<_> = self
.disks
.iter_mut()
.filter_map(|dev| {
if !dev.is_active() {
return None;
}
if let Some(text) = dev.render() {
let name = dev.device.rsplit("/").skip(1).nth(0).unwrap();
Some(format!("{}: {}", name, text))
} else {
None
}
})
.collect();
let mut data = Block::new();
data.use_pango();
disks.iter().for_each(|n| {
data.append_full_text(n);
});
Some(WidgetUpdate {
refresh_interval: std::time::Duration::new(1, 0),
data: Some(data),
})
}
}

View File

@@ -1,4 +1,5 @@
pub mod cpu;
pub mod datetime;
pub mod disk;
pub mod network;
pub mod power;

View File

@@ -7,12 +7,12 @@ use std::{
};
use i3monkit::{Block, Widget, WidgetUpdate};
use regex::Regex;
use crate::spark;
const NETWORK_PATH_PREFIX: &str = "/sys/class/net";
const NETWORK_STAT_SUFFIX: &str = "statistics/dummy";
const DEVICE_PREFIX: &str = "enp";
struct TransferStat {
rx: u64,
@@ -86,8 +86,8 @@ impl NetworkSpeedWidget {
pub fn new(interface: &str, num_samples: usize) -> Self {
let last_stat = TransferStat::read_stat(interface).unwrap();
let interface = interface.to_string();
let rx_history: VecDeque<f32> = (0..num_samples).map(|_| 0.).collect();
let tx_history: VecDeque<f32> = (0..num_samples).map(|_| 0.).collect();
let rx_history: VecDeque<f32> = (0..num_samples).map(|_| 1.).collect();
let tx_history: VecDeque<f32> = (0..num_samples).map(|_| 1.).collect();
Self {
last_stat,
interface,
@@ -97,9 +97,7 @@ impl NetworkSpeedWidget {
}
fn is_active(&self) -> bool {
self.last_stat.rx > 0
|| self.last_stat.tx > 0
|| (self.rx_history.iter().sum::<f32>() + self.tx_history.iter().sum::<f32>()) > 0.
(self.rx_history.iter().sum::<f32>() + self.tx_history.iter().sum::<f32>()) > 0.
}
fn format_rate(rate: f64) -> String {
@@ -177,18 +175,18 @@ impl Widget for NetworkSpeedWidget {
}
pub struct AllNetworkSpeedWidget {
num_samples: usize,
nics: Vec<NetworkSpeedWidget>,
}
impl AllNetworkSpeedWidget {
pub fn new(num_samples: usize) -> Self {
let dev_pat = Regex::new("(enp|eno).*").expect("bad re");
let nics = std::fs::read_dir(NETWORK_PATH_PREFIX)
.expect(&format!("couldn't list {NETWORK_PATH_PREFIX}"))
.filter_map(|dir| {
let d = dir.unwrap();
let p = d.file_name();
let p = p.to_string_lossy();
if p.starts_with(DEVICE_PREFIX) {
if dev_pat.is_match(&p) {
Some(NetworkSpeedWidget::new(&p, num_samples))
} else {
None
@@ -196,7 +194,7 @@ impl AllNetworkSpeedWidget {
})
.collect();
AllNetworkSpeedWidget { num_samples, nics }
AllNetworkSpeedWidget { nics }
}
}

View File

@@ -58,6 +58,14 @@ impl PowerSupply {
})
})
.collect::<Result<_, _>>()?;
// Skip things that aren't battery powered
if !values
.get("POWER_SUPPLY_TYPE")
.map(|t| t == &"Battery")
.unwrap_or(false)
{
continue;
}
let cap: u32 = values
.get("POWER_SUPPLY_CAPACITY")
.unwrap_or(&"0")