Skip to main content

netget/tui/
rail.rs

1//! The one-line instance summary: status glyph, id, protocol, address, live
2//! peers, sparkline, driver badge. A card's header row is one of these.
3
4use crate::tui::app::UiKey;
5use crate::tui::driver::{driver_of, Driver};
6use crate::tui::projection::{ClientRow, ServerRow};
7
8/// How a piece of text should be coloured, resolved by the renderer.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Tone {
11    Normal,
12    Dim,
13    Good,
14    Warn,
15    Bad,
16    Accent,
17    Server,
18    Client,
19    Title,
20    Reasoning,
21}
22
23impl Driver {
24    pub fn tone(&self) -> Tone {
25        match self {
26            Driver::Manual => Tone::Warn,
27            Driver::Llm => Tone::Reasoning,
28            Driver::Silent => Tone::Dim,
29            Driver::Rules => Tone::Good,
30        }
31    }
32}
33
34/// Everything one instance line shows, before it is fitted to a width.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct InstanceLine {
37    pub key: UiKey,
38    pub glyph: &'static str,
39    pub glyph_tone: Tone,
40    pub id: u32,
41    pub protocol: String,
42    /// `:8080` for a server, `→ host:port` for a client.
43    pub target: String,
44    /// `2⇄` live peers for a server; empty for a client.
45    pub peers: String,
46    pub driver: Driver,
47    /// Requests parked for the human.
48    pub waiting: usize,
49    /// The error message when the instance is in an error state.
50    pub error: Option<String>,
51}
52
53pub fn server_line(row: &ServerRow) -> InstanceLine {
54    use crate::state::server::ServerStatus;
55    let (glyph, glyph_tone, error) = match &row.status {
56        ServerStatus::Running => ("●", Tone::Good, None),
57        ServerStatus::Starting => ("◐", Tone::Warn, None),
58        ServerStatus::Stopped => ("○", Tone::Dim, None),
59        ServerStatus::Error(e) => ("✗", Tone::Bad, Some(e.clone())),
60    };
61    let live = row.conns.iter().filter(|c| c.active).count();
62    InstanceLine {
63        key: UiKey::Server(row.id),
64        glyph,
65        glyph_tone,
66        id: row.id.as_u32(),
67        protocol: row.protocol.to_lowercase(),
68        target: row
69            .local_addr
70            .as_deref()
71            .and_then(|a| a.rsplit_once(':').map(|(_, p)| format!(":{p}")))
72            .unwrap_or_else(|| format!(":{}", row.port)),
73        peers: format!("{live}⇄"),
74        driver: driver_of(row.routing.as_ref()),
75        waiting: row.intercepts.len(),
76        error,
77    }
78}
79
80pub fn client_line(row: &ClientRow) -> InstanceLine {
81    use crate::state::client::ClientStatus;
82    let (glyph, glyph_tone, error) = match &row.status {
83        ClientStatus::Connected => ("●", Tone::Good, None),
84        ClientStatus::Connecting => ("◐", Tone::Warn, None),
85        ClientStatus::Disconnected => ("○", Tone::Dim, None),
86        ClientStatus::Error(e) => ("✗", Tone::Bad, Some(e.clone())),
87    };
88    InstanceLine {
89        key: UiKey::Client(row.id),
90        glyph,
91        glyph_tone,
92        id: row.id.as_u32(),
93        protocol: row.protocol.to_lowercase(),
94        target: format!("→{}", row.remote_addr),
95        peers: String::new(),
96        driver: driver_of(row.routing.as_ref()),
97        waiting: row.intercepts.len(),
98        error,
99    }
100}
101
102/// Truncate to `width` columns with an ellipsis, counting chars.
103pub fn fit(text: &str, width: usize) -> String {
104    let count = text.chars().count();
105    if count <= width {
106        return text.to_string();
107    }
108    if width == 0 {
109        return String::new();
110    }
111    let mut out: String = text.chars().take(width - 1).collect();
112    out.push('…');
113    out
114}