refactor: one transfer badge table, not one per screen

The Telegram report and the run summary each carried the same five-arm table:
`Ok`/`Stalled`/`Slow`/`Blocked`/`Error` to the same five labels and the same
three colours. A sixth `TransferStatus` would have had to be added to both, and
a label that drifted in one would still have compiled — the class of bug the
"two implementations is a bug even when both work" rule names.

`tui::widgets::transfer_badge` holds it now, beside `status_color`, with the
same "every variant is named, no `_` arm" rule so a new state fails to build.

The Telegram report also computed its colour and threw it away (`let _ = color;`)
— the report's own DC table colours its status cells, so the dangling value was
an unfinished intent rather than a decision. The download and upload lines now
colour the token the same way. Under `--plain` and `--ascii`, `clean_output`
strips it as it does everywhere else.
This commit is contained in:
Kirill Minovsky 2026-10-10 03:36:07 +03:00
parent a21d8c4808
commit 623badfa7f
3 changed files with 35 additions and 24 deletions

View file

@ -4,6 +4,7 @@
use comfy_table::Color;
use dpi_core::classify::*;
use dpi_core::net::netinfo::flag_emoji;
use dpi_core::probe::telegram::TransferStatus;
use crate::tui::backend::{ascii_mode, plain_mode};
@ -138,6 +139,28 @@ pub(crate) fn warn_mark() -> &'static str {
}
pub(crate) const BOX_WIDTH: usize = 71;
/// The badge and colour of one transfer leg's outcome.
///
/// Two screens report a leg — the Telegram report and the run summary — and
/// each carried its own copy of this table: five arms, the same five labels,
/// the same three colours, kept in step by hand. A sixth `TransferStatus` would
/// have had to be added to both, and a label that drifted in one of them would
/// still have compiled.
///
/// The labels are protocol tokens (Rule 4): `--json` carries them through
/// `TransferStatus` and they never change with the language, so they are not
/// i18n strings. Every variant is named rather than caught by a `_` arm, for the
/// reason [`status_color`] gives — a new one must fail to build here.
pub(crate) fn transfer_badge(status: TransferStatus) -> (&'static str, Color) {
match status {
TransferStatus::Ok => ("OK", Color::Green),
TransferStatus::Stalled => ("STALL", Color::Yellow),
TransferStatus::Slow => ("SLOW", Color::Yellow),
TransferStatus::Blocked => ("BLOCKED", Color::Red),
TransferStatus::Error => ("ERROR", Color::Red),
}
}
/// Maps a probe status to its table cell color: green for OK, yellow for the
/// "no TLS 1.3 / no CA / local IP / DNS fail / NXDOMAIN" outcomes, dark grey
/// for Err, and red for the rest.

View file

@ -2,9 +2,9 @@
use crate::i18n::{Messages, fmt_size, fmt_speed};
use dpi_core::probe::domains::DomainStats;
use dpi_core::probe::telegram::{TelegramFullReport, TransferStatus};
use dpi_core::probe::telegram::TelegramFullReport;
use crate::tui::widgets::{BOX_WIDTH, asc, panel_to_string, strip_ansi_len, wrap_ansi};
use crate::tui::widgets::{BOX_WIDTH, asc, cell_color, panel_to_string, strip_ansi_len, transfer_badge, wrap_ansi};
fn frac_sgr(ok: usize, total: usize) -> &'static str {
if total == 0 || ok == total {
@ -111,20 +111,13 @@ pub(crate) fn render_summary(data: &SummaryData, msg: &Messages) -> String {
&& let Some(t) = data.telegram
{
let tg_row = |label: &str, st: &dpi_core::probe::telegram::TransferStats, speed: f64, size: u64| {
// Every state is named: the wildcard this replaced rendered any
// state it did not know as `ERROR` in red.
let (raw, sgr) = match st.status {
TransferStatus::Ok => ("OK", "32"),
TransferStatus::Stalled => ("STALL", "33"),
TransferStatus::Slow => ("SLOW", "33"),
TransferStatus::Blocked => ("BLOCKED", "31"),
TransferStatus::Error => ("ERROR", "31"),
};
// The badge and its colour come from the one table both screens read.
let (raw, color) = transfer_badge(st.status);
let mut metrics = format!("{} {}, {}", msg.avg_label, fmt_speed(speed, msg.lang), fmt_size(size, msg.lang));
if let Some(sec) = st.drop_at_sec {
metrics += &msg.stall_after.replace("{}", &sec.to_string());
}
(label.to_string(), format!("\x1b[{}m{:<16}\x1b[0m {}", sgr, raw, metrics))
(label.to_string(), format!("{} {}", cell_color(&format!("{raw:<16}"), color), metrics))
};
let (l1, v1) = tg_row(msg.summary_tg_download, &t.download, t.download.avg_bps, t.download.bytes_total);
let (l2, v2) = tg_row(msg.summary_tg_upload, &t.upload, t.upload.avg_bps, t.upload.bytes_total);

View file

@ -2,9 +2,9 @@
use comfy_table::{Cell, Color, ContentArrangement, Table};
use crate::i18n::{Messages, detail_text, fmt_size, fmt_speed, format_bidi};
use dpi_core::probe::telegram::{TelegramFullReport, TransferStatus};
use dpi_core::probe::telegram::TelegramFullReport;
use crate::tui::widgets::{cell_color, table_preset};
use crate::tui::widgets::{cell_color, table_preset, transfer_badge};
pub(crate) fn render_telegram(report: &TelegramFullReport, msg: &Messages) -> String {
@ -48,15 +48,11 @@ pub(crate) fn render_telegram(report: &TelegramFullReport, msg: &Messages) -> St
// Download / upload verdict lines: label, status, peak, average, size, duration.
for (label, t) in [(msg.download_label, &report.download), (msg.upload_label, &report.upload)] {
// Every state is named: the wildcard this replaced rendered any state it
// did not know as `ERROR` in red.
let (st_text, color) = match t.status {
TransferStatus::Ok => ("OK", Color::Green),
TransferStatus::Stalled => ("STALL", Color::Yellow),
TransferStatus::Slow => ("SLOW", Color::Yellow),
TransferStatus::Blocked => ("BLOCKED", Color::Red),
TransferStatus::Error => ("ERROR", Color::Red),
};
// The label and its colour come from the one table both screens read;
// the status token is a protocol token, so it is Latin in every
// language and `cell_color` keeps it legible on a legacy console.
let (st_text, color) = transfer_badge(t.status);
let st_text = cell_color(st_text, color);
let mut line = format!(
" {}: {} {} {} {} {} ({} / {:.0}s",
label,
@ -72,7 +68,6 @@ pub(crate) fn render_telegram(report: &TelegramFullReport, msg: &Messages) -> St
line += &msg.stall_after.replace("{}", &sec.to_string());
}
line += ")";
let _ = color;
out.push_str(&format!("{}\n", line));
}
out.push('\n');