Compare commits

..
Author SHA1 Message Date
3fa2163366
refactor: converge naming from main branch 2026-08-18 14:22:37 -07:00
11 changed files with 45 additions and 463 deletions

25
Cargo.lock generated
View file

@ -292,9 +292,6 @@ name = "bitflags"
version = "2.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
dependencies = [
"serde_core",
]
[[package]]
name = "block"
@ -1383,8 +1380,6 @@ name = "itop"
version = "0.1.0"
dependencies = [
"iced",
"ron",
"serde",
"smol",
"sysinfo",
]
@ -2487,20 +2482,6 @@ 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"
@ -3110,12 +3091,6 @@ 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"

View file

@ -5,23 +5,10 @@ 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"
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" ]

View file

@ -58,6 +58,7 @@
desktopItem = pkgs.makeDesktopItem {
inherit name;
desktopName = name;
icon = name;
exec = name;
};
in
@ -88,13 +89,6 @@
{
packages.default = crate;
packages."${name}-server-only" = craneLib.buildPackage {
name = "${name}-server-only";
src = ./.;
doCheck = false;
cargoExtraArgs = "--locked --no-default-features";
};
checks = {
crate-clippy = craneLib.cargoClippy (
commonArgs

View file

@ -6,42 +6,18 @@ use crate::app;
const CYAN: Color = Color::from_rgb8(0, 255, 255);
#[derive(Default)]
pub struct Graph {
timeline: VecDeque<f32>,
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(),
}
}
timeline: VecDeque<f32>,
}
impl Graph {
pub fn new(value: f32) -> Self {
Self {
timeline: {
let mut vec = VecDeque::new();
vec.push_front(value);
vec
},
..Default::default()
}
}
pub fn update(&mut self, value: f32) {
self.timeline.push_back(value);
if self.timeline.len() > 500 {
if self.timeline.len() > 1000 {
self.timeline.pop_front();
}
self.cache.clear();
}
}
@ -56,19 +32,11 @@ impl canvas::Program<app::Message, Theme, Renderer> for Graph {
_cursor: mouse::Cursor,
) -> Vec<canvas::Geometry<Renderer>> {
let geometry = self.cache.draw(renderer, bounds.size(), |frame| {
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() {
let x = index as f32 / self.timeline.len() as f32 * bounds.width;
let y = (bounds.height - value / 100. * bounds.height)
.clamp(1., bounds.height - 1.);
let y = bounds.height - value / 100. * bounds.height;
builder.line_to(Point::new(x, y))
}
}),

View file

@ -1,64 +0,0 @@
use std::env;
use iced::{
Task,
futures::{self, SinkExt},
stream,
};
use ron::de;
use smol::{Timer, unblock};
use crate::{
app,
sysinfo_snapshot::{self, SysinfoSnapshot},
};
pub fn create() -> Task<app::Message> {
if env::args()
.nth(1)
.is_some_and(|command| command == "client")
{
Task::run(
stream::channel(
1,
async |mut sender: futures::channel::mpsc::Sender<SysinfoSnapshot>| {
let mut buf = String::new();
loop {
buf = unblock(move || {
std::io::stdin().read_line(&mut buf).unwrap();
buf
})
.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<SysinfoSnapshot>| {
let mut sys = sysinfo::System::new();
let refreshes = sysinfo_snapshot::refreshes();
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,
)
}
}

View file

@ -1,198 +1,66 @@
use std::time::Duration;
use iced::{
Color, Error, Event, Length, Renderer, Subscription, Task, Theme, application, event,
keyboard::{self, Key},
theme, widget,
Color, Element, Length, Renderer, Task, Theme, application, futures::SinkExt, stream, theme,
widget,
};
use smol::Timer;
use crate::{
app::{graph::Graph, utils::maybe_widget},
sysinfo_snapshot::{ProcessSnapshot, SysinfoSnapshot},
utils::Toggle,
};
use crate::app::graph::Graph;
mod graph;
mod info_stream;
mod utils;
type Element<'a> = iced::Element<'a, Message, Theme, Renderer>;
enum Message {
ToggleMain,
ToggleCpu,
ToggleMemory,
ToggleProcesses,
Info(SysinfoSnapshot),
Cpu(f32),
}
#[derive(Default)]
struct State {
uptime: Duration,
is_main_section_shown: bool,
total_cpu_graph: Graph,
is_cpu_section_shown: bool,
cpu_graphs: Vec<Graph>,
is_memory_section_shown: bool,
memory_graph: Graph,
swap_graph: Graph,
is_processes_section_shown: bool,
processes: Vec<ProcessSnapshot>,
cpu_graph: Graph,
}
impl State {
fn new() -> (Self, Task<Message>) {
let mut state = Self {
is_main_section_shown: true,
..Default::default()
};
state.total_cpu_graph.show_borders = false;
(
Self {
cpu_graph: Default::default(),
},
Task::run(
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());
(state, info_stream::create())
sys.refresh_specifics(refreshes);
Timer::after(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL).await;
loop {
sys.refresh_specifics(refreshes);
Timer::after(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL).await;
sender.send(sys.global_cpu_usage()).await.unwrap();
}
}),
Message::Cpu,
),
)
}
fn update(&mut self, message: Message) -> Task<Message> {
match message {
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::ToggleProcesses => {
self.is_processes_section_shown.toggle();
self.check_borders();
}
Message::Info(snapshot) => {
self.uptime = snapshot.uptime();
self.total_cpu_graph.update(snapshot.cpu_total);
self.memory_graph
.update(snapshot.memory as f32 / snapshot.memory_total as f32 * 100.);
self.swap_graph
.update(snapshot.swap as f32 / snapshot.swap_total as f32 * 100.);
if self.cpu_graphs.len() != snapshot.cpus.len() {
self.cpu_graphs = snapshot
.cpus
.into_iter()
.map(|value| Graph::new(value))
.collect();
} else {
self.cpu_graphs
.iter_mut()
.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));
Message::Cpu(value) => {
self.cpu_graph.update(value);
self.cpu_graph.cache.clear();
}
}
Task::none()
}
fn view(&self) -> Element<'_> {
widget::column([
widget::row([
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()
}),
])
.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()
}
fn subscription(&self) -> Subscription<Message> {
event::listen().filter_map(|event| match event {
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),
Key::Character("p" | "4") => Some(Message::ToggleProcesses),
_ => None,
}
}
_ => None,
})
}
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;
fn view(&self) -> Element<'_, Message, Theme, Renderer> {
widget::Canvas::new(&self.cpu_graph)
.width(Length::Fill)
.height(Length::Fill)
.into()
}
}
pub fn run() -> Result<(), Error> {
pub fn run() -> Result<(), impl std::error::Error> {
application(State::new, State::update, State::view)
.subscription(State::subscription)
.title(State::title)
.theme(theme::Theme::custom(
"high-contrast-dark",
theme::Palette {

View file

@ -1,14 +0,0 @@
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()
}
}

View file

@ -1,19 +1,5 @@
use std::env;
#[cfg(feature = "app")]
mod app;
mod server;
mod sysinfo_snapshot;
mod utils;
fn main() {
match env::args().nth(1).as_deref() {
#[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}"),
}
fn main() -> Result<(), impl std::error::Error> {
app::run()
}

View file

@ -1,21 +0,0 @@
use std::{io::Write, thread};
use ron::{Error, ser};
use crate::sysinfo_snapshot::{self, SysinfoSnapshot};
pub fn run() -> Result<(), Error> {
let mut sys = sysinfo::System::new();
let refreshes = sysinfo_snapshot::refreshes();
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);
}
}

View file

@ -1,87 +0,0 @@
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,
pub cpus: Vec<f32>,
pub memory: u64,
pub memory_total: u64,
pub swap: u64,
pub swap_total: u64,
pub processes: Vec<ProcessSnapshot>,
}
impl SysinfoSnapshot {
#[cfg_attr(not(feature = "app"), allow(dead_code))]
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(),
memory: value.used_memory(),
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(),
}
}
}

View file

@ -1,10 +0,0 @@
#[cfg_attr(not(feature = "app"), allow(dead_code))]
pub trait Toggle {
fn toggle(&mut self);
}
impl Toggle for bool {
fn toggle(&mut self) {
*self = !*self
}
}