Testing
This commit is contained in:
parent
c3dbaeead2
commit
bd6c14c8c1
@ -449,7 +449,7 @@ impl ServiceCollector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_service_description_throttled(&self, service: &str) -> Option<String> {
|
async fn get_service_description_throttled(&self, service: &str) -> Option<Vec<String>> {
|
||||||
// Simple time-based throttling - only run expensive descriptions every ~30 seconds
|
// Simple time-based throttling - only run expensive descriptions every ~30 seconds
|
||||||
// Use a hash of the current time to spread out when different services get described
|
// Use a hash of the current time to spread out when different services get described
|
||||||
let now = std::time::SystemTime::now()
|
let now = std::time::SystemTime::now()
|
||||||
@ -472,14 +472,14 @@ impl ServiceCollector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_service_description(&self, service: &str) -> Option<String> {
|
async fn get_service_description(&self, service: &str) -> Option<Vec<String>> {
|
||||||
match service {
|
match service {
|
||||||
"sshd" | "ssh" => self.get_ssh_active_users().await,
|
"sshd" | "ssh" => self.get_ssh_active_users().await.map(|s| vec![s]),
|
||||||
"nginx" => self.get_web_server_connections().await, // Use same method for now
|
"nginx" => self.get_nginx_sites().await,
|
||||||
"apache2" | "httpd" => self.get_web_server_connections().await,
|
"apache2" | "httpd" => self.get_web_server_connections().await.map(|s| vec![s]),
|
||||||
"docker" => self.get_docker_containers().await,
|
"docker" => self.get_docker_containers().await.map(|s| vec![s]),
|
||||||
"postgresql" | "postgres" => self.get_postgres_connections().await,
|
"postgresql" | "postgres" => self.get_postgres_connections().await.map(|s| vec![s]),
|
||||||
"mysql" | "mariadb" => self.get_mysql_connections().await,
|
"mysql" | "mariadb" => self.get_mysql_connections().await.map(|s| vec![s]),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -620,6 +620,79 @@ impl ServiceCollector {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_nginx_sites(&self) -> Option<Vec<String>> {
|
||||||
|
// Check enabled sites in sites-enabled directory
|
||||||
|
let sites_enabled_dir = "/etc/nginx/sites-enabled";
|
||||||
|
|
||||||
|
let mut entries = match fs::read_dir(sites_enabled_dir).await {
|
||||||
|
Ok(entries) => entries,
|
||||||
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut sites = Vec::new();
|
||||||
|
|
||||||
|
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
// Skip if it's not a file or is a symlink to default
|
||||||
|
if !path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let filename = path.file_name()?.to_string_lossy();
|
||||||
|
|
||||||
|
// Skip default site unless it's the only one
|
||||||
|
if filename == "default" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to extract server names from the config file
|
||||||
|
if let Ok(config_content) = fs::read_to_string(&path).await {
|
||||||
|
let mut server_names = Vec::new();
|
||||||
|
|
||||||
|
for line in config_content.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if trimmed.starts_with("server_name") {
|
||||||
|
// Extract server names from "server_name example.com www.example.com;"
|
||||||
|
if let Some(names_part) = trimmed.strip_prefix("server_name") {
|
||||||
|
let names_clean = names_part.trim().trim_end_matches(';');
|
||||||
|
for name in names_clean.split_whitespace() {
|
||||||
|
if name != "_" && !name.is_empty() {
|
||||||
|
server_names.push(name.to_string());
|
||||||
|
break; // Only take the first valid server name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !server_names.is_empty() {
|
||||||
|
sites.push(server_names[0].clone());
|
||||||
|
} else {
|
||||||
|
// Fallback to filename if no server_name found
|
||||||
|
sites.push(filename.to_string());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to filename if can't read config
|
||||||
|
sites.push(filename.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no sites found, check for default
|
||||||
|
if sites.is_empty() {
|
||||||
|
let default_path = format!("{}/default", sites_enabled_dir);
|
||||||
|
if fs::metadata(&default_path).await.is_ok() {
|
||||||
|
sites.push("default".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sites.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(sites)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@ -755,7 +828,7 @@ struct ServiceData {
|
|||||||
sandbox_limit: Option<f32>,
|
sandbox_limit: Option<f32>,
|
||||||
disk_used_gb: f32,
|
disk_used_gb: f32,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
description: Option<String>,
|
description: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
|||||||
@ -35,6 +35,7 @@ struct HostRuntimeState {
|
|||||||
smart: Option<SmartMetrics>,
|
smart: Option<SmartMetrics>,
|
||||||
services: Option<ServiceMetrics>,
|
services: Option<ServiceMetrics>,
|
||||||
backup: Option<BackupMetrics>,
|
backup: Option<BackupMetrics>,
|
||||||
|
service_description_cache: HashMap<String, Vec<String>>, // service_name -> last_known_descriptions
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Top-level application state container.
|
/// Top-level application state container.
|
||||||
@ -259,7 +260,25 @@ impl App {
|
|||||||
if service_metrics.timestamp != timestamp {
|
if service_metrics.timestamp != timestamp {
|
||||||
service_metrics.timestamp = timestamp;
|
service_metrics.timestamp = timestamp;
|
||||||
}
|
}
|
||||||
let snapshot = service_metrics.clone();
|
let mut snapshot = service_metrics.clone();
|
||||||
|
|
||||||
|
// Update description cache and fill in missing descriptions
|
||||||
|
for service in &mut snapshot.services {
|
||||||
|
// If service has a new description, cache it
|
||||||
|
if let Some(ref description) = service.description {
|
||||||
|
if !description.is_empty() {
|
||||||
|
state.service_description_cache.insert(service.name.clone(), description.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If service has no description but we have a cached one, use it
|
||||||
|
if service.description.is_none() || service.description.as_ref().map_or(true, |d| d.is_empty()) {
|
||||||
|
if let Some(cached_description) = state.service_description_cache.get(&service.name) {
|
||||||
|
service.description = Some(cached_description.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.history.record_services(service_metrics);
|
self.history.record_services(service_metrics);
|
||||||
state.services = Some(snapshot);
|
state.services = Some(snapshot);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,7 +81,7 @@ pub struct ServiceInfo {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub disk_used_gb: f32,
|
pub disk_used_gb: f32,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub description: Option<String>,
|
pub description: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
@ -5,7 +5,7 @@ use ratatui::Frame;
|
|||||||
|
|
||||||
use crate::app::HostDisplayData;
|
use crate::app::HostDisplayData;
|
||||||
use crate::ui::system::{evaluate_performance, PerfSeverity};
|
use crate::ui::system::{evaluate_performance, PerfSeverity};
|
||||||
use crate::ui::widget::{render_widget_data, WidgetData, WidgetStatus, WidgetValue, StatusLevel};
|
use crate::ui::widget::{render_widget_data, WidgetData, WidgetStatus, StatusLevel};
|
||||||
|
|
||||||
pub fn render(frame: &mut Frame, hosts: &[HostDisplayData], area: Rect) {
|
pub fn render(frame: &mut Frame, hosts: &[HostDisplayData], area: Rect) {
|
||||||
let (severity, ok_count, warn_count, fail_count) = classify_hosts(hosts);
|
let (severity, ok_count, warn_count, fail_count) = classify_hosts(hosts);
|
||||||
@ -41,11 +41,11 @@ pub fn render(frame: &mut Frame, hosts: &[HostDisplayData], area: Rect) {
|
|||||||
if hosts.is_empty() {
|
if hosts.is_empty() {
|
||||||
data.add_row(
|
data.add_row(
|
||||||
None,
|
None,
|
||||||
"",
|
vec![],
|
||||||
vec![
|
vec![
|
||||||
WidgetValue::new("No hosts configured"),
|
"No hosts configured".to_string(),
|
||||||
WidgetValue::new(""),
|
"".to_string(),
|
||||||
WidgetValue::new(""),
|
"".to_string(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -63,11 +63,11 @@ pub fn render(frame: &mut Frame, hosts: &[HostDisplayData], area: Rect) {
|
|||||||
|
|
||||||
data.add_row(
|
data.add_row(
|
||||||
Some(WidgetStatus::new(status_level)),
|
Some(WidgetStatus::new(status_level)),
|
||||||
"",
|
vec![],
|
||||||
vec![
|
vec![
|
||||||
WidgetValue::new(host.name.clone()),
|
host.name.clone(),
|
||||||
WidgetValue::new(status_text),
|
status_text,
|
||||||
WidgetValue::new(update),
|
update,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use ratatui::Frame;
|
|||||||
|
|
||||||
use crate::app::HostDisplayData;
|
use crate::app::HostDisplayData;
|
||||||
use crate::data::metrics::{BackupMetrics, BackupStatus};
|
use crate::data::metrics::{BackupMetrics, BackupStatus};
|
||||||
use crate::ui::widget::{render_placeholder, render_widget_data, WidgetData, WidgetStatus, WidgetValue, StatusLevel};
|
use crate::ui::widget::{render_placeholder, render_widget_data, WidgetData, WidgetStatus, StatusLevel};
|
||||||
|
|
||||||
pub fn render(frame: &mut Frame, host: Option<&HostDisplayData>, area: Rect) {
|
pub fn render(frame: &mut Frame, host: Option<&HostDisplayData>, area: Rect) {
|
||||||
match host {
|
match host {
|
||||||
@ -41,36 +41,36 @@ fn render_metrics(frame: &mut Frame, _host: &HostDisplayData, metrics: &BackupMe
|
|||||||
let repo_status = repo_status_level(metrics);
|
let repo_status = repo_status_level(metrics);
|
||||||
data.add_row(
|
data.add_row(
|
||||||
Some(WidgetStatus::new(repo_status)),
|
Some(WidgetStatus::new(repo_status)),
|
||||||
"",
|
vec![],
|
||||||
vec![
|
vec![
|
||||||
WidgetValue::new("Repo"),
|
"Repo".to_string(),
|
||||||
WidgetValue::new(format!(
|
format!(
|
||||||
"Snapshots: {} • Size: {:.1} GiB",
|
"Snapshots: {} • Size: {:.1} GiB",
|
||||||
metrics.backup.snapshot_count, metrics.backup.size_gb
|
metrics.backup.snapshot_count, metrics.backup.size_gb
|
||||||
)),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
let service_status = service_status_level(metrics);
|
let service_status = service_status_level(metrics);
|
||||||
data.add_row(
|
data.add_row(
|
||||||
Some(WidgetStatus::new(service_status)),
|
Some(WidgetStatus::new(service_status)),
|
||||||
"",
|
vec![],
|
||||||
vec![
|
vec![
|
||||||
WidgetValue::new("Service"),
|
"Service".to_string(),
|
||||||
WidgetValue::new(format!(
|
format!(
|
||||||
"Enabled: {} • Pending jobs: {}",
|
"Enabled: {} • Pending jobs: {}",
|
||||||
metrics.service.enabled, metrics.service.pending_jobs
|
metrics.service.enabled, metrics.service.pending_jobs
|
||||||
)),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(last_failure) = metrics.backup.last_failure.as_ref() {
|
if let Some(last_failure) = metrics.backup.last_failure.as_ref() {
|
||||||
data.add_row(
|
data.add_row(
|
||||||
Some(WidgetStatus::new(StatusLevel::Error)),
|
Some(WidgetStatus::new(StatusLevel::Error)),
|
||||||
"",
|
vec![],
|
||||||
vec![
|
vec![
|
||||||
WidgetValue::new("Last failure"),
|
"Last failure".to_string(),
|
||||||
WidgetValue::new(format_timestamp(Some(last_failure))),
|
format_timestamp(Some(last_failure)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -85,10 +85,10 @@ fn render_metrics(frame: &mut Frame, _host: &HostDisplayData, metrics: &BackupMe
|
|||||||
|
|
||||||
data.add_row(
|
data.add_row(
|
||||||
Some(WidgetStatus::new(status_level)),
|
Some(WidgetStatus::new(status_level)),
|
||||||
"",
|
vec![],
|
||||||
vec![
|
vec![
|
||||||
WidgetValue::new("Last message"),
|
"Last message".to_string(),
|
||||||
WidgetValue::new(message.clone()),
|
message.clone(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use ratatui::Frame;
|
|||||||
|
|
||||||
use crate::app::HostDisplayData;
|
use crate::app::HostDisplayData;
|
||||||
use crate::data::metrics::{ServiceStatus, ServiceSummary};
|
use crate::data::metrics::{ServiceStatus, ServiceSummary};
|
||||||
use crate::ui::widget::{render_placeholder, render_widget_data, WidgetData, WidgetStatus, WidgetValue, StatusLevel};
|
use crate::ui::widget::{render_placeholder, render_widget_data, WidgetData, WidgetStatus, StatusLevel};
|
||||||
|
|
||||||
pub fn render(frame: &mut Frame, host: Option<&HostDisplayData>, area: Rect) {
|
pub fn render(frame: &mut Frame, host: Option<&HostDisplayData>, area: Rect) {
|
||||||
match host {
|
match host {
|
||||||
@ -55,11 +55,11 @@ fn render_metrics(
|
|||||||
if metrics.services.is_empty() {
|
if metrics.services.is_empty() {
|
||||||
data.add_row(
|
data.add_row(
|
||||||
None,
|
None,
|
||||||
"",
|
vec![],
|
||||||
vec![
|
vec![
|
||||||
WidgetValue::new("No services reported"),
|
"No services reported".to_string(),
|
||||||
WidgetValue::new(""),
|
"".to_string(),
|
||||||
WidgetValue::new(""),
|
"".to_string(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
render_widget_data(frame, area, data);
|
render_widget_data(frame, area, data);
|
||||||
@ -81,31 +81,22 @@ fn render_metrics(
|
|||||||
ServiceStatus::Stopped => StatusLevel::Error,
|
ServiceStatus::Stopped => StatusLevel::Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Main service row
|
// Service row with optional description(s)
|
||||||
|
let description = if let Some(desc_vec) = &svc.description {
|
||||||
|
desc_vec.clone()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
data.add_row(
|
data.add_row(
|
||||||
Some(WidgetStatus::new(status_level)),
|
Some(WidgetStatus::new(status_level)),
|
||||||
"",
|
description,
|
||||||
vec![
|
vec![
|
||||||
WidgetValue::new(svc.name.clone()),
|
svc.name.clone(),
|
||||||
WidgetValue::new(format_memory_value(svc.memory_used_mb, svc.memory_quota_mb)),
|
format_memory_value(svc.memory_used_mb, svc.memory_quota_mb),
|
||||||
WidgetValue::new(format_disk_value(svc.disk_used_gb)),
|
format_disk_value(svc.disk_used_gb),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Description row (indented) if description exists
|
|
||||||
if let Some(description) = &svc.description {
|
|
||||||
if !description.trim().is_empty() {
|
|
||||||
data.add_row(
|
|
||||||
None,
|
|
||||||
"",
|
|
||||||
vec![
|
|
||||||
WidgetValue::new(format!(" {}", description)),
|
|
||||||
WidgetValue::new(""),
|
|
||||||
WidgetValue::new(""),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render_widget_data(frame, area, data);
|
render_widget_data(frame, area, data);
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use ratatui::Frame;
|
|||||||
|
|
||||||
use crate::app::HostDisplayData;
|
use crate::app::HostDisplayData;
|
||||||
use crate::data::metrics::SmartMetrics;
|
use crate::data::metrics::SmartMetrics;
|
||||||
use crate::ui::widget::{render_placeholder, render_widget_data, WidgetData, WidgetStatus, WidgetValue, StatusLevel};
|
use crate::ui::widget::{render_placeholder, render_widget_data, WidgetData, WidgetStatus, StatusLevel};
|
||||||
|
|
||||||
pub fn render(frame: &mut Frame, host: Option<&HostDisplayData>, area: Rect) {
|
pub fn render(frame: &mut Frame, host: Option<&HostDisplayData>, area: Rect) {
|
||||||
match host {
|
match host {
|
||||||
@ -49,15 +49,15 @@ fn render_metrics(frame: &mut Frame, _host: &HostDisplayData, metrics: &SmartMet
|
|||||||
if metrics.drives.is_empty() {
|
if metrics.drives.is_empty() {
|
||||||
data.add_row(
|
data.add_row(
|
||||||
None,
|
None,
|
||||||
"",
|
vec![],
|
||||||
vec![
|
vec![
|
||||||
WidgetValue::new("No drives reported"),
|
"No drives reported".to_string(),
|
||||||
WidgetValue::new(""),
|
"".to_string(),
|
||||||
WidgetValue::new(""),
|
"".to_string(),
|
||||||
WidgetValue::new(""),
|
"".to_string(),
|
||||||
WidgetValue::new(""),
|
"".to_string(),
|
||||||
WidgetValue::new(""),
|
"".to_string(),
|
||||||
WidgetValue::new(""),
|
"".to_string(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -65,15 +65,15 @@ fn render_metrics(frame: &mut Frame, _host: &HostDisplayData, metrics: &SmartMet
|
|||||||
let status_level = drive_status_level(metrics, &drive.name);
|
let status_level = drive_status_level(metrics, &drive.name);
|
||||||
data.add_row(
|
data.add_row(
|
||||||
Some(WidgetStatus::new(status_level)),
|
Some(WidgetStatus::new(status_level)),
|
||||||
"",
|
vec![],
|
||||||
vec![
|
vec![
|
||||||
WidgetValue::new(drive.name.clone()),
|
drive.name.clone(),
|
||||||
WidgetValue::new(format_temperature(drive.temperature_c)),
|
format_temperature(drive.temperature_c),
|
||||||
WidgetValue::new(format_percent(drive.wear_level)),
|
format_percent(drive.wear_level),
|
||||||
WidgetValue::new(format_percent(drive.available_spare)),
|
format_percent(drive.available_spare),
|
||||||
WidgetValue::new(drive.power_on_hours.to_string()),
|
drive.power_on_hours.to_string(),
|
||||||
WidgetValue::new(format_capacity(drive.capacity_gb)),
|
format_capacity(drive.capacity_gb),
|
||||||
WidgetValue::new(format_usage(drive.used_gb, drive.capacity_gb)),
|
format_usage(drive.used_gb, drive.capacity_gb),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -81,15 +81,15 @@ fn render_metrics(frame: &mut Frame, _host: &HostDisplayData, metrics: &SmartMet
|
|||||||
if let Some(issue) = metrics.issues.first() {
|
if let Some(issue) = metrics.issues.first() {
|
||||||
data.add_row(
|
data.add_row(
|
||||||
Some(WidgetStatus::new(StatusLevel::Warning)),
|
Some(WidgetStatus::new(StatusLevel::Warning)),
|
||||||
"",
|
vec![],
|
||||||
vec![
|
vec![
|
||||||
WidgetValue::new(format!("Issue: {}", issue)),
|
format!("Issue: {}", issue),
|
||||||
WidgetValue::new(""),
|
"".to_string(),
|
||||||
WidgetValue::new(""),
|
"".to_string(),
|
||||||
WidgetValue::new(""),
|
"".to_string(),
|
||||||
WidgetValue::new(""),
|
"".to_string(),
|
||||||
WidgetValue::new(""),
|
"".to_string(),
|
||||||
WidgetValue::new(""),
|
"".to_string(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ use crate::app::HostDisplayData;
|
|||||||
use crate::data::metrics::{ServiceMetrics, ServiceSummary};
|
use crate::data::metrics::{ServiceMetrics, ServiceSummary};
|
||||||
use crate::ui::widget::{
|
use crate::ui::widget::{
|
||||||
combined_color, render_placeholder, render_combined_widget_data, status_color_for_cpu_load, status_color_from_metric,
|
combined_color, render_placeholder, render_combined_widget_data, status_color_for_cpu_load, status_color_from_metric,
|
||||||
status_color_from_percentage, WidgetDataSet, WidgetStatus, WidgetValue, StatusLevel,
|
status_color_from_percentage, WidgetDataSet, WidgetStatus, StatusLevel,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn render(frame: &mut Frame, host: Option<&HostDisplayData>, area: Rect) {
|
pub fn render(frame: &mut Frame, host: Option<&HostDisplayData>, area: Rect) {
|
||||||
@ -77,8 +77,8 @@ fn render_metrics(
|
|||||||
let mut memory_dataset = WidgetDataSet::new(vec!["Memory usage".to_string()], Some(WidgetStatus::new(memory_status)));
|
let mut memory_dataset = WidgetDataSet::new(vec!["Memory usage".to_string()], Some(WidgetStatus::new(memory_status)));
|
||||||
memory_dataset.add_row(
|
memory_dataset.add_row(
|
||||||
Some(WidgetStatus::new(memory_status)),
|
Some(WidgetStatus::new(memory_status)),
|
||||||
"",
|
vec![],
|
||||||
vec![WidgetValue::new(format!("{:.1} / {:.1}", system_used, system_total))],
|
vec![format!("{:.1} / {:.1}", system_used, system_total)],
|
||||||
);
|
);
|
||||||
|
|
||||||
// CPU dataset
|
// CPU dataset
|
||||||
@ -86,11 +86,11 @@ fn render_metrics(
|
|||||||
let mut cpu_dataset = WidgetDataSet::new(vec!["CPU load".to_string(), "CPU temp".to_string(), "CPU freq".to_string()], Some(WidgetStatus::new(cpu_status)));
|
let mut cpu_dataset = WidgetDataSet::new(vec!["CPU load".to_string(), "CPU temp".to_string(), "CPU freq".to_string()], Some(WidgetStatus::new(cpu_status)));
|
||||||
cpu_dataset.add_row(
|
cpu_dataset.add_row(
|
||||||
Some(WidgetStatus::new(cpu_status)),
|
Some(WidgetStatus::new(cpu_status)),
|
||||||
"",
|
vec![],
|
||||||
vec![
|
vec![
|
||||||
WidgetValue::new(format!("{:.2} • {:.2} • {:.2}", summary.cpu_load_1, summary.cpu_load_5, summary.cpu_load_15)),
|
format!("{:.2} • {:.2} • {:.2}", summary.cpu_load_1, summary.cpu_load_5, summary.cpu_load_15),
|
||||||
WidgetValue::new(format_optional_metric(summary.cpu_temp_c, "°C")),
|
format_optional_metric(summary.cpu_temp_c, "°C"),
|
||||||
WidgetValue::new(format_optional_metric(summary.cpu_freq_mhz, " MHz")),
|
format_optional_metric(summary.cpu_freq_mhz, " MHz"),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -99,16 +99,16 @@ fn render_metrics(
|
|||||||
let mut gpu_dataset = WidgetDataSet::new(vec!["GPU load".to_string(), "GPU temp".to_string()], Some(WidgetStatus::new(gpu_status)));
|
let mut gpu_dataset = WidgetDataSet::new(vec!["GPU load".to_string(), "GPU temp".to_string()], Some(WidgetStatus::new(gpu_status)));
|
||||||
gpu_dataset.add_row(
|
gpu_dataset.add_row(
|
||||||
Some(WidgetStatus::new(gpu_status)),
|
Some(WidgetStatus::new(gpu_status)),
|
||||||
"",
|
vec![],
|
||||||
vec![
|
vec![
|
||||||
WidgetValue::new(summary
|
summary
|
||||||
.gpu_load_percent
|
.gpu_load_percent
|
||||||
.map(|value| format_optional_percent(Some(value)))
|
.map(|value| format_optional_percent(Some(value)))
|
||||||
.unwrap_or_else(|| "—".to_string())),
|
.unwrap_or_else(|| "—".to_string()),
|
||||||
WidgetValue::new(summary
|
summary
|
||||||
.gpu_temp_c
|
.gpu_temp_c
|
||||||
.map(|value| format_optional_metric(Some(value), "°C"))
|
.map(|value| format_optional_metric(Some(value), "°C"))
|
||||||
.unwrap_or_else(|| "—".to_string())),
|
.unwrap_or_else(|| "—".to_string()),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -152,7 +152,7 @@ fn dataset_needs_wrapping_with_width(dataset: &WidgetDataSet, available_width: u
|
|||||||
// Check data rows for this column width
|
// Check data rows for this column width
|
||||||
for row in &dataset.rows {
|
for row in &dataset.rows {
|
||||||
if let Some(widget_value) = row.values.get(col_index) {
|
if let Some(widget_value) = row.values.get(col_index) {
|
||||||
let data_width = widget_value.data.chars().count() as u16;
|
let data_width = widget_value.chars().count() as u16;
|
||||||
max_width = max_width.max(data_width);
|
max_width = max_width.max(data_width);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -186,7 +186,7 @@ fn render_dataset_with_wrapping(frame: &mut Frame, dataset: &WidgetDataSet, inne
|
|||||||
// Check data rows for this column width
|
// Check data rows for this column width
|
||||||
for row in &dataset.rows {
|
for row in &dataset.rows {
|
||||||
if let Some(widget_value) = row.values.get(col_index) {
|
if let Some(widget_value) = row.values.get(col_index) {
|
||||||
let data_width = widget_value.data.chars().count() as u16;
|
let data_width = widget_value.chars().count() as u16;
|
||||||
max_width = max_width.max(data_width);
|
max_width = max_width.max(data_width);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -293,8 +293,7 @@ fn render_dataset_with_wrapping(frame: &mut Frame, dataset: &WidgetDataSet, inne
|
|||||||
|
|
||||||
// Data cells for this section
|
// Data cells for this section
|
||||||
for col_idx in col_start..col_end {
|
for col_idx in col_start..col_end {
|
||||||
if let Some(widget_value) = row.values.get(col_idx) {
|
if let Some(content) = row.values.get(col_idx) {
|
||||||
let content = &widget_value.data;
|
|
||||||
if content.is_empty() {
|
if content.is_empty() {
|
||||||
cells.push(Cell::from(""));
|
cells.push(Cell::from(""));
|
||||||
} else {
|
} else {
|
||||||
@ -321,6 +320,33 @@ fn render_dataset_with_wrapping(frame: &mut Frame, dataset: &WidgetDataSet, inne
|
|||||||
height: 1,
|
height: 1,
|
||||||
});
|
});
|
||||||
current_y += 1;
|
current_y += 1;
|
||||||
|
|
||||||
|
// Render description rows if any exist
|
||||||
|
for description in &row.description {
|
||||||
|
if current_y >= inner.y + inner.height {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render description as a single cell spanning the entire width
|
||||||
|
let desc_cell = Cell::from(Line::from(vec![Span::styled(
|
||||||
|
format!(" {}", description),
|
||||||
|
Style::default().fg(Color::Blue),
|
||||||
|
)]));
|
||||||
|
|
||||||
|
let desc_row = Row::new(vec![desc_cell]);
|
||||||
|
let desc_constraints = vec![Constraint::Length(inner.width)];
|
||||||
|
let desc_table = Table::new(vec![desc_row])
|
||||||
|
.widths(&desc_constraints)
|
||||||
|
.style(neutral_text_style());
|
||||||
|
|
||||||
|
frame.render_widget(desc_table, Rect {
|
||||||
|
x: inner.x,
|
||||||
|
y: current_y,
|
||||||
|
width: inner.width,
|
||||||
|
height: 1,
|
||||||
|
});
|
||||||
|
current_y += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
col_start = col_end;
|
col_start = col_end;
|
||||||
@ -349,12 +375,8 @@ pub struct WidgetDataSet {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct WidgetRow {
|
pub struct WidgetRow {
|
||||||
pub status: Option<WidgetStatus>,
|
pub status: Option<WidgetStatus>,
|
||||||
pub values: Vec<WidgetValue>,
|
pub values: Vec<String>,
|
||||||
}
|
pub description: Vec<String>,
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct WidgetValue {
|
|
||||||
pub data: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
@ -383,10 +405,11 @@ impl WidgetData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_row(&mut self, status: Option<WidgetStatus>, _description: impl Into<String>, values: Vec<WidgetValue>) -> &mut Self {
|
pub fn add_row(&mut self, status: Option<WidgetStatus>, description: Vec<String>, values: Vec<String>) -> &mut Self {
|
||||||
self.dataset.rows.push(WidgetRow {
|
self.dataset.rows.push(WidgetRow {
|
||||||
status,
|
status,
|
||||||
values,
|
values,
|
||||||
|
description,
|
||||||
});
|
});
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@ -401,22 +424,16 @@ impl WidgetDataSet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_row(&mut self, status: Option<WidgetStatus>, _description: impl Into<String>, values: Vec<WidgetValue>) -> &mut Self {
|
pub fn add_row(&mut self, status: Option<WidgetStatus>, description: Vec<String>, values: Vec<String>) -> &mut Self {
|
||||||
self.rows.push(WidgetRow {
|
self.rows.push(WidgetRow {
|
||||||
status,
|
status,
|
||||||
values,
|
values,
|
||||||
|
description,
|
||||||
});
|
});
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WidgetValue {
|
|
||||||
pub fn new(data: impl Into<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
data: data.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetStatus {
|
impl WidgetStatus {
|
||||||
pub fn new(status: StatusLevel) -> Self {
|
pub fn new(status: StatusLevel) -> Self {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user