pub use crossterm;
pub use ratatui;
pub mod terminal;
use ratatui::{
layout::{Alignment, Constraint, Layout, Rect},
style::{Color, Modifier, Style, Stylize},
text::{Line, Span},
widgets::{Block, BorderType, Borders, Paragraph, Tabs},
Frame,
};
pub const SPINNER_FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
pub struct Theme {
pub primary: Color,
pub secondary: Color,
pub accent: Color,
pub success: Color,
pub warning: Color,
pub error: Color,
pub bg: Color,
pub fg: Color,
pub highlight: Color,
pub inactive: Color,
}
impl Default for Theme {
fn default() -> Self {
Self {
primary: Color::Cyan,
secondary: Color::Blue,
accent: Color::Magenta,
success: Color::Green,
warning: Color::Yellow,
error: Color::Red,
bg: Color::Black,
fg: Color::White,
highlight: Color::Rgb(50, 50, 50),
inactive: Color::DarkGray,
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn draw_header(
frame: &mut Frame,
area: Rect,
title: &str,
status: &str,
status_color: Color,
pid: Option<i32>,
url: &str,
theme: &Theme,
) {
let pid_info = pid.map_or_else(|| "PID: ?".to_string(), |p| format!("PID: {p}"));
let header_content = Line::from(vec![
Span::styled(
format!(" 🔬 {} ", title.to_uppercase()),
Style::default()
.fg(theme.primary)
.add_modifier(Modifier::BOLD),
),
Span::raw(" │ "),
Span::styled(status, Style::default().fg(status_color)),
Span::raw(" │ "),
Span::styled(pid_info, Style::default().fg(theme.accent)),
Span::raw(" │ ").fg(theme.inactive),
Span::styled(
url,
Style::default()
.fg(theme.secondary)
.add_modifier(Modifier::ITALIC),
),
]);
let header = Paragraph::new(header_content).block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.primary)),
);
frame.render_widget(header, area);
}
pub fn draw_footer(frame: &mut Frame, area: Rect, keys: &[(&str, &str)], theme: &Theme) {
let mut spans = Vec::with_capacity(keys.len() * 2);
for (k, v) in keys {
spans.push(Span::styled(
format!(" {k} "),
Style::default()
.fg(theme.bg)
.bg(theme.primary)
.add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(
format!(" {v} "),
Style::default().fg(theme.fg),
));
spans.push(Span::raw(" "));
}
let footer = Paragraph::new(Line::from(spans)).block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.primary)),
);
frame.render_widget(footer, area);
}
pub fn draw_tabs(frame: &mut Frame, area: Rect, titles: Vec<&str>, selected: usize) {
let theme = Theme::default();
let tabs = Tabs::new(titles)
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded),
)
.select(selected)
.style(Style::default().fg(theme.primary))
.highlight_style(Style::default().fg(theme.warning).bold().underlined());
frame.render_widget(tabs, area);
}
#[allow(clippy::too_many_arguments)]
pub fn draw_popup(
frame: &mut Frame,
area: Rect,
title: &str,
lines: &[Line],
percent_x: u16,
percent_y: u16,
theme: &Theme,
) {
let popup_area = centered_rect(percent_x, percent_y, area);
frame.render_widget(
Block::default().style(Style::default().bg(theme.bg)),
popup_area,
);
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.primary))
.title(format!(" {title} "))
.style(Style::default().bg(theme.bg));
let inner = block.inner(popup_area);
frame.render_widget(block, popup_area);
let paragraph = Paragraph::new(lines.to_vec())
.alignment(Alignment::Left)
.style(Style::default().fg(theme.fg));
frame.render_widget(paragraph, inner);
}
#[must_use]
pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::vertical([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
Layout::horizontal([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn format_bytes(bytes: u64) -> String {
if bytes < 1024 {
format!("{bytes} B")
} else if bytes < 1024 * 1024 {
format!("{:.1} KiB", bytes as f64 / 1024.0)
} else if bytes < 1024 * 1024 * 1024 {
format!("{:.1} MiB", bytes as f64 / (1024.0 * 1024.0))
} else {
format!("{:.2} GiB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
}
}
#[must_use]
pub fn format_duration(seconds: u64) -> String {
let days = seconds / 86400;
let hours = (seconds % 86400) / 3600;
let minutes = (seconds % 3600) / 60;
let secs = seconds % 60;
if days > 0 {
format!("{days}d {hours}h {minutes}m")
} else if hours > 0 {
format!("{hours}h {minutes}m {secs}s")
} else if minutes > 0 {
format!("{minutes}m {secs}s")
} else {
format!("{secs}s")
}
}