Tray: About window, fixed-width device table with totals; WTFPL
Release / tsunagi-linux-aarch64 (push) Successful in 2m19s
Release / tsunagi-linux-x86_64 (push) Successful in 3m56s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
Release / tsunagi-linux-aarch64 (push) Successful in 2m19s
Release / tsunagi-linux-x86_64 (push) Successful in 3m56s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
Add an About window (client and agent details, protocol versions), pin the device table's column widths and add a total row, show the version in the header and drop the refresh button. Relicense under WTFPL. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
f4dcdec662
commit
2e178a6f9d
12 files changed
+370
-34
No files matched your search
+1
-1
@@ -14,7 +14,7 @@ mainline = { path = "vendor/mainline" }
|
|||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.91"
|
rust-version = "1.91"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "WTFPL"
|
||||||
repository = "https://github.com/tsunagi-net/tsunagi"
|
repository = "https://github.com/tsunagi-net/tsunagi"
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
Version 2, December 2004
|
||||||
|
|
||||||
|
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim or modified
|
||||||
|
copies of this license document, and changing it is allowed as long
|
||||||
|
as the name is changed.
|
||||||
|
|
||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||||
@@ -763,4 +763,4 @@ any of this.
|
|||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
MIT OR Apache-2.0.
|
[WTFPL](LICENSE): do what the fuck you want to.
|
||||||
@@ -4342,6 +4342,11 @@ async fn build_report(
|
|||||||
state_dir: paths.state_dir.display().to_string(),
|
state_dir: paths.state_dir.display().to_string(),
|
||||||
cache_dir: paths.cache_dir.display().to_string(),
|
cache_dir: paths.cache_dir.display().to_string(),
|
||||||
program: program_path(),
|
program: program_path(),
|
||||||
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||||
|
protocols: PROTOCOLS
|
||||||
|
.iter()
|
||||||
|
.map(|spec| format!("{} v{}", spec.name, spec.version))
|
||||||
|
.collect(),
|
||||||
privilege: match tsunagi::overlay::probe_net_admin() {
|
privilege: match tsunagi::overlay::probe_net_admin() {
|
||||||
tsunagi::overlay::Privilege::Available => tsunagi::ipc::PrivilegeReport::Available,
|
tsunagi::overlay::Privilege::Available => tsunagi::ipc::PrivilegeReport::Available,
|
||||||
tsunagi::overlay::Privilege::Missing(reason) => {
|
tsunagi::overlay::Privilege::Missing(reason) => {
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
//! The About window: what this software is, and what it is talking to.
|
||||||
|
//!
|
||||||
|
//! Compact on purpose — one table for the client, one for the agent. The agent
|
||||||
|
//! half is whatever the agent says about itself, so a client and an agent that
|
||||||
|
//! are not the same build show up as such.
|
||||||
|
|
||||||
|
use eframe::egui;
|
||||||
|
|
||||||
|
use tsunagi::ipc::{PrivilegeReport, StatusReport};
|
||||||
|
|
||||||
|
use crate::agent::Snapshot;
|
||||||
|
use crate::format;
|
||||||
|
|
||||||
|
const REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
|
||||||
|
const AUTHOR: &str = "AB <tsunagi@hexor.cy>";
|
||||||
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
/// Draws the window body into its viewport.
|
||||||
|
pub(crate) fn show(ctx: &egui::Context, socket: &std::path::Path, snapshot: &Snapshot) {
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
// Long paths and a taller agent table must scroll rather than be cut.
|
||||||
|
egui::ScrollArea::both()
|
||||||
|
.auto_shrink([false, false])
|
||||||
|
.show(ui, |ui| body(ui, socket, snapshot));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body(ui: &mut egui::Ui, socket: &std::path::Path, snapshot: &Snapshot) {
|
||||||
|
{
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.heading("tsunagi");
|
||||||
|
ui.label(egui::RichText::new(VERSION).small().weak());
|
||||||
|
});
|
||||||
|
ui.label("Peer-to-peer mesh network with no server.");
|
||||||
|
ui.add_space(6.0);
|
||||||
|
|
||||||
|
section(ui, "client");
|
||||||
|
grid(ui, "about client", |ui| {
|
||||||
|
row(ui, "repository", |ui| {
|
||||||
|
ui.hyperlink(REPOSITORY);
|
||||||
|
});
|
||||||
|
row(ui, "author", |ui| {
|
||||||
|
format::copy_field(ui, AUTHOR, AUTHOR);
|
||||||
|
});
|
||||||
|
row(ui, "license", |ui| {
|
||||||
|
ui.label(env!("CARGO_PKG_LICENSE"));
|
||||||
|
});
|
||||||
|
row(ui, "build", |ui| {
|
||||||
|
ui.label(format!(
|
||||||
|
"{} {} · {}",
|
||||||
|
std::env::consts::OS,
|
||||||
|
std::env::consts::ARCH,
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
"debug"
|
||||||
|
} else {
|
||||||
|
"release"
|
||||||
|
}
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(8.0);
|
||||||
|
section(ui, "agent");
|
||||||
|
let socket = socket.display().to_string();
|
||||||
|
match &snapshot.status {
|
||||||
|
None => {
|
||||||
|
grid(ui, "about agent", |ui| {
|
||||||
|
row(ui, "socket", |ui| format::copy_field(ui, &socket, &socket));
|
||||||
|
row(ui, "state", |ui| {
|
||||||
|
ui.label("connecting…");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(Err(error)) => {
|
||||||
|
grid(ui, "about agent", |ui| {
|
||||||
|
row(ui, "socket", |ui| format::copy_field(ui, &socket, &socket));
|
||||||
|
row(ui, "state", |ui| {
|
||||||
|
ui.colored_label(egui::Color32::from_rgb(0xd8, 0x4a, 0x3a), "unreachable");
|
||||||
|
});
|
||||||
|
row(ui, "reason", |ui| {
|
||||||
|
ui.add(egui::Label::new(error).wrap());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(Ok(report)) => agent_table(ui, &socket, report),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn agent_table(ui: &mut egui::Ui, socket: &str, report: &StatusReport) {
|
||||||
|
grid(ui, "about agent", |ui| {
|
||||||
|
row(ui, "socket", |ui| format::copy_field(ui, socket, socket));
|
||||||
|
row(ui, "state", |ui| {
|
||||||
|
ui.colored_label(egui::Color32::from_rgb(0x3c, 0xb0, 0x4a), "connected");
|
||||||
|
});
|
||||||
|
row(ui, "version", |ui| {
|
||||||
|
if report.version.is_empty() {
|
||||||
|
ui.weak("unknown");
|
||||||
|
} else if report.version == VERSION {
|
||||||
|
ui.label(&report.version);
|
||||||
|
} else {
|
||||||
|
ui.colored_label(
|
||||||
|
egui::Color32::from_rgb(0xd8, 0x9a, 0x2a),
|
||||||
|
format!("{} (client is {VERSION})", report.version),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if !report.protocols.is_empty() {
|
||||||
|
row(ui, "protocols", |ui| {
|
||||||
|
ui.label(report.protocols.join(" · "));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
row(ui, "endpoint id", |ui| {
|
||||||
|
format::copy_field(ui, &format::short(&report.endpoint_id), &report.endpoint_id);
|
||||||
|
});
|
||||||
|
row(ui, "hostname", |ui| {
|
||||||
|
ui.label(&report.hostname);
|
||||||
|
});
|
||||||
|
if !report.program.is_empty() {
|
||||||
|
row(ui, "binary", |ui| {
|
||||||
|
format::copy_field(ui, &report.program, &report.program);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if !report.state_dir.is_empty() {
|
||||||
|
row(ui, "state dir", |ui| {
|
||||||
|
format::copy_field(ui, &report.state_dir, &report.state_dir);
|
||||||
|
});
|
||||||
|
row(ui, "cache dir", |ui| {
|
||||||
|
format::copy_field(ui, &report.cache_dir, &report.cache_dir);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
row(ui, "interface", |ui| match &report.privilege {
|
||||||
|
PrivilegeReport::Unknown => {
|
||||||
|
ui.weak("unknown");
|
||||||
|
}
|
||||||
|
PrivilegeReport::Available => {
|
||||||
|
ui.label("can be managed (CAP_NET_ADMIN)");
|
||||||
|
}
|
||||||
|
PrivilegeReport::Missing(_) => {
|
||||||
|
ui.colored_label(egui::Color32::from_rgb(0xd8, 0x9a, 0x2a), "no privileges");
|
||||||
|
}
|
||||||
|
PrivilegeReport::Unsupported => {
|
||||||
|
ui.label("not supported on this platform");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
row(ui, "cache", |ui| {
|
||||||
|
ui.label(if report.cache_healthy {
|
||||||
|
"usable"
|
||||||
|
} else {
|
||||||
|
"unavailable"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
row(ui, "listening", |ui| {
|
||||||
|
ui.label(report.bound_sockets.join(", "));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn section(ui: &mut egui::Ui, title: &str) {
|
||||||
|
ui.label(egui::RichText::new(title).strong());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grid(ui: &mut egui::Ui, id: &str, add: impl FnOnce(&mut egui::Ui)) {
|
||||||
|
egui::Grid::new(id)
|
||||||
|
.num_columns(2)
|
||||||
|
.spacing([12.0, 3.0])
|
||||||
|
.show(ui, add);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One label/value line of a table.
|
||||||
|
fn row(ui: &mut egui::Ui, label: &str, value: impl FnOnce(&mut egui::Ui)) {
|
||||||
|
ui.weak(label);
|
||||||
|
value(ui);
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
@@ -21,8 +21,6 @@ const POLL_INTERVAL: Duration = Duration::from_secs(2);
|
|||||||
|
|
||||||
/// A request from the UI to the agent.
|
/// A request from the UI to the agent.
|
||||||
pub(crate) enum Command {
|
pub(crate) enum Command {
|
||||||
/// Re-read status now.
|
|
||||||
Refresh,
|
|
||||||
/// Stop serving a network, or start serving it again.
|
/// Stop serving a network, or start serving it again.
|
||||||
SetActive { network_id: String, active: bool },
|
SetActive { network_id: String, active: bool },
|
||||||
/// Turn local broadcast participation on or off for a network.
|
/// Turn local broadcast participation on or off for a network.
|
||||||
@@ -170,7 +168,6 @@ async fn run(
|
|||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
|
|
||||||
let outcome: Result<String, String> = match command {
|
let outcome: Result<String, String> = match command {
|
||||||
Command::Refresh => Ok(String::new()),
|
|
||||||
Command::SetActive { network_id, active } => ipc::set_active(socket, &network_id, active)
|
Command::SetActive { network_id, active } => ipc::set_active(socket, &network_id, active)
|
||||||
.await
|
.await
|
||||||
.map(|report| {
|
.map(|report| {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
|
||||||
|
use crate::about;
|
||||||
use crate::agent::{AgentClient, resolve_socket};
|
use crate::agent::{AgentClient, resolve_socket};
|
||||||
use crate::devices;
|
use crate::devices;
|
||||||
use crate::stats::Traffic;
|
use crate::stats::Traffic;
|
||||||
@@ -59,6 +60,7 @@ impl eframe::App for App {
|
|||||||
|
|
||||||
ui::draw(ctx, &self.agent, &mut self.state, &snapshot, &self.traffic);
|
ui::draw(ctx, &self.agent, &mut self.state, &snapshot, &self.traffic);
|
||||||
self.show_devices_windows(ctx, &snapshot);
|
self.show_devices_windows(ctx, &snapshot);
|
||||||
|
self.show_about_window(ctx, &snapshot);
|
||||||
|
|
||||||
// A secret the worker fetched for the clipboard: copy it here, on the
|
// A secret the worker fetched for the clipboard: copy it here, on the
|
||||||
// UI thread that owns the clipboard, exactly once.
|
// UI thread that owns the clipboard, exactly once.
|
||||||
@@ -72,6 +74,29 @@ impl eframe::App for App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
/// Renders the About window while it is open.
|
||||||
|
fn show_about_window(&mut self, ctx: &egui::Context, snapshot: &crate::agent::Snapshot) {
|
||||||
|
if !self.state.about_open {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let builder = egui::ViewportBuilder::default()
|
||||||
|
.with_title("About tsunagi")
|
||||||
|
.with_inner_size([560.0, 520.0])
|
||||||
|
.with_min_inner_size([380.0, 280.0]);
|
||||||
|
let socket = self.agent.socket();
|
||||||
|
let keep = ctx.show_viewport_immediate(
|
||||||
|
egui::ViewportId::from_hash_of("about"),
|
||||||
|
builder,
|
||||||
|
|vctx, _class| {
|
||||||
|
about::show(vctx, socket, snapshot);
|
||||||
|
!vctx.input(|i| i.viewport().close_requested())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if !keep {
|
||||||
|
self.state.about_open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Renders one separate window per open network-devices view.
|
/// Renders one separate window per open network-devices view.
|
||||||
fn show_devices_windows(&mut self, ctx: &egui::Context, snapshot: &crate::agent::Snapshot) {
|
fn show_devices_windows(&mut self, ctx: &egui::Context, snapshot: &crate::agent::Snapshot) {
|
||||||
let Some(Ok(report)) = &snapshot.status else {
|
let Some(Ok(report)) = &snapshot.status else {
|
||||||
|
|||||||
@@ -233,6 +233,54 @@ fn devices<'a>(network: &'a NetworkReport, own_id: &str) -> Vec<Device<'a>> {
|
|||||||
devices
|
devices
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The widest value each column is expected to hold, so the table starts out
|
||||||
|
/// wide enough and does not reflow as numbers grow.
|
||||||
|
const COLUMN_SAMPLES: [&str; 8] = [
|
||||||
|
"",
|
||||||
|
"tcp-tls",
|
||||||
|
"255.255.255.255",
|
||||||
|
"23h 59m",
|
||||||
|
"99999/99999",
|
||||||
|
"1023.9 MB / 1023.9 MB",
|
||||||
|
"1023.9 kB/s",
|
||||||
|
"",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Column widths that only ever grow, kept across frames.
|
||||||
|
///
|
||||||
|
/// A grid sizes each column to its widest cell *this frame*, so a value that
|
||||||
|
/// gets shorter — `14.4 MB/s` giving way to `382 pkt/s` — would pull the
|
||||||
|
/// column in and push every cell after it. Remembering the widest seen keeps
|
||||||
|
/// the table where it is.
|
||||||
|
fn column_widths(ui: &egui::Ui, id: egui::Id) -> [f32; 8] {
|
||||||
|
let stored: Option<[f32; 8]> = ui.memory(|m| m.data.get_temp(id));
|
||||||
|
stored.unwrap_or_else(|| {
|
||||||
|
let font = egui::TextStyle::Body.resolve(ui.style());
|
||||||
|
let mut widths = [0.0; 8];
|
||||||
|
for (width, sample) in widths.iter_mut().zip(COLUMN_SAMPLES) {
|
||||||
|
if !sample.is_empty() {
|
||||||
|
*width = ui.fonts(|fonts| {
|
||||||
|
fonts
|
||||||
|
.layout_no_wrap(sample.to_owned(), font.clone(), egui::Color32::WHITE)
|
||||||
|
.size()
|
||||||
|
.x
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
widths
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One cell, at least as wide as its column has ever been.
|
||||||
|
fn cell(ui: &mut egui::Ui, column: usize, widths: &mut [f32; 8], add: impl FnOnce(&mut egui::Ui)) {
|
||||||
|
let floor = widths[column];
|
||||||
|
let response = ui.scope(|ui| {
|
||||||
|
ui.set_min_width(floor);
|
||||||
|
add(ui);
|
||||||
|
});
|
||||||
|
widths[column] = widths[column].max(response.response.rect.width());
|
||||||
|
}
|
||||||
|
|
||||||
/// One table of all devices: online rows carry live counters and a plot,
|
/// One table of all devices: online rows carry live counters and a plot,
|
||||||
/// offline rows show what is known (name and address) and dashes.
|
/// offline rows show what is known (name and address) and dashes.
|
||||||
fn device_table(
|
fn device_table(
|
||||||
@@ -246,12 +294,14 @@ fn device_table(
|
|||||||
ui.weak("none");
|
ui.weak("none");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let widths_id = egui::Id::new(("devices column widths", network_id));
|
||||||
|
let mut widths = column_widths(ui, widths_id);
|
||||||
egui::Grid::new("devices")
|
egui::Grid::new("devices")
|
||||||
.num_columns(8)
|
.num_columns(8)
|
||||||
.striped(true)
|
.striped(true)
|
||||||
.spacing([10.0, 4.0])
|
.spacing([10.0, 4.0])
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
for header in [
|
for (column, header) in [
|
||||||
"device",
|
"device",
|
||||||
"proto",
|
"proto",
|
||||||
"address",
|
"address",
|
||||||
@@ -260,49 +310,112 @@ fn device_table(
|
|||||||
"bytes",
|
"bytes",
|
||||||
"rate",
|
"rate",
|
||||||
"plot",
|
"plot",
|
||||||
] {
|
]
|
||||||
ui.weak(header);
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
cell(ui, column, &mut widths, |ui| {
|
||||||
|
ui.weak(header);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
for device in devices {
|
for device in devices {
|
||||||
name_cell(ui, device);
|
cell(ui, 0, &mut widths, |ui| name_cell(ui, device));
|
||||||
ui.label(device.proto);
|
cell(ui, 1, &mut widths, |ui| {
|
||||||
match &device.address {
|
ui.label(device.proto);
|
||||||
|
});
|
||||||
|
cell(ui, 2, &mut widths, |ui| match &device.address {
|
||||||
Some(address) => format::copy_field(ui, address, address),
|
Some(address) => format::copy_field(ui, address, address),
|
||||||
None => {
|
None => {
|
||||||
ui.weak("—");
|
ui.weak("—");
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
ui.label(
|
cell(ui, 3, &mut widths, |ui| {
|
||||||
device
|
ui.label(
|
||||||
.handshake_secs
|
device
|
||||||
.map_or_else(|| "offline".to_string(), format::age),
|
.handshake_secs
|
||||||
);
|
.map_or_else(|| "offline".to_string(), format::age),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
match device.overlay.filter(|_| device.online) {
|
match device.overlay.filter(|_| device.online) {
|
||||||
Some(peer) => {
|
Some(peer) => {
|
||||||
ui.label(format!("{}/{}", peer.tx_packets, peer.rx_packets));
|
|
||||||
ui.label(format!(
|
|
||||||
"{} / {}",
|
|
||||||
format::bytes(peer.tx_bytes as f64),
|
|
||||||
format::bytes(peer.rx_bytes as f64)
|
|
||||||
));
|
|
||||||
let series = traffic.peer(network_id, &peer.public_key);
|
let series = traffic.peer(network_id, &peer.public_key);
|
||||||
ui.add(egui::Label::new(format::series_rate(series, unit)).truncate());
|
cell(ui, 4, &mut widths, |ui| {
|
||||||
|
ui.label(format!("{}/{}", peer.tx_packets, peer.rx_packets));
|
||||||
|
});
|
||||||
|
cell(ui, 5, &mut widths, |ui| {
|
||||||
|
ui.label(format!(
|
||||||
|
"{} / {}",
|
||||||
|
format::bytes(peer.tx_bytes as f64),
|
||||||
|
format::bytes(peer.rx_bytes as f64)
|
||||||
|
));
|
||||||
|
});
|
||||||
|
cell(ui, 6, &mut widths, |ui| {
|
||||||
|
ui.add(
|
||||||
|
egui::Label::new(format::series_rate(series, unit))
|
||||||
|
.wrap_mode(egui::TextWrapMode::Extend),
|
||||||
|
);
|
||||||
|
});
|
||||||
ui.allocate_ui(egui::vec2(100.0, 20.0), |ui| {
|
ui.allocate_ui(egui::vec2(100.0, 20.0), |ui| {
|
||||||
format::sparkline(ui, series, unit, 18.0);
|
format::sparkline(ui, series, unit, 18.0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
for _ in 0..4 {
|
for column in 4..7 {
|
||||||
ui.weak("—");
|
cell(ui, column, &mut widths, |ui| {
|
||||||
|
ui.weak("—");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
ui.weak("—");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Everything this agent has moved through the overlay, including
|
||||||
|
// devices that have since gone away.
|
||||||
|
let (mut tx_packets, mut rx_packets, mut tx_bytes, mut rx_bytes) = (0u64, 0, 0, 0);
|
||||||
|
for peer in devices.iter().filter_map(|d| d.overlay) {
|
||||||
|
tx_packets += peer.tx_packets;
|
||||||
|
rx_packets += peer.rx_packets;
|
||||||
|
tx_bytes += peer.tx_bytes;
|
||||||
|
rx_bytes += peer.rx_bytes;
|
||||||
|
}
|
||||||
|
cell(ui, 0, &mut widths, |ui| {
|
||||||
|
ui.strong("total");
|
||||||
|
});
|
||||||
|
for column in 1..4 {
|
||||||
|
cell(ui, column, &mut widths, |ui| {
|
||||||
|
ui.label("");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cell(ui, 4, &mut widths, |ui| {
|
||||||
|
ui.strong(format!("{tx_packets}/{rx_packets}"));
|
||||||
|
});
|
||||||
|
cell(ui, 5, &mut widths, |ui| {
|
||||||
|
ui.strong(format!(
|
||||||
|
"{} / {}",
|
||||||
|
format::bytes(tx_bytes as f64),
|
||||||
|
format::bytes(rx_bytes as f64)
|
||||||
|
));
|
||||||
|
});
|
||||||
|
cell(ui, 6, &mut widths, |ui| {
|
||||||
|
ui.add(
|
||||||
|
egui::Label::new(
|
||||||
|
egui::RichText::new(format::series_rate(traffic.network(network_id), unit))
|
||||||
|
.strong(),
|
||||||
|
)
|
||||||
|
.wrap_mode(egui::TextWrapMode::Extend),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ui.end_row();
|
||||||
});
|
});
|
||||||
|
ui.memory_mut(|m| {
|
||||||
|
m.data
|
||||||
|
.insert_temp(egui::Id::new("devices column widths"), widths)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn yes_no(value: bool) -> &'static str {
|
fn yes_no(value: bool) -> &'static str {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
// Nothing here is a library.
|
// Nothing here is a library.
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
mod about;
|
||||||
mod agent;
|
mod agent;
|
||||||
mod app;
|
mod app;
|
||||||
mod devices;
|
mod devices;
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ pub(crate) struct UiState {
|
|||||||
pub(crate) host_edit: Option<String>,
|
pub(crate) host_edit: Option<String>,
|
||||||
/// Network ids whose devices window is open.
|
/// Network ids whose devices window is open.
|
||||||
pub(crate) open_devices: BTreeSet<String>,
|
pub(crate) open_devices: BTreeSet<String>,
|
||||||
|
/// Whether the About window is open.
|
||||||
|
pub(crate) about_open: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UiState {
|
impl Default for UiState {
|
||||||
@@ -49,6 +51,7 @@ impl Default for UiState {
|
|||||||
unit: Unit::Packets,
|
unit: Unit::Packets,
|
||||||
host_edit: None,
|
host_edit: None,
|
||||||
open_devices: BTreeSet::new(),
|
open_devices: BTreeSet::new(),
|
||||||
|
about_open: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,13 +68,14 @@ pub(crate) fn draw(
|
|||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.heading("tsunagi");
|
ui.heading("tsunagi");
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new(env!("CARGO_PKG_VERSION"))
|
||||||
|
.small()
|
||||||
|
.weak(),
|
||||||
|
);
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
if ui
|
if ui.button(icon::INFO).on_hover_text("About").clicked() {
|
||||||
.button(icon::ARROWS_CLOCKWISE)
|
state.about_open = true;
|
||||||
.on_hover_text("Refresh")
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
agent.send(Command::Refresh);
|
|
||||||
}
|
}
|
||||||
if snapshot.busy {
|
if snapshot.busy {
|
||||||
ui.add(egui::Spinner::new());
|
ui.add(egui::Spinner::new());
|
||||||
|
|||||||
@@ -366,6 +366,10 @@ pub struct StatusReport {
|
|||||||
/// It is the agent that creates the interface, so the client's own
|
/// It is the agent that creates the interface, so the client's own
|
||||||
/// capabilities say nothing about it.
|
/// capabilities say nothing about it.
|
||||||
pub privilege: PrivilegeReport,
|
pub privilege: PrivilegeReport,
|
||||||
|
/// The version of the agent's software.
|
||||||
|
pub version: String,
|
||||||
|
/// The data plane protocols it has, each as `name vN` (wire version).
|
||||||
|
pub protocols: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What the agent found when it checked whether it may manage an interface.
|
/// What the agent found when it checked whether it may manage an interface.
|
||||||
|
|||||||
Vendored
+1
-2
@@ -69,8 +69,7 @@ builddate = $(date +%s)
|
|||||||
packager = AB <ab@hexor.cy>
|
packager = AB <ab@hexor.cy>
|
||||||
size = ${size}
|
size = ${size}
|
||||||
arch = ${arch}
|
arch = ${arch}
|
||||||
${relations}license = MIT
|
${relations}license = WTFPL
|
||||||
license = Apache
|
|
||||||
PKGINFO
|
PKGINFO
|
||||||
|
|
||||||
# Scriptlet: create the dedicated system user the service runs as, keep systemd
|
# Scriptlet: create the dedicated system user the service runs as, keep systemd
|
||||||
|
|||||||
Reference in new issue
Block a user