use super::*;
pub(crate) const SPINNER_FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
pub(crate) const ASCII_SPINNER: &[&str] = &["|", "/", "-", "\\"];
pub(crate) fn rounded_block(theme: &Theme, active: bool) -> Block<'static> {
let color = if active {
theme.border_active
} else {
theme.border_idle
};
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(color))
}
pub(crate) fn titled_block(
theme: &Theme,
raw_title: &str,
active: bool,
accent: Color,
) -> Block<'static> {
let trimmed = raw_title.trim();
let decorated = match theme.icons {
IconStyle::Ascii => format!("[ {trimmed} ]"),
IconStyle::Powerline => format!(" {trimmed} "),
IconStyle::Unicode => format!("[ ◆ {trimmed} ◆ ]"),
};
rounded_block(theme, active).title(Span::styled(
decorated,
Style::default().fg(accent).add_modifier(Modifier::BOLD),
))
}
pub(crate) fn pill(text: &str, fg: Color, bg: Color) -> Span<'static> {
Span::styled(
format!(" {text} "),
Style::default().fg(fg).bg(bg).add_modifier(Modifier::BOLD),
)
}
pub(crate) fn pending_glyph(theme: &Theme) -> &'static str {
match theme.icons {
IconStyle::Ascii => "* ",
_ => "⏳ ",
}
}
pub(crate) fn glyph<'a>(icons: IconStyle, unicode: &'a str, ascii: &'a str) -> &'a str {
match icons {
IconStyle::Ascii => ascii,
_ => unicode,
}
}
pub(crate) fn multi_select_glyph(theme: &Theme) -> &'static str {
match theme.icons {
IconStyle::Ascii => "+ ",
_ => "▶ ",
}
}
pub(crate) fn incident_glyph(theme: &Theme) -> &'static str {
match theme.icons {
IconStyle::Ascii => "!! ",
_ => "🚨 ",
}
}
pub(crate) fn health_dot(health: &str, theme: &Theme) -> Span<'static> {
let c = health_color(health, theme);
let glyph = match theme.icons {
IconStyle::Ascii => "*",
IconStyle::Powerline => "\u{f111}",
IconStyle::Unicode => "●",
};
Span::styled(glyph, Style::default().fg(c).add_modifier(Modifier::BOLD))
}
pub(crate) fn spinner(elapsed_ms: u128, icons: IconStyle) -> &'static str {
match icons {
IconStyle::Unicode | IconStyle::Powerline => {
SPINNER_FRAMES[(elapsed_ms / 100) as usize % SPINNER_FRAMES.len()]
}
IconStyle::Ascii => ASCII_SPINNER[(elapsed_ms / 100) as usize % ASCII_SPINNER.len()],
}
}
pub(crate) fn tab_icon(t: DetailTab, icons: IconStyle) -> &'static str {
match (icons, t) {
(IconStyle::Unicode, DetailTab::Health) => "♥",
(IconStyle::Unicode, DetailTab::Events) => "⚡",
(IconStyle::Unicode, DetailTab::Instances) => "▣",
(IconStyle::Unicode, DetailTab::Metrics) => "▆",
(IconStyle::Unicode, DetailTab::Queue) => "✉",
(IconStyle::Unicode, DetailTab::Logs) => "≣",
(IconStyle::Unicode, DetailTab::Config) => "⚙",
(IconStyle::Powerline, DetailTab::Health) => "\u{f02d1}", (IconStyle::Powerline, DetailTab::Events) => "\u{f0e7}", (IconStyle::Powerline, DetailTab::Instances) => "\u{f048b}", (IconStyle::Powerline, DetailTab::Metrics) => "\u{f0680}", (IconStyle::Powerline, DetailTab::Queue) => "\u{f01ee}", (IconStyle::Powerline, DetailTab::Logs) => "\u{f021a}", (IconStyle::Powerline, DetailTab::Config) => "\u{f0493}", (IconStyle::Ascii, DetailTab::Health) => "H",
(IconStyle::Ascii, DetailTab::Events) => "E",
(IconStyle::Ascii, DetailTab::Instances) => "I",
(IconStyle::Ascii, DetailTab::Metrics) => "M",
(IconStyle::Ascii, DetailTab::Queue) => "Q",
(IconStyle::Ascii, DetailTab::Logs) => "L",
(IconStyle::Ascii, DetailTab::Config) => "C",
}
}
pub(crate) fn micro_bar(value: i64, max: i64, width: usize) -> String {
if max <= 0 || width == 0 || value < 0 {
return String::new();
}
let frac = (value as f64 / max as f64).clamp(0.0, 1.0);
let total_eighths = (frac * (width as f64) * 8.0).round() as usize;
let full = total_eighths / 8;
let rem = total_eighths % 8;
let mut out = String::new();
for _ in 0..full.min(width) {
out.push('█');
}
if full < width && rem > 0 {
out.push(match rem {
1 => '▏',
2 => '▎',
3 => '▍',
4 => '▌',
5 => '▋',
6 => '▊',
7 => '▉',
_ => ' ',
});
}
out
}
pub(crate) const SPARKLINE_WIDTH: usize = 10;
pub(crate) const DIVIDER_FILL_WIDTH: usize = 200;
pub fn visible_window(cursor: usize, total: usize, budget: usize) -> (usize, usize) {
if total == 0 {
return (0, 0);
}
let budget = budget.max(1).min(total);
if total <= budget {
return (0, total);
}
let half = budget / 2;
let start = cursor.saturating_sub(half);
let start = start.min(total - budget);
(start, start + budget)
}
pub(crate) fn truncate_for_display(s: &str, max: usize) -> String {
if s.chars().count() <= max {
return s.to_string();
}
let prefix: String = s.chars().take(max.saturating_sub(1)).collect();
format!("{prefix}…")
}
pub(crate) fn pad_right(s: &str, width: usize) -> String {
let n = s.chars().count();
if n >= width {
s.to_string()
} else {
let mut out = String::with_capacity(width);
out.push_str(s);
for _ in 0..(width - n) {
out.push(' ');
}
out
}
}
pub(crate) fn humanize_duration(secs: u64) -> String {
if secs < 60 {
format!("{secs}s")
} else if secs < 3600 {
format!("{}m", secs / 60)
} else if secs < 86_400 {
format!("{}h{}m", secs / 3600, (secs % 3600) / 60)
} else {
format!("{}d{}h", secs / 86_400, (secs % 86_400) / 3600)
}
}
pub(crate) fn kv<'a>(key: &'a str, value: &'a str, theme: &Theme) -> Vec<Span<'a>> {
vec![
Span::styled(format!("{key}: "), Style::default().fg(theme.muted)),
Span::styled(
value.to_string(),
Style::default().fg(theme.text).add_modifier(Modifier::BOLD),
),
]
}
pub(crate) fn sep(theme: &Theme) -> Span<'static> {
let glyph = if theme.icons == IconStyle::Powerline {
" \u{e0b1} "
} else {
" • "
};
Span::styled(glyph, Style::default().fg(theme.muted))
}
pub(crate) fn ieq_any(s: &str, options: &[&str]) -> bool {
options.iter().any(|o| s.eq_ignore_ascii_case(o))
}
pub(crate) fn cursor_marker(theme: &Theme) -> &'static str {
if theme.icons == IconStyle::Powerline {
"\u{e0b0} "
} else {
"▌ "
}
}
pub(crate) fn caret_glyph(theme: &Theme) -> &'static str {
if theme.icons == IconStyle::Ascii {
"_"
} else {
"\u{258e}"
}
}
pub(crate) fn input_caret_spans(
text: &str,
cursor_col: usize,
text_style: Style,
caret_style: Style,
theme: &Theme,
) -> Vec<Span<'static>> {
let byte = text
.char_indices()
.nth(cursor_col)
.map(|(i, _)| i)
.unwrap_or(text.len());
let (before, after) = text.split_at(byte);
vec![
Span::styled(before.to_string(), text_style),
Span::styled(caret_glyph(theme), caret_style),
Span::styled(after.to_string(), text_style),
]
}
pub(crate) fn separator_glyph(icons: IconStyle) -> &'static str {
match icons {
IconStyle::Ascii => ">",
_ => "▶",
}
}
pub(crate) fn warn_glyph(icons: IconStyle) -> &'static str {
match icons {
IconStyle::Ascii => "! ",
_ => "⚠ ",
}
}
pub(crate) fn hint_glyph(icons: IconStyle) -> &'static str {
match icons {
IconStyle::Ascii => "? ",
_ => "💡 ",
}
}
pub(crate) fn stale_glyph(icons: IconStyle) -> &'static str {
match icons {
IconStyle::Ascii => "^",
_ => "↑",
}
}
pub(crate) fn stripe_glyph(icons: IconStyle) -> &'static str {
match icons {
IconStyle::Ascii => "|",
_ => "▎",
}
}
pub(crate) fn sparkline_for(
samples: Option<&std::collections::VecDeque<String>>,
theme: &Theme,
pulse_last: bool,
) -> Line<'static> {
let Some(samples) = samples else {
return Line::from(Span::raw(" ".repeat(SPARKLINE_WIDTH)));
};
let pad = SPARKLINE_WIDTH.saturating_sub(samples.len());
let mut spans: Vec<Span<'static>> = Vec::with_capacity(SPARKLINE_WIDTH);
if pad > 0 {
spans.push(Span::raw(" ".repeat(pad)));
}
let start = samples.len().saturating_sub(SPARKLINE_WIDTH);
let visible: Vec<&String> = samples.iter().skip(start).collect();
let visible_len = visible.len();
for (i, h) in visible.iter().enumerate() {
let color = health_color(h, theme);
let darker = scale_rgb(color, 0.6);
let style = Style::default().fg(color).bg(darker);
let (glyph, style) = if pulse_last && i + 1 == visible_len {
(
"█",
style.add_modifier(Modifier::BOLD | Modifier::SLOW_BLINK),
)
} else {
("▇", style)
};
spans.push(Span::styled(glyph, style));
}
Line::from(spans)
}
pub(crate) fn scale_rgb(color: Color, factor: f32) -> Color {
let factor = factor.clamp(0.0, 1.0);
if let Color::Rgb(r, g, b) = color {
Color::Rgb(
(r as f32 * factor) as u8,
(g as f32 * factor) as u8,
(b as f32 * factor) as u8,
)
} else {
color
}
}
pub(crate) fn health_style(health: &str, theme: &Theme) -> Style {
Style::default()
.fg(health_color(health, theme))
.add_modifier(Modifier::BOLD)
}
pub(crate) fn health_color(health: &str, theme: &Theme) -> Color {
if ieq_any(health, &["green", "ok"]) {
theme.health_green
} else if ieq_any(health, &["yellow", "warning"]) {
theme.health_yellow
} else if ieq_any(health, &["red", "severe", "degraded"]) {
theme.health_red
} else if ieq_any(health, &["grey", "gray", "info", "no data", "pending"]) {
theme.health_grey
} else {
theme.text
}
}
pub(crate) fn redact(value: &str, on: bool) -> String {
if !on || value.is_empty() || value == "—" {
return value.to_string();
}
"▓".repeat(value.chars().count())
}