Merge pull request #4 from jasonwitty/feature/connection-profiles
Feature/connection profiles
This commit is contained in:
commit
9f675fa804
16
README.md
16
README.md
@ -190,6 +190,12 @@ First time you specify a new `--profile/-P` name together with a URL (and option
|
|||||||
socktop --profile prod ws://prod-host:3000/ws
|
socktop --profile prod ws://prod-host:3000/ws
|
||||||
# With TLS pinning:
|
# With TLS pinning:
|
||||||
socktop --profile prod-tls --tls-ca /path/to/cert.pem wss://prod-host:8443/ws
|
socktop --profile prod-tls --tls-ca /path/to/cert.pem wss://prod-host:8443/ws
|
||||||
|
|
||||||
|
You can also set custom intervals (milliseconds):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
socktop --profile prod --metrics-interval-ms 750 --processes-interval-ms 3000 ws://prod-host:3000/ws
|
||||||
|
```
|
||||||
```
|
```
|
||||||
|
|
||||||
If a profile already exists you will be prompted before overwriting:
|
If a profile already exists you will be prompted before overwriting:
|
||||||
@ -214,7 +220,7 @@ socktop --profile prod
|
|||||||
socktop -P prod-tls # short flag
|
socktop -P prod-tls # short flag
|
||||||
```
|
```
|
||||||
|
|
||||||
The stored URL (and TLS CA path, if any) will be used. TLS auto-upgrade still applies if a CA path is stored alongside a ws:// URL.
|
The stored URL (and TLS CA path, if any) plus any saved intervals will be used. TLS auto-upgrade still applies if a CA path is stored alongside a ws:// URL.
|
||||||
|
|
||||||
### Interactive selection (no args)
|
### Interactive selection (no args)
|
||||||
|
|
||||||
@ -238,7 +244,12 @@ An example `profiles.json` (pretty‑printed):
|
|||||||
{
|
{
|
||||||
"profiles": {
|
"profiles": {
|
||||||
"prod": { "url": "ws://prod-host:3000/ws" },
|
"prod": { "url": "ws://prod-host:3000/ws" },
|
||||||
"prod-tls": { "url": "wss://prod-host:8443/ws", "tls_ca": "/home/user/certs/prod-cert.pem" }
|
"prod-tls": {
|
||||||
|
"url": "wss://prod-host:8443/ws",
|
||||||
|
"tls_ca": "/home/user/certs/prod-cert.pem",
|
||||||
|
"metrics_interval_ms": 500,
|
||||||
|
"processes_interval_ms": 2000
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"version": 0
|
"version": 0
|
||||||
}
|
}
|
||||||
@ -248,6 +259,7 @@ Notes:
|
|||||||
- The `tls_ca` path is stored as given; if you move or rotate the certificate update the profile by re-running with `--profile NAME --save`.
|
- The `tls_ca` path is stored as given; if you move or rotate the certificate update the profile by re-running with `--profile NAME --save`.
|
||||||
- Deleting a profile: edit the JSON file and remove the entry (TUI does not yet have an in-app delete command).
|
- Deleting a profile: edit the JSON file and remove the entry (TUI does not yet have an in-app delete command).
|
||||||
- Profiles are client-side convenience only; they do not affect the agent.
|
- Profiles are client-side convenience only; they do not affect the agent.
|
||||||
|
- Intervals: `metrics_interval_ms` controls the fast metrics poll (default 500 ms). `processes_interval_ms` controls process list polling (default 2000 ms). Values below 100 ms (metrics) or 200 ms (processes) are clamped.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -63,9 +63,13 @@ pub struct App {
|
|||||||
last_disks_poll: Instant,
|
last_disks_poll: Instant,
|
||||||
procs_interval: Duration,
|
procs_interval: Duration,
|
||||||
disks_interval: Duration,
|
disks_interval: Duration,
|
||||||
|
metrics_interval: Duration,
|
||||||
|
|
||||||
// For reconnects
|
// For reconnects
|
||||||
ws_url: String,
|
ws_url: String,
|
||||||
|
// Security / status flags
|
||||||
|
pub is_tls: bool,
|
||||||
|
pub has_token: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
@ -94,10 +98,29 @@ impl App {
|
|||||||
.unwrap_or_else(Instant::now),
|
.unwrap_or_else(Instant::now),
|
||||||
procs_interval: Duration::from_secs(2),
|
procs_interval: Duration::from_secs(2),
|
||||||
disks_interval: Duration::from_secs(5),
|
disks_interval: Duration::from_secs(5),
|
||||||
|
metrics_interval: Duration::from_millis(500),
|
||||||
ws_url: String::new(),
|
ws_url: String::new(),
|
||||||
|
is_tls: false,
|
||||||
|
has_token: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_intervals(mut self, metrics_ms: Option<u64>, procs_ms: Option<u64>) -> Self {
|
||||||
|
if let Some(m) = metrics_ms {
|
||||||
|
self.metrics_interval = Duration::from_millis(m.max(100));
|
||||||
|
}
|
||||||
|
if let Some(p) = procs_ms {
|
||||||
|
self.procs_interval = Duration::from_millis(p.max(200));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_status(mut self, is_tls: bool, has_token: bool) -> Self {
|
||||||
|
self.is_tls = is_tls;
|
||||||
|
self.has_token = has_token;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn run(
|
pub async fn run(
|
||||||
&mut self,
|
&mut self,
|
||||||
url: &str,
|
url: &str,
|
||||||
@ -284,7 +307,7 @@ impl App {
|
|||||||
terminal.draw(|f| self.draw(f))?;
|
terminal.draw(|f| self.draw(f))?;
|
||||||
|
|
||||||
// Tick rate
|
// Tick rate
|
||||||
sleep(Duration::from_millis(500)).await;
|
sleep(self.metrics_interval).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -351,7 +374,15 @@ impl App {
|
|||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
draw_header(f, rows[0], self.last_metrics.as_ref());
|
draw_header(
|
||||||
|
f,
|
||||||
|
rows[0],
|
||||||
|
self.last_metrics.as_ref(),
|
||||||
|
self.is_tls,
|
||||||
|
self.has_token,
|
||||||
|
self.metrics_interval,
|
||||||
|
self.procs_interval,
|
||||||
|
);
|
||||||
|
|
||||||
// Top row: left CPU avg, right Per-core (full top-right)
|
// Top row: left CPU avg, right Per-core (full top-right)
|
||||||
let top_lr = ratatui::layout::Layout::default()
|
let top_lr = ratatui::layout::Layout::default()
|
||||||
@ -471,7 +502,10 @@ impl Default for App {
|
|||||||
.unwrap_or_else(Instant::now),
|
.unwrap_or_else(Instant::now),
|
||||||
procs_interval: Duration::from_secs(2),
|
procs_interval: Duration::from_secs(2),
|
||||||
disks_interval: Duration::from_secs(5),
|
disks_interval: Duration::from_secs(5),
|
||||||
|
metrics_interval: Duration::from_millis(500),
|
||||||
ws_url: String::new(),
|
ws_url: String::new(),
|
||||||
|
is_tls: false,
|
||||||
|
has_token: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,8 @@ pub(crate) struct ParsedArgs {
|
|||||||
save: bool,
|
save: bool,
|
||||||
demo: bool,
|
demo: bool,
|
||||||
dry_run: bool, // hidden test helper: skip connecting
|
dry_run: bool, // hidden test helper: skip connecting
|
||||||
|
metrics_interval_ms: Option<u64>,
|
||||||
|
processes_interval_ms: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<ParsedArgs, String> {
|
pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<ParsedArgs, String> {
|
||||||
@ -30,10 +32,12 @@ pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Pars
|
|||||||
let mut save = false;
|
let mut save = false;
|
||||||
let mut demo = false;
|
let mut demo = false;
|
||||||
let mut dry_run = false;
|
let mut dry_run = false;
|
||||||
|
let mut metrics_interval_ms: Option<u64> = None;
|
||||||
|
let mut processes_interval_ms: Option<u64> = None;
|
||||||
while let Some(arg) = it.next() {
|
while let Some(arg) = it.next() {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
"-h" | "--help" => {
|
"-h" | "--help" => {
|
||||||
return Err(format!("Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--profile NAME|-P NAME] [--save] [--demo] [ws://HOST:PORT/ws]\n"));
|
return Err(format!("Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--profile NAME|-P NAME] [--save] [--demo] [--metrics-interval-ms N] [--processes-interval-ms N] [ws://HOST:PORT/ws]\n"));
|
||||||
}
|
}
|
||||||
"--tls-ca" | "-t" => {
|
"--tls-ca" | "-t" => {
|
||||||
tls_ca = it.next();
|
tls_ca = it.next();
|
||||||
@ -51,6 +55,12 @@ pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Pars
|
|||||||
// intentionally undocumented
|
// intentionally undocumented
|
||||||
dry_run = true;
|
dry_run = true;
|
||||||
}
|
}
|
||||||
|
"--metrics-interval-ms" => {
|
||||||
|
metrics_interval_ms = it.next().and_then(|v| v.parse().ok());
|
||||||
|
}
|
||||||
|
"--processes-interval-ms" => {
|
||||||
|
processes_interval_ms = it.next().and_then(|v| v.parse().ok());
|
||||||
|
}
|
||||||
_ if arg.starts_with("--tls-ca=") => {
|
_ if arg.starts_with("--tls-ca=") => {
|
||||||
if let Some((_, v)) = arg.split_once('=') {
|
if let Some((_, v)) = arg.split_once('=') {
|
||||||
if !v.is_empty() {
|
if !v.is_empty() {
|
||||||
@ -65,6 +75,16 @@ pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Pars
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ if arg.starts_with("--metrics-interval-ms=") => {
|
||||||
|
if let Some((_, v)) = arg.split_once('=') {
|
||||||
|
metrics_interval_ms = v.parse().ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ if arg.starts_with("--processes-interval-ms=") => {
|
||||||
|
if let Some((_, v)) = arg.split_once('=') {
|
||||||
|
processes_interval_ms = v.parse().ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if url.is_none() {
|
if url.is_none() {
|
||||||
url = Some(arg);
|
url = Some(arg);
|
||||||
@ -81,6 +101,8 @@ pub(crate) fn parse_args<I: IntoIterator<Item = String>>(args: I) -> Result<Pars
|
|||||||
save,
|
save,
|
||||||
demo,
|
demo,
|
||||||
dry_run,
|
dry_run,
|
||||||
|
metrics_interval_ms,
|
||||||
|
processes_interval_ms,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,20 +126,32 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
};
|
};
|
||||||
let resolved = req.resolve(&profiles_file);
|
let resolved = req.resolve(&profiles_file);
|
||||||
let mut profiles_mut = profiles_file.clone();
|
let mut profiles_mut = profiles_file.clone();
|
||||||
let (url, tls_ca): (String, Option<String>) = match resolved {
|
let (url, tls_ca, metrics_interval_ms, processes_interval_ms): (
|
||||||
|
String,
|
||||||
|
Option<String>,
|
||||||
|
Option<u64>,
|
||||||
|
Option<u64>,
|
||||||
|
) = match resolved {
|
||||||
ResolveProfile::Direct(u, t) => {
|
ResolveProfile::Direct(u, t) => {
|
||||||
if let Some(name) = parsed.profile.as_ref() {
|
if let Some(name) = parsed.profile.as_ref() {
|
||||||
let existing = profiles_mut.profiles.get(name);
|
let existing = profiles_mut.profiles.get(name);
|
||||||
match existing {
|
match existing {
|
||||||
None => {
|
None => {
|
||||||
|
let (mi, pi) = gather_intervals(
|
||||||
|
parsed.metrics_interval_ms,
|
||||||
|
parsed.processes_interval_ms,
|
||||||
|
)?;
|
||||||
profiles_mut.profiles.insert(
|
profiles_mut.profiles.insert(
|
||||||
name.clone(),
|
name.clone(),
|
||||||
ProfileEntry {
|
ProfileEntry {
|
||||||
url: u.clone(),
|
url: u.clone(),
|
||||||
tls_ca: t.clone(),
|
tls_ca: t.clone(),
|
||||||
|
metrics_interval_ms: mi,
|
||||||
|
processes_interval_ms: pi,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let _ = save_profiles(&profiles_mut);
|
let _ = save_profiles(&profiles_mut);
|
||||||
|
(u, t, mi, pi)
|
||||||
}
|
}
|
||||||
Some(entry) => {
|
Some(entry) => {
|
||||||
let changed = entry.url != u || entry.tls_ca != t;
|
let changed = entry.url != u || entry.tls_ca != t;
|
||||||
@ -130,22 +164,45 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
))
|
))
|
||||||
};
|
};
|
||||||
if overwrite {
|
if overwrite {
|
||||||
|
let (mi, pi) = gather_intervals(
|
||||||
|
parsed.metrics_interval_ms,
|
||||||
|
parsed.processes_interval_ms,
|
||||||
|
)?;
|
||||||
profiles_mut.profiles.insert(
|
profiles_mut.profiles.insert(
|
||||||
name.clone(),
|
name.clone(),
|
||||||
ProfileEntry {
|
ProfileEntry {
|
||||||
url: u.clone(),
|
url: u.clone(),
|
||||||
tls_ca: t.clone(),
|
tls_ca: t.clone(),
|
||||||
|
metrics_interval_ms: mi,
|
||||||
|
processes_interval_ms: pi,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let _ = save_profiles(&profiles_mut);
|
let _ = save_profiles(&profiles_mut);
|
||||||
|
(u, t, mi, pi)
|
||||||
|
} else {
|
||||||
|
(u, t, entry.metrics_interval_ms, entry.processes_interval_ms)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
(u, t, entry.metrics_interval_ms, entry.processes_interval_ms)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
u,
|
||||||
|
t,
|
||||||
|
parsed.metrics_interval_ms,
|
||||||
|
parsed.processes_interval_ms,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
(u, t)
|
|
||||||
}
|
}
|
||||||
ResolveProfile::Loaded(u, t) => (u, t),
|
ResolveProfile::Loaded(u, t) => {
|
||||||
|
let entry = profiles_mut
|
||||||
|
.profiles
|
||||||
|
.get(parsed.profile.as_ref().unwrap())
|
||||||
|
.unwrap();
|
||||||
|
(u, t, entry.metrics_interval_ms, entry.processes_interval_ms)
|
||||||
|
}
|
||||||
ResolveProfile::PromptSelect(mut names) => {
|
ResolveProfile::PromptSelect(mut names) => {
|
||||||
if !names.iter().any(|n| n == "demo") {
|
if !names.iter().any(|n| n == "demo") {
|
||||||
names.push("demo".into());
|
names.push("demo".into());
|
||||||
@ -165,7 +222,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
return run_demo_mode(parsed.tls_ca.as_deref()).await;
|
return run_demo_mode(parsed.tls_ca.as_deref()).await;
|
||||||
}
|
}
|
||||||
if let Some(entry) = profiles_mut.profiles.get(name) {
|
if let Some(entry) = profiles_mut.profiles.get(name) {
|
||||||
(entry.url.clone(), entry.tls_ca.clone())
|
(
|
||||||
|
entry.url.clone(),
|
||||||
|
entry.tls_ca.clone(),
|
||||||
|
entry.metrics_interval_ms,
|
||||||
|
entry.processes_interval_ms,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@ -191,22 +253,30 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
} else {
|
} else {
|
||||||
Some(ca.trim().to_string())
|
Some(ca.trim().to_string())
|
||||||
};
|
};
|
||||||
|
let (mi, pi) =
|
||||||
|
gather_intervals(parsed.metrics_interval_ms, parsed.processes_interval_ms)?;
|
||||||
profiles_mut.profiles.insert(
|
profiles_mut.profiles.insert(
|
||||||
name.clone(),
|
name.clone(),
|
||||||
ProfileEntry {
|
ProfileEntry {
|
||||||
url: url.trim().to_string(),
|
url: url.trim().to_string(),
|
||||||
tls_ca: ca_opt.clone(),
|
tls_ca: ca_opt.clone(),
|
||||||
|
metrics_interval_ms: mi,
|
||||||
|
processes_interval_ms: pi,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let _ = save_profiles(&profiles_mut);
|
let _ = save_profiles(&profiles_mut);
|
||||||
(url.trim().to_string(), ca_opt)
|
(url.trim().to_string(), ca_opt, mi, pi)
|
||||||
}
|
}
|
||||||
ResolveProfile::None => {
|
ResolveProfile::None => {
|
||||||
eprintln!("No URL provided and no profiles to select.");
|
eprintln!("No URL provided and no profiles to select.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut app = App::new();
|
let is_tls = url.starts_with("wss://");
|
||||||
|
let has_token = url.contains("token=");
|
||||||
|
let mut app = App::new()
|
||||||
|
.with_intervals(metrics_interval_ms, processes_interval_ms)
|
||||||
|
.with_status(is_tls, has_token);
|
||||||
if parsed.dry_run {
|
if parsed.dry_run {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@ -231,6 +301,43 @@ fn prompt_string(prompt: &str) -> io::Result<String> {
|
|||||||
Ok(line)
|
Ok(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn gather_intervals(
|
||||||
|
arg_metrics: Option<u64>,
|
||||||
|
arg_procs: Option<u64>,
|
||||||
|
) -> Result<(Option<u64>, Option<u64>), Box<dyn std::error::Error>> {
|
||||||
|
let default_metrics = 500u64;
|
||||||
|
let default_procs = 2000u64;
|
||||||
|
let metrics = match arg_metrics {
|
||||||
|
Some(v) => Some(v),
|
||||||
|
None => {
|
||||||
|
let inp = prompt_string(&format!(
|
||||||
|
"Metrics interval ms (default {default_metrics}, Enter for default): "
|
||||||
|
))?;
|
||||||
|
let t = inp.trim();
|
||||||
|
if t.is_empty() {
|
||||||
|
Some(default_metrics)
|
||||||
|
} else {
|
||||||
|
Some(t.parse()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let procs = match arg_procs {
|
||||||
|
Some(v) => Some(v),
|
||||||
|
None => {
|
||||||
|
let inp = prompt_string(&format!(
|
||||||
|
"Processes interval ms (default {default_procs}, Enter for default): "
|
||||||
|
))?;
|
||||||
|
let t = inp.trim();
|
||||||
|
if t.is_empty() {
|
||||||
|
Some(default_procs)
|
||||||
|
} else {
|
||||||
|
Some(t.parse()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok((metrics, procs))
|
||||||
|
}
|
||||||
|
|
||||||
// Demo mode implementation
|
// Demo mode implementation
|
||||||
async fn run_demo_mode(_tls_ca: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
|
async fn run_demo_mode(_tls_ca: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let port = 3231;
|
let port = 3231;
|
||||||
|
|||||||
@ -9,6 +9,10 @@ pub struct ProfileEntry {
|
|||||||
pub url: String,
|
pub url: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub tls_ca: Option<String>,
|
pub tls_ca: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub metrics_interval_ms: Option<u64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub processes_interval_ms: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
|||||||
@ -5,9 +5,18 @@ use ratatui::{
|
|||||||
layout::Rect,
|
layout::Rect,
|
||||||
widgets::{Block, Borders},
|
widgets::{Block, Borders},
|
||||||
};
|
};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
pub fn draw_header(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>) {
|
pub fn draw_header(
|
||||||
let title = if let Some(mm) = m {
|
f: &mut ratatui::Frame<'_>,
|
||||||
|
area: Rect,
|
||||||
|
m: Option<&Metrics>,
|
||||||
|
is_tls: bool,
|
||||||
|
has_token: bool,
|
||||||
|
metrics_interval: Duration,
|
||||||
|
procs_interval: Duration,
|
||||||
|
) {
|
||||||
|
let base = if let Some(mm) = m {
|
||||||
let temp = mm
|
let temp = mm
|
||||||
.cpu_temp_c
|
.cpu_temp_c
|
||||||
.map(|t| {
|
.map(|t| {
|
||||||
@ -21,12 +30,23 @@ pub fn draw_header(f: &mut ratatui::Frame<'_>, area: Rect, m: Option<&Metrics>)
|
|||||||
format!("CPU Temp: {t:.1}°C {icon}")
|
format!("CPU Temp: {t:.1}°C {icon}")
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| "CPU Temp: N/A".into());
|
.unwrap_or_else(|| "CPU Temp: N/A".into());
|
||||||
format!(
|
format!("socktop — host: {} | {}", mm.hostname, temp)
|
||||||
"socktop — host: {} | {} (press 'q' to quit)",
|
|
||||||
mm.hostname, temp
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
"socktop — connecting... (press 'q' to quit)".into()
|
"socktop — connecting...".into()
|
||||||
};
|
};
|
||||||
|
// TLS indicator: lock vs lock with cross (using ✗). Keep explicit label for clarity.
|
||||||
|
let tls_txt = if is_tls { "🔒 TLS" } else { "🔒✗ TLS" };
|
||||||
|
// Token indicator
|
||||||
|
let tok_txt = if has_token { "🔑 token" } else { "" };
|
||||||
|
let mi = metrics_interval.as_millis();
|
||||||
|
let pi = procs_interval.as_millis();
|
||||||
|
let intervals = format!("⏱ {mi}ms metrics | {pi}ms procs");
|
||||||
|
let mut parts = vec![base, tls_txt.into()];
|
||||||
|
if !tok_txt.is_empty() {
|
||||||
|
parts.push(tok_txt.into());
|
||||||
|
}
|
||||||
|
parts.push(intervals);
|
||||||
|
parts.push("(q to quit)".into());
|
||||||
|
let title = parts.join(" | ");
|
||||||
f.render_widget(Block::default().title(title).borders(Borders::BOTTOM), area);
|
f.render_widget(Block::default().title(title).borders(Borders::BOTTOM), area);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user