pub mod detail;
pub mod graphs;
pub mod ports;
pub mod processes;
use crate::core::pane::PaneShared;
use crate::core::theme;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{List, ListItem, ListState, Paragraph};
use ratatui::{layout::Rect, Frame};
use std::collections::HashSet;
pub const NO_ACCESS: &str = "(no access)";
pub fn dim() -> Style {
Style::default().fg(Color::DarkGray)
}
pub fn load_style(pct: f32) -> Style {
if pct >= 80.0 {
Style::default().fg(Color::Red)
} else if pct >= 50.0 {
Style::default().fg(Color::Yellow)
} else {
Style::default().fg(Color::Green)
}
}
pub fn area_chart(samples: &[f32], rows: usize, width: usize, max: f32) -> Vec<Line<'static>> {
area_chart_with(samples, rows, width, max, load_style)
}
pub fn area_chart_plain(
samples: &[f32],
rows: usize,
width: usize,
max: f32,
style: Style,
) -> Vec<Line<'static>> {
area_chart_with(samples, rows, width, max, move |_| style)
}
fn area_chart_with(
samples: &[f32],
rows: usize,
width: usize,
max: f32,
style_of: impl Fn(f32) -> Style,
) -> Vec<Line<'static>> {
const LEVELS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
if rows == 0 || width == 0 {
return vec![];
}
let max = if max > 0.0 { max } else { 1.0 };
let take = samples.len().min(width);
let slice = &samples[samples.len() - take..];
let cols: Vec<(usize, Style)> = slice
.iter()
.map(|&v| {
let frac = (f64::from(v) / f64::from(max)).clamp(0.0, 1.0);
let mut eighths = (frac * (rows * 8) as f64).round() as usize;
if v > 0.0 {
eighths = eighths.max(1);
}
(eighths, style_of(frac as f32 * 100.0))
})
.collect();
(0..rows)
.map(|row| {
let below = (rows - 1 - row) * 8;
let mut spans = Vec::new();
if take < width {
spans.push(Span::raw(" ".repeat(width - take)));
}
let mut run = String::new();
let mut run_style = Style::default();
for &(eighths, style) in &cols {
let fill = eighths.saturating_sub(below).min(8);
let (ch, st) = if fill == 0 {
(' ', Style::default())
} else {
(LEVELS[fill - 1], style)
};
if st != run_style && !run.is_empty() {
spans.push(Span::styled(std::mem::take(&mut run), run_style));
}
run_style = st;
run.push(ch);
}
if !run.is_empty() {
spans.push(Span::styled(run, run_style));
}
Line::from(spans)
})
.collect()
}
pub fn truncate_chars(s: &str, max: usize) -> String {
if s.chars().count() <= max {
return s.to_string();
}
if max == 0 {
return String::new();
}
let mut out: String = s.chars().take(max - 1).collect();
out.push('…');
out
}
#[allow(clippy::too_many_arguments)]
pub fn render_table_pane(
f: &mut Frame,
area: Rect,
shared: &PaneShared,
pane_id: usize,
title: &str,
header: &str,
selected: Option<usize>,
emphasized: bool,
empty: Option<&str>,
build_items: impl FnOnce(&HashSet<usize>, Option<usize>) -> Vec<ListItem<'static>>,
) {
let block = theme::pane_block(title, shared.focused_pane == pane_id);
if let Some(message) = empty {
theme::render_empty_list(f, area, block, message);
return;
}
let inner = block.inner(area);
f.render_widget(block, area);
if inner.height == 0 || inner.width == 0 {
return;
}
let header_area = Rect { height: 1, ..inner };
f.render_widget(
Paragraph::new(Line::from(Span::styled(header.to_string(), dim()))),
header_area,
);
let list_area = Rect {
y: inner.y + 1,
height: inner.height - 1,
..inner
};
if list_area.height == 0 {
return;
}
let (match_set, current_match_idx) = theme::list_search_highlights(shared, pane_id);
let items = build_items(&match_set, current_match_idx);
let highlight = if emphasized {
theme::list_highlight_style(selected.is_some_and(|idx| match_set.contains(&idx)))
} else {
Style::default().add_modifier(Modifier::BOLD)
};
let list = List::new(items).highlight_style(highlight);
let mut state = ListState::default();
state.select(selected);
f.render_stateful_widget(list, list_area, &mut state);
}
#[cfg(test)]
mod tests {
use super::*;
fn row_text(line: &Line<'_>) -> String {
line.spans.iter().map(|s| s.content.as_ref()).collect()
}
#[test]
fn area_chart_fills_columns_bottom_up() {
let lines = area_chart(&[0.0, 25.0, 50.0, 75.0, 100.0], 2, 5, 100.0);
assert_eq!(lines.len(), 2);
assert_eq!(row_text(&lines[0]), " ▄█"); assert_eq!(row_text(&lines[1]), " ▄███"); }
#[test]
fn area_chart_right_aligns_and_keeps_newest() {
let lines = area_chart(&[100.0], 1, 4, 100.0);
assert_eq!(row_text(&lines[0]), " █");
let lines = area_chart(&[100.0, 0.0, 25.0], 1, 2, 100.0);
assert_eq!(row_text(&lines[0]), " ▂");
let lines = area_chart(&[], 1, 3, 100.0);
assert_eq!(row_text(&lines[0]), " ");
}
#[test]
fn area_chart_rounds_eighths_and_shows_tiny_loads() {
let lines = area_chart(&[6.25, 50.0, 100.0, 1.0, 0.0], 1, 5, 100.0);
assert_eq!(row_text(&lines[0]), "▁▄█▁ ");
}
#[test]
fn area_chart_colors_columns_by_load() {
let lines = area_chart(&[10.0, 60.0, 90.0], 1, 3, 100.0);
let spans = &lines[0].spans;
assert_eq!(spans.len(), 3);
assert_eq!(spans[0].style.fg, Some(Color::Green));
assert_eq!(spans[1].style.fg, Some(Color::Yellow));
assert_eq!(spans[2].style.fg, Some(Color::Red));
assert_eq!(load_style(49.9).fg, Some(Color::Green));
assert_eq!(load_style(50.0).fg, Some(Color::Yellow));
assert_eq!(load_style(79.9).fg, Some(Color::Yellow));
assert_eq!(load_style(80.0).fg, Some(Color::Red));
}
#[test]
fn area_chart_scales_to_max_and_handles_degenerate_sizes() {
let lines = area_chart(&[100.0, 200.0, 300.0], 1, 3, 200.0);
assert_eq!(row_text(&lines[0]), "▄██"); assert_eq!(lines[0].spans[0].style.fg, Some(Color::Yellow));
assert_eq!(lines[0].spans[1].style.fg, Some(Color::Red));
assert!(area_chart(&[1.0], 0, 3, 1.0).is_empty());
assert!(area_chart(&[1.0], 1, 0, 1.0).is_empty());
assert_eq!(row_text(&area_chart(&[0.5], 1, 1, 0.0)[0]), "▄");
}
#[test]
fn area_chart_plain_uses_one_style() {
let style = Style::default().fg(Color::Cyan);
let lines = area_chart_plain(&[10.0, 90.0], 1, 2, 100.0, style);
assert_eq!(lines[0].spans.len(), 1); assert_eq!(lines[0].spans[0].style.fg, Some(Color::Cyan));
assert_eq!(row_text(&lines[0]), "▁▇");
}
#[test]
fn truncate_marks_the_cut() {
assert_eq!(truncate_chars("abc", 5), "abc");
assert_eq!(truncate_chars("abcdef", 4), "abc…");
assert_eq!(truncate_chars("abcdef", 0), "");
assert_eq!(truncate_chars("日本語テキスト", 4), "日本語…");
}
}