From 98b7a8d201942f904f1aa9af40669445cab13157 Mon Sep 17 00:00:00 2001 From: electria Date: Sat, 15 Aug 2026 18:16:16 -0700 Subject: [PATCH 01/26] feat: multiple graphs --- src/app/cpu_graph.rs | 19 ++++++++++++- src/app/mod.rs | 60 ++++++++++++++++++++++++++++++----------- src/main.rs | 1 + src/sysinfo_snapshot.rs | 18 +++++++++++++ 4 files changed, 82 insertions(+), 16 deletions(-) create mode 100644 src/sysinfo_snapshot.rs diff --git a/src/app/cpu_graph.rs b/src/app/cpu_graph.rs index 42a4a22..9fe439c 100644 --- a/src/app/cpu_graph.rs +++ b/src/app/cpu_graph.rs @@ -8,11 +8,23 @@ const CYAN: Color = Color::from_rgb8(0, 255, 255); #[derive(Default)] pub struct CpuGraph { - pub cache: canvas::Cache, timeline: VecDeque, + + pub cache: canvas::Cache, } impl CpuGraph { + pub fn new(usage: f32) -> Self { + Self { + timeline: { + let mut vec = VecDeque::new(); + vec.push_front(usage); + vec + }, + ..Default::default() + } + } + pub fn update(&mut self, usage: f32) { self.timeline.push_back(usage); if self.timeline.len() > 1000 { @@ -32,6 +44,11 @@ impl canvas::Program for CpuGraph { _cursor: mouse::Cursor, ) -> Vec> { let geometry = self.cache.draw(renderer, bounds.size(), |frame| { + frame.stroke_rectangle( + Point::new(0., 0.), + bounds.size(), + canvas::Stroke::default().with_color(Color::from_rgb8(255, 0, 0)), + ); frame.stroke( &canvas::Path::new(|builder| { for (index, value) in self.timeline.iter().enumerate() { diff --git a/src/app/mod.rs b/src/app/mod.rs index 1e0262a..d989b14 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -4,24 +4,24 @@ use iced::{ }; use smol::Timer; -use crate::app::cpu_graph::CpuGraph; +use crate::{app::cpu_graph::CpuGraph, sysinfo_snapshot::SysinfoSnapshot}; mod cpu_graph; enum Message { - Cpu(f32), + Info(SysinfoSnapshot), } +#[derive(Default)] struct State { - cpu_graph: CpuGraph, + total_cpu_graph: CpuGraph, + cpu_graphs: Vec<(String, CpuGraph)>, } impl State { fn new() -> (Self, Task) { ( - Self { - cpu_graph: Default::default(), - }, + Self::default(), Task::run( channel(1, async |mut sender| { let mut sys = sysinfo::System::new(); @@ -34,28 +34,58 @@ impl State { loop { sys.refresh_specifics(refreshes); Timer::after(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL).await; - sender.send(sys.global_cpu_usage()).await.unwrap(); + sender.send(SysinfoSnapshot::from(&sys)).await.unwrap(); } }), - Message::Cpu, + Message::Info, ), ) } fn update(&mut self, message: Message) -> Task { match message { - Message::Cpu(value) => { - self.cpu_graph.update(value); - self.cpu_graph.cache.clear(); + Message::Info(snapshot) => { + self.total_cpu_graph.update(snapshot.cpu_total); + self.total_cpu_graph.cache.clear(); + + if self.cpu_graphs.len() != snapshot.cpus.len() { + self.cpu_graphs = snapshot + .cpus + .into_iter() + .map(|(name, usage)| (name, CpuGraph::new(usage))) + .collect(); + } else { + for ((_, graph), (_, usage)) in + self.cpu_graphs.iter_mut().zip(snapshot.cpus.into_iter()) + { + graph.update(usage); + graph.cache.clear(); + } + } } } Task::none() } fn view(&self) -> Element<'_, Message, Theme, Renderer> { - widget::Canvas::new(&self.cpu_graph) - .width(Length::Fill) - .height(Length::Fill) - .into() + widget::row([ + widget::Canvas::new(&self.total_cpu_graph) + .width(Length::Fill) + .height(Length::Fill) + .into(), + widget::grid(self.cpu_graphs.iter().map(|(name, graph)| { + widget::row([ + // widget::text(name).into(), + widget::Canvas::new(graph) + .width(Length::Fill) + .height(Length::Fill) + .into(), + ]) + .into() + })) + // .columns(8) + .into(), + ]) + .into() } } diff --git a/src/main.rs b/src/main.rs index 2e13470..fa3e73b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod app; +mod sysinfo_snapshot; fn main() -> Result<(), impl std::error::Error> { app::run() diff --git a/src/sysinfo_snapshot.rs b/src/sysinfo_snapshot.rs new file mode 100644 index 0000000..2e6955c --- /dev/null +++ b/src/sysinfo_snapshot.rs @@ -0,0 +1,18 @@ +#[derive(Clone, Debug)] +pub struct SysinfoSnapshot { + pub cpu_total: f32, + pub cpus: Vec<(String, f32)>, +} + +impl From<&sysinfo::System> for SysinfoSnapshot { + fn from(value: &sysinfo::System) -> Self { + Self { + cpu_total: value.global_cpu_usage(), + cpus: value + .cpus() + .iter() + .map(|cpu| (cpu.name().into(), cpu.cpu_usage())) + .collect(), + } + } +} From 7264d127d939abab0af2c0622b046b0313654e78 Mon Sep 17 00:00:00 2001 From: electria Date: Sat, 15 Aug 2026 18:36:29 -0700 Subject: [PATCH 02/26] feat: improve style --- src/app/mod.rs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index d989b14..7f073f5 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -72,17 +72,13 @@ impl State { .width(Length::Fill) .height(Length::Fill) .into(), - widget::grid(self.cpu_graphs.iter().map(|(name, graph)| { - widget::row([ - // widget::text(name).into(), - widget::Canvas::new(graph) - .width(Length::Fill) - .height(Length::Fill) - .into(), - ]) - .into() - })) - // .columns(8) + widget::grid( + self.cpu_graphs + .iter() + .map(|(_name, graph)| widget::Canvas::new(graph).into()), + ) + .columns(2) + .height(Length::Fill) .into(), ]) .into() From 93de64f4b967b6b040987e3da2f484b99405ec26 Mon Sep 17 00:00:00 2001 From: electria Date: Sat, 15 Aug 2026 18:42:53 -0700 Subject: [PATCH 03/26] refactor: remove unused name attribute --- src/app/mod.rs | 9 ++++----- src/sysinfo_snapshot.rs | 8 ++------ 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 7f073f5..acc8959 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -15,7 +15,7 @@ enum Message { #[derive(Default)] struct State { total_cpu_graph: CpuGraph, - cpu_graphs: Vec<(String, CpuGraph)>, + cpu_graphs: Vec, } impl State { @@ -51,11 +51,10 @@ impl State { self.cpu_graphs = snapshot .cpus .into_iter() - .map(|(name, usage)| (name, CpuGraph::new(usage))) + .map(|usage| CpuGraph::new(usage)) .collect(); } else { - for ((_, graph), (_, usage)) in - self.cpu_graphs.iter_mut().zip(snapshot.cpus.into_iter()) + for (graph, usage) in self.cpu_graphs.iter_mut().zip(snapshot.cpus.into_iter()) { graph.update(usage); graph.cache.clear(); @@ -75,7 +74,7 @@ impl State { widget::grid( self.cpu_graphs .iter() - .map(|(_name, graph)| widget::Canvas::new(graph).into()), + .map(|graph| widget::Canvas::new(graph).into()), ) .columns(2) .height(Length::Fill) diff --git a/src/sysinfo_snapshot.rs b/src/sysinfo_snapshot.rs index 2e6955c..cb55f06 100644 --- a/src/sysinfo_snapshot.rs +++ b/src/sysinfo_snapshot.rs @@ -1,18 +1,14 @@ #[derive(Clone, Debug)] pub struct SysinfoSnapshot { pub cpu_total: f32, - pub cpus: Vec<(String, f32)>, + pub cpus: Vec, } impl From<&sysinfo::System> for SysinfoSnapshot { fn from(value: &sysinfo::System) -> Self { Self { cpu_total: value.global_cpu_usage(), - cpus: value - .cpus() - .iter() - .map(|cpu| (cpu.name().into(), cpu.cpu_usage())) - .collect(), + cpus: value.cpus().iter().map(|cpu| cpu.cpu_usage()).collect(), } } } From 96a972f9f0e2364e576010c314e135f388ca4d76 Mon Sep 17 00:00:00 2001 From: electria Date: Sat, 15 Aug 2026 18:48:45 -0700 Subject: [PATCH 04/26] refactor: generalize CpuGraph -> Graph --- src/app/{cpu_graph.rs => graph.rs} | 14 +++++++------- src/app/mod.rs | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) rename src/app/{cpu_graph.rs => graph.rs} (85%) diff --git a/src/app/cpu_graph.rs b/src/app/graph.rs similarity index 85% rename from src/app/cpu_graph.rs rename to src/app/graph.rs index 9fe439c..c16fe56 100644 --- a/src/app/cpu_graph.rs +++ b/src/app/graph.rs @@ -7,33 +7,33 @@ use crate::app; const CYAN: Color = Color::from_rgb8(0, 255, 255); #[derive(Default)] -pub struct CpuGraph { +pub struct Graph { timeline: VecDeque, pub cache: canvas::Cache, } -impl CpuGraph { - pub fn new(usage: f32) -> Self { +impl Graph { + pub fn new(value: f32) -> Self { Self { timeline: { let mut vec = VecDeque::new(); - vec.push_front(usage); + vec.push_front(value); vec }, ..Default::default() } } - pub fn update(&mut self, usage: f32) { - self.timeline.push_back(usage); + pub fn update(&mut self, value: f32) { + self.timeline.push_back(value); if self.timeline.len() > 1000 { self.timeline.pop_front(); } } } -impl canvas::Program for CpuGraph { +impl canvas::Program for Graph { type State = (); fn draw( &self, diff --git a/src/app/mod.rs b/src/app/mod.rs index acc8959..bcc6e52 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -4,9 +4,9 @@ use iced::{ }; use smol::Timer; -use crate::{app::cpu_graph::CpuGraph, sysinfo_snapshot::SysinfoSnapshot}; +use crate::{app::graph::Graph, sysinfo_snapshot::SysinfoSnapshot}; -mod cpu_graph; +mod graph; enum Message { Info(SysinfoSnapshot), @@ -14,8 +14,8 @@ enum Message { #[derive(Default)] struct State { - total_cpu_graph: CpuGraph, - cpu_graphs: Vec, + total_cpu_graph: Graph, + cpu_graphs: Vec, } impl State { @@ -51,12 +51,12 @@ impl State { self.cpu_graphs = snapshot .cpus .into_iter() - .map(|usage| CpuGraph::new(usage)) + .map(|value| Graph::new(value)) .collect(); } else { - for (graph, usage) in self.cpu_graphs.iter_mut().zip(snapshot.cpus.into_iter()) + for (graph, value) in self.cpu_graphs.iter_mut().zip(snapshot.cpus.into_iter()) { - graph.update(usage); + graph.update(value); graph.cache.clear(); } } From fa0284f1650ddd757994ea13ae0c7b77961d7ff2 Mon Sep 17 00:00:00 2001 From: electria Date: Sat, 15 Aug 2026 19:08:05 -0700 Subject: [PATCH 05/26] feat: half the history this should probably be dynamic (based on screen space).... somehow.... --- src/app/graph.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/graph.rs b/src/app/graph.rs index c16fe56..9ae9c10 100644 --- a/src/app/graph.rs +++ b/src/app/graph.rs @@ -27,7 +27,7 @@ impl Graph { pub fn update(&mut self, value: f32) { self.timeline.push_back(value); - if self.timeline.len() > 1000 { + if self.timeline.len() > 500 { self.timeline.pop_front(); } } From 9d0a6fdd07ed55b890d9d8575200f60735479cb5 Mon Sep 17 00:00:00 2001 From: electria Date: Sat, 15 Aug 2026 19:11:34 -0700 Subject: [PATCH 06/26] fix: graphs clipping into the bottom of the box this isn't the greatest solution, but it works as expected --- src/app/graph.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/graph.rs b/src/app/graph.rs index 9ae9c10..cdbd212 100644 --- a/src/app/graph.rs +++ b/src/app/graph.rs @@ -53,7 +53,7 @@ impl canvas::Program for Graph { &canvas::Path::new(|builder| { for (index, value) in self.timeline.iter().enumerate() { let x = index as f32 / self.timeline.len() as f32 * bounds.width; - let y = bounds.height - value / 100. * bounds.height; + let y = bounds.height - value / 100. * bounds.height - 1.; builder.line_to(Point::new(x, y)) } }), From 374cbd546d4dad6e29b537291f3f75644d342fd0 Mon Sep 17 00:00:00 2001 From: electria Date: Sat, 15 Aug 2026 19:22:31 -0700 Subject: [PATCH 07/26] refactor: qualify path slightly more consistently --- src/app/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index bcc6e52..c4a0eb6 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,6 +1,6 @@ use iced::{ - Color, Element, Length, Renderer, Task, Theme, application, futures::SinkExt, stream::channel, - theme, widget, + Color, Element, Length, Renderer, Task, Theme, application, futures::SinkExt, stream, theme, + widget, }; use smol::Timer; @@ -23,7 +23,7 @@ impl State { ( Self::default(), Task::run( - channel(1, async |mut sender| { + stream::channel(1, async |mut sender| { let mut sys = sysinfo::System::new(); let refreshes = sysinfo::RefreshKind::nothing() .with_cpu(sysinfo::CpuRefreshKind::nothing().with_cpu_usage()); From d068d110e267be0b782139501c7fca22f8eb8da9 Mon Sep 17 00:00:00 2001 From: electria Date: Sun, 16 Aug 2026 08:41:53 -0700 Subject: [PATCH 08/26] feat: memory and swap graphs the values are in the SysinfoSnapshot verbatim, in case they might be displayed in another way (textually) --- src/app/mod.rs | 45 ++++++++++++++++++++++++++++++++--------- src/sysinfo_snapshot.rs | 10 +++++++++ 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index c4a0eb6..c0d5ab8 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -16,6 +16,8 @@ enum Message { struct State { total_cpu_graph: Graph, cpu_graphs: Vec, + memory_graph: Graph, + swap_graph: Graph, } impl State { @@ -26,6 +28,7 @@ impl State { stream::channel(1, async |mut sender| { let mut sys = sysinfo::System::new(); let refreshes = sysinfo::RefreshKind::nothing() + .with_memory(sysinfo::MemoryRefreshKind::everything()) .with_cpu(sysinfo::CpuRefreshKind::nothing().with_cpu_usage()); sys.refresh_specifics(refreshes); @@ -47,6 +50,14 @@ impl State { self.total_cpu_graph.update(snapshot.cpu_total); self.total_cpu_graph.cache.clear(); + self.memory_graph + .update(snapshot.memory as f32 / snapshot.memory_total as f32 * 100.); + self.memory_graph.cache.clear(); + + self.swap_graph + .update(snapshot.swap as f32 / snapshot.swap_total as f32 * 100.); + self.swap_graph.cache.clear(); + if self.cpu_graphs.len() != snapshot.cpus.len() { self.cpu_graphs = snapshot .cpus @@ -66,18 +77,32 @@ impl State { Task::none() } fn view(&self) -> Element<'_, Message, Theme, Renderer> { - widget::row([ - widget::Canvas::new(&self.total_cpu_graph) - .width(Length::Fill) + widget::column([ + widget::row([ + widget::Canvas::new(&self.total_cpu_graph) + .width(Length::Fill) + .height(Length::Fill) + .into(), + widget::grid( + self.cpu_graphs + .iter() + .map(|graph| widget::Canvas::new(graph).into()), + ) + .columns(2) .height(Length::Fill) .into(), - widget::grid( - self.cpu_graphs - .iter() - .map(|graph| widget::Canvas::new(graph).into()), - ) - .columns(2) - .height(Length::Fill) + ]) + .into(), + widget::row([ + widget::canvas(&self.memory_graph) + .width(Length::Fill) + .height(Length::Fill) + .into(), + widget::canvas(&self.swap_graph) + .width(Length::Fill) + .height(Length::Fill) + .into(), + ]) .into(), ]) .into() diff --git a/src/sysinfo_snapshot.rs b/src/sysinfo_snapshot.rs index cb55f06..340a215 100644 --- a/src/sysinfo_snapshot.rs +++ b/src/sysinfo_snapshot.rs @@ -2,6 +2,11 @@ pub struct SysinfoSnapshot { pub cpu_total: f32, pub cpus: Vec, + + pub memory: u64, + pub memory_total: u64, + pub swap: u64, + pub swap_total: u64, } impl From<&sysinfo::System> for SysinfoSnapshot { @@ -9,6 +14,11 @@ impl From<&sysinfo::System> for SysinfoSnapshot { Self { cpu_total: value.global_cpu_usage(), cpus: value.cpus().iter().map(|cpu| cpu.cpu_usage()).collect(), + + memory: value.used_memory(), + memory_total: value.total_memory(), + swap: value.used_swap(), + swap_total: value.total_swap(), } } } From 8589c0c66637e2bda866a6a67e85fd784c65e246 Mon Sep 17 00:00:00 2001 From: electria Date: Sun, 16 Aug 2026 10:16:52 -0700 Subject: [PATCH 09/26] refactor: consistency in widget creation --- src/app/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index c0d5ab8..600c93f 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -79,7 +79,7 @@ impl State { fn view(&self) -> Element<'_, Message, Theme, Renderer> { widget::column([ widget::row([ - widget::Canvas::new(&self.total_cpu_graph) + widget::canvas(&self.total_cpu_graph) .width(Length::Fill) .height(Length::Fill) .into(), From 865fecd694ba06dfb7074211cbdb2c57a91f2657 Mon Sep 17 00:00:00 2001 From: electria Date: Sun, 16 Aug 2026 10:18:24 -0700 Subject: [PATCH 10/26] refactor: clear cache in Graph::update --- src/app/graph.rs | 2 ++ src/app/mod.rs | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/app/graph.rs b/src/app/graph.rs index cdbd212..5b93a80 100644 --- a/src/app/graph.rs +++ b/src/app/graph.rs @@ -30,6 +30,8 @@ impl Graph { if self.timeline.len() > 500 { self.timeline.pop_front(); } + + self.cache.clear(); } } diff --git a/src/app/mod.rs b/src/app/mod.rs index 600c93f..68197fa 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -48,15 +48,12 @@ impl State { match message { Message::Info(snapshot) => { self.total_cpu_graph.update(snapshot.cpu_total); - self.total_cpu_graph.cache.clear(); self.memory_graph .update(snapshot.memory as f32 / snapshot.memory_total as f32 * 100.); - self.memory_graph.cache.clear(); self.swap_graph .update(snapshot.swap as f32 / snapshot.swap_total as f32 * 100.); - self.swap_graph.cache.clear(); if self.cpu_graphs.len() != snapshot.cpus.len() { self.cpu_graphs = snapshot @@ -68,7 +65,6 @@ impl State { for (graph, value) in self.cpu_graphs.iter_mut().zip(snapshot.cpus.into_iter()) { graph.update(value); - graph.cache.clear(); } } } From 79aff5185675a331ba48e32c566c7968d67d8ed4 Mon Sep 17 00:00:00 2001 From: electria Date: Sun, 16 Aug 2026 10:28:38 -0700 Subject: [PATCH 11/26] fix: send before sleeping should make the initial graph come in ~200ms faster, for free --- src/app/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 68197fa..260003e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -36,8 +36,8 @@ impl State { loop { sys.refresh_specifics(refreshes); - Timer::after(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL).await; sender.send(SysinfoSnapshot::from(&sys)).await.unwrap(); + Timer::after(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL).await; } }), Message::Info, From 534b6cde9916c5827f74ea3b53929017cb6e967f Mon Sep 17 00:00:00 2001 From: electria Date: Sun, 16 Aug 2026 10:49:41 -0700 Subject: [PATCH 12/26] feat: add uptime to title it will be displayed somewhere else later, hopefully with better formatting too. --- src/app/mod.rs | 11 +++++++++++ src/sysinfo_snapshot.rs | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/app/mod.rs b/src/app/mod.rs index 260003e..37d6376 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use iced::{ Color, Element, Length, Renderer, Task, Theme, application, futures::SinkExt, stream, theme, widget, @@ -14,6 +16,8 @@ enum Message { #[derive(Default)] struct State { + uptime: Duration, + total_cpu_graph: Graph, cpu_graphs: Vec, memory_graph: Graph, @@ -47,6 +51,8 @@ impl State { fn update(&mut self, message: Message) -> Task { match message { Message::Info(snapshot) => { + self.uptime = snapshot.uptime(); + self.total_cpu_graph.update(snapshot.cpu_total); self.memory_graph @@ -103,10 +109,15 @@ impl State { ]) .into() } + + fn title(&self) -> String { + format!("itop {:?}", self.uptime) + } } pub fn run() -> Result<(), impl std::error::Error> { application(State::new, State::update, State::view) + .title(State::title) .theme(theme::Theme::custom( "high-contrast-dark", theme::Palette { diff --git a/src/sysinfo_snapshot.rs b/src/sysinfo_snapshot.rs index 340a215..c4bee85 100644 --- a/src/sysinfo_snapshot.rs +++ b/src/sysinfo_snapshot.rs @@ -1,5 +1,9 @@ +use std::time::Duration; + #[derive(Clone, Debug)] pub struct SysinfoSnapshot { + uptime: u64, + pub cpu_total: f32, pub cpus: Vec, @@ -8,10 +12,17 @@ pub struct SysinfoSnapshot { pub swap: u64, pub swap_total: u64, } +impl SysinfoSnapshot { + pub fn uptime(&self) -> Duration { + Duration::from_secs(self.uptime) + } +} impl From<&sysinfo::System> for SysinfoSnapshot { fn from(value: &sysinfo::System) -> Self { Self { + uptime: sysinfo::System::uptime(), + cpu_total: value.global_cpu_usage(), cpus: value.cpus().iter().map(|cpu| cpu.cpu_usage()).collect(), From 171194e99f0168f6bbf6b2fe058763e2a3bd6584 Mon Sep 17 00:00:00 2001 From: electria Date: Sun, 16 Aug 2026 11:02:32 -0700 Subject: [PATCH 13/26] refactor: split info_stream into module in preparation to create a client-server model --- src/app/info_stream.rs | 28 ++++++++++++++++++++++++++++ src/app/mod.rs | 26 +++----------------------- 2 files changed, 31 insertions(+), 23 deletions(-) create mode 100644 src/app/info_stream.rs diff --git a/src/app/info_stream.rs b/src/app/info_stream.rs new file mode 100644 index 0000000..e7d7632 --- /dev/null +++ b/src/app/info_stream.rs @@ -0,0 +1,28 @@ +use iced::{ + futures::{self, SinkExt}, + stream, +}; +use smol::Timer; + +use crate::sysinfo_snapshot::SysinfoSnapshot; + +pub fn create() -> impl futures::Stream { + stream::channel( + 1, + async |mut sender: futures::channel::mpsc::Sender| { + let mut sys = sysinfo::System::new(); + let refreshes = sysinfo::RefreshKind::nothing() + .with_memory(sysinfo::MemoryRefreshKind::everything()) + .with_cpu(sysinfo::CpuRefreshKind::nothing().with_cpu_usage()); + + sys.refresh_specifics(refreshes); + Timer::after(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL).await; + + loop { + sys.refresh_specifics(refreshes); + sender.send(SysinfoSnapshot::from(&sys)).await.unwrap(); + Timer::after(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL).await; + } + }, + ) +} diff --git a/src/app/mod.rs b/src/app/mod.rs index 37d6376..427e90e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,14 +1,11 @@ use std::time::Duration; -use iced::{ - Color, Element, Length, Renderer, Task, Theme, application, futures::SinkExt, stream, theme, - widget, -}; -use smol::Timer; +use iced::{Color, Element, Length, Renderer, Task, Theme, application, theme, widget}; use crate::{app::graph::Graph, sysinfo_snapshot::SysinfoSnapshot}; mod graph; +mod info_stream; enum Message { Info(SysinfoSnapshot), @@ -28,24 +25,7 @@ impl State { fn new() -> (Self, Task) { ( Self::default(), - Task::run( - stream::channel(1, async |mut sender| { - let mut sys = sysinfo::System::new(); - let refreshes = sysinfo::RefreshKind::nothing() - .with_memory(sysinfo::MemoryRefreshKind::everything()) - .with_cpu(sysinfo::CpuRefreshKind::nothing().with_cpu_usage()); - - sys.refresh_specifics(refreshes); - Timer::after(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL).await; - - loop { - sys.refresh_specifics(refreshes); - sender.send(SysinfoSnapshot::from(&sys)).await.unwrap(); - Timer::after(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL).await; - } - }), - Message::Info, - ), + Task::run(info_stream::create(), Message::Info), ) } fn update(&mut self, message: Message) -> Task { From a6893663248ce5f67c9a40ccb7f42865c6e20c46 Mon Sep 17 00:00:00 2001 From: electria Date: Sun, 16 Aug 2026 12:15:46 -0700 Subject: [PATCH 14/26] feat: client-server mode --- Cargo.lock | 25 ++++++++++++++ Cargo.toml | 2 ++ src/app/info_stream.rs | 73 ++++++++++++++++++++++++++++++----------- src/app/mod.rs | 9 ++--- src/main.rs | 13 ++++++-- src/server/mod.rs | 23 +++++++++++++ src/sysinfo_snapshot.rs | 2 +- 7 files changed, 119 insertions(+), 28 deletions(-) create mode 100644 src/server/mod.rs diff --git a/Cargo.lock b/Cargo.lock index b9c2f0d..d771ddf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -292,6 +292,9 @@ name = "bitflags" version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] [[package]] name = "block" @@ -1380,6 +1383,8 @@ name = "itop" version = "0.1.0" dependencies = [ "iced", + "ron", + "serde", "smol", "sysinfo", ] @@ -2482,6 +2487,20 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "ron" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81116b9531d61eabc41aeb228e4b6b2435bcca3233b98cf3b3077d4e6e9debb3" +dependencies = [ + "bitflags 2.13.1", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + [[package]] name = "roxmltree" version = "0.20.0" @@ -3091,6 +3110,12 @@ dependencies = [ "core_maths", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "uds_windows" version = "1.2.1" diff --git a/Cargo.toml b/Cargo.toml index 5a74d3e..3fbbfc9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,8 @@ version = "0.1.0" edition = "2024" [dependencies] +ron = "0.12.2" +serde = "1.0.229" smol = "2.0.2" sysinfo = "0.39.6" diff --git a/src/app/info_stream.rs b/src/app/info_stream.rs index e7d7632..ed21a87 100644 --- a/src/app/info_stream.rs +++ b/src/app/info_stream.rs @@ -1,28 +1,63 @@ +use std::env; + use iced::{ + Task, futures::{self, SinkExt}, stream, }; -use smol::Timer; +use ron::de; +use smol::{Timer, unblock}; -use crate::sysinfo_snapshot::SysinfoSnapshot; +use crate::{app, sysinfo_snapshot::SysinfoSnapshot}; -pub fn create() -> impl futures::Stream { - stream::channel( - 1, - async |mut sender: futures::channel::mpsc::Sender| { - let mut sys = sysinfo::System::new(); - let refreshes = sysinfo::RefreshKind::nothing() - .with_memory(sysinfo::MemoryRefreshKind::everything()) - .with_cpu(sysinfo::CpuRefreshKind::nothing().with_cpu_usage()); +pub fn create() -> Task { + if env::args() + .nth(1) + .is_some_and(|command| command == "client") + { + Task::run( + stream::channel( + 1, + async |mut sender: futures::channel::mpsc::Sender| { + let mut buf = String::new(); + loop { + buf = unblock(move || { + std::io::stdin().read_line(&mut buf).unwrap(); + buf + }) + .await; - sys.refresh_specifics(refreshes); - Timer::after(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL).await; + match de::from_str(&buf) { + Ok(info) => sender.send(info).await.unwrap(), + Err(e) => eprintln!("{e}"), + } + buf.clear(); + } + }, + ), + app::Message::Info, + ) + } else { + Task::run( + stream::channel( + 1, + async |mut sender: futures::channel::mpsc::Sender| { + let mut sys = sysinfo::System::new(); + let refreshes = sysinfo::RefreshKind::nothing() + .with_memory(sysinfo::MemoryRefreshKind::everything()) + .with_cpu(sysinfo::CpuRefreshKind::nothing().with_cpu_usage()); - loop { - sys.refresh_specifics(refreshes); - sender.send(SysinfoSnapshot::from(&sys)).await.unwrap(); - Timer::after(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL).await; - } - }, - ) + sys.refresh_specifics(refreshes); + Timer::after(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL).await; + + loop { + sys.refresh_specifics(refreshes); + sender.send(SysinfoSnapshot::from(&sys)).await.unwrap(); + Timer::after(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL).await; + } + }, + ), + app::Message::Info, + ) + } } diff --git a/src/app/mod.rs b/src/app/mod.rs index 427e90e..0f038bd 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use iced::{Color, Element, Length, Renderer, Task, Theme, application, theme, widget}; +use iced::{Color, Element, Error, Length, Renderer, Task, Theme, application, theme, widget}; use crate::{app::graph::Graph, sysinfo_snapshot::SysinfoSnapshot}; @@ -23,10 +23,7 @@ struct State { impl State { fn new() -> (Self, Task) { - ( - Self::default(), - Task::run(info_stream::create(), Message::Info), - ) + (Self::default(), info_stream::create()) } fn update(&mut self, message: Message) -> Task { match message { @@ -95,7 +92,7 @@ impl State { } } -pub fn run() -> Result<(), impl std::error::Error> { +pub fn run() -> Result<(), Error> { application(State::new, State::update, State::view) .title(State::title) .theme(theme::Theme::custom( diff --git a/src/main.rs b/src/main.rs index fa3e73b..c6dcb49 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,15 @@ +use std::env; + mod app; +mod server; mod sysinfo_snapshot; -fn main() -> Result<(), impl std::error::Error> { - app::run() +fn main() { + match env::args().nth(1).as_deref() { + Some("server") => server::run().unwrap(), + + None | Some("client") => app::run().unwrap(), + + Some(s) => panic!("invalid argument: {s}"), + } } diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 0000000..445f393 --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,23 @@ +use std::{io::Write, thread}; + +use ron::{Error, ser}; + +use crate::sysinfo_snapshot::SysinfoSnapshot; + +pub fn run() -> Result<(), Error> { + let mut sys = sysinfo::System::new(); + let refreshes = sysinfo::RefreshKind::nothing() + .with_memory(sysinfo::MemoryRefreshKind::everything()) + .with_cpu(sysinfo::CpuRefreshKind::nothing().with_cpu_usage()); + + sys.refresh_specifics(refreshes); + thread::sleep(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL); + + let mut stdout = std::io::stdout(); + + loop { + sys.refresh_specifics(refreshes); + stdout.write_all((ser::to_string(&SysinfoSnapshot::from(&sys))? + "\n").as_bytes())?; + thread::sleep(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL); + } +} diff --git a/src/sysinfo_snapshot.rs b/src/sysinfo_snapshot.rs index c4bee85..6189cdb 100644 --- a/src/sysinfo_snapshot.rs +++ b/src/sysinfo_snapshot.rs @@ -1,6 +1,6 @@ use std::time::Duration; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct SysinfoSnapshot { uptime: u64, From e310ca570b0aeab24f8836aa6cf6c444b1016c5e Mon Sep 17 00:00:00 2001 From: electria Date: Sun, 16 Aug 2026 13:56:18 -0700 Subject: [PATCH 15/26] feat: keybinds for toggling displays --- src/app/mod.rs | 103 +++++++++++++++++++++++++++++++++++------------ src/app/utils.rs | 14 +++++++ src/main.rs | 1 + src/utils.rs | 9 +++++ 4 files changed, 101 insertions(+), 26 deletions(-) create mode 100644 src/app/utils.rs create mode 100644 src/utils.rs diff --git a/src/app/mod.rs b/src/app/mod.rs index 0f038bd..ce3a1d0 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,13 +1,28 @@ use std::time::Duration; -use iced::{Color, Element, Error, Length, Renderer, Task, Theme, application, theme, widget}; +use iced::{ + Color, Error, Event, Length, Renderer, Subscription, Task, Theme, application, event, + keyboard::{self, Key}, + theme, widget, +}; -use crate::{app::graph::Graph, sysinfo_snapshot::SysinfoSnapshot}; +use crate::{ + app::{graph::Graph, utils::maybe_widget}, + sysinfo_snapshot::SysinfoSnapshot, + utils::Toggle, +}; mod graph; mod info_stream; +mod utils; + +type Element<'a> = iced::Element<'a, Message, Theme, Renderer>; enum Message { + ToggleMain, + ToggleCpu, + ToggleMemory, + Info(SysinfoSnapshot), } @@ -15,18 +30,33 @@ enum Message { struct State { uptime: Duration, + is_main_section_shown: bool, total_cpu_graph: Graph, + + is_cpu_section_shown: bool, cpu_graphs: Vec, + + is_memory_section_shown: bool, memory_graph: Graph, swap_graph: Graph, } impl State { fn new() -> (Self, Task) { - (Self::default(), info_stream::create()) + ( + Self { + is_main_section_shown: true, + ..Default::default() + }, + info_stream::create(), + ) } fn update(&mut self, message: Message) -> Task { match message { + Message::ToggleMain => self.is_main_section_shown.toggle(), + Message::ToggleCpu => self.is_cpu_section_shown.toggle(), + Message::ToggleMemory => self.is_memory_section_shown.toggle(), + Message::Info(snapshot) => { self.uptime = snapshot.uptime(); @@ -55,38 +85,58 @@ impl State { Task::none() } - fn view(&self) -> Element<'_, Message, Theme, Renderer> { + fn view(&self) -> Element<'_> { widget::column([ widget::row([ - widget::canvas(&self.total_cpu_graph) - .width(Length::Fill) + maybe_widget(self.is_main_section_shown, || { + widget::canvas(&self.total_cpu_graph) + .width(Length::Fill) + .height(Length::Fill) + .into() + }), + maybe_widget(self.is_cpu_section_shown, || { + widget::grid( + self.cpu_graphs + .iter() + .map(|graph| widget::Canvas::new(graph).into()), + ) + .columns(2) .height(Length::Fill) - .into(), - widget::grid( - self.cpu_graphs - .iter() - .map(|graph| widget::Canvas::new(graph).into()), - ) - .columns(2) - .height(Length::Fill) - .into(), - ]) - .into(), - widget::row([ - widget::canvas(&self.memory_graph) - .width(Length::Fill) - .height(Length::Fill) - .into(), - widget::canvas(&self.swap_graph) - .width(Length::Fill) - .height(Length::Fill) - .into(), + .into() + }), ]) .into(), + maybe_widget(self.is_memory_section_shown, || { + widget::row([ + widget::canvas(&self.memory_graph) + .width(Length::Fill) + .height(Length::Fill) + .into(), + widget::canvas(&self.swap_graph) + .width(Length::Fill) + .height(Length::Fill) + .into(), + ]) + .into() + }), ]) .into() } + fn subscription(&self) -> Subscription { + event::listen().filter_map(|event| match event { + Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) => { + match key.as_ref() { + Key::Character("m" | "1") if modifiers.shift() => Some(Message::ToggleMain), + Key::Character("c" | "2") => Some(Message::ToggleCpu), + Key::Character("m" | "3") => Some(Message::ToggleMemory), + _ => None, + } + } + _ => None, + }) + } + fn title(&self) -> String { format!("itop {:?}", self.uptime) } @@ -94,6 +144,7 @@ impl State { pub fn run() -> Result<(), Error> { application(State::new, State::update, State::view) + .subscription(State::subscription) .title(State::title) .theme(theme::Theme::custom( "high-contrast-dark", diff --git a/src/app/utils.rs b/src/app/utils.rs new file mode 100644 index 0000000..ceef158 --- /dev/null +++ b/src/app/utils.rs @@ -0,0 +1,14 @@ +use iced::widget; + +use crate::app; + +pub fn maybe_widget<'a>( + condition: bool, + widget: impl FnOnce() -> app::Element<'a>, +) -> app::Element<'a> { + if condition { + widget() + } else { + widget::space().into() + } +} diff --git a/src/main.rs b/src/main.rs index c6dcb49..d8e6e6e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use std::env; mod app; mod server; mod sysinfo_snapshot; +mod utils; fn main() { match env::args().nth(1).as_deref() { diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..3556bf9 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,9 @@ +pub trait Toggle { + fn toggle(&mut self); +} + +impl Toggle for bool { + fn toggle(&mut self) { + *self = !*self + } +} From 1546c67fa394533d876236e8af7e0cadc5a08b73 Mon Sep 17 00:00:00 2001 From: electria Date: Sun, 16 Aug 2026 14:13:41 -0700 Subject: [PATCH 16/26] feat: add server-only build it's drastically faster to build, mostly due to only including 1/10th of the dependencies --- Cargo.toml | 15 +++++++++++++-- flake.nix | 6 ++++++ src/main.rs | 7 +++++-- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3fbbfc9..00abd23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,12 +5,23 @@ license = "AGPL-3.0-or-later" version = "0.1.0" edition = "2024" +[features] +default = [ "app" ] +app = [ "dep:iced", "dep:smol" ] + [dependencies] ron = "0.12.2" -serde = "1.0.229" -smol = "2.0.2" sysinfo = "0.39.6" [dependencies.iced] version = "0.14.0" features = [ "canvas", "smol" ] +optional = true + +[dependencies.smol] +version = "2.0.2" +optional = true + +[dependencies.serde] +version = "1.0.229" +features = [ "derive" ] diff --git a/flake.nix b/flake.nix index 827d996..d37cbd5 100644 --- a/flake.nix +++ b/flake.nix @@ -89,6 +89,12 @@ { packages.default = crate; + packages.server-only = craneLib.buildPackage { + name = name + "-server-only"; + src = ./.; + cargoExtraArgs = "--no-default-features"; + }; + checks = { crate-clippy = craneLib.cargoClippy ( commonArgs diff --git a/src/main.rs b/src/main.rs index d8e6e6e..327e179 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use std::env; +#[cfg(feature = "app")] mod app; mod server; mod sysinfo_snapshot; @@ -7,10 +8,12 @@ mod utils; fn main() { match env::args().nth(1).as_deref() { - Some("server") => server::run().unwrap(), - + #[cfg(feature = "app")] None | Some("client") => app::run().unwrap(), + #[cfg_attr(feature = "app", allow(unreachable_patterns))] + None | Some("server") => server::run().unwrap(), + Some(s) => panic!("invalid argument: {s}"), } } From c39d48fd07302723f0e44b16bc35d58f02177075 Mon Sep 17 00:00:00 2001 From: electria Date: Sun, 16 Aug 2026 14:39:50 -0700 Subject: [PATCH 17/26] fix: don't require shift for number toggles --- src/app/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index ce3a1d0..34d2e37 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -125,9 +125,9 @@ impl State { fn subscription(&self) -> Subscription { event::listen().filter_map(|event| match event { - Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) => { - match key.as_ref() { - Key::Character("m" | "1") if modifiers.shift() => Some(Message::ToggleMain), + Event::Keyboard(keyboard::Event::KeyPressed { modified_key, .. }) => { + match modified_key.as_ref() { + Key::Character("M" | "1") => Some(Message::ToggleMain), Key::Character("c" | "2") => Some(Message::ToggleCpu), Key::Character("m" | "3") => Some(Message::ToggleMemory), _ => None, From d1fb0ca78a2af63e246b84fd1beec8570327576c Mon Sep 17 00:00:00 2001 From: electria Date: Sun, 16 Aug 2026 18:39:25 -0700 Subject: [PATCH 18/26] refactor: replace for with for_each --- src/app/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 34d2e37..57aec5d 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -75,10 +75,10 @@ impl State { .map(|value| Graph::new(value)) .collect(); } else { - for (graph, value) in self.cpu_graphs.iter_mut().zip(snapshot.cpus.into_iter()) - { - graph.update(value); - } + self.cpu_graphs + .iter_mut() + .zip(snapshot.cpus.into_iter()) + .for_each(|(graph, value)| graph.update(value)); } } } From d3f94b4acccbdb1c3db67610f032473dcd4c2b30 Mon Sep 17 00:00:00 2001 From: electria Date: Sun, 16 Aug 2026 20:34:21 -0700 Subject: [PATCH 19/26] fix: improve anti-clipping for graphs --- src/app/graph.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/graph.rs b/src/app/graph.rs index 5b93a80..f370cfa 100644 --- a/src/app/graph.rs +++ b/src/app/graph.rs @@ -55,7 +55,8 @@ impl canvas::Program for Graph { &canvas::Path::new(|builder| { for (index, value) in self.timeline.iter().enumerate() { let x = index as f32 / self.timeline.len() as f32 * bounds.width; - let y = bounds.height - value / 100. * bounds.height - 1.; + let y = (bounds.height - value / 100. * bounds.height) + .clamp(1., bounds.height - 1.); builder.line_to(Point::new(x, y)) } }), From cb2461b64f748c07f8e1cfd7538c8694914c8ed4 Mon Sep 17 00:00:00 2001 From: electria Date: Sun, 16 Aug 2026 21:02:56 -0700 Subject: [PATCH 20/26] feat: hide borders when only main graph is shown --- src/app/graph.rs | 24 ++++++++++++++++++------ src/app/mod.rs | 20 +++++++++++++++++--- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/app/graph.rs b/src/app/graph.rs index f370cfa..5dea431 100644 --- a/src/app/graph.rs +++ b/src/app/graph.rs @@ -6,12 +6,22 @@ use crate::app; const CYAN: Color = Color::from_rgb8(0, 255, 255); -#[derive(Default)] pub struct Graph { timeline: VecDeque, + pub show_borders: bool, pub cache: canvas::Cache, } +impl Default for Graph { + fn default() -> Self { + Self { + show_borders: true, + + timeline: Default::default(), + cache: Default::default(), + } + } +} impl Graph { pub fn new(value: f32) -> Self { @@ -46,11 +56,13 @@ impl canvas::Program for Graph { _cursor: mouse::Cursor, ) -> Vec> { let geometry = self.cache.draw(renderer, bounds.size(), |frame| { - frame.stroke_rectangle( - Point::new(0., 0.), - bounds.size(), - canvas::Stroke::default().with_color(Color::from_rgb8(255, 0, 0)), - ); + if self.show_borders { + frame.stroke_rectangle( + Point::new(0., 0.), + bounds.size(), + canvas::Stroke::default().with_color(Color::from_rgb8(255, 0, 0)), + ); + } frame.stroke( &canvas::Path::new(|builder| { for (index, value) in self.timeline.iter().enumerate() { diff --git a/src/app/mod.rs b/src/app/mod.rs index 57aec5d..c46d289 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -53,9 +53,18 @@ impl State { } fn update(&mut self, message: Message) -> Task { match message { - Message::ToggleMain => self.is_main_section_shown.toggle(), - Message::ToggleCpu => self.is_cpu_section_shown.toggle(), - Message::ToggleMemory => self.is_memory_section_shown.toggle(), + Message::ToggleMain => { + self.is_main_section_shown.toggle(); + self.check_borders(); + } + Message::ToggleCpu => { + self.is_cpu_section_shown.toggle(); + self.check_borders(); + } + Message::ToggleMemory => { + self.is_memory_section_shown.toggle(); + self.check_borders(); + } Message::Info(snapshot) => { self.uptime = snapshot.uptime(); @@ -140,6 +149,11 @@ impl State { fn title(&self) -> String { format!("itop {:?}", self.uptime) } + + fn check_borders(&mut self) { + self.total_cpu_graph.show_borders = + self.is_cpu_section_shown || self.is_memory_section_shown; + } } pub fn run() -> Result<(), Error> { From 5acc09b471988f0b90613ac89e282c67a9a15086 Mon Sep 17 00:00:00 2001 From: electria Date: Sun, 16 Aug 2026 21:10:44 -0700 Subject: [PATCH 21/26] build: improve server-only derivation --- flake.nix | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index d37cbd5..a7a47ea 100644 --- a/flake.nix +++ b/flake.nix @@ -89,10 +89,11 @@ { packages.default = crate; - packages.server-only = craneLib.buildPackage { - name = name + "-server-only"; + packages."${name}-server-only" = craneLib.buildPackage { + name = "${name}-server-only"; src = ./.; - cargoExtraArgs = "--no-default-features"; + doCheck = false; + cargoExtraArgs = "--locked --no-default-features"; }; checks = { From 86f7fc12b4cfe0832851f6a0e6594fa44610ad73 Mon Sep 17 00:00:00 2001 From: electria Date: Sun, 16 Aug 2026 21:15:59 -0700 Subject: [PATCH 22/26] build: remove nonexistent icon --- flake.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/flake.nix b/flake.nix index a7a47ea..1a338f0 100644 --- a/flake.nix +++ b/flake.nix @@ -58,7 +58,6 @@ desktopItem = pkgs.makeDesktopItem { inherit name; desktopName = name; - icon = name; exec = name; }; in From a894a02e66117731b5ed691517564374821e3acf Mon Sep 17 00:00:00 2001 From: electria Date: Mon, 17 Aug 2026 15:07:18 -0700 Subject: [PATCH 23/26] fix: borders showing on launch --- src/app/mod.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index c46d289..8725669 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -43,13 +43,13 @@ struct State { impl State { fn new() -> (Self, Task) { - ( - Self { - is_main_section_shown: true, - ..Default::default() - }, - info_stream::create(), - ) + let mut state = Self { + is_main_section_shown: true, + ..Default::default() + }; + state.total_cpu_graph.show_borders = false; + + (state, info_stream::create()) } fn update(&mut self, message: Message) -> Task { match message { From afa6104c28d25caf3808424d4cae6b3031ab2d2e Mon Sep 17 00:00:00 2001 From: electria Date: Mon, 17 Aug 2026 15:11:18 -0700 Subject: [PATCH 24/26] build: fix dead_code warnings without "app" feature --- src/sysinfo_snapshot.rs | 1 + src/utils.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/sysinfo_snapshot.rs b/src/sysinfo_snapshot.rs index 6189cdb..917b2f6 100644 --- a/src/sysinfo_snapshot.rs +++ b/src/sysinfo_snapshot.rs @@ -13,6 +13,7 @@ pub struct SysinfoSnapshot { pub swap_total: u64, } impl SysinfoSnapshot { + #[cfg_attr(not(feature = "app"), allow(dead_code))] pub fn uptime(&self) -> Duration { Duration::from_secs(self.uptime) } diff --git a/src/utils.rs b/src/utils.rs index 3556bf9..622bff1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,3 +1,4 @@ +#[cfg_attr(not(feature = "app"), allow(dead_code))] pub trait Toggle { fn toggle(&mut self); } From b20158820b243af5c929efd913e460956c1aed1a Mon Sep 17 00:00:00 2001 From: electria Date: Tue, 18 Aug 2026 14:05:36 -0700 Subject: [PATCH 25/26] feat: process list this is unnacceptably unperformant; I think I've reached the point at which sysinfo doesn't do what I want, which is keep track of a table of processes efficiently enough. --- src/app/info_stream.rs | 9 +++--- src/app/mod.rs | 61 +++++++++++++++++++++++++++++++---------- src/server/mod.rs | 6 ++-- src/sysinfo_snapshot.rs | 51 ++++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 22 deletions(-) diff --git a/src/app/info_stream.rs b/src/app/info_stream.rs index ed21a87..787e459 100644 --- a/src/app/info_stream.rs +++ b/src/app/info_stream.rs @@ -8,7 +8,10 @@ use iced::{ use ron::de; use smol::{Timer, unblock}; -use crate::{app, sysinfo_snapshot::SysinfoSnapshot}; +use crate::{ + app, + sysinfo_snapshot::{self, SysinfoSnapshot}, +}; pub fn create() -> Task { if env::args() @@ -43,9 +46,7 @@ pub fn create() -> Task { 1, async |mut sender: futures::channel::mpsc::Sender| { let mut sys = sysinfo::System::new(); - let refreshes = sysinfo::RefreshKind::nothing() - .with_memory(sysinfo::MemoryRefreshKind::everything()) - .with_cpu(sysinfo::CpuRefreshKind::nothing().with_cpu_usage()); + let refreshes = sysinfo_snapshot::refreshes(); sys.refresh_specifics(refreshes); Timer::after(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL).await; diff --git a/src/app/mod.rs b/src/app/mod.rs index 8725669..c636868 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -8,7 +8,7 @@ use iced::{ use crate::{ app::{graph::Graph, utils::maybe_widget}, - sysinfo_snapshot::SysinfoSnapshot, + sysinfo_snapshot::{ProcessSnapshot, SysinfoSnapshot}, utils::Toggle, }; @@ -22,6 +22,7 @@ enum Message { ToggleMain, ToggleCpu, ToggleMemory, + ToggleProcesses, Info(SysinfoSnapshot), } @@ -39,6 +40,9 @@ struct State { is_memory_section_shown: bool, memory_graph: Graph, swap_graph: Graph, + + is_processes_section_shown: bool, + processes: Vec, } impl State { @@ -65,6 +69,10 @@ impl State { self.is_memory_section_shown.toggle(); self.check_borders(); } + Message::ToggleProcesses => { + self.is_processes_section_shown.toggle(); + self.check_borders(); + } Message::Info(snapshot) => { self.uptime = snapshot.uptime(); @@ -89,6 +97,10 @@ impl State { .zip(snapshot.cpus.into_iter()) .for_each(|(graph, value)| graph.update(value)); } + + self.processes = snapshot.processes; + self.processes + .sort_by(|a, b| b.cpu_usage.total_cmp(&a.cpu_usage)); } } @@ -115,19 +127,39 @@ impl State { }), ]) .into(), - maybe_widget(self.is_memory_section_shown, || { - widget::row([ - widget::canvas(&self.memory_graph) - .width(Length::Fill) - .height(Length::Fill) - .into(), - widget::canvas(&self.swap_graph) - .width(Length::Fill) - .height(Length::Fill) - .into(), - ]) - .into() - }), + widget::row([ + maybe_widget(self.is_memory_section_shown, || { + widget::row([ + widget::canvas(&self.memory_graph) + .width(Length::Fill) + .height(Length::Fill) + .into(), + widget::canvas(&self.swap_graph) + .width(Length::Fill) + .height(Length::Fill) + .into(), + ]) + .into() + }), + maybe_widget(self.is_processes_section_shown, || { + widget::scrollable( + widget::grid(self.processes.iter().flat_map(|process| { + [ + widget::text(&process.name).into(), + widget::text(process.cpu_usage).into(), + widget::text(format!("{:?}", process.cpu_time())).into(), + widget::text(process.pid).into(), + ] + })) + .height(Length::Shrink) + .columns(4), + ) + .width(Length::Fill) + .height(Length::Fill) + .into() + }), + ]) + .into(), ]) .into() } @@ -139,6 +171,7 @@ impl State { Key::Character("M" | "1") => Some(Message::ToggleMain), Key::Character("c" | "2") => Some(Message::ToggleCpu), Key::Character("m" | "3") => Some(Message::ToggleMemory), + Key::Character("p" | "4") => Some(Message::ToggleProcesses), _ => None, } } diff --git a/src/server/mod.rs b/src/server/mod.rs index 445f393..5777c84 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -2,13 +2,11 @@ use std::{io::Write, thread}; use ron::{Error, ser}; -use crate::sysinfo_snapshot::SysinfoSnapshot; +use crate::sysinfo_snapshot::{self, SysinfoSnapshot}; pub fn run() -> Result<(), Error> { let mut sys = sysinfo::System::new(); - let refreshes = sysinfo::RefreshKind::nothing() - .with_memory(sysinfo::MemoryRefreshKind::everything()) - .with_cpu(sysinfo::CpuRefreshKind::nothing().with_cpu_usage()); + let refreshes = sysinfo_snapshot::refreshes(); sys.refresh_specifics(refreshes); thread::sleep(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL); diff --git a/src/sysinfo_snapshot.rs b/src/sysinfo_snapshot.rs index 917b2f6..ef0f36e 100644 --- a/src/sysinfo_snapshot.rs +++ b/src/sysinfo_snapshot.rs @@ -1,7 +1,20 @@ use std::time::Duration; +pub fn refreshes() -> sysinfo::RefreshKind { + sysinfo::RefreshKind::nothing() + .with_memory(sysinfo::MemoryRefreshKind::everything()) + .with_cpu(sysinfo::CpuRefreshKind::nothing().with_cpu_usage()) + .with_processes( + sysinfo::ProcessRefreshKind::nothing() + .with_cpu() + .with_cmd(sysinfo::UpdateKind::OnlyIfNotSet) + .with_exe(sysinfo::UpdateKind::OnlyIfNotSet), + ) +} + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct SysinfoSnapshot { + /// in seconds uptime: u64, pub cpu_total: f32, @@ -11,6 +24,8 @@ pub struct SysinfoSnapshot { pub memory_total: u64, pub swap: u64, pub swap_total: u64, + + pub processes: Vec, } impl SysinfoSnapshot { #[cfg_attr(not(feature = "app"), allow(dead_code))] @@ -31,6 +46,42 @@ impl From<&sysinfo::System> for SysinfoSnapshot { memory_total: value.total_memory(), swap: value.used_swap(), swap_total: value.total_swap(), + + processes: value + .processes() + .iter() + .map(|(_pid, process)| ProcessSnapshot::from(process)) + .collect(), + } + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct ProcessSnapshot { + pub pid: usize, + + /// in CPU-milliseconds + accumulated_cpu_time: u64, + + /// note: it's by core, and can be up to eg 1600 on a 16-core device + pub cpu_usage: f32, + + pub name: String, +} +impl ProcessSnapshot { + pub fn cpu_time(&self) -> Duration { + Duration::from_millis(self.accumulated_cpu_time) + } +} + +impl From<&sysinfo::Process> for ProcessSnapshot { + fn from(value: &sysinfo::Process) -> Self { + Self { + pid: value.pid().into(), + accumulated_cpu_time: value.accumulated_cpu_time(), + cpu_usage: value.cpu_usage(), + + name: value.name().to_string_lossy().into_owned(), } } } From 3fa2163366babea674c327949407c5a59ab9c52d Mon Sep 17 00:00:00 2001 From: electria Date: Tue, 18 Aug 2026 14:15:18 -0700 Subject: [PATCH 26/26] refactor: converge naming from main branch --- src/app/{cpu_graph.rs => graph.rs} | 10 +++++----- src/app/mod.rs | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) rename src/app/{cpu_graph.rs => graph.rs} (86%) diff --git a/src/app/cpu_graph.rs b/src/app/graph.rs similarity index 86% rename from src/app/cpu_graph.rs rename to src/app/graph.rs index 42a4a22..8a3796a 100644 --- a/src/app/cpu_graph.rs +++ b/src/app/graph.rs @@ -7,21 +7,21 @@ use crate::app; const CYAN: Color = Color::from_rgb8(0, 255, 255); #[derive(Default)] -pub struct CpuGraph { +pub struct Graph { pub cache: canvas::Cache, timeline: VecDeque, } -impl CpuGraph { - pub fn update(&mut self, usage: f32) { - self.timeline.push_back(usage); +impl Graph { + pub fn update(&mut self, value: f32) { + self.timeline.push_back(value); if self.timeline.len() > 1000 { self.timeline.pop_front(); } } } -impl canvas::Program for CpuGraph { +impl canvas::Program for Graph { type State = (); fn draw( &self, diff --git a/src/app/mod.rs b/src/app/mod.rs index 1e0262a..f48d29a 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,19 +1,19 @@ use iced::{ - Color, Element, Length, Renderer, Task, Theme, application, futures::SinkExt, stream::channel, - theme, widget, + Color, Element, Length, Renderer, Task, Theme, application, futures::SinkExt, stream, theme, + widget, }; use smol::Timer; -use crate::app::cpu_graph::CpuGraph; +use crate::app::graph::Graph; -mod cpu_graph; +mod graph; enum Message { Cpu(f32), } struct State { - cpu_graph: CpuGraph, + cpu_graph: Graph, } impl State { @@ -23,7 +23,7 @@ impl State { cpu_graph: Default::default(), }, Task::run( - channel(1, async |mut sender| { + stream::channel(1, async |mut sender| { let mut sys = sysinfo::System::new(); let refreshes = sysinfo::RefreshKind::nothing() .with_cpu(sysinfo::CpuRefreshKind::nothing().with_cpu_usage());