2026-10-02 15:37:08 +01:00
|
|
|
//! The eframe application: wires the tray, the main window, the per-network
|
|
|
|
|
//! devices windows, and the agent worker.
|
2026-10-02 14:48:52 +01:00
|
|
|
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
|
|
|
|
use eframe::egui;
|
|
|
|
|
|
|
|
|
|
use crate::agent::{AgentClient, resolve_socket};
|
2026-10-02 15:37:08 +01:00
|
|
|
use crate::devices;
|
2026-10-02 15:08:29 +01:00
|
|
|
use crate::stats::Traffic;
|
2026-10-02 14:48:52 +01:00
|
|
|
use crate::tray::{Action, Tray};
|
2026-10-02 15:37:08 +01:00
|
|
|
use crate::ui::{self, UiState};
|
2026-10-02 14:48:52 +01:00
|
|
|
|
|
|
|
|
/// The tray application state.
|
|
|
|
|
pub(crate) struct App {
|
|
|
|
|
agent: AgentClient,
|
|
|
|
|
tray: Tray,
|
2026-10-02 15:37:08 +01:00
|
|
|
state: UiState,
|
2026-10-02 15:08:29 +01:00
|
|
|
/// Traffic rates derived from successive snapshots.
|
|
|
|
|
traffic: Traffic,
|
2026-10-02 15:37:08 +01:00
|
|
|
/// The snapshot generation last folded into `traffic`.
|
2026-10-02 15:08:29 +01:00
|
|
|
sampled: u64,
|
2026-10-02 15:37:08 +01:00
|
|
|
/// Set when the user chose Quit, so the next close exits rather than hides.
|
2026-10-02 14:48:52 +01:00
|
|
|
quitting: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl App {
|
|
|
|
|
/// Builds the app: resolves the socket, starts the worker, creates the tray.
|
|
|
|
|
pub(crate) fn new(
|
|
|
|
|
cc: &eframe::CreationContext<'_>,
|
|
|
|
|
runtime: tokio::runtime::Handle,
|
|
|
|
|
) -> Result<Self, String> {
|
|
|
|
|
let ctx = cc.egui_ctx.clone();
|
|
|
|
|
let agent = AgentClient::spawn(&runtime, ctx.clone(), resolve_socket());
|
|
|
|
|
let tray = Tray::new(ctx)?;
|
|
|
|
|
Ok(Self {
|
|
|
|
|
agent,
|
|
|
|
|
tray,
|
2026-10-02 15:37:08 +01:00
|
|
|
state: UiState::default(),
|
2026-10-02 15:08:29 +01:00
|
|
|
traffic: Traffic::default(),
|
|
|
|
|
sampled: 0,
|
2026-10-02 14:48:52 +01:00
|
|
|
quitting: false,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl eframe::App for App {
|
|
|
|
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
|
|
|
|
for action in self.tray.poll() {
|
|
|
|
|
match action {
|
|
|
|
|
Action::Open => {
|
|
|
|
|
ctx.send_viewport_cmd(egui::ViewportCommand::Visible(true));
|
|
|
|
|
ctx.send_viewport_cmd(egui::ViewportCommand::Focus);
|
|
|
|
|
}
|
|
|
|
|
Action::Quit => {
|
|
|
|
|
self.quitting = true;
|
|
|
|
|
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-02 15:37:08 +01:00
|
|
|
// Closing the main window hides it to the tray; only Quit truly exits.
|
2026-10-02 14:48:52 +01:00
|
|
|
if ctx.input(|i| i.viewport().close_requested()) && !self.quitting {
|
|
|
|
|
ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
|
|
|
|
|
ctx.send_viewport_cmd(egui::ViewportCommand::Visible(false));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let snapshot = self.agent.snapshot();
|
2026-10-02 15:08:29 +01:00
|
|
|
if snapshot.generation != self.sampled
|
|
|
|
|
&& let (Some(Ok(report)), Some(at)) = (&snapshot.status, snapshot.at)
|
|
|
|
|
{
|
|
|
|
|
self.traffic.observe(report, at);
|
|
|
|
|
self.sampled = snapshot.generation;
|
|
|
|
|
}
|
2026-10-02 14:48:52 +01:00
|
|
|
|
2026-10-02 15:37:08 +01:00
|
|
|
ui::draw(ctx, &self.agent, &mut self.state, &snapshot, &self.traffic);
|
|
|
|
|
self.show_devices_windows(ctx, &snapshot);
|
|
|
|
|
|
2026-10-02 15:48:32 +01:00
|
|
|
// A secret the worker fetched for the clipboard: copy it here, on the
|
|
|
|
|
// UI thread that owns the clipboard, exactly once.
|
|
|
|
|
if let Some(secret) = self.agent.take_pending_copy() {
|
|
|
|
|
ctx.copy_text(secret);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-02 15:37:08 +01:00
|
|
|
// Keep ticking so fresh status and tray actions are picked up.
|
2026-10-02 14:48:52 +01:00
|
|
|
ctx.request_repaint_after(Duration::from_millis(500));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-02 15:37:08 +01:00
|
|
|
|
|
|
|
|
impl App {
|
|
|
|
|
/// Renders one separate window per open network-devices view.
|
|
|
|
|
fn show_devices_windows(&mut self, ctx: &egui::Context, snapshot: &crate::agent::Snapshot) {
|
|
|
|
|
let Some(Ok(report)) = &snapshot.status else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
let unit = self.state.unit;
|
|
|
|
|
let open: Vec<String> = self.state.open_devices.iter().cloned().collect();
|
|
|
|
|
for id in open {
|
|
|
|
|
let Some(network) = report.networks.iter().find(|n| n.network_id == id) else {
|
|
|
|
|
// The network is gone (left/forgotten); close its window.
|
|
|
|
|
self.state.open_devices.remove(&id);
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
let viewport_id = egui::ViewportId::from_hash_of(("devices", &id));
|
|
|
|
|
let builder = egui::ViewportBuilder::default()
|
|
|
|
|
.with_title(devices::title(network))
|
|
|
|
|
.with_inner_size([720.0, 540.0])
|
|
|
|
|
.with_min_inner_size([480.0, 320.0]);
|
|
|
|
|
let agent = &self.agent;
|
|
|
|
|
let traffic = &self.traffic;
|
|
|
|
|
let keep = ctx.show_viewport_immediate(viewport_id, builder, |vctx, _class| {
|
|
|
|
|
devices::show(vctx, agent, unit, network, traffic);
|
|
|
|
|
!vctx.input(|i| i.viewport().close_requested())
|
|
|
|
|
});
|
|
|
|
|
if !keep {
|
|
|
|
|
self.state.open_devices.remove(&id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|