Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
b20158820b |
|||
|
afa6104c28 |
|||
|
a894a02e66 |
|||
|
86f7fc12b4 |
|||
|
5acc09b471 |
|||
|
cb2461b64f |
|||
|
d3f94b4acc |
|||
|
d1fb0ca78a |
|||
|
c39d48fd07 |
|||
|
1546c67fa3 |
|||
|
e310ca570b |
|||
|
a689366324 |
|||
|
171194e99f |
|||
|
534b6cde99 |
|||
|
79aff51856 |
|||
|
865fecd694 |
|||
|
8589c0c666 |
|||
|
d068d110e2 |
|||
|
374cbd546d |
|||
|
9d0a6fdd07 |
|||
|
fa0284f165 |
|||
|
96a972f9f0 |
|||
|
93de64f4b9 |
|||
|
7264d127d9 |
|||
|
98b7a8d201 |
12 changed files with 508 additions and 90 deletions
25
Cargo.lock
generated
25
Cargo.lock
generated
|
|
@ -292,6 +292,9 @@ name = "bitflags"
|
||||||
version = "2.13.1"
|
version = "2.13.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block"
|
name = "block"
|
||||||
|
|
@ -1380,6 +1383,8 @@ name = "itop"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"iced",
|
"iced",
|
||||||
|
"ron",
|
||||||
|
"serde",
|
||||||
"smol",
|
"smol",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
]
|
]
|
||||||
|
|
@ -2482,6 +2487,20 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
|
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]]
|
[[package]]
|
||||||
name = "roxmltree"
|
name = "roxmltree"
|
||||||
version = "0.20.0"
|
version = "0.20.0"
|
||||||
|
|
@ -3091,6 +3110,12 @@ dependencies = [
|
||||||
"core_maths",
|
"core_maths",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typeid"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uds_windows"
|
name = "uds_windows"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
|
|
|
||||||
15
Cargo.toml
15
Cargo.toml
|
|
@ -5,10 +5,23 @@ license = "AGPL-3.0-or-later"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = [ "app" ]
|
||||||
|
app = [ "dep:iced", "dep:smol" ]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
smol = "2.0.2"
|
ron = "0.12.2"
|
||||||
sysinfo = "0.39.6"
|
sysinfo = "0.39.6"
|
||||||
|
|
||||||
[dependencies.iced]
|
[dependencies.iced]
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
features = [ "canvas", "smol" ]
|
features = [ "canvas", "smol" ]
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.smol]
|
||||||
|
version = "2.0.2"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
version = "1.0.229"
|
||||||
|
features = [ "derive" ]
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,6 @@
|
||||||
desktopItem = pkgs.makeDesktopItem {
|
desktopItem = pkgs.makeDesktopItem {
|
||||||
inherit name;
|
inherit name;
|
||||||
desktopName = name;
|
desktopName = name;
|
||||||
icon = name;
|
|
||||||
exec = name;
|
exec = name;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
|
|
@ -89,6 +88,13 @@
|
||||||
{
|
{
|
||||||
packages.default = crate;
|
packages.default = crate;
|
||||||
|
|
||||||
|
packages."${name}-server-only" = craneLib.buildPackage {
|
||||||
|
name = "${name}-server-only";
|
||||||
|
src = ./.;
|
||||||
|
doCheck = false;
|
||||||
|
cargoExtraArgs = "--locked --no-default-features";
|
||||||
|
};
|
||||||
|
|
||||||
checks = {
|
checks = {
|
||||||
crate-clippy = craneLib.cargoClippy (
|
crate-clippy = craneLib.cargoClippy (
|
||||||
commonArgs
|
commonArgs
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
use std::collections::VecDeque;
|
|
||||||
|
|
||||||
use iced::{Color, Point, Renderer, Theme, mouse, widget::canvas};
|
|
||||||
|
|
||||||
use crate::app;
|
|
||||||
|
|
||||||
const CYAN: Color = Color::from_rgb8(0, 255, 255);
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct CpuGraph {
|
|
||||||
pub cache: canvas::Cache,
|
|
||||||
timeline: VecDeque<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CpuGraph {
|
|
||||||
pub fn update(&mut self, usage: f32) {
|
|
||||||
self.timeline.push_back(usage);
|
|
||||||
if self.timeline.len() > 1000 {
|
|
||||||
self.timeline.pop_front();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl canvas::Program<app::Message, Theme, Renderer> for CpuGraph {
|
|
||||||
type State = ();
|
|
||||||
fn draw(
|
|
||||||
&self,
|
|
||||||
_state: &Self::State,
|
|
||||||
renderer: &Renderer,
|
|
||||||
_theme: &Theme,
|
|
||||||
bounds: iced::Rectangle,
|
|
||||||
_cursor: mouse::Cursor,
|
|
||||||
) -> Vec<canvas::Geometry<Renderer>> {
|
|
||||||
let geometry = self.cache.draw(renderer, bounds.size(), |frame| {
|
|
||||||
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;
|
|
||||||
builder.line_to(Point::new(x, y))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
canvas::Stroke::default().with_color(CYAN),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
vec![geometry]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
81
src/app/graph.rs
Normal file
81
src/app/graph.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
use iced::{Color, Point, Renderer, Theme, mouse, widget::canvas};
|
||||||
|
|
||||||
|
use crate::app;
|
||||||
|
|
||||||
|
const CYAN: Color = Color::from_rgb8(0, 255, 255);
|
||||||
|
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
self.timeline.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl canvas::Program<app::Message, Theme, Renderer> for Graph {
|
||||||
|
type State = ();
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
_state: &Self::State,
|
||||||
|
renderer: &Renderer,
|
||||||
|
_theme: &Theme,
|
||||||
|
bounds: iced::Rectangle,
|
||||||
|
_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.);
|
||||||
|
builder.line_to(Point::new(x, y))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
canvas::Stroke::default().with_color(CYAN),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
vec![geometry]
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/app/info_stream.rs
Normal file
64
src/app/info_stream.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
204
src/app/mod.rs
204
src/app/mod.rs
|
|
@ -1,66 +1,198 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use iced::{
|
use iced::{
|
||||||
Color, Element, Length, Renderer, Task, Theme, application, futures::SinkExt, stream::channel,
|
Color, Error, Event, Length, Renderer, Subscription, Task, Theme, application, event,
|
||||||
|
keyboard::{self, Key},
|
||||||
theme, widget,
|
theme, widget,
|
||||||
};
|
};
|
||||||
use smol::Timer;
|
|
||||||
|
|
||||||
use crate::app::cpu_graph::CpuGraph;
|
use crate::{
|
||||||
|
app::{graph::Graph, utils::maybe_widget},
|
||||||
|
sysinfo_snapshot::{ProcessSnapshot, SysinfoSnapshot},
|
||||||
|
utils::Toggle,
|
||||||
|
};
|
||||||
|
|
||||||
mod cpu_graph;
|
mod graph;
|
||||||
|
mod info_stream;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
type Element<'a> = iced::Element<'a, Message, Theme, Renderer>;
|
||||||
|
|
||||||
enum Message {
|
enum Message {
|
||||||
Cpu(f32),
|
ToggleMain,
|
||||||
|
ToggleCpu,
|
||||||
|
ToggleMemory,
|
||||||
|
ToggleProcesses,
|
||||||
|
|
||||||
|
Info(SysinfoSnapshot),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
struct State {
|
struct State {
|
||||||
cpu_graph: CpuGraph,
|
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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
fn new() -> (Self, Task<Message>) {
|
fn new() -> (Self, Task<Message>) {
|
||||||
(
|
let mut state = Self {
|
||||||
Self {
|
is_main_section_shown: true,
|
||||||
cpu_graph: Default::default(),
|
..Default::default()
|
||||||
},
|
};
|
||||||
Task::run(
|
state.total_cpu_graph.show_borders = false;
|
||||||
channel(1, async |mut sender| {
|
|
||||||
let mut sys = sysinfo::System::new();
|
|
||||||
let refreshes = sysinfo::RefreshKind::nothing()
|
|
||||||
.with_cpu(sysinfo::CpuRefreshKind::nothing().with_cpu_usage());
|
|
||||||
|
|
||||||
sys.refresh_specifics(refreshes);
|
(state, info_stream::create())
|
||||||
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> {
|
fn update(&mut self, message: Message) -> Task<Message> {
|
||||||
match message {
|
match message {
|
||||||
Message::Cpu(value) => {
|
Message::ToggleMain => {
|
||||||
self.cpu_graph.update(value);
|
self.is_main_section_shown.toggle();
|
||||||
self.cpu_graph.cache.clear();
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
fn view(&self) -> Element<'_, Message, Theme, Renderer> {
|
fn view(&self) -> Element<'_> {
|
||||||
widget::Canvas::new(&self.cpu_graph)
|
widget::column([
|
||||||
.width(Length::Fill)
|
widget::row([
|
||||||
.height(Length::Fill)
|
maybe_widget(self.is_main_section_shown, || {
|
||||||
.into()
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run() -> Result<(), impl std::error::Error> {
|
pub fn run() -> Result<(), Error> {
|
||||||
application(State::new, State::update, State::view)
|
application(State::new, State::update, State::view)
|
||||||
|
.subscription(State::subscription)
|
||||||
|
.title(State::title)
|
||||||
.theme(theme::Theme::custom(
|
.theme(theme::Theme::custom(
|
||||||
"high-contrast-dark",
|
"high-contrast-dark",
|
||||||
theme::Palette {
|
theme::Palette {
|
||||||
|
|
|
||||||
14
src/app/utils.rs
Normal file
14
src/app/utils.rs
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/main.rs
20
src/main.rs
|
|
@ -1,5 +1,19 @@
|
||||||
mod app;
|
use std::env;
|
||||||
|
|
||||||
fn main() -> Result<(), impl std::error::Error> {
|
#[cfg(feature = "app")]
|
||||||
app::run()
|
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}"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
21
src/server/mod.rs
Normal file
21
src/server/mod.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/sysinfo_snapshot.rs
Normal file
87
src/sysinfo_snapshot.rs
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/utils.rs
Normal file
10
src/utils.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#[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
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue