use scrin::core::buffer::Cell;
use scrin::interaction::{HitRegion, MouseCursor, WidgetAction, WidgetRole};
use scrin::layout::{Constraint, Direction, Layout};
use scrin::widgets::{
Clear, Widget,
block::{Block, BorderStyle},
};
use scrin::{Color, Frame, Rect};
use scrin_widgets::{AislingPalette, Paragraph, StatusBar, StreamPanel, Table};
use crate::app::{ActivePane, AnomalyRow, App, DetailTab, Page, ThemeMode};
use crate::daemon::{self, Daemon};
pub const SERVICES_ID: &str = "dae:services";
pub const ANOMALIES_ID: &str = "dae:anomalies";
pub const DETAIL_PREFIX: &str = "dae:detail";
pub const INSPECTOR_ID: &str = "dae:pane:inspector";
const JOURNAL_ID: &str = "dae:detail:journal";
const STATUS_ID: &str = "dae:detail:status";
const EVENTS_ID: &str = "dae:detail:events";
pub fn render(frame: &mut Frame<'_>, app: &mut App) {
let palette = palette_for(app);
let area = frame.area();
frame.render_widget_timed("clear", Clear::with_bg(root_bg()), area);
frame.mark_dirty(area);
if area.width < 64 || area.height < 18 {
render_tiny(frame, area, app, palette);
return;
}
let root = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(10), Constraint::Length(1)])
.split(area);
let body = if root[0].width < 110 {
Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(54), Constraint::Percentage(46)])
.split(root[0])
} else {
Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(62), Constraint::Percentage(38)])
.split(root[0])
};
match app.page {
Page::Services => render_service_table(frame, body[0], app, palette),
Page::Anomalies => render_anomaly_table(frame, body[0], app, palette),
}
render_detail(frame, body[1], app, palette);
render_hover_overlay(frame, area, app, palette);
render_command_overlay(frame, area, app, palette);
render_status(frame, root[1], app, palette);
}
pub fn service_row_from_id(id: &str) -> Option<usize> {
id.strip_prefix("dae:services:row:")?.parse().ok()
}
pub fn anomaly_row_from_id(id: &str) -> Option<usize> {
id.strip_prefix("dae:anomalies:row:")?.parse().ok()
}
pub fn pane_from_id(id: &str) -> Option<ActivePane> {
if id == SERVICES_ID || id.starts_with("dae:services:") {
Some(ActivePane::Services)
} else if id == ANOMALIES_ID || id.starts_with("dae:anomalies:") {
Some(ActivePane::Services)
} else if id == INSPECTOR_ID {
Some(ActivePane::Inspector)
} else if id.starts_with(DETAIL_PREFIX) {
Some(ActivePane::Detail)
} else {
None
}
}
fn palette_for(app: &App) -> AislingPalette {
match app.theme_mode {
ThemeMode::Auto | ThemeMode::Cypher => knott_aev_palette(),
ThemeMode::Dream => knott_studio_palette(),
ThemeMode::Phosphor => knott_sage_palette(),
ThemeMode::Flare => knott_threat_palette(),
}
}
fn knott_aev_palette() -> AislingPalette {
AislingPalette {
low: Color::rgb(54, 65, 80),
mid: Color::rgb(126, 138, 155),
high: Color::rgb(226, 232, 240),
pulse: Color::rgb(96, 128, 190),
shadow: panel_bg(),
}
}
fn knott_studio_palette() -> AislingPalette {
AislingPalette {
low: Color::rgb(63, 68, 84),
mid: Color::rgb(130, 136, 154),
high: Color::rgb(229, 232, 240),
pulse: Color::rgb(111, 130, 183),
shadow: Color::rgb(26, 29, 39),
}
}
fn knott_sage_palette() -> AislingPalette {
AislingPalette {
low: Color::rgb(65, 83, 72),
mid: Color::rgb(135, 148, 132),
high: Color::rgb(221, 226, 216),
pulse: Color::rgb(126, 155, 126),
shadow: Color::rgb(29, 38, 33),
}
}
fn knott_threat_palette() -> AislingPalette {
AislingPalette {
low: Color::rgb(80, 61, 65),
mid: Color::rgb(152, 132, 136),
high: Color::rgb(226, 232, 240),
pulse: threat_red(),
shadow: panel_bg(),
}
}
fn root_bg() -> Color {
Color::rgb(5, 7, 12)
}
fn panel_bg() -> Color {
Color::rgb(10, 14, 22)
}
fn quiet_track() -> Color {
Color::rgb(14, 19, 29)
}
fn success_green() -> Color {
Color::rgb(63, 185, 80)
}
fn warning_gold() -> Color {
Color::rgb(255, 178, 72)
}
fn threat_red() -> Color {
Color::rgb(248, 81, 73)
}
fn pane_block<'a>(
app: &App,
pane: ActivePane,
title: &'a str,
palette: AislingPalette,
) -> Block<'a> {
let block = Block::new(title)
.with_borders(BorderStyle::Plain)
.with_border_color(if app.active_pane == pane {
palette.pulse
} else {
palette.low
})
.with_bg(palette.shadow);
if app.active_pane == pane {
block.with_title_right("active")
} else {
block
}
}
fn register_pane(frame: &mut Frame<'_>, id: &'static str, area: Rect, pane: ActivePane) {
frame.register_hit_region(
HitRegion::new(id, area)
.with_role(WidgetRole::Pane)
.with_label(pane.label())
.with_action(WidgetAction::Focus)
.with_cursor(MouseCursor::Pointer)
.with_z_index(-10),
);
}
fn render_scrollbar(
frame: &mut Frame<'_>,
area: Rect,
total_rows: usize,
viewport_rows: usize,
top_offset: usize,
palette: AislingPalette,
active: bool,
) {
if area.width == 0 || area.height < 2 || total_rows <= viewport_rows.max(1) {
return;
}
let track_height = usize::from(area.height);
let max_offset = total_rows.saturating_sub(viewport_rows.max(1)).max(1);
let thumb_len = ((viewport_rows.max(1) as f64 / total_rows.max(1) as f64) * track_height as f64)
.round()
.clamp(1.0, track_height as f64) as usize;
let travel = track_height.saturating_sub(thumb_len);
let thumb_pos = if travel == 0 {
0
} else {
((top_offset.min(max_offset) as f64 / max_offset as f64) * travel as f64)
.round()
.min(travel as f64) as usize
};
let x = area.right().saturating_sub(1);
let thumb = if active { palette.pulse } else { palette.low };
for row in 0..track_height {
let y = area.y.saturating_add(row as u16);
let in_thumb = row >= thumb_pos && row < thumb_pos.saturating_add(thumb_len);
let cell = if in_thumb {
Cell::new(' ', thumb, Some(thumb)).with_bold(active)
} else {
Cell::new(' ', palette.low, Some(quiet_track()))
};
frame.buffer().set(usize::from(x), usize::from(y), cell);
}
}
fn render_tiny(frame: &mut Frame<'_>, area: Rect, app: &App, palette: AislingPalette) {
let text = format!(
"dae needs at least 64x18. current: {}x{}\nservices: {} anomalies: {}\nq quits",
area.width,
area.height,
app.daemons.len(),
app.anomaly_count()
);
frame.render_widget_timed(
"tiny",
Paragraph::new(text).palette(palette).block(
Block::new("dae")
.with_borders(BorderStyle::Plain)
.with_border_color(if app.anomaly_count() > 0 {
threat_red()
} else {
palette.low
})
.with_bg(palette.shadow),
),
area,
);
frame.mark_dirty(area);
}
fn render_service_table(frame: &mut Frame<'_>, area: Rect, app: &mut App, palette: AislingPalette) {
let table_rows = area.height.saturating_sub(4).max(1);
app.table_rows_hint = table_rows;
app.ensure_selected_visible();
fill(frame, area, palette.shadow);
frame.mark_dirty(area);
let rows = app
.visible_indices()
.into_iter()
.filter_map(|idx| app.daemons.get(idx))
.map(service_row)
.collect::<Vec<_>>();
let unit_w = (area.width.saturating_mul(34) / 100).clamp(18, 44);
let state_w = (area.width.saturating_mul(18) / 100).clamp(12, 22);
let pid_w = 7;
let mem_w = 10;
let rst_w = 5;
let used = unit_w
.saturating_add(state_w)
.saturating_add(pid_w)
.saturating_add(mem_w)
.saturating_add(rst_w);
let anom_w = area.width.saturating_sub(used).max(12);
Table::new(["unit", "state", "pid", "mem", "rst", "signal"])
.rows(rows)
.widths(vec![unit_w, state_w, pid_w, mem_w, rst_w, anom_w])
.selected(Some(app.selected))
.scroll_offset(app.table_scroll)
.palette(palette)
.block(pane_block(app, ActivePane::Services, "services", palette))
.render_with_interaction(frame, SERVICES_ID, area);
register_pane(frame, SERVICES_ID, area, ActivePane::Services);
render_scrollbar(
frame,
area.inner(Rect::new(1, 2, 1, 1)),
app.visible_count(),
usize::from(app.table_rows_hint.max(1)),
usize::from(app.table_scroll),
palette,
app.active_pane == ActivePane::Services,
);
frame.mark_dirty(area);
}
fn render_anomaly_table(frame: &mut Frame<'_>, area: Rect, app: &mut App, palette: AislingPalette) {
let table_rows = area.height.saturating_sub(4).max(1);
app.anomaly_rows_hint = table_rows;
app.ensure_selected_visible();
fill(frame, area, palette.shadow);
frame.mark_dirty(area);
let anomalies = app.anomaly_rows();
if anomalies.is_empty() {
render_anomaly_empty(frame, area, app, palette);
return;
}
let rows = anomalies.iter().map(anomaly_row).collect::<Vec<_>>();
let sev_w: u16 = 5;
let unit_w = (area.width.saturating_mul(30) / 100).clamp(18, 38);
let state_w = (area.width.saturating_mul(16) / 100).clamp(10, 18);
let signal_w = (area.width.saturating_mul(16) / 100).clamp(10, 18);
let used = sev_w
.saturating_add(unit_w)
.saturating_add(state_w)
.saturating_add(signal_w);
let detail_w = area.width.saturating_sub(used).max(18);
let title = anomaly_title(&anomalies);
Table::new(["sev", "unit", "state", "signal", "detail"])
.rows(rows)
.widths(vec![sev_w, unit_w, state_w, signal_w, detail_w])
.selected(Some(app.anomaly_selected))
.scroll_offset(app.anomaly_scroll)
.palette(palette)
.block(pane_block(app, ActivePane::Services, &title, palette))
.render_with_interaction(frame, ANOMALIES_ID, area);
register_pane(frame, ANOMALIES_ID, area, ActivePane::Services);
render_scrollbar(
frame,
area.inner(Rect::new(1, 2, 1, 1)),
anomalies.len(),
usize::from(app.anomaly_rows_hint.max(1)),
usize::from(app.anomaly_scroll),
palette,
app.active_pane == ActivePane::Services,
);
frame.mark_dirty(area);
}
fn render_anomaly_empty(frame: &mut Frame<'_>, area: Rect, app: &App, palette: AislingPalette) {
let text = format!(
"No anomaly signals in the current fleet view.\n\nservices scanned: {}\nfilter: {}\n\nR refresh S services Ctrl+P run",
app.daemons.len(),
if app.filter.is_empty() {
"all".to_string()
} else {
app.filter.clone()
}
);
Paragraph::new(text)
.palette(palette)
.block(pane_block(
app,
ActivePane::Services,
"anomalies: clear",
palette,
))
.render(frame.buffer(), area);
register_pane(frame, ANOMALIES_ID, area, ActivePane::Services);
frame.mark_dirty(area);
}
fn anomaly_title(rows: &[AnomalyRow]) -> String {
let critical = rows.iter().filter(|row| row.severity >= 80).count();
let elevated = rows
.iter()
.filter(|row| (50..80).contains(&row.severity))
.count();
let watch = rows.iter().filter(|row| row.severity < 50).count();
format!(
"anomalies {} | critical {} elevated {} watch {}",
rows.len(),
critical,
elevated,
watch
)
}
fn render_detail(frame: &mut Frame<'_>, area: Rect, app: &mut App, palette: AislingPalette) {
let selected_height = if area.height >= 24 { 12 } else { 10 };
let parts = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(selected_height), Constraint::Min(8)])
.split(area);
render_selected_panel(frame, parts[0], app, palette);
let detail_id = match app.detail_tab {
DetailTab::Journal => JOURNAL_ID,
DetailTab::Status => STATUS_ID,
DetailTab::Events => EVENTS_ID,
};
let title = match app.detail_tab {
DetailTab::Journal => "journal",
DetailTab::Status => "status",
DetailTab::Events => "events",
};
app.detail_rows_hint = parts[1].height.saturating_sub(2).max(1);
StreamPanel::new()
.lines(app.detail_lines().iter().cloned())
.show_line_numbers(true)
.follow_tail(false)
.scroll_offset(app.detail_scroll)
.palette(palette)
.block(pane_block(app, ActivePane::Detail, title, palette))
.render_with_interaction(frame, detail_id, parts[1]);
let total = app.detail_lines().len();
let viewport = usize::from(app.detail_rows_hint.max(1));
let max_top = total.saturating_sub(viewport);
let top_offset = max_top.saturating_sub(usize::from(app.detail_scroll));
render_scrollbar(
frame,
parts[1].inner(Rect::new(1, 1, 1, 1)),
total,
viewport,
top_offset,
palette,
app.active_pane == ActivePane::Detail,
);
frame.mark_dirty(parts[1]);
}
fn render_selected_panel(
frame: &mut Frame<'_>,
area: Rect,
app: &mut App,
palette: AislingPalette,
) {
let Some(daemon) = app.selected_daemon().cloned() else {
frame.render_widget_timed(
"empty detail",
Paragraph::new("no daemon selected")
.palette(palette)
.block(pane_block(app, ActivePane::Inspector, "inspector", palette)),
area,
);
frame.mark_dirty(area);
return;
};
let text = inspector_text(&daemon);
let total = text.lines().count();
app.inspector_total_hint = total;
app.inspector_rows_hint = area.height.saturating_sub(2).max(1);
if area.width >= 72 && area.height >= 9 {
let parts = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(70), Constraint::Percentage(30)])
.split(area);
Paragraph::new(text)
.scroll_offset(app.inspector_scroll)
.palette(palette)
.block(pane_block(app, ActivePane::Inspector, "inspector", palette))
.render(frame.buffer(), parts[0]);
register_pane(frame, INSPECTOR_ID, parts[0], ActivePane::Inspector);
render_scrollbar(
frame,
parts[0].inner(Rect::new(1, 1, 1, 1)),
total,
usize::from(app.inspector_rows_hint.max(1)),
usize::from(app.inspector_scroll),
palette,
app.active_pane == ActivePane::Inspector,
);
frame.mark_dirty(parts[0]);
render_threat_panel(frame, parts[1], app, &daemon, palette);
} else {
Paragraph::new(text)
.scroll_offset(app.inspector_scroll)
.palette(palette)
.block(pane_block(app, ActivePane::Inspector, "inspector", palette))
.render(frame.buffer(), area);
register_pane(frame, INSPECTOR_ID, area, ActivePane::Inspector);
frame.mark_dirty(area);
}
}
fn render_threat_panel(
frame: &mut Frame<'_>,
area: Rect,
app: &App,
daemon: &Daemon,
palette: AislingPalette,
) {
let severity = daemon.severity();
let threat = threat_color(severity);
fill(frame, area, palette.shadow);
let block = Block::new("threat")
.with_borders(BorderStyle::Plain)
.with_border_color(if severity > 0 { threat } else { palette.low })
.with_bg(palette.shadow);
block.render(frame.buffer(), area);
let inner = block.inner(area);
if inner.width == 0 || inner.height == 0 {
frame.mark_dirty(area);
return;
}
draw_line(
frame,
inner.x,
inner.y,
inner.width,
threat_label(severity),
threat,
Some(palette.shadow),
);
if inner.height > 2 {
draw_threat_meter(
frame,
inner.x,
inner.y.saturating_add(2),
inner.width,
severity,
threat,
palette,
);
}
if inner.height > 4 {
draw_line(
frame,
inner.x,
inner.y.saturating_add(4),
inner.width,
&format!("severity {:>3}/100", severity),
palette.mid,
Some(palette.shadow),
);
}
if inner.height > 5 {
draw_line(
frame,
inner.x,
inner.y.saturating_add(5),
inner.width,
&format!("unit {}", daemon.state_label()),
palette.mid,
Some(palette.shadow),
);
}
if inner.height > 6 {
let signal = if daemon.anomalies.is_empty() {
"signals clear".to_string()
} else {
format!("signals {}", daemon.anomaly_summary())
};
draw_line(
frame,
inner.x,
inner.y.saturating_add(6),
inner.width,
&signal,
if severity > 0 { threat } else { palette.mid },
Some(palette.shadow),
);
}
if inner.height > 8 {
draw_line(
frame,
inner.x,
inner.y.saturating_add(8),
inner.width,
&format!("fleet flagged {}", app.anomaly_count()),
palette.mid,
Some(palette.shadow),
);
}
if inner.height > 10 {
draw_line(
frame,
inner.x,
inner.bottom().saturating_sub(1),
inner.width,
"Ctrl+P run A anomalies",
palette.low,
Some(palette.shadow),
);
}
frame.mark_dirty(area);
}
fn threat_label(severity: u8) -> &'static str {
match severity {
80..=u8::MAX => "critical",
50..=79 => "elevated",
1..=49 => "watch",
0 => "stable",
}
}
fn threat_color(severity: u8) -> Color {
match severity {
80..=u8::MAX => threat_red(),
50..=79 => warning_gold(),
1..=49 => Color::rgb(255, 184, 77),
0 => success_green(),
}
}
fn draw_threat_meter(
frame: &mut Frame<'_>,
x: u16,
y: u16,
width: u16,
severity: u8,
color: Color,
palette: AislingPalette,
) {
if width < 8 {
return;
}
let bar_width = width.min(24).saturating_sub(5);
let filled = usize::from(bar_width) * usize::from(severity) / 100;
for col in 0..bar_width {
let is_filled = usize::from(col) < filled;
let bg = if is_filled { color } else { quiet_track() };
frame.buffer().set(
usize::from(x.saturating_add(col)),
usize::from(y),
Cell::new(' ', palette.high, Some(bg)),
);
}
frame.buffer().set_str(
usize::from(x.saturating_add(bar_width).saturating_add(1)),
usize::from(y),
&format!("{:>3}", severity),
palette.mid,
Some(palette.shadow),
);
}
fn render_hover_overlay(frame: &mut Frame<'_>, root: Rect, app: &App, palette: AislingPalette) {
let Some(row) = app.hovered_row else {
return;
};
let Some(daemon) = app.visible_daemon_at(row) else {
return;
};
let Some((mx, my)) = app.mouse_position else {
return;
};
let width = root.width.min(52).max(38);
let height = 10.min(root.height.saturating_sub(2)).max(8);
let x = if mx.saturating_add(width).saturating_add(2) >= root.right() {
root.right().saturating_sub(width).saturating_sub(1)
} else {
mx.saturating_add(2)
};
let y = if my.saturating_add(height).saturating_add(1) >= root.bottom() {
root.bottom().saturating_sub(height).saturating_sub(1)
} else {
my.saturating_add(1)
};
let area = Rect::new(x, y, width, height);
fill(frame, area, palette.shadow);
let block = Block::new("commands")
.with_borders(BorderStyle::Plain)
.with_border_color(if daemon.severity() > 0 {
threat_color(daemon.severity())
} else {
palette.low
})
.with_bg(palette.shadow);
block.render(frame.buffer(), area);
let inner = block.inner(area);
draw_line(
frame,
inner.x,
inner.y,
inner.width,
&daemon.unit,
palette.high,
Some(palette.shadow),
);
draw_line(
frame,
inner.x,
inner.y.saturating_add(1),
inner.width,
&format!(
"{} | pid {} | {}",
daemon.state_label(),
daemon
.main_pid
.map(|pid| pid.to_string())
.unwrap_or_else(|| "-".to_string()),
daemon.anomaly_summary()
),
palette.mid,
Some(palette.shadow),
);
let commands = [
"r restart s start x stop",
"l reload t TERM K KILL",
"e enable d disable v detail",
"A anomalies Ctrl+P run Wheel/Pg scroll",
];
for (idx, line) in commands.iter().enumerate() {
draw_line(
frame,
inner.x,
inner.y.saturating_add(3 + idx as u16),
inner.width,
line,
if idx == 0 { palette.high } else { palette.mid },
Some(palette.shadow),
);
}
draw_line(
frame,
inner.x,
inner.bottom().saturating_sub(1),
inner.width,
"select row, then press a command key",
palette.low,
Some(palette.shadow),
);
frame.mark_dirty(area);
}
fn render_command_overlay(frame: &mut Frame<'_>, root: Rect, app: &App, palette: AislingPalette) {
if !app.command_overlay_open {
return;
}
let area = centered_rect(root, 78, 24);
fill(frame, area, root_bg());
let block = Block::new("run")
.with_borders(BorderStyle::Plain)
.with_border_color(palette.pulse)
.with_bg(palette.shadow)
.with_title_right("Ctrl+P / F1");
block.render(frame.buffer(), area);
let inner = block.inner(area);
if inner.width == 0 || inner.height == 0 {
frame.mark_dirty(area);
return;
}
let prompt = if app.command_overlay_filter_mode {
format!("search: {}", app.command_overlay_filter)
} else if app.command_overlay_filter.is_empty() {
"press key, / filters, Enter runs selected, Esc closes".to_string()
} else {
format!("filter: {}", app.command_overlay_filter)
};
draw_line(
frame,
inner.x,
inner.y,
inner.width,
&prompt,
if app.command_overlay_filter_mode {
palette.high
} else {
palette.mid
},
Some(palette.shadow),
);
let items = app.filtered_command_items();
let list_y = inner.y.saturating_add(2);
let list_height = inner.height.saturating_sub(3) as usize;
let selected = app
.command_overlay_selected
.min(items.len().saturating_sub(1));
let offset = selected.saturating_sub(list_height.saturating_sub(1));
if items.is_empty() {
draw_line(
frame,
inner.x,
list_y,
inner.width,
"no commands match",
palette.low,
Some(palette.shadow),
);
}
for (visible_idx, item) in items.iter().skip(offset).take(list_height).enumerate() {
let item_idx = offset + visible_idx;
let y = list_y.saturating_add(visible_idx as u16);
let selected_row = item_idx == selected;
let bg = if selected_row {
Some(quiet_track())
} else {
Some(palette.shadow)
};
let fg = if selected_row {
palette.high
} else {
palette.mid
};
let line = format!("[{}] {:<18} {}", item.key, item.title, item.detail);
draw_line(frame, inner.x, y, inner.width, &line, fg, bg);
}
if inner.height > 3 {
draw_line(
frame,
inner.x,
inner.bottom().saturating_sub(1),
inner.width,
"/ search A anomalies S services ? close",
palette.low,
Some(palette.shadow),
);
}
frame.mark_dirty(area);
}
fn render_status(frame: &mut Frame<'_>, area: Rect, app: &App, palette: AislingPalette) {
let filter = if app.filter.is_empty() {
"filter: all".to_string()
} else {
format!("filter: {}", app.filter)
};
let left = match app.page {
Page::Services => format!(
"dae | services {} / {} | {}{}",
app.visible_service_count(),
app.daemons.len(),
filter,
if app.anomalies_only {
" | flagged-only"
} else {
""
}
),
Page::Anomalies => format!(
"dae | anomalies {} / {} | services {} | {}",
app.visible_count(),
app.total_anomaly_count(),
app.daemons.len(),
filter
),
};
let center = format!(
"page {} | pane {} | tab {} | Ctrl+P run | F1 keys",
app.page.label(),
app.active_pane.label(),
app.detail_tab.label(),
);
let right = clip(&app.message, usize::from(area.width / 3));
frame.render_widget_timed(
"status",
StatusBar::new()
.left(left)
.center(center)
.right(right)
.palette(palette),
area,
);
frame.mark_dirty(area);
}
fn service_row(daemon: &Daemon) -> Vec<String> {
vec![
daemon.unit.clone(),
daemon.state_label(),
daemon
.main_pid
.map(|pid| pid.to_string())
.unwrap_or_else(|| "-".to_string()),
daemon::format_bytes(daemon.memory_bytes),
daemon
.restarts
.map(|value| value.to_string())
.unwrap_or_else(|| "-".to_string()),
daemon.anomaly_summary(),
]
}
fn anomaly_row(row: &AnomalyRow) -> Vec<String> {
vec![
row.severity.to_string(),
row.unit.clone(),
row.state.clone(),
row.label.clone(),
row.detail.clone(),
]
}
fn inspector_text(daemon: &Daemon) -> String {
let pid = daemon
.main_pid
.map(|pid| pid.to_string())
.unwrap_or_else(|| "-".to_string());
let restarts = daemon
.restarts
.map(|value| value.to_string())
.unwrap_or_else(|| "-".to_string());
let exec_status = daemon
.exec_status
.map(|value| value.to_string())
.unwrap_or_else(|| "-".to_string());
let mut lines = vec![
daemon.unit.clone(),
daemon.description.clone(),
String::new(),
format!("state: {}", daemon.state_label()),
format!(
"load: {} result: {}",
daemon.load,
blank_dash(&daemon.result)
),
format!("pid: {pid} restarts: {restarts} exec: {exec_status}"),
format!(
"memory: {} cpu: {}",
daemon::format_bytes(daemon.memory_bytes),
daemon::format_cpu(daemon.cpu_nsec)
),
format!("unit file: {}", blank_dash(&daemon.unit_file_state)),
format!("active since: {}", blank_dash(&daemon.active_enter)),
format!("fragment: {}", blank_dash(&daemon.fragment_path)),
String::new(),
"anomalies:".to_string(),
];
if daemon.anomalies.is_empty() {
lines.push("ok".to_string());
} else {
for anomaly in &daemon.anomalies {
lines.push(format!(
"{} [{}]: {}",
anomaly.label, anomaly.severity, anomaly.detail
));
}
}
lines.extend([
String::new(),
"controls:".to_string(),
"Tab focus pane, A anomaly page, S services page, Ctrl+P/F1 command overlay".to_string(),
"v journal/status/events, wheel/PgUp/PgDn scroll active pane".to_string(),
"s start, x stop, r restart, l reload, t TERM, K KILL, e enable, d disable".to_string(),
]);
lines.join("\n")
}
fn blank_dash(value: &str) -> &str {
if value.is_empty() { "-" } else { value }
}
fn fill(frame: &mut Frame<'_>, area: Rect, color: Color) {
let buffer = frame.buffer();
for y in area.y..area.bottom() {
for x in area.x..area.right() {
buffer.set(
usize::from(x),
usize::from(y),
Cell::new(' ', color, Some(color)),
);
}
}
}
fn centered_rect(root: Rect, max_width: u16, max_height: u16) -> Rect {
let width = root.width.saturating_sub(4).min(max_width).max(1);
let height = root.height.saturating_sub(4).min(max_height).max(1);
let x = root.x.saturating_add(root.width.saturating_sub(width) / 2);
let y = root
.y
.saturating_add(root.height.saturating_sub(height) / 2);
Rect::new(x, y, width, height)
}
fn draw_line(
frame: &mut Frame<'_>,
x: u16,
y: u16,
width: u16,
text: &str,
fg: Color,
bg: Option<Color>,
) {
frame.buffer().set_str(
usize::from(x),
usize::from(y),
&clip(text, usize::from(width)),
fg,
bg,
);
}
fn clip(text: &str, max: usize) -> String {
if max == 0 {
return String::new();
}
let mut out = String::new();
for ch in text.chars().take(max) {
out.push(ch);
}
out
}