Compare commits
9 Commits
2bce347cd5
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| bee2189ae5 | |||
| 3b2c74c01e | |||
| b21a06b622 | |||
| 4fcbf23210 | |||
| b124148b0b | |||
| b9935c991f | |||
| f51bbf62d9 | |||
| 376db0eeb4 | |||
| ee79a10968 |
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -208,6 +208,7 @@ dependencies = [
|
|||||||
"glob",
|
"glob",
|
||||||
"i3monkit",
|
"i3monkit",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
|
"regex",
|
||||||
"structopt",
|
"structopt",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -27,3 +27,4 @@ chrono = "0.4"
|
|||||||
chrono-tz = "0.5"
|
chrono-tz = "0.5"
|
||||||
glob = "0.3.1"
|
glob = "0.3.1"
|
||||||
thiserror = "1.0.40"
|
thiserror = "1.0.40"
|
||||||
|
regex = "1.9.1"
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use chrono::NaiveTime;
|
use chrono::NaiveTime;
|
||||||
use i3monkit::{ColorRGB, Header, I3Protocol, WidgetCollection};
|
use i3monkit::{widgets::VolumeWidget, ColorRGB, Header, I3Protocol, WidgetCollection};
|
||||||
use i3xs::widgets::{
|
use i3xs::widgets::{
|
||||||
cpu::CpuWidget,
|
cpu::CpuWidget,
|
||||||
datetime::{DateTimeWidget, TimeColor},
|
datetime::{DateTimeWidget, TimeColor},
|
||||||
|
disk::AllDiskSpeedWidget,
|
||||||
network::AllNetworkSpeedWidget,
|
network::AllNetworkSpeedWidget,
|
||||||
power::PowerSupply,
|
power::PowerSupply,
|
||||||
};
|
};
|
||||||
@@ -20,9 +21,14 @@ fn main() {
|
|||||||
bar.push(PowerSupply::default());
|
bar.push(PowerSupply::default());
|
||||||
bar.push(CpuWidget::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));
|
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");
|
let mut dt = DateTimeWidget::new("%m/%d %H:%M");
|
||||||
dt.set_colors(vec![
|
dt.set_colors(vec![
|
||||||
TimeColor {
|
TimeColor {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use chrono::NaiveTime;
|
use chrono::NaiveTime;
|
||||||
use i3monkit::{ColorRGB, Header, I3Protocol, WidgetCollection};
|
use i3monkit::{widgets::VolumeWidget, ColorRGB, Header, I3Protocol, WidgetCollection};
|
||||||
use i3xs::widgets::{
|
use i3xs::widgets::{
|
||||||
cpu::CpuWidget,
|
cpu::CpuWidget,
|
||||||
datetime::{DateTimeWidget, TimeColor},
|
datetime::{DateTimeWidget, TimeColor},
|
||||||
|
disk::AllDiskSpeedWidget,
|
||||||
network::AllNetworkSpeedWidget,
|
network::AllNetworkSpeedWidget,
|
||||||
power::PowerSupply,
|
power::PowerSupply,
|
||||||
};
|
};
|
||||||
@@ -23,7 +24,11 @@ fn main() {
|
|||||||
// Realtime upload/download rate for a interface
|
// Realtime upload/download rate for a interface
|
||||||
bar.push(AllNetworkSpeedWidget::new(6));
|
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![
|
dt.set_colors(vec![
|
||||||
TimeColor {
|
TimeColor {
|
||||||
start: NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
|
start: NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
|
||||||
@@ -44,6 +49,8 @@ fn main() {
|
|||||||
]);
|
]);
|
||||||
bar.push(dt);
|
bar.push(dt);
|
||||||
|
|
||||||
|
bar.push(VolumeWidget::new("default", "Master", 0));
|
||||||
|
|
||||||
let mut dt = DateTimeWidget::new("%m/%d %H:%M");
|
let mut dt = DateTimeWidget::new("%m/%d %H:%M");
|
||||||
dt.set_colors(vec![
|
dt.set_colors(vec![
|
||||||
TimeColor {
|
TimeColor {
|
||||||
@@ -64,6 +71,8 @@ fn main() {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
bar.push(dt);
|
bar.push(dt);
|
||||||
|
let dt_utc = DateTimeWidget::tz("UTC %H:%M", chrono::offset::Utc);
|
||||||
|
bar.push(dt_utc);
|
||||||
|
|
||||||
// Then start updating the status bar
|
// Then start updating the status bar
|
||||||
bar.update_loop(I3Protocol::new(Header::new(1), std::io::stdout()));
|
bar.update_loop(I3Protocol::new(Header::new(1), std::io::stdout()));
|
||||||
|
|||||||
199
src/widgets/disk.rs
Normal file
199
src/widgets/disk.rs
Normal 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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod cpu;
|
pub mod cpu;
|
||||||
pub mod datetime;
|
pub mod datetime;
|
||||||
|
pub mod disk;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
pub mod power;
|
pub mod power;
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use i3monkit::{Block, Widget, WidgetUpdate};
|
use i3monkit::{Block, Widget, WidgetUpdate};
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
use crate::spark;
|
use crate::spark;
|
||||||
|
|
||||||
const NETWORK_PATH_PREFIX: &str = "/sys/class/net";
|
const NETWORK_PATH_PREFIX: &str = "/sys/class/net";
|
||||||
const NETWORK_STAT_SUFFIX: &str = "statistics/dummy";
|
const NETWORK_STAT_SUFFIX: &str = "statistics/dummy";
|
||||||
const DEVICE_PREFIX: &str = "enp";
|
|
||||||
|
|
||||||
struct TransferStat {
|
struct TransferStat {
|
||||||
rx: u64,
|
rx: u64,
|
||||||
@@ -86,8 +86,8 @@ impl NetworkSpeedWidget {
|
|||||||
pub fn new(interface: &str, num_samples: usize) -> Self {
|
pub fn new(interface: &str, num_samples: usize) -> Self {
|
||||||
let last_stat = TransferStat::read_stat(interface).unwrap();
|
let last_stat = TransferStat::read_stat(interface).unwrap();
|
||||||
let interface = interface.to_string();
|
let interface = interface.to_string();
|
||||||
let rx_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(|_| 0.).collect();
|
let tx_history: VecDeque<f32> = (0..num_samples).map(|_| 1.).collect();
|
||||||
Self {
|
Self {
|
||||||
last_stat,
|
last_stat,
|
||||||
interface,
|
interface,
|
||||||
@@ -97,9 +97,7 @@ impl NetworkSpeedWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_active(&self) -> bool {
|
fn is_active(&self) -> bool {
|
||||||
self.last_stat.rx > 0
|
(self.rx_history.iter().sum::<f32>() + self.tx_history.iter().sum::<f32>()) > 0.
|
||||||
|| self.last_stat.tx > 0
|
|
||||||
|| (self.rx_history.iter().sum::<f32>() + self.tx_history.iter().sum::<f32>()) > 0.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_rate(rate: f64) -> String {
|
fn format_rate(rate: f64) -> String {
|
||||||
@@ -177,18 +175,18 @@ impl Widget for NetworkSpeedWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct AllNetworkSpeedWidget {
|
pub struct AllNetworkSpeedWidget {
|
||||||
num_samples: usize,
|
|
||||||
nics: Vec<NetworkSpeedWidget>,
|
nics: Vec<NetworkSpeedWidget>,
|
||||||
}
|
}
|
||||||
impl AllNetworkSpeedWidget {
|
impl AllNetworkSpeedWidget {
|
||||||
pub fn new(num_samples: usize) -> Self {
|
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)
|
let nics = std::fs::read_dir(NETWORK_PATH_PREFIX)
|
||||||
.expect(&format!("couldn't list {NETWORK_PATH_PREFIX}"))
|
.expect(&format!("couldn't list {NETWORK_PATH_PREFIX}"))
|
||||||
.filter_map(|dir| {
|
.filter_map(|dir| {
|
||||||
let d = dir.unwrap();
|
let d = dir.unwrap();
|
||||||
let p = d.file_name();
|
let p = d.file_name();
|
||||||
let p = p.to_string_lossy();
|
let p = p.to_string_lossy();
|
||||||
if p.starts_with(DEVICE_PREFIX) {
|
if dev_pat.is_match(&p) {
|
||||||
Some(NetworkSpeedWidget::new(&p, num_samples))
|
Some(NetworkSpeedWidget::new(&p, num_samples))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -196,7 +194,7 @@ impl AllNetworkSpeedWidget {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
AllNetworkSpeedWidget { num_samples, nics }
|
AllNetworkSpeedWidget { nics }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,14 @@ impl PowerSupply {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Result<_, _>>()?;
|
.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
|
let cap: u32 = values
|
||||||
.get("POWER_SUPPLY_CAPACITY")
|
.get("POWER_SUPPLY_CAPACITY")
|
||||||
.unwrap_or(&"0")
|
.unwrap_or(&"0")
|
||||||
|
|||||||
Reference in New Issue
Block a user