fmt: apply rustfmt

This commit is contained in:
jasonwitty 2025-08-08 17:25:15 -07:00
parent 747aef0005
commit 274a485f8d
20 changed files with 298 additions and 139 deletions

View File

@ -1,6 +1,10 @@
//! App state and main loop: input handling, fetching metrics, updating history, and drawing. //! App state and main loop: input handling, fetching metrics, updating history, and drawing.
use std::{collections::VecDeque, io, time::{Duration, Instant}}; use std::{
collections::VecDeque,
io,
time::{Duration, Instant},
};
use crossterm::{ use crossterm::{
event::{self, Event, KeyCode}, event::{self, Event, KeyCode},
@ -16,7 +20,15 @@ use tokio::time::sleep;
use crate::history::{push_capped, PerCoreHistory}; use crate::history::{push_capped, PerCoreHistory};
use crate::types::Metrics; use crate::types::Metrics;
use crate::ui::{header::draw_header, cpu::{draw_cpu_avg_graph, draw_per_core_bars}, mem::draw_mem, swap::draw_swap, disks::draw_disks, net::draw_net_spark, processes::draw_top_processes}; use crate::ui::{
cpu::{draw_cpu_avg_graph, draw_per_core_bars},
disks::draw_disks,
header::draw_header,
mem::draw_mem,
net::draw_net_spark,
processes::draw_top_processes,
swap::draw_swap,
};
use crate::ws::{connect, request_metrics}; use crate::ws::{connect, request_metrics};
pub struct App { pub struct App {
@ -88,7 +100,10 @@ impl App {
// Input (non-blocking) // Input (non-blocking)
while event::poll(Duration::from_millis(10))? { while event::poll(Duration::from_millis(10))? {
if let Event::Key(k) = event::read()? { if let Event::Key(k) = event::read()? {
if matches!(k.code, KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc) { if matches!(
k.code,
KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc
) {
self.should_quit = true; self.should_quit = true;
} }
} }
@ -130,7 +145,9 @@ impl App {
let rx = ((rx_total.saturating_sub(prx)) as f64 / dt / 1024.0).round() as u64; let rx = ((rx_total.saturating_sub(prx)) as f64 / dt / 1024.0).round() as u64;
let tx = ((tx_total.saturating_sub(ptx)) as f64 / dt / 1024.0).round() as u64; let tx = ((tx_total.saturating_sub(ptx)) as f64 / dt / 1024.0).round() as u64;
(rx, tx) (rx, tx)
} else { (0, 0) }; } else {
(0, 0)
};
self.last_net_totals = Some((rx_total, tx_total, now)); self.last_net_totals = Some((rx_total, tx_total, now));
push_capped(&mut self.rx_hist, rx_kb, 600); push_capped(&mut self.rx_hist, rx_kb, 600);
push_capped(&mut self.tx_hist, tx_kb, 600); push_capped(&mut self.tx_hist, tx_kb, 600);
@ -174,21 +191,33 @@ impl App {
let left_stack = ratatui::layout::Layout::default() let left_stack = ratatui::layout::Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([Constraint::Min(6), Constraint::Length(4), Constraint::Length(4)]) .constraints([
Constraint::Min(6),
Constraint::Length(4),
Constraint::Length(4),
])
.split(bottom[0]); .split(bottom[0]);
draw_disks(f, left_stack[0], self.last_metrics.as_ref()); draw_disks(f, left_stack[0], self.last_metrics.as_ref());
draw_net_spark( draw_net_spark(
f, f,
left_stack[1], left_stack[1],
&format!("Download (KB/s) — now: {} | peak: {}", self.rx_hist.back().copied().unwrap_or(0), self.rx_peak), &format!(
"Download (KB/s) — now: {} | peak: {}",
self.rx_hist.back().copied().unwrap_or(0),
self.rx_peak
),
&self.rx_hist, &self.rx_hist,
ratatui::style::Color::Green, ratatui::style::Color::Green,
); );
draw_net_spark( draw_net_spark(
f, f,
left_stack[2], left_stack[2],
&format!("Upload (KB/s) — now: {} | peak: {}", self.tx_hist.back().copied().unwrap_or(0), self.tx_peak), &format!(
"Upload (KB/s) — now: {} | peak: {}",
self.tx_hist.back().copied().unwrap_or(0),
self.tx_peak
),
&self.tx_hist, &self.tx_hist,
ratatui::style::Color::Blue, ratatui::style::Color::Blue,
); );

View File

@ -17,7 +17,10 @@ pub struct PerCoreHistory {
impl PerCoreHistory { impl PerCoreHistory {
pub fn new(cap: usize) -> Self { pub fn new(cap: usize) -> Self {
Self { deques: Vec::new(), cap } Self {
deques: Vec::new(),
cap,
}
} }
// Ensure we have one deque per core; resize on CPU topology changes // Ensure we have one deque per core; resize on CPU topology changes

View File

@ -6,8 +6,8 @@ mod types;
mod ui; mod ui;
mod ws; mod ws;
use std::env;
use app::App; use app::App;
use std::env;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {

View File

@ -1,12 +1,12 @@
//! CPU average sparkline + per-core mini bars. //! CPU average sparkline + per-core mini bars.
use ratatui::style::Modifier;
use ratatui::{ use ratatui::{
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style}, style::{Color, Style},
text::{Line, Span}, text::{Line, Span},
widgets::{Block, Borders, Paragraph, Sparkline}, widgets::{Block, Borders, Paragraph, Sparkline},
}; };
use ratatui::style::Modifier;
use crate::history::PerCoreHistory; use crate::history::PerCoreHistory;
use crate::types::Metrics; use crate::types::Metrics;
@ -17,7 +17,11 @@ pub fn draw_cpu_avg_graph(
hist: &std::collections::VecDeque<u64>, hist: &std::collections::VecDeque<u64>,
m: Option<&Metrics>, m: Option<&Metrics>,
) { ) {
let title = if let Some(mm) = m { format!("CPU avg (now: {:>5.1}%)", mm.cpu_total) } else { "CPU avg".into() }; let title = if let Some(mm) = m {
format!("CPU avg (now: {:>5.1}%)", mm.cpu_total)
} else {
"CPU avg".into()
};
let max_points = area.width.saturating_sub(2) as usize; let max_points = area.width.saturating_sub(2) as usize;
let start = hist.len().saturating_sub(max_points); let start = hist.len().saturating_sub(max_points);
let data: Vec<u64> = hist.iter().skip(start).cloned().collect(); let data: Vec<u64> = hist.iter().skip(start).cloned().collect();
@ -35,16 +39,31 @@ pub fn draw_per_core_bars(
m: Option<&Metrics>, m: Option<&Metrics>,
per_core_hist: &PerCoreHistory, per_core_hist: &PerCoreHistory,
) { ) {
f.render_widget(Block::default().borders(Borders::ALL).title("Per-core"), area); f.render_widget(
let Some(mm) = m else { return; }; Block::default().borders(Borders::ALL).title("Per-core"),
area,
);
let Some(mm) = m else {
return;
};
let inner = Rect { x: area.x + 1, y: area.y + 1, width: area.width.saturating_sub(2), height: area.height.saturating_sub(2) }; let inner = Rect {
if inner.height == 0 { return; } x: area.x + 1,
y: area.y + 1,
width: area.width.saturating_sub(2),
height: area.height.saturating_sub(2),
};
if inner.height == 0 {
return;
}
let rows = inner.height as usize; let rows = inner.height as usize;
let show_n = rows.min(mm.cpu_per_core.len()); let show_n = rows.min(mm.cpu_per_core.len());
let constraints: Vec<Constraint> = (0..show_n).map(|_| Constraint::Length(1)).collect(); let constraints: Vec<Constraint> = (0..show_n).map(|_| Constraint::Length(1)).collect();
let vchunks = Layout::default().direction(Direction::Vertical).constraints(constraints).split(inner); let vchunks = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.split(inner);
for i in 0..show_n { for i in 0..show_n {
let rect = vchunks[i]; let rect = vchunks[i];
@ -54,13 +73,19 @@ pub fn draw_per_core_bars(
.split(rect); .split(rect);
let curr = mm.cpu_per_core[i].clamp(0.0, 100.0); let curr = mm.cpu_per_core[i].clamp(0.0, 100.0);
let older = per_core_hist.deques.get(i) let older = per_core_hist
.deques
.get(i)
.and_then(|d| d.iter().rev().nth(20).copied()) .and_then(|d| d.iter().rev().nth(20).copied())
.map(|v| v as f32) .map(|v| v as f32)
.unwrap_or(curr); .unwrap_or(curr);
let trend = if curr > older + 0.2 { "" } let trend = if curr > older + 0.2 {
else if curr + 0.2 < older { "" } ""
else { "" }; } else if curr + 0.2 < older {
""
} else {
""
};
let fg = match curr { let fg = match curr {
x if x < 25.0 => Color::Green, x if x < 25.0 => Color::Green,
@ -85,7 +110,10 @@ pub fn draw_per_core_bars(
f.render_widget(spark, hchunks[0]); f.render_widget(spark, hchunks[0]);
let label = format!("cpu{:<2}{}{:>5.1}%", i, trend, curr); let label = format!("cpu{:<2}{}{:>5.1}%", i, trend, curr);
let line = Line::from(Span::styled(label, Style::default().fg(fg).add_modifier(Modifier::BOLD))); let line = Line::from(Span::styled(
label,
Style::default().fg(fg).add_modifier(Modifier::BOLD),
));
f.render_widget(Paragraph::new(line).right_aligned(), hchunks[1]); f.render_widget(Paragraph::new(line).right_aligned(), hchunks[1]);
} }
} }

View File

@ -1,16 +1,18 @@
//! Disk cards with per-device gauge and title line. //! Disk cards with per-device gauge and title line.
use crate::types::Metrics;
use crate::ui::util::{disk_icon, human, truncate_middle};
use ratatui::{ use ratatui::{
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout, Rect},
style::Style, style::Style,
widgets::{Block, Borders, Gauge}, widgets::{Block, Borders, Gauge},
}; };
use crate::types::Metrics;
use crate::ui::util::{human, truncate_middle, disk_icon};
pub fn draw_disks(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) { pub fn draw_disks(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) {
f.render_widget(Block::default().borders(Borders::ALL).title("Disks"), area); f.render_widget(Block::default().borders(Borders::ALL).title("Disks"), area);
let Some(mm) = m else { return; }; let Some(mm) = m else {
return;
};
let inner = Rect { let inner = Rect {
x: area.x + 1, x: area.x + 1,
@ -18,12 +20,16 @@ pub fn draw_disks(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) {
width: area.width.saturating_sub(2), width: area.width.saturating_sub(2),
height: area.height.saturating_sub(2), height: area.height.saturating_sub(2),
}; };
if inner.height < 3 { return; } if inner.height < 3 {
return;
}
let per_disk_h = 3u16; let per_disk_h = 3u16;
let max_cards = (inner.height / per_disk_h).min(mm.disks.len() as u16) as usize; let max_cards = (inner.height / per_disk_h).min(mm.disks.len() as u16) as usize;
let constraints: Vec<Constraint> = (0..max_cards).map(|_| Constraint::Length(per_disk_h)).collect(); let constraints: Vec<Constraint> = (0..max_cards)
.map(|_| Constraint::Length(per_disk_h))
.collect();
let rows = Layout::default() let rows = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints(constraints) .constraints(constraints)
@ -32,10 +38,20 @@ pub fn draw_disks(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) {
for (i, slot) in rows.iter().enumerate() { for (i, slot) in rows.iter().enumerate() {
let d = &mm.disks[i]; let d = &mm.disks[i];
let used = d.total.saturating_sub(d.available); let used = d.total.saturating_sub(d.available);
let ratio = if d.total > 0 { used as f64 / d.total as f64 } else { 0.0 }; let ratio = if d.total > 0 {
used as f64 / d.total as f64
} else {
0.0
};
let pct = (ratio * 100.0).round() as u16; let pct = (ratio * 100.0).round() as u16;
let color = if pct < 70 { ratatui::style::Color::Green } else if pct < 90 { ratatui::style::Color::Yellow } else { ratatui::style::Color::Red }; let color = if pct < 70 {
ratatui::style::Color::Green
} else if pct < 90 {
ratatui::style::Color::Yellow
} else {
ratatui::style::Color::Red
};
let title = format!( let title = format!(
"{} {} {} / {} ({}%)", "{} {} {} / {} ({}%)",
@ -55,7 +71,9 @@ pub fn draw_disks(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) {
width: slot.width.saturating_sub(2), width: slot.width.saturating_sub(2),
height: slot.height.saturating_sub(2), height: slot.height.saturating_sub(2),
}; };
if inner_card.height == 0 { continue; } if inner_card.height == 0 {
continue;
}
let gauge_rect = Rect { let gauge_rect = Rect {
x: inner_card.x, x: inner_card.x,

View File

@ -1,18 +1,30 @@
//! Top header with hostname and CPU temperature indicator. //! Top header with hostname and CPU temperature indicator.
use crate::types::Metrics;
use ratatui::{ use ratatui::{
layout::Rect, layout::Rect,
widgets::{Block, Borders}, widgets::{Block, Borders},
}; };
use crate::types::Metrics;
pub fn draw_header(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) { pub fn draw_header(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) {
let title = if let Some(mm) = m { let title = if let Some(mm) = m {
let temp = mm.cpu_temp_c.map(|t| { let temp = mm
let icon = if t < 50.0 { "😎" } else if t < 85.0 { "⚠️" } else { "🔥" }; .cpu_temp_c
.map(|t| {
let icon = if t < 50.0 {
"😎"
} else if t < 85.0 {
"⚠️"
} else {
"🔥"
};
format!("CPU Temp: {:.1}°C {}", t, icon) format!("CPU Temp: {:.1}°C {}", t, icon)
}).unwrap_or_else(|| "CPU Temp: N/A".into()); })
format!("socktop — host: {} | {} (press 'q' to quit)", mm.hostname, temp) .unwrap_or_else(|| "CPU Temp: N/A".into());
format!(
"socktop — host: {} | {} (press 'q' to quit)",
mm.hostname, temp
)
} else { } else {
"socktop — connecting... (press 'q' to quit)".into() "socktop — connecting... (press 'q' to quit)".into()
}; };

View File

@ -1,18 +1,24 @@
//! Memory gauge. //! Memory gauge.
use crate::types::Metrics;
use crate::ui::util::human;
use ratatui::{ use ratatui::{
layout::Rect, layout::Rect,
style::{Color, Style}, style::{Color, Style},
widgets::{Block, Borders, Gauge}, widgets::{Block, Borders, Gauge},
}; };
use crate::types::Metrics;
use crate::ui::util::human;
pub fn draw_mem(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) { pub fn draw_mem(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) {
let (used, total, pct) = if let Some(mm) = m { let (used, total, pct) = if let Some(mm) = m {
let pct = if mm.mem_total > 0 { (mm.mem_used as f64 / mm.mem_total as f64 * 100.0) as u16 } else { 0 }; let pct = if mm.mem_total > 0 {
(mm.mem_used as f64 / mm.mem_total as f64 * 100.0) as u16
} else {
0
};
(mm.mem_used, mm.mem_total, pct) (mm.mem_used, mm.mem_total, pct)
} else { (0, 0, 0) }; } else {
(0, 0, 0)
};
let g = Gauge::default() let g = Gauge::default()
.block(Block::default().borders(Borders::ALL).title("Memory")) .block(Block::default().borders(Borders::ALL).title("Memory"))

View File

@ -1,10 +1,10 @@
//! UI module root: exposes drawing functions for individual panels. //! UI module root: exposes drawing functions for individual panels.
pub mod header;
pub mod cpu; pub mod cpu;
pub mod mem;
pub mod swap;
pub mod disks; pub mod disks;
pub mod header;
pub mod mem;
pub mod net; pub mod net;
pub mod processes; pub mod processes;
pub mod swap;
pub mod util; pub mod util;

View File

@ -1,11 +1,11 @@
//! Network sparklines (download/upload). //! Network sparklines (download/upload).
use std::collections::VecDeque;
use ratatui::{ use ratatui::{
layout::Rect, layout::Rect,
style::{Color, Style}, style::{Color, Style},
widgets::{Block, Borders, Sparkline}, widgets::{Block, Borders, Sparkline},
}; };
use std::collections::VecDeque;
pub fn draw_net_spark( pub fn draw_net_spark(
f: &mut ratatui::Frame<'_>, f: &mut ratatui::Frame<'_>,
@ -19,7 +19,11 @@ pub fn draw_net_spark(
let data: Vec<u64> = hist.iter().skip(start).cloned().collect(); let data: Vec<u64> = hist.iter().skip(start).cloned().collect();
let spark = Sparkline::default() let spark = Sparkline::default()
.block(Block::default().borders(Borders::ALL).title(title.to_string())) .block(
Block::default()
.borders(Borders::ALL)
.title(title.to_string()),
)
.data(&data) .data(&data)
.style(Style::default().fg(color)); .style(Style::default().fg(color));
f.render_widget(spark, area); f.render_widget(spark, area);

View File

@ -1,26 +1,39 @@
//! Top processes table with per-cell coloring and zebra striping. //! Top processes table with per-cell coloring and zebra striping.
use ratatui::style::Modifier;
use ratatui::{ use ratatui::{
layout::{Constraint, Rect}, layout::{Constraint, Rect},
style::{Color, Style}, style::{Color, Style},
widgets::{Block, Borders, Cell, Row, Table}, widgets::{Block, Borders, Cell, Row, Table},
}; };
use ratatui::style::Modifier;
use crate::types::Metrics; use crate::types::Metrics;
use crate::ui::util::human; use crate::ui::util::human;
pub fn draw_top_processes(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) { pub fn draw_top_processes(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) {
let Some(mm) = m else { let Some(mm) = m else {
f.render_widget(Block::default().borders(Borders::ALL).title("Top Processes"), area); f.render_widget(
Block::default()
.borders(Borders::ALL)
.title("Top Processes"),
area,
);
return; return;
}; };
let total_mem_bytes = mm.mem_total.max(1); let total_mem_bytes = mm.mem_total.max(1);
let title = format!("Top Processes ({} total)", mm.process_count); let title = format!("Top Processes ({} total)", mm.process_count);
let peak_cpu = mm.top_processes.iter().map(|p| p.cpu_usage).fold(0.0_f32, f32::max); let peak_cpu = mm
.top_processes
.iter()
.map(|p| p.cpu_usage)
.fold(0.0_f32, f32::max);
let rows: Vec<Row> = mm.top_processes.iter().enumerate().map(|(i, p)| { let rows: Vec<Row> = mm
.top_processes
.iter()
.enumerate()
.map(|(i, p)| {
let mem_pct = (p.mem_bytes as f64 / total_mem_bytes as f64) * 100.0; let mem_pct = (p.mem_bytes as f64 / total_mem_bytes as f64) * 100.0;
let cpu_fg = match p.cpu_usage { let cpu_fg = match p.cpu_usage {
@ -34,11 +47,17 @@ pub fn draw_top_processes(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Met
_ => Color::Red, _ => Color::Red,
}; };
let zebra = if i % 2 == 0 { Style::default().fg(Color::Gray) } else { Style::default() }; let zebra = if i % 2 == 0 {
Style::default().fg(Color::Gray)
} else {
Style::default()
};
let emphasis = if (p.cpu_usage - peak_cpu).abs() < f32::EPSILON { let emphasis = if (p.cpu_usage - peak_cpu).abs() < f32::EPSILON {
Style::default().add_modifier(Modifier::BOLD) Style::default().add_modifier(Modifier::BOLD)
} else { Style::default() }; } else {
Style::default()
};
Row::new(vec![ Row::new(vec![
Cell::from(p.pid.to_string()).style(Style::default().fg(Color::DarkGray)), Cell::from(p.pid.to_string()).style(Style::default().fg(Color::DarkGray)),
@ -48,10 +67,14 @@ pub fn draw_top_processes(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Met
Cell::from(format!("{:.2}%", mem_pct)).style(Style::default().fg(mem_fg)), Cell::from(format!("{:.2}%", mem_pct)).style(Style::default().fg(mem_fg)),
]) ])
.style(zebra.patch(emphasis)) .style(zebra.patch(emphasis))
}).collect(); })
.collect();
let header = Row::new(vec!["PID", "Name", "CPU %", "Mem", "Mem %"]) let header = Row::new(vec!["PID", "Name", "CPU %", "Mem", "Mem %"]).style(
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)); Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
);
let table = Table::new( let table = Table::new(
rows, rows,

View File

@ -1,18 +1,24 @@
//! Swap gauge. //! Swap gauge.
use crate::types::Metrics;
use crate::ui::util::human;
use ratatui::{ use ratatui::{
layout::Rect, layout::Rect,
style::{Color, Style}, style::{Color, Style},
widgets::{Block, Borders, Gauge}, widgets::{Block, Borders, Gauge},
}; };
use crate::types::Metrics;
use crate::ui::util::human;
pub fn draw_swap(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) { pub fn draw_swap(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) {
let (used, total, pct) = if let Some(mm) = m { let (used, total, pct) = if let Some(mm) = m {
let pct = if mm.swap_total > 0 { (mm.swap_used as f64 / mm.swap_total as f64 * 100.0) as u16 } else { 0 }; let pct = if mm.swap_total > 0 {
(mm.swap_used as f64 / mm.swap_total as f64 * 100.0) as u16
} else {
0
};
(mm.swap_used, mm.swap_total, pct) (mm.swap_used, mm.swap_total, pct)
} else { (0, 0, 0) }; } else {
(0, 0, 0)
};
let g = Gauge::default() let g = Gauge::default()
.block(Block::default().borders(Borders::ALL).title("Swap")) .block(Block::default().borders(Borders::ALL).title("Swap"))

View File

@ -3,31 +3,49 @@
pub fn human(b: u64) -> String { pub fn human(b: u64) -> String {
const K: f64 = 1024.0; const K: f64 = 1024.0;
let b = b as f64; let b = b as f64;
if b < K { return format!("{b:.0}B"); } if b < K {
return format!("{b:.0}B");
}
let kb = b / K; let kb = b / K;
if kb < K { return format!("{kb:.1}KB"); } if kb < K {
return format!("{kb:.1}KB");
}
let mb = kb / K; let mb = kb / K;
if mb < K { return format!("{mb:.1}MB"); } if mb < K {
return format!("{mb:.1}MB");
}
let gb = mb / K; let gb = mb / K;
if gb < K { return format!("{gb:.1}GB"); } if gb < K {
return format!("{gb:.1}GB");
}
let tb = gb / K; let tb = gb / K;
format!("{tb:.2}TB") format!("{tb:.2}TB")
} }
pub fn truncate_middle(s: &str, max: usize) -> String { pub fn truncate_middle(s: &str, max: usize) -> String {
if s.len() <= max { return s.to_string(); } if s.len() <= max {
if max <= 3 { return "...".into(); } return s.to_string();
}
if max <= 3 {
return "...".into();
}
let keep = max - 3; let keep = max - 3;
let left = keep / 2; let left = keep / 2;
let right = keep - left; let right = keep - left;
format!("{}...{}", &s[..left], &s[s.len()-right..]) format!("{}...{}", &s[..left], &s[s.len() - right..])
} }
pub fn disk_icon(name: &str) -> &'static str { pub fn disk_icon(name: &str) -> &'static str {
let n = name.to_ascii_lowercase(); let n = name.to_ascii_lowercase();
if n.contains(':') { "🗄️" } if n.contains(':') {
else if n.contains("nvme") { "" } "🗄️"
else if n.starts_with("sd") { "💽" } } else if n.contains("nvme") {
else if n.contains("overlay") { "📦" } ""
else { "🖴" } } else if n.starts_with("sd") {
"💽"
} else if n.contains("overlay") {
"📦"
} else {
"🖴"
}
} }

View File

@ -4,25 +4,26 @@
mod metrics; mod metrics;
mod sampler; mod sampler;
mod state; mod state;
mod ws;
mod types; mod types;
mod ws;
use axum::{routing::get, Router}; use axum::{routing::get, Router};
use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration, sync::atomic::AtomicUsize}; use std::{
use sysinfo::{ collections::HashMap, net::SocketAddr, sync::atomic::AtomicUsize, sync::Arc, time::Duration,
Components, CpuRefreshKind, Disks, MemoryRefreshKind, Networks, ProcessRefreshKind, RefreshKind,
System,
}; };
use tokio::sync::{Mutex, RwLock, Notify}; use sysinfo::{
Components, CpuRefreshKind, Disks, MemoryRefreshKind, Networks, ProcessRefreshKind,
RefreshKind, System,
};
use tokio::sync::{Mutex, Notify, RwLock};
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
use state::{AppState, SharedTotals};
use sampler::spawn_sampler; use sampler::spawn_sampler;
use state::{AppState, SharedTotals};
use ws::ws_handler; use ws::ws_handler;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// Init logging; configure with RUST_LOG (e.g., RUST_LOG=info). // Init logging; configure with RUST_LOG (e.g., RUST_LOG=info).
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env()) .with_env_filter(EnvFilter::from_default_env())
@ -60,7 +61,9 @@ async fn main() {
// new: adaptive sampling controls // new: adaptive sampling controls
client_count: Arc::new(AtomicUsize::new(0)), client_count: Arc::new(AtomicUsize::new(0)),
wake_sampler: Arc::new(Notify::new()), wake_sampler: Arc::new(Notify::new()),
auth_token: std::env::var("SOCKTOP_TOKEN").ok().filter(|s| !s.is_empty()), auth_token: std::env::var("SOCKTOP_TOKEN")
.ok()
.filter(|s| !s.is_empty()),
}; };
// Start background sampler (adjust cadence as needed) // Start background sampler (adjust cadence as needed)
@ -68,7 +71,9 @@ async fn main() {
// Web app // Web app
let port = resolve_port(); let port = resolve_port();
let app = Router::new().route("/ws", get(ws_handler)).with_state(state); let app = Router::new()
.route("/ws", get(ws_handler))
.with_state(state);
let addr = SocketAddr::from(([0, 0, 0, 0], port)); let addr = SocketAddr::from(([0, 0, 0, 0], port));
@ -82,7 +87,6 @@ async fn main() {
let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap(); axum::serve(listener, app).await.unwrap();
} }
// Resolve the listening port from CLI args/env with a 3000 default. // Resolve the listening port from CLI args/env with a 3000 default.
@ -97,7 +101,10 @@ fn resolve_port() -> u16 {
return p; return p;
} }
} }
eprintln!("Warning: invalid SOCKTOP_PORT='{}'; using default {}", s, DEFAULT); eprintln!(
"Warning: invalid SOCKTOP_PORT='{}'; using default {}",
s, DEFAULT
);
} }
let mut args = std::env::args().skip(1); let mut args = std::env::args().skip(1);
@ -133,4 +140,3 @@ fn resolve_port() -> u16 {
DEFAULT DEFAULT
} }

View File

@ -121,7 +121,10 @@ pub fn best_cpu_temp(components: &Components) -> Option<f32> {
.iter() .iter()
.filter(|c| { .filter(|c| {
let label = c.label().to_lowercase(); let label = c.label().to_lowercase();
label.contains("cpu") || label.contains("package") || label.contains("tctl") || label.contains("tdie") label.contains("cpu")
|| label.contains("package")
|| label.contains("tctl")
|| label.contains("tdie")
}) })
.filter_map(|c| c.temperature()) .filter_map(|c| c.temperature())
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))

View File

@ -5,13 +5,16 @@ use crate::metrics::collect_metrics;
use crate::state::AppState; use crate::state::AppState;
//use serde_json::to_string; //use serde_json::to_string;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tokio::time::{Duration, interval, MissedTickBehavior}; use tokio::time::{interval, Duration, MissedTickBehavior};
pub fn spawn_sampler(state: AppState, period: Duration) -> JoinHandle<()> { pub fn spawn_sampler(state: AppState, period: Duration) -> JoinHandle<()> {
tokio::spawn(async move { tokio::spawn(async move {
let idle_period = Duration::from_secs(10); let idle_period = Duration::from_secs(10);
loop { loop {
let active = state.client_count.load(std::sync::atomic::Ordering::Relaxed) > 0; let active = state
.client_count
.load(std::sync::atomic::Ordering::Relaxed)
> 0;
let mut ticker = interval(if active { period } else { idle_period }); let mut ticker = interval(if active { period } else { idle_period });
ticker.set_missed_tick_behavior(MissedTickBehavior::Skip); ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
ticker.tick().await; ticker.tick().await;

View File

@ -1,9 +1,9 @@
//! Shared agent state: sysinfo handles and hot JSON cache. //! Shared agent state: sysinfo handles and hot JSON cache.
use std::{collections::HashMap, sync::Arc};
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::{collections::HashMap, sync::Arc};
use sysinfo::{Components, Disks, Networks, System}; use sysinfo::{Components, Disks, Networks, System};
use tokio::sync::{Mutex, RwLock, Notify}; use tokio::sync::{Mutex, Notify, RwLock};
pub type SharedSystem = Arc<Mutex<System>>; pub type SharedSystem = Arc<Mutex<System>>;
pub type SharedNetworks = Arc<Mutex<Networks>>; pub type SharedNetworks = Arc<Mutex<Networks>>;