use ratatui::{
Frame,
buffer::Buffer,
style::{Color, Modifier, Style},
};
use std::time::Duration;
use crate::app::{App, mount_col_width, right_col_width};
use crate::helpers::{format_bytes, format_latency, format_rate, format_uptime, truncate_mount};
use crate::system::{chrono_now, dedup_disk_totals, get_battery, get_local_ip, get_tty, get_username};
use crate::types::*;
pub fn palette(mode: ColorMode) -> (Color, Color, Color, Color, Color, Color) {
match mode {
ColorMode::Default => (
Color::Indexed(27),
Color::Indexed(48),
Color::Indexed(135),
Color::Indexed(141),
Color::Indexed(63),
Color::Indexed(99),
),
ColorMode::Green => (
Color::Indexed(28),
Color::Indexed(46),
Color::Indexed(34),
Color::Indexed(40),
Color::Indexed(22),
Color::Indexed(35),
),
ColorMode::Blue => (
Color::Indexed(19),
Color::Indexed(39),
Color::Indexed(25),
Color::Indexed(33),
Color::Indexed(21),
Color::Indexed(32),
),
ColorMode::Purple => (
Color::Indexed(91),
Color::Indexed(177),
Color::Indexed(128),
Color::Indexed(134),
Color::Indexed(93),
Color::Indexed(97),
),
ColorMode::Amber => (
Color::Indexed(172),
Color::Indexed(214),
Color::Indexed(178),
Color::Indexed(220),
Color::Indexed(166),
Color::Indexed(130),
),
ColorMode::Cyan => (
Color::Indexed(37),
Color::Indexed(50),
Color::Indexed(44),
Color::Indexed(87),
Color::Indexed(30),
Color::Indexed(23),
),
ColorMode::Red => (
Color::Indexed(160),
Color::Indexed(203),
Color::Indexed(196),
Color::Indexed(210),
Color::Indexed(124),
Color::Indexed(88),
),
ColorMode::Sakura => (
Color::Indexed(175),
Color::Indexed(218),
Color::Indexed(182),
Color::Indexed(225),
Color::Indexed(169),
Color::Indexed(132),
),
ColorMode::Matrix => (
Color::Indexed(22),
Color::Indexed(46),
Color::Indexed(28),
Color::Indexed(119),
Color::Indexed(34),
Color::Indexed(22),
),
ColorMode::Sunset => (
Color::Indexed(202),
Color::Indexed(220),
Color::Indexed(196),
Color::Indexed(213),
Color::Indexed(160),
Color::Indexed(125),
),
ColorMode::NeonNoir => (
Color::Indexed(201), Color::Indexed(231), Color::Indexed(93), Color::Indexed(219), Color::Indexed(57), Color::Indexed(53), ),
ColorMode::ChromeHeart => (
Color::Indexed(250), Color::Indexed(255), Color::Indexed(246), Color::Indexed(253), Color::Indexed(243), Color::Indexed(239), ),
ColorMode::BladeRunner => (
Color::Indexed(208), Color::Indexed(37), Color::Indexed(166), Color::Indexed(73), Color::Indexed(130), Color::Indexed(23), ),
ColorMode::VoidWalker => (
Color::Indexed(55), Color::Indexed(99), Color::Indexed(54), Color::Indexed(141), Color::Indexed(92), Color::Indexed(17), ),
ColorMode::ToxicWaste => (
Color::Indexed(118), Color::Indexed(190), Color::Indexed(154), Color::Indexed(226), Color::Indexed(82), Color::Indexed(58), ),
ColorMode::CyberFrost => (
Color::Indexed(159), Color::Indexed(195), Color::Indexed(153), Color::Indexed(189), Color::Indexed(111), Color::Indexed(67), ),
ColorMode::PlasmaCore => (
Color::Indexed(199), Color::Indexed(213), Color::Indexed(163), Color::Indexed(207), Color::Indexed(126), Color::Indexed(89), ),
ColorMode::SteelNerve => (
Color::Indexed(68), Color::Indexed(110), Color::Indexed(60), Color::Indexed(146), Color::Indexed(24), Color::Indexed(236), ),
ColorMode::DarkSignal => (
Color::Indexed(30), Color::Indexed(43), Color::Indexed(23), Color::Indexed(79), Color::Indexed(29), Color::Indexed(16), ),
ColorMode::GlitchPop => (
Color::Indexed(201), Color::Indexed(51), Color::Indexed(226), Color::Indexed(47), Color::Indexed(196), Color::Indexed(21), ),
ColorMode::HoloShift => (
Color::Indexed(123), Color::Indexed(219), Color::Indexed(159), Color::Indexed(183), Color::Indexed(87), Color::Indexed(133), ),
ColorMode::NightCity => (
Color::Indexed(214), Color::Indexed(227), Color::Indexed(209), Color::Indexed(223), Color::Indexed(172), Color::Indexed(94), ),
ColorMode::DeepNet => (
Color::Indexed(19), Color::Indexed(33), Color::Indexed(17), Color::Indexed(75), Color::Indexed(26), Color::Indexed(16), ),
ColorMode::LaserGrid => (
Color::Indexed(46), Color::Indexed(201), Color::Indexed(51), Color::Indexed(226), Color::Indexed(196), Color::Indexed(21), ),
ColorMode::QuantumFlux => (
Color::Indexed(135), Color::Indexed(75), Color::Indexed(171), Color::Indexed(111), Color::Indexed(98), Color::Indexed(61), ),
ColorMode::BioHazard => (
Color::Indexed(148), Color::Indexed(184), Color::Indexed(106), Color::Indexed(192), Color::Indexed(64), Color::Indexed(22), ),
ColorMode::Darkwave => (
Color::Indexed(53), Color::Indexed(140), Color::Indexed(89), Color::Indexed(176), Color::Indexed(127), Color::Indexed(52), ),
ColorMode::Overlock => (
Color::Indexed(196), Color::Indexed(208), Color::Indexed(160), Color::Indexed(214), Color::Indexed(124), Color::Indexed(52), ),
ColorMode::Megacorp => (
Color::Indexed(252), Color::Indexed(39), Color::Indexed(245), Color::Indexed(81), Color::Indexed(242), Color::Indexed(236), ),
ColorMode::Zaibatsu => (
Color::Indexed(167), Color::Indexed(216), Color::Indexed(131), Color::Indexed(224), Color::Indexed(95), Color::Indexed(52), ),
}
}
pub fn palette_for_prefs(
prefs: &crate::prefs::Prefs,
) -> (Color, Color, Color, Color, Color, Color) {
if let Some(ref name) = prefs.active_theme
&& let Some(theme) = prefs.custom_themes.get(name)
{
return (
Color::Indexed(theme.blue),
Color::Indexed(theme.green),
Color::Indexed(theme.purple),
Color::Indexed(theme.light_purple),
Color::Indexed(theme.royal),
Color::Indexed(theme.dark_purple),
);
}
palette(prefs.color_mode)
}
fn is_alert_flashing(app: &App) -> bool {
app.alert
.flash
.map(|t| t.elapsed().as_millis() < 2000 && (t.elapsed().as_millis() / 300) % 2 == 0)
.unwrap_or(false)
}
fn border_color(app: &App) -> Color {
if is_alert_flashing(app) {
return Color::Indexed(196); }
let (blue, ..) = palette_for_prefs(&app.prefs);
if app.paused { DIM_BORDER } else { blue }
}
fn thresh_color(pct: f64, app: &App) -> (Color, Option<Color>, &'static str) {
let (_, green, _, lpurple, royal, _) = palette_for_prefs(&app.prefs);
if pct >= app.prefs.thresh_crit as f64 {
(royal, Some(royal), "\u{2716}")
} else if pct >= app.prefs.thresh_warn as f64 {
(lpurple, Some(lpurple), "\u{26A0}")
} else {
(green, None, "\u{25C8}")
}
}
pub fn gradient_color_at(frac: f64, mode: ColorMode) -> Color {
let (blue, green, purple, _, _, dpurple) = palette(mode);
if frac < 0.33 {
green
} else if frac < 0.55 {
blue
} else if frac < 0.80 {
purple
} else {
dpurple
}
}
fn gradient_color_at_prefs(frac: f64, prefs: &crate::prefs::Prefs) -> Color {
let (blue, green, purple, _, _, dpurple) = palette_for_prefs(prefs);
if frac < 0.33 {
green
} else if frac < 0.55 {
blue
} else if frac < 0.80 {
purple
} else {
dpurple
}
}
fn set_cell(buf: &mut Buffer, x: u16, y: u16, ch: &str, style: Style) {
let area = buf.area();
if x < area.x + area.width && y < area.y + area.height && x >= area.x && y >= area.y {
let cell = &mut buf[(x, y)];
cell.set_symbol(ch);
cell.set_style(style);
}
}
fn set_str(buf: &mut Buffer, x: u16, y: u16, s: &str, style: Style, max_w: u16) {
let mut cx = x;
for ch in s.chars() {
if cx >= x.saturating_add(max_w) {
break;
}
set_cell(buf, cx, y, &ch.to_string(), style);
cx += 1;
}
}
pub fn draw(frame: &mut Frame, app: &App) {
let area = frame.area();
let w = area.width;
let h = area.height;
if w < 40 || h < 10 {
let buf = frame.buffer_mut();
set_str(
buf,
0,
0,
"Terminal too small (need 40x10)",
Style::default().fg(Color::Red),
w,
);
return;
}
if app.drill.mode == ViewMode::DrillDown {
draw_drilldown(frame, app);
return;
}
let buf = frame.buffer_mut();
let bc = border_color(app);
let border_s = Style::default().fg(bc);
let (pal_blue, pal_green, _pal_purple, pal_lpurple, _pal_royal, _pal_dpurple) =
palette_for_prefs(&app.prefs);
let show_border = app.prefs.show_border;
let lm: u16 = if show_border { 1 } else { 0 };
let rm: u16 = if show_border { 1 } else { 0 };
let inner_w = w.saturating_sub(lm + rm);
let right_w = right_col_width(app);
let pct_w: u16 = if app.prefs.col_pct_w > 0 {
app.prefs.col_pct_w
} else {
5
};
for y in 0..h {
for x in 0..w {
let cell = &mut buf[(x, y)];
cell.set_symbol(" ");
cell.set_style(Style::default());
}
}
if show_border {
set_cell(buf, 0, 0, "\u{2554}", border_s);
for x in 1..w - 1 {
set_cell(buf, x, 0, "\u{2550}", border_s);
}
set_cell(buf, w - 1, 0, "\u{2557}", border_s);
}
if show_border {
set_cell(buf, 0, h - 1, "\u{255A}", border_s);
for x in 1..w - 1 {
set_cell(buf, x, h - 1, "\u{2550}", border_s);
}
set_cell(buf, w - 1, h - 1, "\u{255D}", border_s);
}
if show_border {
for y in 1..h - 1 {
set_cell(buf, 0, y, "\u{2551}", border_s);
set_cell(buf, w - 1, y, "\u{2551}", border_s);
}
}
let mut row: u16 = if show_border { 1 } else { 0 };
{
let banner_s = Style::default().fg(pal_green).bg(DARK_BG);
let accent_s = Style::default()
.fg(pal_blue)
.bg(DARK_BG)
.add_modifier(Modifier::BOLD);
for x in lm..w.saturating_sub(rm) {
set_cell(buf, x, row, " ", Style::default().bg(DARK_BG));
}
let now = chrono_now();
let hostname = &app.stats.hostname;
let s = " \u{2502} ";
let mut title = format!(
" \u{25B6}\u{25B6}\u{25B6} DISK MATRIX \u{25C0}\u{25C0}\u{25C0}{s}node:{}{s}date:{}{s}clock:{}",
hostname, now.0, now.1
);
if app.paused {
title.push_str(&format!("{s}\u{23F8} PAUSED"));
}
if app.prefs.show_local {
title.push_str(&format!("{s}LOCAL"));
}
if app.prefs.show_all {
title.push_str(&format!("{s}ALL"));
}
if !app.filter.text.is_empty() {
title.push_str(&format!("{s}filter:{}", app.filter.text));
}
let load = app.stats.load_avg;
let mem_pct = if app.stats.mem_total > 0 {
(app.stats.mem_used as f64 / app.stats.mem_total as f64) * 100.0
} else {
0.0
};
title.push_str(&format!(
"{s}load:{:.2}/{:.2}/{:.2}{s}mem:{}/{}({:.0}%){s}cpu:{}",
load.0,
load.1,
load.2,
format_bytes(app.stats.mem_used, UnitMode::Human),
format_bytes(app.stats.mem_total, UnitMode::Human),
mem_pct,
app.stats.cpu_count
));
if inner_w > 100 {
title.push_str(&format!("{s}procs:{}", app.stats.process_count));
}
if inner_w > 120 {
title.push_str(&format!(
"{s}swap:{}/{}",
format_bytes(app.stats.swap_used, UnitMode::Human),
format_bytes(app.stats.swap_total, UnitMode::Human)
));
}
if inner_w > 140 && !app.stats.kernel.is_empty() {
title.push_str(&format!("{s}kern:{}", app.stats.kernel));
}
if inner_w > 160 && !app.stats.arch.is_empty() {
title.push_str(&format!("{s}arch:{}", app.stats.arch));
}
let help_hint = " \u{2502} h=help ";
let avail = inner_w as usize;
if title.chars().count() + help_hint.len() < avail {
let pad = avail - title.chars().count() - help_hint.len();
title.push_str(&" ".repeat(pad));
title.push_str(help_hint);
}
let title_display: String = title.chars().take(inner_w as usize).collect();
set_str(buf, lm, row, &title_display, banner_s, inner_w);
if let Some(idx) = title_display.find("DISK MATRIX") {
let char_offset = title_display[..idx].chars().count() as u16;
set_str(buf, lm + char_offset, row, "DISK MATRIX", accent_s, 11);
}
row += 1;
}
draw_separator(buf, row, w, show_border, border_s);
row += 1;
let disks = app.sorted_disks();
let disk_count = disks.len();
let (max_used_w, max_total_w) = if app.prefs.show_used {
let mut mu = 4usize;
let mut mt = 4usize;
for d in disks {
mu = mu.max(format_bytes(d.used, app.prefs.unit_mode).len());
mt = mt.max(format_bytes(d.total, app.prefs.unit_mode).len());
}
(mu, mt)
} else {
(0, 0)
};
if app.prefs.show_header {
let hdr_s = Style::default()
.fg(pal_lpurple)
.add_modifier(Modifier::BOLD);
for x in lm..w.saturating_sub(rm) {
set_cell(buf, x, row, " ", Style::default());
}
let mount_w = mount_col_width(inner_w, &app.prefs);
let sort_arrow = if app.prefs.sort_rev {
"\u{25BC}"
} else {
"\u{25B2}"
};
let name_arrow = if app.prefs.sort_mode == SortMode::Name {
sort_arrow
} else {
" "
};
let pct_arrow = if app.prefs.sort_mode == SortMode::Pct {
sort_arrow
} else {
" "
};
let size_arrow = if app.prefs.sort_mode == SortMode::Size {
sort_arrow
} else {
" "
};
let mount_hdr = format!(" MOUNT{}", name_arrow);
set_str(buf, lm, row, &mount_hdr, hdr_s, (mount_w + 3) as u16);
let bar_col_start = lm + 3 + mount_w as u16;
set_cell(buf, bar_col_start, row, "\u{2502}", border_s);
if app.prefs.show_bars {
let bar_start = bar_col_start + 1;
set_str(buf, bar_start, row, "USAGE", hdr_s, 5);
let bar_end = w.saturating_sub(rm + right_w + 1);
if bar_end < w.saturating_sub(rm) {
set_cell(buf, bar_end, row, "\u{2502}", border_s);
}
}
{
let right_start = w.saturating_sub(rm + right_w);
let pct_hdr = format!("PCT{}", pct_arrow);
set_str(buf, right_start, row, &pct_hdr, hdr_s, pct_w);
if app.prefs.show_used {
let pct_sep_x = right_start + pct_w;
set_cell(buf, pct_sep_x, row, "\u{2502}", border_s);
let used_hdr = format!(
" {:>uw$}/{:>tw$}{}",
"USED",
"SIZE",
size_arrow,
uw = max_used_w,
tw = max_total_w
);
let remaining = right_w.saturating_sub(pct_w + 1);
set_str(buf, pct_sep_x + 1, row, &used_hdr, hdr_s, remaining);
}
}
row += 1;
draw_separator(buf, row, w, show_border, border_s);
row += 1;
}
let footer_rows: u16 = 2 + (if show_border { 1 } else { 0 });
let disk_area_end = h.saturating_sub(footer_rows);
let mount_w = mount_col_width(inner_w, &app.prefs);
for (di, disk) in disks.iter().enumerate().skip(app.scroll_offset) {
if row >= disk_area_end {
break;
}
let is_selected = app.selected == Some(di);
let (fg_color, bg_pct, icon) = thresh_color(disk.pct, app);
let is_alert_row = is_alert_flashing(app) && app.alert.mounts.contains(&disk.mount);
if is_alert_row {
let flash_bg = Style::default().bg(Color::Indexed(52)); for x in lm..w.saturating_sub(rm) {
set_cell(buf, x, row, " ", flash_bg);
}
} else if is_selected {
let sel_bg = Style::default().bg(Color::Indexed(237));
for x in lm..w.saturating_sub(rm) {
set_cell(buf, x, row, " ", sel_bg);
}
}
let is_bookmarked = app.prefs.bookmarks.contains(&disk.mount);
let icon_str = if is_selected {
format!("\u{25B8}{} ", icon)
} else if is_bookmarked {
format!("\u{2605}{} ", icon)
} else {
format!(" {} ", icon)
};
let icon_style = if is_selected {
Style::default().fg(fg_color).bg(Color::Indexed(237))
} else if is_bookmarked {
Style::default().fg(Color::Indexed(220)) } else {
Style::default().fg(fg_color)
};
set_str(buf, lm, row, &icon_str, icon_style, 3);
let mount_display = if app.prefs.full_mount {
format!("{:<width$}", disk.mount, width = mount_w.saturating_sub(1))
} else {
truncate_mount(&disk.mount, mount_w.saturating_sub(1))
};
set_str(
buf,
lm + 3,
row,
&mount_display,
Style::default().fg(pal_green),
mount_w as u16,
);
if let Some(smart) = disk.smart_status
&& smart != SmartHealth::Unknown
{
let (smart_icon, smart_color) = match smart {
SmartHealth::Verified => ("\u{2714}", Color::Indexed(48)),
SmartHealth::Failing => ("\u{2718}", Color::Indexed(196)),
SmartHealth::Unknown => unreachable!(),
};
let smart_x = lm + 3 + mount_w as u16 - 2;
set_cell(
buf,
smart_x,
row,
smart_icon,
Style::default().fg(smart_color).add_modifier(Modifier::DIM),
);
}
if let Some(lat) = disk.latency_ms {
let badge = format_latency(lat);
let lat_color = if lat < 50.0 {
pal_green
} else if lat < 200.0 {
pal_lpurple
} else {
Color::Indexed(196)
};
let badge_len = badge.len() as u16;
let badge_x = lm + 3 + mount_w as u16 - badge_len - 1;
set_str(
buf,
badge_x,
row,
&badge,
Style::default().fg(lat_color).add_modifier(Modifier::DIM),
badge_len,
);
}
let bar_col_start = lm + 3 + mount_w as u16;
set_cell(buf, bar_col_start, row, "\u{2502}", border_s);
if app.prefs.show_bars {
let bar_end = w.saturating_sub(rm + right_w + 1);
let bar_start = bar_col_start + 1;
if bar_end > bar_start + 2 {
let bar_w = (bar_end - bar_start) as usize;
let filled = ((disk.pct / 100.0) * bar_w as f64).round() as usize;
let filled = filled.min(bar_w);
for j in 0..bar_w {
let x = bar_start + j as u16;
if x >= w.saturating_sub(rm) {
break;
}
if j < filled {
match app.prefs.bar_style {
BarStyle::Gradient => {
let frac = j as f64 / bar_w as f64;
let gc = gradient_color_at_prefs(frac, &app.prefs);
let ch = if j == filled - 1 {
"\u{25B8}"
} else if frac < 0.33 {
"\u{2588}"
} else if frac < 0.55 {
"\u{2593}"
} else if frac < 0.80 {
"\u{2592}"
} else {
"\u{2591}"
};
set_cell(buf, x, row, ch, Style::default().fg(gc));
}
BarStyle::Solid => {
set_cell(buf, x, row, "\u{2588}", Style::default().fg(fg_color));
}
BarStyle::Thin => {
if j == filled - 1 {
set_cell(
buf,
x,
row,
"\u{25B8}",
Style::default().fg(fg_color),
);
} else {
set_cell(
buf,
x,
row,
"\u{25AC}",
Style::default().fg(fg_color),
);
}
}
BarStyle::Ascii => {
if j == filled - 1 {
set_cell(buf, x, row, ">", Style::default().fg(fg_color));
} else {
set_cell(buf, x, row, "#", Style::default().fg(fg_color));
}
}
}
} else {
let (ch, color) = match app.prefs.bar_style {
BarStyle::Gradient | BarStyle::Solid => (" ", Color::Indexed(240)),
BarStyle::Thin => ("\u{00B7}", Color::Indexed(240)),
BarStyle::Ascii => ("-", Color::Indexed(240)),
};
set_cell(buf, x, row, ch, Style::default().fg(color));
}
}
if bar_end < w.saturating_sub(rm) {
set_cell(buf, bar_end, row, "\u{2502}", border_s);
}
let has_io = disk.io_read_rate.is_some_and(|r| r > 0.0)
|| disk.io_write_rate.is_some_and(|w| w > 0.0);
if has_io && bar_w > 20 {
let rd = disk.io_read_rate.unwrap_or(0.0);
let wr = disk.io_write_rate.unwrap_or(0.0);
let io_str = if rd > 0.0 && wr > 0.0 {
format!("\u{25B2}{} \u{25BC}{}", format_rate(rd), format_rate(wr))
} else if rd > 0.0 {
format!("\u{25B2}{}", format_rate(rd))
} else {
format!("\u{25BC}{}", format_rate(wr))
};
let io_len = io_str.chars().count() as u16;
let io_x = bar_end.saturating_sub(io_len + 1);
if io_x > bar_start {
let io_style = Style::default()
.fg(Color::Indexed(248))
.add_modifier(Modifier::DIM);
set_str(buf, io_x, row, &io_str, io_style, io_len);
}
}
}
}
let pct_str = format!("{:>3.0}%", disk.pct);
let pct_style = if let Some(bg) = bg_pct {
Style::default().fg(Color::White).bg(bg)
} else {
Style::default().fg(pal_green)
};
{
let right_start = w.saturating_sub(rm + right_w);
set_str(buf, right_start, row, &pct_str, pct_style, pct_w);
if app.prefs.show_used {
let pct_sep_x = right_start + pct_w;
set_cell(buf, pct_sep_x, row, "\u{2502}", border_s);
let used_s = format!(
"{:>width$}",
format_bytes(disk.used, app.prefs.unit_mode),
width = max_used_w
);
let total_s = format!(
"{:>width$}",
format_bytes(disk.total, app.prefs.unit_mode),
width = max_total_w
);
let size_str = format!(" {}/{}", used_s, total_s);
let remaining = right_w.saturating_sub(pct_w + 1);
set_str(
buf,
pct_sep_x + 1,
row,
&size_str,
Style::default().fg(pal_lpurple),
remaining,
);
}
}
row += 1;
}
if disk_area_end < h {
draw_separator(buf, disk_area_end, w, show_border, border_s);
}
{
let frow = disk_area_end + 1;
if frow < h {
let footer_s = Style::default().fg(pal_green).bg(DARK_BG);
for x in lm..w.saturating_sub(rm) {
set_cell(buf, x, frow, " ", Style::default().bg(DARK_BG));
}
let sort_name = match app.prefs.sort_mode {
SortMode::Name => "name",
SortMode::Pct => "pct",
SortMode::Size => "size",
};
let sort_dir = if app.prefs.sort_rev {
"\u{25BC}"
} else {
"\u{25B2}"
};
let bar_name = match app.prefs.bar_style {
BarStyle::Gradient => "gradient",
BarStyle::Solid => "solid",
BarStyle::Thin => "thin",
BarStyle::Ascii => "ascii",
};
let color_name = if let Some(ref name) = app.prefs.active_theme {
name.as_str()
} else {
app.prefs.color_mode.name()
};
let unit_name = match app.prefs.unit_mode {
UnitMode::Human => "human",
UnitMode::GiB => "GiB",
UnitMode::MiB => "MiB",
UnitMode::Bytes => "bytes",
};
let mut footer = format!(
" \u{27E6}zpwr\u{22B7}cyberdeck\u{27E7} \u{25C0}\u{25C0}\u{25C0} vol:{} \u{2502} sort:{}{} \u{2502} {}s \u{2502} {} \u{2502} {} \u{2502} {}",
disk_count,
sort_name,
sort_dir,
app.prefs.refresh_rate,
bar_name,
color_name,
unit_name
);
footer.push_str(&format!(" \u{2502} up:{}", format_uptime(app.stats.uptime)));
if inner_w > 80
&& let Some(user) = get_username()
{
footer.push_str(&format!(" \u{2502} user:{}", user));
}
if inner_w > 95 {
footer.push_str(&format!(" \u{2502} ip:{}", get_local_ip()));
}
if inner_w > 110 {
footer.push_str(&format!(
" \u{2502} os:{}{}",
app.stats.os_name,
if app.stats.os_version.is_empty() {
String::new()
} else {
format!(" {}", app.stats.os_version)
}
));
}
if inner_w > 130
&& let Ok(shell) = std::env::var("SHELL")
{
let shell_name = shell.rsplit('/').next().unwrap_or(&shell);
footer.push_str(&format!(" \u{2502} sh:{}", shell_name));
}
if inner_w > 140
&& let Some(tty) = get_tty()
{
footer.push_str(&format!(" \u{2502} tty:{}", tty));
}
if inner_w > 150 {
footer.push_str(&format!(" \u{2502} disks:{}", disk_count));
}
if inner_w > 160
&& let Some(bat) = get_battery()
{
footer.push_str(&format!(" \u{2502} bat:{}%", bat));
}
if inner_w > 190 {
footer.push_str(&format!(" \u{2502} {}x{}", w, h));
}
if app.filter.active {
footer.push_str(&format!(" \u{2502} FILTER> {}_", app.filter.buf));
}
if let Some((ref msg, t)) = app.status_msg
&& t.elapsed() < Duration::from_secs(3)
{
footer.push_str(&format!(" \u{2502} {}", msg));
}
let footer_display: String = footer.chars().take(inner_w as usize).collect();
set_str(buf, lm, frow, &footer_display, footer_s, inner_w);
}
}
if app.filter.active {
draw_filter_popup(buf, w, h, app);
}
if app.show_help {
draw_help(buf, w, h, app);
}
if app.theme_chooser.active {
draw_theme_chooser(buf, w, h, app);
}
if app.theme_edit.active {
draw_theme_editor(buf, w, h, app);
}
if !app.show_help
&& !app.theme_edit.active
&& !app.theme_chooser.active
&& !app.filter.active
&& app.hover_ready()
{
match app.hovered_zone(h) {
HoverZone::DiskRow(idx) => {
if app.hover.right_click {
let disks = app.sorted_disks();
if let Some(disk) = disks.get(idx) {
draw_hover_tooltip(buf, w, h, app, disk);
}
}
}
HoverZone::TitleBar => {
draw_hover_bar_tooltip(buf, w, h, app, true);
}
HoverZone::FooterBar => {
draw_hover_bar_tooltip(buf, w, h, app, false);
}
HoverZone::None => {}
}
}
}
fn draw_separator(buf: &mut Buffer, row: u16, w: u16, show_border: bool, border_s: Style) {
if show_border {
set_cell(buf, 0, row, "\u{2560}", border_s);
for x in 1..w - 1 {
set_cell(buf, x, row, "\u{2550}", border_s);
}
set_cell(buf, w - 1, row, "\u{2563}", border_s);
} else {
for x in 0..w {
set_cell(buf, x, row, "\u{2550}", border_s);
}
}
}
fn draw_filter_popup(buf: &mut Buffer, w: u16, h: u16, app: &App) {
let box_w: u16 = 54u16.min(w.saturating_sub(4));
let box_h: u16 = 9;
let x0 = (w.saturating_sub(box_w)) / 2;
let y0 = (h.saturating_sub(box_h)) / 2;
let bc = border_color(app);
let border_s = Style::default().fg(bc);
let bg_s = Style::default().fg(Color::White).bg(HELP_BG);
let title_s = Style::default()
.fg(Color::Indexed(27))
.bg(HELP_BG)
.add_modifier(Modifier::BOLD);
let input_s = Style::default()
.fg(Color::Indexed(48))
.bg(Color::Indexed(235));
let hint_s = Style::default().fg(Color::Indexed(240)).bg(HELP_BG);
let label_s = Style::default().fg(Color::Indexed(141)).bg(HELP_BG);
for y in y0..y0 + box_h {
for x in x0..x0 + box_w {
set_cell(buf, x, y, " ", Style::default().bg(HELP_BG));
}
}
set_cell(buf, x0, y0, "\u{2554}", border_s);
set_cell(buf, x0 + box_w - 1, y0, "\u{2557}", border_s);
set_cell(buf, x0, y0 + box_h - 1, "\u{255A}", border_s);
set_cell(buf, x0 + box_w - 1, y0 + box_h - 1, "\u{255D}", border_s);
for x in x0 + 1..x0 + box_w - 1 {
set_cell(buf, x, y0, "\u{2550}", border_s);
set_cell(buf, x, y0 + box_h - 1, "\u{2550}", border_s);
}
for y in y0 + 1..y0 + box_h - 1 {
set_cell(buf, x0, y, "\u{2551}", border_s);
set_cell(buf, x0 + box_w - 1, y, "\u{2551}", border_s);
}
let title = "\u{26A1} FILTER MOUNTS";
let tlen = title.chars().count() as u16;
let tx = x0 + (box_w.saturating_sub(tlen)) / 2;
set_str(buf, tx, y0 + 1, title, title_s, box_w - 2);
let current_label = "Active: ";
let current_val = if app.filter.text.is_empty() {
"(none)"
} else {
&app.filter.text
};
set_str(buf, x0 + 2, y0 + 2, current_label, label_s, 8);
set_str(
buf,
x0 + 10,
y0 + 2,
current_val,
bg_s,
box_w.saturating_sub(13),
);
let input_w = box_w.saturating_sub(4);
let field_y = y0 + 3;
for x in x0 + 2..x0 + 2 + input_w {
set_cell(buf, x, field_y, " ", input_s);
}
set_str(buf, x0 + 2, field_y, "\u{25B8} ", input_s, 2);
let max_visible = (input_w as usize).saturating_sub(3);
let cursor_pos = app.filter.cursor;
let buf_len = app.filter.buf.len();
let (vis_start, vis_end) = if buf_len <= max_visible {
(0, buf_len)
} else if cursor_pos <= max_visible / 2 {
(0, max_visible)
} else if cursor_pos >= buf_len.saturating_sub(max_visible / 2) {
(buf_len.saturating_sub(max_visible), buf_len)
} else {
(cursor_pos - max_visible / 2, cursor_pos + max_visible / 2)
};
let display_buf = &app.filter.buf[vis_start..vis_end.min(buf_len)];
set_str(
buf,
x0 + 4,
field_y,
display_buf,
input_s,
input_w.saturating_sub(3),
);
let cursor_x = x0 + 4 + (cursor_pos - vis_start) as u16;
if cursor_x < x0 + 2 + input_w {
let ch = app.filter.buf.chars().nth(cursor_pos).unwrap_or(' ');
let cursor_s = Style::default()
.fg(Color::Indexed(235))
.bg(Color::Indexed(48));
set_cell(buf, cursor_x, field_y, &ch.to_string(), cursor_s);
}
let hints1 = "Enter=apply Esc=cancel ^W=del word";
let h1x = x0 + (box_w.saturating_sub(hints1.len() as u16)) / 2;
set_str(buf, h1x, y0 + 5, hints1, hint_s, box_w.saturating_sub(2));
let hints2 = "^A=home ^E=end ^B/^F=\u{2190}/\u{2192} ^U=clear ^K=kill";
let h2x = x0 + (box_w.saturating_sub(hints2.chars().count() as u16)) / 2;
set_str(buf, h2x, y0 + 6, hints2, hint_s, box_w.saturating_sub(2));
let hints3 = "\u{2190}/\u{2192}=move cursor Del=delete forward";
let h3x = x0 + (box_w.saturating_sub(hints3.chars().count() as u16)) / 2;
set_str(buf, h3x, y0 + 7, hints3, hint_s, box_w.saturating_sub(2));
}
fn draw_drilldown(frame: &mut Frame, app: &App) {
let area = frame.area();
let w = area.width;
let h = area.height;
let buf = frame.buffer_mut();
let bc = border_color(app);
let border_s = Style::default().fg(bc);
let (pal_blue, pal_green, _pal_purple, pal_lpurple, _pal_royal, _pal_dpurple) =
palette_for_prefs(&app.prefs);
let show_border = app.prefs.show_border;
let lm: u16 = if show_border { 1 } else { 0 };
let rm: u16 = if show_border { 1 } else { 0 };
let inner_w = w.saturating_sub(lm + rm);
for y in 0..h {
for x in 0..w {
let cell = &mut buf[(x, y)];
cell.set_symbol(" ");
cell.set_style(Style::default());
}
}
if show_border {
set_cell(buf, 0, 0, "\u{2554}", border_s);
for x in 1..w - 1 {
set_cell(buf, x, 0, "\u{2550}", border_s);
}
set_cell(buf, w - 1, 0, "\u{2557}", border_s);
set_cell(buf, 0, h - 1, "\u{255A}", border_s);
for x in 1..w - 1 {
set_cell(buf, x, h - 1, "\u{2550}", border_s);
}
set_cell(buf, w - 1, h - 1, "\u{255D}", border_s);
for y in 1..h - 1 {
set_cell(buf, 0, y, "\u{2551}", border_s);
set_cell(buf, w - 1, y, "\u{2551}", border_s);
}
}
let mut row: u16 = if show_border { 1 } else { 0 };
{
let banner_s = Style::default().fg(pal_green).bg(DARK_BG);
let accent_s = Style::default()
.fg(pal_blue)
.bg(DARK_BG)
.add_modifier(Modifier::BOLD);
for x in lm..w.saturating_sub(rm) {
set_cell(buf, x, row, " ", Style::default().bg(DARK_BG));
}
set_str(
buf,
lm,
row,
" \u{25B6} DRILL DOWN \u{25C0} ",
accent_s,
inner_w,
);
let path_start = lm + 19;
let path_display = app.drill_current_path();
let remaining = inner_w.saturating_sub(19);
set_str(buf, path_start, row, &path_display, banner_s, remaining);
row += 1;
}
draw_separator(buf, row, w, show_border, border_s);
row += 1;
{
let hdr_s = Style::default()
.fg(pal_lpurple)
.add_modifier(Modifier::BOLD);
let sort_arrow = if app.drill.sort_rev {
"\u{25BC}"
} else {
"\u{25B2}"
};
let name_arrow = if app.drill.sort == DrillSortMode::Name {
sort_arrow
} else {
" "
};
let size_arrow = if app.drill.sort == DrillSortMode::Size {
sort_arrow
} else {
" "
};
let hdr = format!(
" {}{:<name_w$} {:>9}{}",
name_arrow,
"NAME",
"SIZE",
size_arrow,
name_w = (inner_w as usize).saturating_sub(16)
);
set_str(buf, lm, row, &hdr, hdr_s, inner_w);
row += 1;
draw_separator(buf, row, w, show_border, border_s);
row += 1;
}
let footer_rows: u16 = 2 + (if show_border { 1 } else { 0 });
let entry_area_end = h.saturating_sub(footer_rows);
if app.drill.scanning {
let scan_count = *app.drill.scan_count.lock().unwrap();
let scan_total = *app.drill.scan_total.lock().unwrap();
let scanning_s = Style::default().fg(pal_blue).add_modifier(Modifier::BOLD);
if scan_total > 0 {
let label = format!(" \u{25CB} Scanning {}/{} ", scan_count, scan_total);
let label_len = label.chars().count() as u16;
set_str(buf, lm + 1, row, &label, scanning_s, inner_w);
let bar_max = (inner_w as usize).saturating_sub(label_len as usize + 3);
if bar_max > 4 {
let frac = scan_count as f64 / scan_total as f64;
let filled = (frac * bar_max as f64).round() as usize;
let bar_x = lm + 1 + label_len;
for j in 0..bar_max {
let x = bar_x + j as u16;
if j < filled {
let gc = gradient_color_at_prefs(j as f64 / bar_max as f64, &app.prefs);
set_cell(buf, x, row, "\u{2588}", Style::default().fg(gc));
} else {
set_cell(buf, x, row, "\u{2591}", Style::default().fg(DIM_BORDER));
}
}
}
} else {
set_str(
buf,
lm + 2,
row,
"\u{25CB} Scanning\u{2026}",
scanning_s,
inner_w,
);
}
row += 1;
}
let max_size = app
.drill
.entries
.first()
.map(|e| e.size)
.unwrap_or(1)
.max(1);
for (i, entry) in app
.drill
.entries
.iter()
.enumerate()
.skip(app.drill.scroll_offset)
{
if row >= entry_area_end {
break;
}
let is_selected = i == app.drill.selected;
if is_selected {
let sel_bg = Style::default().bg(Color::Indexed(237));
for x in lm..w.saturating_sub(rm) {
set_cell(buf, x, row, " ", sel_bg);
}
}
let (icon, icon_color) = if entry.is_dir {
("\u{25B8} \u{1F4C1} ", pal_blue)
} else {
(" \u{25CB} ", pal_green)
};
let icon_style = if is_selected {
Style::default().fg(icon_color).bg(Color::Indexed(237))
} else {
Style::default().fg(icon_color)
};
set_str(buf, lm, row, icon, icon_style, 5);
let size_col_w = 10u16;
let bar_col_w = 12u16;
let name_max =
(inner_w as usize).saturating_sub(5 + size_col_w as usize + bar_col_w as usize + 2);
let name_display = truncate_mount(&entry.name, name_max);
let name_style = if is_selected {
Style::default().fg(pal_green).bg(Color::Indexed(237))
} else {
Style::default().fg(pal_green)
};
set_str(buf, lm + 5, row, &name_display, name_style, name_max as u16);
let bar_start = lm + 5 + name_max as u16 + 1;
let bar_w = bar_col_w as usize;
let frac = entry.size as f64 / max_size as f64;
let filled = (frac * bar_w as f64).round() as usize;
for j in 0..bar_w {
let x = bar_start + j as u16;
if j < filled {
let gc = gradient_color_at_prefs(j as f64 / bar_w as f64, &app.prefs);
set_cell(buf, x, row, "\u{2588}", Style::default().fg(gc));
} else {
set_cell(buf, x, row, "\u{00B7}", Style::default().fg(DIM_BORDER));
}
}
let size_str = format_bytes(entry.size, app.prefs.unit_mode);
let size_display = format!("{:>10}", size_str);
let size_style = if is_selected {
Style::default().fg(pal_lpurple).bg(Color::Indexed(237))
} else {
Style::default().fg(pal_lpurple)
};
let size_x = w.saturating_sub(rm + size_col_w);
set_str(buf, size_x, row, &size_display, size_style, size_col_w);
row += 1;
}
if app.drill.entries.is_empty() && !app.drill.scanning {
let empty_s = Style::default().fg(DIM_BORDER);
set_str(
buf,
lm + 2,
row,
"(empty or access denied)",
empty_s,
inner_w,
);
}
if entry_area_end < h {
draw_separator(buf, entry_area_end, w, show_border, border_s);
}
{
let frow = entry_area_end + 1;
if frow < h {
let footer_s = Style::default().fg(pal_green).bg(DARK_BG);
for x in lm..w.saturating_sub(rm) {
set_cell(buf, x, frow, " ", Style::default().bg(DARK_BG));
}
let entry_count = app.drill.entries.len();
let total_size: u64 = app.drill.entries.iter().map(|e| e.size).sum();
let sort_name = match app.drill.sort {
DrillSortMode::Size => "size",
DrillSortMode::Name => "name",
};
let sort_dir = if app.drill.sort_rev {
"\u{25BC}"
} else {
"\u{25B2}"
};
let footer = format!(
" \u{27E6}drill\u{22B7}down\u{27E7} \u{25C0}\u{25C0}\u{25C0} items:{} \u{2502} total:{} \u{2502} sort:{}{} \u{2502} s:size \u{2502} n:name \u{2502} r:rev \u{2502} bksp:back",
entry_count,
format_bytes(total_size, app.prefs.unit_mode),
sort_name,
sort_dir,
);
let footer_display: String = footer.chars().take(inner_w as usize).collect();
set_str(buf, lm, frow, &footer_display, footer_s, inner_w);
}
}
if app.hover_ready()
&& app.hover.right_click
&& let Some(idx) = app.hovered_drill_index()
&& let Some(entry) = app.drill.entries.get(idx)
{
draw_hover_drill_tooltip(buf, w, h, app, entry);
}
}
fn draw_hover_drill_tooltip(buf: &mut Buffer, w: u16, h: u16, app: &App, entry: &DirEntry) {
let (hover_x, hover_y) = match app.hover.pos {
Some(pos) => pos,
None => return,
};
let mut lines: Vec<(String, String)> = Vec::new();
let kind = if entry.is_dir {
"\u{1F4C1} Directory"
} else {
"\u{25CB} File"
};
lines.push(("\u{25B6} Name".into(), entry.name.clone()));
lines.push((" Type".into(), kind.into()));
lines.push((
" Size".into(),
format_bytes(entry.size, app.prefs.unit_mode),
));
if app.prefs.unit_mode != UnitMode::Bytes {
lines.push((" Size (bytes)".into(), format!("{} bytes", entry.size)));
}
if app.prefs.unit_mode != UnitMode::Human {
lines.push((
" Size (human)".into(),
format_bytes(entry.size, UnitMode::Human),
));
}
lines.push((" Path".into(), entry.path.clone()));
let parent_total: u64 = app.drill.entries.iter().map(|e| e.size).sum();
if parent_total > 0 {
let pct = (entry.size as f64 / parent_total as f64) * 100.0;
lines.push((" Share".into(), format!("{:.1}% of directory", pct)));
let filled = (pct / 5.0).round() as usize;
let bar: String =
"\u{2588}".repeat(filled) + &"\u{2591}".repeat(20usize.saturating_sub(filled));
lines.push((" Share bar".into(), bar));
}
let rank = app
.drill
.entries
.iter()
.filter(|e| e.size > entry.size)
.count()
+ 1;
let total_siblings = app.drill.entries.len();
lines.push((
" Rank".into(),
format!("#{} of {} entries", rank, total_siblings),
));
let depth = app.drill.path.len();
lines.push((
" Depth".into(),
format!("{} level{} deep", depth, if depth == 1 { "" } else { "s" }),
));
lines.push((
" Parent".into(),
app.drill.path.last().cloned().unwrap_or_default(),
));
lines.push((
" Sort".into(),
format!(
"{:?} {}",
app.drill.sort,
if app.drill.sort_rev {
"\u{25BC} desc"
} else {
"\u{25B2} asc"
}
),
));
lines.push((
" Scan src".into(),
"recursive read_dir() with progress".into(),
));
lines.push((
" Actions".into(),
"Enter=open Bksp=up s=sort r=reverse".into(),
));
render_tooltip(buf, w, h, hover_x, hover_y, app, &lines);
}
fn draw_hover_tooltip(buf: &mut Buffer, w: u16, h: u16, app: &App, disk: &DiskEntry) {
let (hover_x, hover_y) = match app.hover.pos {
Some(pos) => pos,
None => return,
};
let mut lines: Vec<(String, String)> = vec![
("\u{25B6} Mount".into(), disk.mount.clone()),
(" Filesystem".into(), disk.fs.clone()),
(" \u{2500}\u{2500}\u{2500} Capacity".into(), "\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}".into()),
(" Used".into(), format_bytes(disk.used, app.prefs.unit_mode)),
];
let free = disk.total.saturating_sub(disk.used);
lines.push((" Free".into(), format_bytes(free, app.prefs.unit_mode)));
lines.push((
" Total".into(),
format_bytes(disk.total, app.prefs.unit_mode),
));
if app.prefs.unit_mode != UnitMode::Bytes {
lines.push((" Used (bytes)".into(), format!("{}", disk.used)));
lines.push((" Free (bytes)".into(), format!("{}", free)));
lines.push((" Total (bytes)".into(), format!("{}", disk.total)));
}
if app.prefs.unit_mode != UnitMode::Human {
lines.push((
" Used (human)".into(),
format_bytes(disk.used, UnitMode::Human),
));
lines.push((" Free (human)".into(), format_bytes(free, UnitMode::Human)));
lines.push((
" Total (human)".into(),
format_bytes(disk.total, UnitMode::Human),
));
}
lines.push((" Usage".into(), format!("{:.1}%", disk.pct)));
lines.push((" Free %".into(), format!("{:.1}%", 100.0 - disk.pct)));
let filled = (disk.pct / 5.0).round() as usize;
let bar: String =
"\u{2588}".repeat(filled) + &"\u{2591}".repeat(20usize.saturating_sub(filled));
lines.push((" Usage bar".into(), format!("{} {:.1}%", bar, disk.pct)));
let sorted = app.sorted_disks();
let rank_by_size = sorted.iter().filter(|d| d.total > disk.total).count() + 1;
let rank_by_usage = sorted.iter().filter(|d| d.pct > disk.pct).count() + 1;
let disk_count = sorted.len();
lines.push((
" Rank (size)".into(),
format!("#{} of {} volumes", rank_by_size, disk_count),
));
lines.push((
" Rank (usage)".into(),
format!("#{} of {} volumes", rank_by_usage, disk_count),
));
lines.push((" \u{2500}\u{2500}\u{2500} Hardware".into(), "\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}".into()));
lines.push((
" Kind".into(),
match disk.kind {
sysinfo::DiskKind::SSD => "SSD (Solid State Drive)".into(),
sysinfo::DiskKind::HDD => "HDD (Hard Disk Drive)".into(),
_ => "Unknown".into(),
},
));
let (thresh_status, thresh_desc) = if disk.pct >= app.prefs.thresh_crit as f64 {
(
"\u{2716} CRITICAL",
format!("Above crit threshold ({}%)", app.prefs.thresh_crit),
)
} else if disk.pct >= app.prefs.thresh_warn as f64 {
(
"\u{26A0} WARNING",
format!("Above warn threshold ({}%)", app.prefs.thresh_warn),
)
} else {
(
"\u{25C8} Nominal",
format!("Below warn threshold ({}%)", app.prefs.thresh_warn),
)
};
lines.push((" Status".into(), thresh_status.into()));
lines.push((" Threshold".into(), thresh_desc));
let headroom_crit = (app.prefs.thresh_crit as f64 - disk.pct).max(0.0);
let headroom_warn = (app.prefs.thresh_warn as f64 - disk.pct).max(0.0);
if disk.pct < app.prefs.thresh_crit as f64 {
let bytes_to_crit = ((headroom_crit / 100.0) * disk.total as f64) as u64;
lines.push((
" Headroom (crit)".into(),
format!(
"{:.1}% / {} until critical",
headroom_crit,
format_bytes(bytes_to_crit, UnitMode::Human)
),
));
}
if disk.pct < app.prefs.thresh_warn as f64 {
let bytes_to_warn = ((headroom_warn / 100.0) * disk.total as f64) as u64;
lines.push((
" Headroom (warn)".into(),
format!(
"{:.1}% / {} until warning",
headroom_warn,
format_bytes(bytes_to_warn, UnitMode::Human)
),
));
}
if let Some(smart) = disk.smart_status {
lines.push((" \u{2500}\u{2500}\u{2500} Health".into(), "\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}".into()));
let (smart_val, smart_src) = match smart {
SmartHealth::Verified => (
"\u{2714} Verified — drive healthy",
"diskutil info (macOS) or /sys/block (Linux)",
),
SmartHealth::Failing => (
"\u{2718} FAILING — replace drive immediately!",
"diskutil info (macOS) or /sys/block (Linux)",
),
SmartHealth::Unknown => (
"? Unknown — SMART not available",
"SMART not supported or not available for this device",
),
};
lines.push((" SMART".into(), smart_val.into()));
lines.push((" SMART src".into(), smart_src.into()));
}
if disk.latency_ms.is_some() || disk.io_read_rate.is_some() || disk.io_write_rate.is_some() {
lines.push((" \u{2500}\u{2500}\u{2500} I/O".into(), "\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}".into()));
}
if let Some(lat) = disk.latency_ms {
lines.push((" Latency".into(), format_latency(lat)));
let lat_rating = if lat < 1.0 {
"Excellent"
} else if lat < 10.0 {
"Good"
} else if lat < 100.0 {
"Fair"
} else if lat < 1000.0 {
"Slow"
} else {
"Very slow"
};
lines.push((" Lat rating".into(), lat_rating.into()));
lines.push((" Lat raw".into(), format!("{:.3} ms", lat)));
lines.push((
" Lat src".into(),
"timed read_dir() with 2s timeout".into(),
));
}
if let Some(rd) = disk.io_read_rate {
lines.push((" \u{25B2} Read".into(), format!("{}/s", format_rate(rd))));
lines.push((" Read raw".into(), format!("{:.0} bytes/s", rd)));
}
if let Some(wr) = disk.io_write_rate {
lines.push((" \u{25BC} Write".into(), format!("{}/s", format_rate(wr))));
lines.push((" Write raw".into(), format!("{:.0} bytes/s", wr)));
}
if disk.io_read_rate.is_some() || disk.io_write_rate.is_some() {
let total_io = disk.io_read_rate.unwrap_or(0.0) + disk.io_write_rate.unwrap_or(0.0);
lines.push((
" \u{25C6} Total I/O".into(),
format!("{}/s", format_rate(total_io)),
));
lines.push((
" I/O src".into(),
"IOKit (macOS) or /proc/diskstats (Linux)".into(),
));
}
lines.push((" \u{2500}\u{2500}\u{2500} Meta".into(), "\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}".into()));
if app.prefs.bookmarks.contains(&disk.mount) {
lines.push((
" \u{2605} Pinned".into(),
"Bookmarked — appears at top of list".into(),
));
} else {
lines.push((
" \u{2606} Not pinned".into(),
"Press B to bookmark this volume".into(),
));
}
let (global_total, _) = dedup_disk_totals(&app.disks);
if global_total > 0 {
let vol_share = (disk.total as f64 / global_total as f64) * 100.0;
lines.push((
" System share".into(),
format!(
"{:.1}% of total storage ({})",
vol_share,
format_bytes(global_total, UnitMode::Human)
),
));
}
lines.push((
" Source".into(),
"getmntinfo (macOS) / /proc/mounts (Linux)".into(),
));
lines.push((
" Data age".into(),
format!("Refreshed every {}s", app.prefs.refresh_rate),
));
lines.push((
" Actions".into(),
"Enter=drill o=open y=copy B=bookmark".into(),
));
render_tooltip(buf, w, h, hover_x, hover_y, app, &lines);
}
fn render_tooltip(
buf: &mut Buffer,
w: u16,
h: u16,
hover_x: u16,
hover_y: u16,
app: &App,
lines: &[(String, String)],
) {
let (_, pal_green, _, pal_lpurple, _, _) = palette_for_prefs(&app.prefs);
let bc = border_color(app);
let border_s = Style::default().fg(bc);
let label_s = Style::default().fg(pal_lpurple).bg(HELP_BG);
let val_s = Style::default().fg(pal_green).bg(HELP_BG);
let max_label_w = lines
.iter()
.map(|(l, _)| l.chars().count())
.max()
.unwrap_or(6);
let max_val_w = lines
.iter()
.map(|(_, v)| v.chars().count())
.max()
.unwrap_or(6);
let box_w = (max_label_w + max_val_w + 5).min(w as usize - 4) as u16;
let box_h = (lines.len() + 2) as u16;
let x0 = if hover_x + box_w + 2 < w {
hover_x + 2
} else {
w.saturating_sub(box_w + 2)
};
let y0 = if hover_y + box_h + 1 < h {
hover_y + 1
} else {
h.saturating_sub(box_h + 1)
};
for y in y0..y0 + box_h {
for x in x0..x0 + box_w {
if x < w && y < h {
set_cell(buf, x, y, " ", Style::default().bg(HELP_BG));
}
}
}
set_cell(buf, x0, y0, "\u{256D}", border_s);
set_cell(buf, x0 + box_w - 1, y0, "\u{256E}", border_s);
set_cell(buf, x0, y0 + box_h - 1, "\u{2570}", border_s);
set_cell(buf, x0 + box_w - 1, y0 + box_h - 1, "\u{256F}", border_s);
for x in x0 + 1..x0 + box_w - 1 {
set_cell(buf, x, y0, "\u{2500}", border_s);
set_cell(buf, x, y0 + box_h - 1, "\u{2500}", border_s);
}
for y in y0 + 1..y0 + box_h - 1 {
set_cell(buf, x0, y, "\u{2502}", border_s);
set_cell(buf, x0 + box_w - 1, y, "\u{2502}", border_s);
}
let lw = max_label_w + 1;
for (i, (label, val)) in lines.iter().enumerate() {
let y = y0 + 1 + i as u16;
if y >= y0 + box_h - 1 {
break;
}
set_str(buf, x0 + 1, y, label, label_s, lw as u16);
if !val.is_empty() {
let vx = x0 + 1 + lw as u16 + 1;
let remaining = box_w.saturating_sub(lw as u16 + 3);
set_str(buf, vx, y, val, val_s, remaining);
}
}
}
fn segment_at_x(rendered: &str, hover_x: u16, bar_start_x: u16) -> Option<String> {
let rel_x = hover_x.saturating_sub(bar_start_x) as usize;
let mut pos = 0usize;
for segment in rendered.split('\u{2502}') {
let seg_len = segment.chars().count() + 1; if rel_x < pos + seg_len {
return Some(segment.trim().to_string());
}
pos += seg_len;
}
rendered
.split('\u{2502}')
.next_back()
.map(|s| s.trim().to_string())
}
fn title_segment_tooltip(segment: &str, app: &App) -> Vec<(String, String)> {
let seg_lower = segment.to_lowercase();
if seg_lower.contains("disk matrix") {
vec![
("\u{25B6} App".into(), "STORAGESHOWER".into()),
(
" Version".into(),
format!("v{}", env!("CARGO_PKG_VERSION")),
),
(" Desc".into(), "Cyberpunk disk usage TUI monitor".into()),
(" Author".into(), "MenkeTechnologies".into()),
(
" Repo".into(),
"github.com/MenkeTechnologies/storageshower".into(),
),
(" Crate".into(), "crates.io/crates/storageshower".into()),
(" License".into(), "MIT".into()),
(" Install".into(), "cargo install storageshower".into()),
(
" Platform".into(),
format!("{} {}", app.stats.os_name, app.stats.arch),
),
(
" Built with".into(),
"Rust + ratatui + sysinfo + crossterm".into(),
),
(" Config".into(), "~/.storageshower.conf (TOML)".into()),
(" Volumes".into(), format!("{} mounted", app.disks.len())),
(" Uptime".into(), format_uptime(app.stats.uptime)),
]
} else if seg_lower.starts_with("node:") {
vec![
("\u{25B6} Hostname".into(), app.stats.hostname.clone()),
(
" OS".into(),
format!("{} {}", app.stats.os_name, app.stats.os_version),
),
(" Kernel".into(), app.stats.kernel.clone()),
(" Arch".into(), app.stats.arch.clone()),
(
" CPU cores".into(),
format!("{} logical", app.stats.cpu_count),
),
(
" RAM".into(),
format_bytes(app.stats.mem_total, UnitMode::Human),
),
(
" Load".into(),
format!(
"{:.2} / {:.2} / {:.2}",
app.stats.load_avg.0, app.stats.load_avg.1, app.stats.load_avg.2
),
),
(" Processes".into(), format!("{}", app.stats.process_count)),
(" Uptime".into(), format_uptime(app.stats.uptime)),
(" Source".into(), "sysinfo::System::host_name()".into()),
]
} else if seg_lower.starts_with("date:") || seg_lower.starts_with("clock:") {
let now = chrono_now();
vec![
("\u{25B6} Date".into(), now.0),
(" Time".into(), now.1),
(" Uptime".into(), format_uptime(app.stats.uptime)),
(
" Raw uptime".into(),
format!("{} seconds", app.stats.uptime),
),
(" Timezone".into(), "System local timezone".into()),
(" Source".into(), "libc::localtime_r (UNIX epoch)".into()),
]
} else if seg_lower.starts_with("load:") {
let l = app.stats.load_avg;
let cpus = app.stats.cpu_count as f64;
let sat_1 = if cpus > 0.0 { l.0 / cpus * 100.0 } else { 0.0 };
let sat_5 = if cpus > 0.0 { l.1 / cpus * 100.0 } else { 0.0 };
let sat_15 = if cpus > 0.0 { l.2 / cpus * 100.0 } else { 0.0 };
let trend = if l.0 > l.1 && l.1 > l.2 {
"\u{25B2} Increasing"
} else if l.0 < l.1 && l.1 < l.2 {
"\u{25BC} Decreasing"
} else {
"\u{25C6} Stable"
};
vec![
("\u{25B6} Load Average".into(), String::new()),
(
" 1 min".into(),
format!(
"{:.2} ({:.0}% of {} cores)",
l.0, sat_1, app.stats.cpu_count
),
),
(
" 5 min".into(),
format!(
"{:.2} ({:.0}% of {} cores)",
l.1, sat_5, app.stats.cpu_count
),
),
(
" 15 min".into(),
format!(
"{:.2} ({:.0}% of {} cores)",
l.2, sat_15, app.stats.cpu_count
),
),
(" Trend".into(), trend.into()),
(
" Desc".into(),
"Average runnable+uninterruptible processes".into(),
),
(" Ideal".into(), format!("< {:.1} (1.0 per core)", cpus)),
(" Source".into(), "sysinfo::System::load_average()".into()),
]
} else if seg_lower.starts_with("mem:") {
let pct = if app.stats.mem_total > 0 {
(app.stats.mem_used as f64 / app.stats.mem_total as f64) * 100.0
} else {
0.0
};
let free = app.stats.mem_total.saturating_sub(app.stats.mem_used);
let filled = (pct / 5.0).round() as usize;
let bar: String =
"\u{2588}".repeat(filled) + &"\u{2591}".repeat(20usize.saturating_sub(filled));
let swap_pct = if app.stats.swap_total > 0 {
(app.stats.swap_used as f64 / app.stats.swap_total as f64) * 100.0
} else {
0.0
};
vec![
("\u{25B6} Memory".into(), String::new()),
(
" Used".into(),
format_bytes(app.stats.mem_used, UnitMode::Human),
),
(" Free".into(), format_bytes(free, UnitMode::Human)),
(
" Total".into(),
format_bytes(app.stats.mem_total, UnitMode::Human),
),
(" Usage".into(), format!("{:.1}%", pct)),
(" Usage bar".into(), format!("{} {:.1}%", bar, pct)),
(" Used (bytes)".into(), format!("{}", app.stats.mem_used)),
(" Total (bytes)".into(), format!("{}", app.stats.mem_total)),
(
" Per process".into(),
format!(
"~{} across {} processes",
format_bytes(
if app.stats.process_count > 0 {
app.stats.mem_used / app.stats.process_count as u64
} else {
0
},
UnitMode::Human
),
app.stats.process_count
),
),
(
" Swap used".into(),
format!(
"{} / {} ({:.1}%)",
format_bytes(app.stats.swap_used, UnitMode::Human),
format_bytes(app.stats.swap_total, UnitMode::Human),
swap_pct
),
),
(" Source".into(), "sysinfo::System::used_memory()".into()),
]
} else if seg_lower.starts_with("cpu:") {
let l = app.stats.load_avg;
let cpus = app.stats.cpu_count;
vec![
("\u{25B6} CPU".into(), String::new()),
(" Cores".into(), format!("{} logical", cpus)),
(" Arch".into(), app.stats.arch.clone()),
(" Load (1m)".into(), format!("{:.2}", l.0)),
(" Load (5m)".into(), format!("{:.2}", l.1)),
(" Load (15m)".into(), format!("{:.2}", l.2)),
(
" Processes".into(),
format!("{} running", app.stats.process_count),
),
(" Source".into(), "sysinfo::System::cpus().len()".into()),
]
} else if seg_lower.starts_with("procs:") {
let avg_mem = if app.stats.process_count > 0 {
app.stats.mem_used / app.stats.process_count as u64
} else {
0
};
vec![
(
"\u{25B6} Processes".into(),
format!("{}", app.stats.process_count),
),
(" Desc".into(), "Total running system processes".into()),
(
" Avg memory".into(),
format!("{} per process", format_bytes(avg_mem, UnitMode::Human)),
),
(
" CPU cores".into(),
format!("{} logical", app.stats.cpu_count),
),
(" Load (1m)".into(), format!("{:.2}", app.stats.load_avg.0)),
(
" Source".into(),
"sysinfo::System::processes().len()".into(),
),
]
} else if seg_lower.starts_with("swap:") {
let swap_free = app.stats.swap_total.saturating_sub(app.stats.swap_used);
let swap_pct = if app.stats.swap_total > 0 {
(app.stats.swap_used as f64 / app.stats.swap_total as f64) * 100.0
} else {
0.0
};
let filled = (swap_pct / 5.0).round() as usize;
let bar: String =
"\u{2588}".repeat(filled) + &"\u{2591}".repeat(20usize.saturating_sub(filled));
let mem_pct = if app.stats.mem_total > 0 {
(app.stats.mem_used as f64 / app.stats.mem_total as f64) * 100.0
} else {
0.0
};
vec![
("\u{25B6} Swap".into(), String::new()),
(
" Used".into(),
format_bytes(app.stats.swap_used, UnitMode::Human),
),
(" Free".into(), format_bytes(swap_free, UnitMode::Human)),
(
" Total".into(),
format_bytes(app.stats.swap_total, UnitMode::Human),
),
(" Usage".into(), format!("{:.1}%", swap_pct)),
(" Usage bar".into(), format!("{} {:.1}%", bar, swap_pct)),
(" Used (bytes)".into(), format!("{}", app.stats.swap_used)),
(
" Total (bytes)".into(),
format!("{}", app.stats.swap_total),
),
(
" RAM usage".into(),
format!(
"{:.1}% ({} / {})",
mem_pct,
format_bytes(app.stats.mem_used, UnitMode::Human),
format_bytes(app.stats.mem_total, UnitMode::Human)
),
),
(
" Desc".into(),
"Virtual memory on disk when RAM is full".into(),
),
(" Source".into(), "sysinfo::System::used_swap()".into()),
]
} else if seg_lower.starts_with("kern:") {
vec![
("\u{25B6} Kernel".into(), app.stats.kernel.clone()),
(
" OS".into(),
format!("{} {}", app.stats.os_name, app.stats.os_version),
),
(" Arch".into(), app.stats.arch.clone()),
(" Uptime".into(), format_uptime(app.stats.uptime)),
(" Hostname".into(), app.stats.hostname.clone()),
(
" Source".into(),
"sysinfo::System::kernel_version()".into(),
),
]
} else if seg_lower.starts_with("arch:") {
vec![
("\u{25B6} Architecture".into(), app.stats.arch.clone()),
(" Kernel".into(), app.stats.kernel.clone()),
(
" OS".into(),
format!("{} {}", app.stats.os_name, app.stats.os_version),
),
(
" CPU cores".into(),
format!("{} logical", app.stats.cpu_count),
),
(" Source".into(), "sysinfo::System::cpu_arch()".into()),
]
} else if seg_lower.contains("paused") {
vec![
("\u{25B6} \u{23F8} PAUSED".into(), String::new()),
(" Desc".into(), "Data refresh is paused".into()),
(" Effect".into(), "Disk and system stats are frozen".into()),
(
" Refresh rate".into(),
format!("{}s (when active)", app.prefs.refresh_rate),
),
(" Volumes".into(), format!("{} mounted", app.disks.len())),
(" Resume".into(), "Press p to resume live data".into()),
]
} else if seg_lower.contains("h=help") {
vec![
("\u{25B6} Help".into(), String::new()),
(" Open".into(), "Press h / H / ? to show keybinds".into()),
(" Close".into(), "Same keys or Esc to dismiss".into()),
(" Layout".into(), "3-column keybind reference".into()),
(
" Categories".into(),
"Navigation, Display, Sort, I/O, Theme".into(),
),
(
" Mouse".into(),
"Click headers, scroll, R-click for tips".into(),
),
]
} else {
vec![("\u{25B6} Info".into(), segment.to_string())]
}
}
fn footer_segment_tooltip(segment: &str, app: &App) -> Vec<(String, String)> {
let seg_lower = segment.to_lowercase();
if seg_lower.contains("cyberdeck") || seg_lower.contains("zpwr") {
let (total_storage, total_used) = dedup_disk_totals(&app.disks);
let total_free = total_storage.saturating_sub(total_used);
let overall_pct = if total_storage > 0 {
(total_used as f64 / total_storage as f64) * 100.0
} else {
0.0
};
vec![
("\u{25B6} App".into(), "STORAGESHOWER".into()),
(
" Version".into(),
format!("v{}", env!("CARGO_PKG_VERSION")),
),
(" Desc".into(), "Cyberpunk disk usage TUI monitor".into()),
(" Author".into(), "MenkeTechnologies".into()),
(
" Repo".into(),
"github.com/MenkeTechnologies/storageshower".into(),
),
(" Crate".into(), "crates.io/crates/storageshower".into()),
(
" Config".into(),
"~/.storageshower.conf (auto-saved)".into(),
),
(" License".into(), "MIT".into()),
(
" Storage".into(),
format!(
"{} used / {} total ({:.1}%)",
format_bytes(total_used, UnitMode::Human),
format_bytes(total_storage, UnitMode::Human),
overall_pct
),
),
(" Free".into(), format_bytes(total_free, UnitMode::Human)),
(" Volumes".into(), format!("{} mounted", app.disks.len())),
(
" Refresh".into(),
format!("Every {}s", app.prefs.refresh_rate),
),
(" Uptime".into(), format_uptime(app.stats.uptime)),
]
} else if seg_lower.starts_with("vol:") {
let total = app.disks.len();
let visible: usize = segment.trim_start_matches("vol:").parse().unwrap_or(0);
let hidden = total.saturating_sub(visible);
let (total_storage, total_used) = dedup_disk_totals(&app.disks);
let overall_pct = if total_storage > 0 {
(total_used as f64 / total_storage as f64) * 100.0
} else {
0.0
};
let ssd_count = app
.disks
.iter()
.filter(|d| matches!(d.kind, sysinfo::DiskKind::SSD))
.count();
let hdd_count = app
.disks
.iter()
.filter(|d| matches!(d.kind, sysinfo::DiskKind::HDD))
.count();
let other_count = total.saturating_sub(ssd_count + hdd_count);
let fs_types: Vec<String> = {
let mut fss: Vec<String> = app.disks.iter().map(|d| d.fs.clone()).collect();
fss.sort();
fss.dedup();
fss
};
vec![
("\u{25B6} Volumes".into(), format!("{} visible", visible)),
(" Total mounted".into(), format!("{} filesystems", total)),
(" Hidden".into(), format!("{} (filtered out)", hidden)),
(" SSDs".into(), format!("{}", ssd_count)),
(" HDDs".into(), format!("{}", hdd_count)),
(" Other".into(), format!("{}", other_count)),
(" FS types".into(), fs_types.join(", ")),
(
" Total storage".into(),
format_bytes(total_storage, UnitMode::Human),
),
(
" Total used".into(),
format!(
"{} ({:.1}%)",
format_bytes(total_used, UnitMode::Human),
overall_pct
),
),
(
" Total free".into(),
format_bytes(total_storage.saturating_sub(total_used), UnitMode::Human),
),
(
" Filters".into(),
"show_all / show_local / filter text".into(),
),
(" Keys".into(), "a=show all l=local only /=filter".into()),
]
} else if seg_lower.starts_with("sort:") {
let sorted = app.sorted_disks();
let first = sorted.first().map(|d| d.mount.as_str()).unwrap_or("(none)");
let last = sorted.last().map(|d| d.mount.as_str()).unwrap_or("(none)");
vec![
(
"\u{25B6} Sort Mode".into(),
format!("{:?}", app.prefs.sort_mode),
),
(
" Direction".into(),
if app.prefs.sort_rev {
"Descending \u{25BC}"
} else {
"Ascending \u{25B2}"
}
.into(),
),
(" First".into(), first.into()),
(" Last".into(), last.into()),
(" Volumes".into(), format!("{} sorted", sorted.len())),
(" Keys".into(), "n=name u=usage% s=size".into()),
(" Reverse".into(), "r or press same sort key again".into()),
(
" Mouse".into(),
"Click column header to sort/reverse".into(),
),
(" Config".into(), "sort_mode / sort_rev in prefs".into()),
]
} else if seg_lower.ends_with('s')
&& seg_lower.chars().next().is_some_and(|c| c.is_ascii_digit())
{
let rates = [1u64, 2, 5, 10];
let current = app.prefs.refresh_rate;
let idx = rates.iter().position(|&r| r == current).unwrap_or(0);
let next = rates[(idx + 1) % rates.len()];
let prev = rates[(idx + rates.len() - 1) % rates.len()];
vec![
("\u{25B6} Refresh Rate".into(), format!("{}s", current)),
(
" Desc".into(),
"How often disk data is re-collected".into(),
),
(" Next (f)".into(), format!("{}s", next)),
(" Prev (F)".into(), format!("{}s", prev)),
(
" Cycle".into(),
"1s \u{2192} 2s \u{2192} 5s \u{2192} 10s".into(),
),
(
" Paused".into(),
if app.paused {
"Yes (press p)"
} else {
"No — live updates"
}
.into(),
),
(
" Source".into(),
"Background thread via Arc<Mutex<>>".into(),
),
(" Config".into(), "refresh_rate in prefs".into()),
]
} else if seg_lower == "gradient"
|| seg_lower == "solid"
|| seg_lower == "thin"
|| seg_lower == "ascii"
{
let styles = ["Gradient", "Solid", "Thin", "Ascii"];
let current_idx = match app.prefs.bar_style {
BarStyle::Gradient => 0,
BarStyle::Solid => 1,
BarStyle::Thin => 2,
BarStyle::Ascii => 3,
};
let next_idx = (current_idx + 1) % styles.len();
vec![
(
"\u{25B6} Bar Style".into(),
format!("{:?}", app.prefs.bar_style),
),
(
" Desc".into(),
"Visual style of usage progress bars".into(),
),
(
" Gradient".into(),
"\u{2588}\u{2593}\u{2592}\u{2591} color-graded fill".into(),
),
(
" Solid".into(),
"\u{2588}\u{2588}\u{2588}\u{2588} single-color fill".into(),
),
(
" Thin".into(),
"\u{2501}\u{2501}\u{2501}\u{2501} horizontal line".into(),
),
(" Ascii".into(), "#### portable ASCII chars".into()),
(" Next (b)".into(), styles[next_idx].into()),
(" Config".into(), "bar_style in prefs".into()),
(
" CLI".into(),
"--bar-style <gradient|solid|thin|ascii>".into(),
),
]
} else if seg_lower.starts_with("up:") {
let secs = app.stats.uptime;
let days = secs / 86400;
let hours = (secs % 86400) / 3600;
let mins = (secs % 3600) / 60;
vec![
("\u{25B6} System Uptime".into(), format_uptime(secs)),
(" Days".into(), format!("{}", days)),
(" Hours".into(), format!("{}", hours)),
(" Minutes".into(), format!("{}", mins)),
(" Raw".into(), format!("{} seconds", secs)),
(" Hostname".into(), app.stats.hostname.clone()),
(" Kernel".into(), app.stats.kernel.clone()),
(" Source".into(), "sysinfo::System::uptime()".into()),
]
} else if seg_lower.starts_with("user:") {
let username = segment.trim_start_matches("user:");
vec![
("\u{25B6} User".into(), username.into()),
(" Hostname".into(), app.stats.hostname.clone()),
(
" OS".into(),
format!("{} {}", app.stats.os_name, app.stats.os_version),
),
(" Arch".into(), app.stats.arch.clone()),
(
" Shell".into(),
std::env::var("SHELL").unwrap_or_else(|_| "unknown".into()),
),
(" Source".into(), "$USER or $USERNAME env var".into()),
]
} else if seg_lower.starts_with("ip:") {
let ip = segment.trim_start_matches("ip:");
vec![
("\u{25B6} Local IP".into(), ip.into()),
(" Desc".into(), "Primary network interface address".into()),
(" Method".into(), "UDP socket bind to 8.8.8.8:80".into()),
(
" Note".into(),
"No data is sent — only local addr probed".into(),
),
(" Hostname".into(), app.stats.hostname.clone()),
(" Source".into(), "std::net::UdpSocket::connect()".into()),
]
} else if seg_lower.starts_with("os:") {
vec![
(
"\u{25B6} OS".into(),
format!("{} {}", app.stats.os_name, app.stats.os_version),
),
(" Kernel".into(), app.stats.kernel.clone()),
(" Arch".into(), app.stats.arch.clone()),
(" Hostname".into(), app.stats.hostname.clone()),
(
" CPU cores".into(),
format!("{} logical", app.stats.cpu_count),
),
(
" RAM".into(),
format_bytes(app.stats.mem_total, UnitMode::Human),
),
(" Uptime".into(), format_uptime(app.stats.uptime)),
(
" Source".into(),
"sysinfo::System::name() + os_version()".into(),
),
]
} else if seg_lower.starts_with("sh:") {
let shell = segment.trim_start_matches("sh:");
vec![
("\u{25B6} Shell".into(), shell.into()),
(" Desc".into(), "Login shell for current user".into()),
(" Env var".into(), "$SHELL".into()),
(
" User".into(),
std::env::var("USER").unwrap_or_else(|_| "unknown".into()),
),
(" Source".into(), "$SHELL environment variable".into()),
]
} else if seg_lower.starts_with("tty:") {
let tty = segment.trim_start_matches("tty:");
vec![
("\u{25B6} TTY".into(), tty.into()),
(" Desc".into(), "Terminal device for this session".into()),
(" FD".into(), "stdin (fd 0)".into()),
(" Source".into(), "libc::ttyname(0)".into()),
]
} else if seg_lower.starts_with("bat:") {
let bat_str = segment.trim_start_matches("bat:");
let bat_val: u8 = bat_str.trim_end_matches('%').parse().unwrap_or(0);
let status = if bat_val > 80 {
"\u{2714} Good"
} else if bat_val > 20 {
"\u{25C8} OK"
} else {
"\u{26A0} Low"
};
let filled = (bat_val as usize) / 5;
let bar: String =
"\u{2588}".repeat(filled) + &"\u{2591}".repeat(20usize.saturating_sub(filled));
vec![
("\u{25B6} Battery".into(), bat_str.into()),
(" Status".into(), status.into()),
(" Level bar".into(), format!("{} {}%", bar, bat_val)),
(
" Source".into(),
"pmset (macOS) or /sys/class/power_supply (Linux)".into(),
),
]
} else if seg_lower.starts_with("disks:") {
let count_str = segment.trim_start_matches("disks:");
let (total_storage, total_used) = dedup_disk_totals(&app.disks);
let overall_pct = if total_storage > 0 {
(total_used as f64 / total_storage as f64) * 100.0
} else {
0.0
};
vec![
("\u{25B6} Disk Count".into(), count_str.into()),
(" Desc".into(), "Total visible filesystems".into()),
(" Total mounted".into(), format!("{}", app.disks.len())),
(
" Total storage".into(),
format_bytes(total_storage, UnitMode::Human),
),
(" Overall usage".into(), format!("{:.1}%", overall_pct)),
(
" Source".into(),
"getmntinfo (macOS) / /proc/mounts (Linux)".into(),
),
]
} else if seg_lower.contains("filter>") {
let filter_len = app.filter.buf.len();
let matched = app.sorted_disks().len();
let total = app.disks.len();
vec![
("\u{25B6} Filter Active".into(), app.filter.buf.clone()),
(" Length".into(), format!("{} chars", filter_len)),
(
" Matched".into(),
format!("{} of {} volumes", matched, total),
),
(
" Hidden".into(),
format!("{} filtered out", total.saturating_sub(matched)),
),
(
" Type".into(),
"Case-insensitive mount path substring".into(),
),
(
" Cursor".into(),
format!("Position {} of {}", app.filter.cursor, filter_len),
),
(
" Nav keys".into(),
"Enter=confirm Esc=cancel /=open".into(),
),
(
" Edit keys".into(),
"Ctrl+a/e=home/end Ctrl+b/f=left/right".into(),
),
(
" Delete".into(),
"Ctrl+w=word Ctrl+u=line Ctrl+k=to-end".into(),
),
(
" Clear".into(),
"0 to clear filter (when not in filter mode)".into(),
),
]
} else {
let color_name = if let Some(ref n) = app.prefs.active_theme {
n.clone()
} else {
app.prefs.color_mode.name().into()
};
if segment.trim() == color_name {
let (pal_b, pal_g, pal_p, pal_lp, pal_r, pal_dp) = palette_for_prefs(&app.prefs);
fn idx(c: Color) -> String {
match c {
Color::Indexed(n) => format!("{}", n),
_ => "?".into(),
}
}
return vec![
("\u{25B6} Color Theme".into(), color_name),
(
" Builtins".into(),
format!("{} palettes", ColorMode::ALL.len()),
),
(
" Custom".into(),
format!("{} user themes", app.prefs.custom_themes.len()),
),
(
" Total".into(),
format!(
"{} available themes",
ColorMode::ALL.len() + app.prefs.custom_themes.len()
),
),
(
" Palette".into(),
format!(
"blue={} green={} purple={}",
idx(pal_b),
idx(pal_g),
idx(pal_p)
),
),
(
" Palette+".into(),
format!(
"lpurple={} royal={} dark={}",
idx(pal_lp),
idx(pal_r),
idx(pal_dp)
),
),
(
" Preview".into(),
"c=chooser (live preview + mouse click)".into(),
),
(" Editor".into(), "C=create custom themes".into()),
(" CLI".into(), "--color, --theme, --export-theme".into()),
(
" Config".into(),
"color_mode / active_theme in prefs".into(),
),
];
}
let unit_name = match app.prefs.unit_mode {
UnitMode::Human => "human",
UnitMode::GiB => "GiB",
UnitMode::MiB => "MiB",
UnitMode::Bytes => "bytes",
};
if segment.trim() == unit_name {
let (total_storage, _) = dedup_disk_totals(&app.disks);
return vec![
("\u{25B6} Unit Mode".into(), unit_name.into()),
(" Desc".into(), "How byte sizes are formatted".into()),
(
" Human".into(),
format!(
"Auto-scale ({} example)",
format_bytes(total_storage, UnitMode::Human)
),
),
(
" GiB".into(),
format!(
"Gibibytes ({} example)",
format_bytes(total_storage, UnitMode::GiB)
),
),
(
" MiB".into(),
format!(
"Mebibytes ({} example)",
format_bytes(total_storage, UnitMode::MiB)
),
),
(
" Bytes".into(),
format!(
"Raw bytes ({} example)",
format_bytes(total_storage, UnitMode::Bytes)
),
),
(
" Key".into(),
"i to cycle forward, I to cycle backward".into(),
),
(" CLI".into(), "--unit <human|gib|mib|bytes>".into()),
(" Config".into(), "unit_mode in prefs".into()),
];
}
vec![("Info".into(), segment.to_string())]
}
}
fn draw_hover_bar_tooltip(buf: &mut Buffer, w: u16, h: u16, app: &App, is_title: bool) {
let (hover_x, hover_y) = match app.hover.pos {
Some(pos) => pos,
None => return,
};
let lm: u16 = if app.prefs.show_border { 1 } else { 0 };
let mut bar_text = String::new();
for x in lm..w.saturating_sub(lm) {
let cell = &buf[(x, hover_y)];
bar_text.push_str(cell.symbol());
}
let segment = match segment_at_x(&bar_text, hover_x, lm) {
Some(s) if !s.is_empty() => s,
_ => return,
};
let lines = if is_title {
title_segment_tooltip(&segment, app)
} else {
footer_segment_tooltip(&segment, app)
};
render_tooltip(buf, w, h, hover_x, hover_y, app, &lines);
}
fn draw_theme_chooser(buf: &mut Buffer, w: u16, h: u16, app: &App) {
let themes = app.all_themes();
let box_w: u16 = 50u16.min(w.saturating_sub(4));
let box_h: u16 = (themes.len() as u16 + 4).min(h.saturating_sub(4));
let x0 = (w.saturating_sub(box_w)) / 2;
let y0 = (h.saturating_sub(box_h)) / 2;
let bc = border_color(app);
let border_s = Style::default().fg(bc);
let title_s = Style::default()
.fg(Color::Indexed(27))
.bg(HELP_BG)
.add_modifier(Modifier::BOLD);
let hint_s = Style::default().fg(Color::Indexed(240)).bg(HELP_BG);
let sel_s = Style::default().fg(Color::White).bg(Color::Indexed(237));
let name_s = Style::default().fg(Color::Indexed(48)).bg(HELP_BG);
for y in y0..y0 + box_h {
for x in x0..x0 + box_w {
if x < w && y < h {
set_cell(buf, x, y, " ", Style::default().bg(HELP_BG));
}
}
}
set_cell(buf, x0, y0, "\u{2554}", border_s);
set_cell(buf, x0 + box_w - 1, y0, "\u{2557}", border_s);
set_cell(buf, x0, y0 + box_h - 1, "\u{255A}", border_s);
set_cell(buf, x0 + box_w - 1, y0 + box_h - 1, "\u{255D}", border_s);
for x in x0 + 1..x0 + box_w - 1 {
set_cell(buf, x, y0, "\u{2550}", border_s);
set_cell(buf, x, y0 + box_h - 1, "\u{2550}", border_s);
}
for y in y0 + 1..y0 + box_h - 1 {
set_cell(buf, x0, y, "\u{2551}", border_s);
set_cell(buf, x0 + box_w - 1, y, "\u{2551}", border_s);
}
let title = "\u{1F3A8} CHOOSE THEME";
let tlen = title.chars().count() as u16;
let tx = x0 + (box_w.saturating_sub(tlen)) / 2;
set_str(buf, tx, y0 + 1, title, title_s, box_w - 2);
let content_start = y0 + 2;
let content_end = y0 + box_h - 2;
let visible = (content_end - content_start) as usize;
let scroll = if app.theme_chooser.selected >= visible {
app.theme_chooser.selected - visible + 1
} else {
0
};
let current_key = if let Some(ref name) = app.prefs.active_theme {
name.clone()
} else {
format!("{:?}", app.prefs.color_mode).to_lowercase()
};
for (i, (key, display)) in themes.iter().enumerate().skip(scroll) {
let row_y = content_start + (i - scroll) as u16;
if row_y >= content_end {
break;
}
let is_sel = i == app.theme_chooser.selected;
let is_active = *key == current_key;
if is_sel {
for x in x0 + 1..x0 + box_w - 1 {
set_cell(buf, x, row_y, " ", sel_s);
}
}
let marker = if is_active { "\u{25B8}" } else { " " };
let row_style = if is_sel { sel_s } else { name_s };
set_str(buf, x0 + 2, row_y, marker, row_style, 1);
set_str(buf, x0 + 4, row_y, display, row_style, 20);
let swatch_x = x0 + 25;
let colors: [u8; 6] = if let Some(theme) = app.prefs.custom_themes.get(key) {
[
theme.blue,
theme.green,
theme.purple,
theme.light_purple,
theme.royal,
theme.dark_purple,
]
} else {
let mode = ColorMode::ALL
.iter()
.find(|&&m| format!("{:?}", m).to_lowercase() == *key)
.copied()
.unwrap_or(ColorMode::Default);
let (a, b, c, d, e, f) = palette(mode);
fn idx(col: Color) -> u8 {
match col {
Color::Indexed(n) => n,
_ => 0,
}
}
[idx(a), idx(b), idx(c), idx(d), idx(e), idx(f)]
};
for (j, &ci) in colors.iter().enumerate() {
let sx = swatch_x + (j as u16 * 3);
if sx + 2 < x0 + box_w - 1 {
let swatch_style = Style::default().fg(Color::Indexed(ci));
set_str(buf, sx, row_y, "\u{2588}\u{2588}", swatch_style, 2);
}
}
}
let hint_y = y0 + box_h - 1;
let hint = " j/k/click:nav Enter:select Esc:cancel ";
let hx = x0 + (box_w.saturating_sub(hint.len() as u16)) / 2;
set_str(buf, hx, hint_y, hint, hint_s, box_w - 2);
}
fn draw_theme_editor(buf: &mut Buffer, w: u16, h: u16, app: &App) {
let box_w: u16 = 56u16.min(w.saturating_sub(4));
let box_h: u16 = if app.theme_edit.naming { 16 } else { 15 };
let x0 = (w.saturating_sub(box_w)) / 2;
let y0 = (h.saturating_sub(box_h)) / 2;
let bc = border_color(app);
let border_s = Style::default().fg(bc);
let bg_s = Style::default().fg(Color::White).bg(HELP_BG);
let title_s = Style::default()
.fg(Color::Indexed(27))
.bg(HELP_BG)
.add_modifier(Modifier::BOLD);
let hint_s = Style::default().fg(Color::Indexed(240)).bg(HELP_BG);
let sel_s = Style::default().fg(Color::White).bg(Color::Indexed(237));
for y in y0..y0 + box_h {
for x in x0..x0 + box_w {
set_cell(buf, x, y, " ", Style::default().bg(HELP_BG));
}
}
set_cell(buf, x0, y0, "\u{2554}", border_s);
set_cell(buf, x0 + box_w - 1, y0, "\u{2557}", border_s);
set_cell(buf, x0, y0 + box_h - 1, "\u{255A}", border_s);
set_cell(buf, x0 + box_w - 1, y0 + box_h - 1, "\u{255D}", border_s);
for x in x0 + 1..x0 + box_w - 1 {
set_cell(buf, x, y0, "\u{2550}", border_s);
set_cell(buf, x, y0 + box_h - 1, "\u{2550}", border_s);
}
for y in y0 + 1..y0 + box_h - 1 {
set_cell(buf, x0, y, "\u{2551}", border_s);
set_cell(buf, x0 + box_w - 1, y, "\u{2551}", border_s);
}
let title = "\u{1F3A8} THEME EDITOR";
let tlen = title.chars().count() as u16;
let tx = x0 + (box_w.saturating_sub(tlen)) / 2;
set_str(buf, tx, y0 + 1, title, title_s, box_w - 2);
let labels = [
"blue",
"green",
"purple",
"light_purple",
"royal",
"dark_purple",
];
let colors = app.theme_edit.colors;
for (i, label) in labels.iter().enumerate() {
let row_y = y0 + 3 + i as u16;
let is_sel = i == app.theme_edit.slot;
let row_style = if is_sel { sel_s } else { bg_s };
if is_sel {
for x in x0 + 1..x0 + box_w - 1 {
set_cell(buf, x, row_y, " ", sel_s);
}
}
let marker = if is_sel { "\u{25B8} " } else { " " };
set_str(buf, x0 + 2, row_y, marker, row_style, 2);
let label_str = format!("{:<14}", label);
set_str(buf, x0 + 4, row_y, &label_str, row_style, 14);
let val_str = format!("{:>3}", colors[i]);
set_str(buf, x0 + 19, row_y, &val_str, row_style, 3);
let swatch_style = Style::default().fg(Color::Indexed(colors[i])).bg(HELP_BG);
set_str(
buf,
x0 + 24,
row_y,
"\u{2588}\u{2588}\u{2588}\u{2588}\u{2588}",
swatch_style,
5,
);
let preview_color = Color::Indexed(colors[i]);
set_str(
buf,
x0 + 30,
row_y,
" \u{25C0}\u{2500}\u{2500}\u{25B6}",
Style::default().fg(preview_color).bg(HELP_BG),
5,
);
}
let preview_y = y0 + 10;
set_str(buf, x0 + 2, preview_y, "preview:", hint_s, 8);
let preview_w = (box_w as usize).saturating_sub(13);
for j in 0..preview_w {
let frac = j as f64 / preview_w as f64;
let c = if frac < 0.33 {
Color::Indexed(colors[1]) } else if frac < 0.55 {
Color::Indexed(colors[0]) } else if frac < 0.80 {
Color::Indexed(colors[2]) } else {
Color::Indexed(colors[5]) };
set_cell(
buf,
x0 + 11 + j as u16,
preview_y,
"\u{2588}",
Style::default().fg(c).bg(HELP_BG),
);
}
if app.theme_edit.naming {
let name_y = y0 + 12;
let input_s = Style::default()
.fg(Color::Indexed(48))
.bg(Color::Indexed(235));
set_str(buf, x0 + 2, name_y, "Theme name:", bg_s, 11);
let name_display = format!("{}_", app.theme_edit.name);
set_str(buf, x0 + 14, name_y, &name_display, input_s, box_w - 16);
set_str(
buf,
x0 + 2,
name_y + 1,
"Enter:save Esc:back",
hint_s,
box_w - 4,
);
} else {
let hint_y = y0 + 12;
set_str(
buf,
x0 + 2,
hint_y,
"j/k:select h/l:\u{00B1}1 H/L:\u{00B1}10",
hint_s,
box_w - 4,
);
set_str(
buf,
x0 + 2,
hint_y + 1,
"Enter/s:save Esc/q:cancel",
hint_s,
box_w - 4,
);
}
}
fn draw_help(buf: &mut Buffer, w: u16, h: u16, app: &App) {
let box_w: u16 = 120u16.min(w.saturating_sub(4));
let box_h: u16 = 48u16.min(h.saturating_sub(4));
let x0 = (w.saturating_sub(box_w)) / 2;
let y0 = (h.saturating_sub(box_h)) / 2;
let (pal_blue, pal_green, _, pal_lpurple, _, pal_dpurple) = palette_for_prefs(&app.prefs);
let bc = border_color(app);
let border_s = Style::default().fg(bc);
let bg_s = Style::default().fg(Color::White).bg(HELP_BG);
let key_s = Style::default().fg(pal_green).bg(HELP_BG);
let val_s = Style::default().fg(pal_lpurple).bg(HELP_BG);
let title_s = Style::default()
.fg(pal_blue)
.bg(HELP_BG)
.add_modifier(Modifier::BOLD);
let section_s = Style::default()
.fg(pal_dpurple)
.bg(HELP_BG)
.add_modifier(Modifier::BOLD);
for y in y0..y0 + box_h {
for x in x0..x0 + box_w {
set_cell(buf, x, y, " ", Style::default().bg(HELP_BG));
}
}
set_cell(buf, x0, y0, "\u{2554}", border_s);
set_cell(buf, x0 + box_w - 1, y0, "\u{2557}", border_s);
set_cell(buf, x0, y0 + box_h - 1, "\u{255A}", border_s);
set_cell(buf, x0 + box_w - 1, y0 + box_h - 1, "\u{255D}", border_s);
for x in x0 + 1..x0 + box_w - 1 {
set_cell(buf, x, y0, "\u{2550}", border_s);
set_cell(buf, x, y0 + box_h - 1, "\u{2550}", border_s);
}
for y in y0 + 1..y0 + box_h - 1 {
set_cell(buf, x0, y, "\u{2551}", border_s);
set_cell(buf, x0 + box_w - 1, y, "\u{2551}", border_s);
}
let title = format!(
"\u{2328} DISK MATRIX v{} \u{2014} KEYBOARD SHORTCUTS",
env!("CARGO_PKG_VERSION")
);
let tlen = title.chars().count() as u16;
let tx = x0 + (box_w.saturating_sub(tlen)) / 2;
set_str(buf, tx, y0 + 1, &title, title_s, box_w - 2);
let byline = "by MenkeTechnologies";
let byline_s = Style::default().fg(Color::Indexed(240)).bg(HELP_BG);
let bx = x0 + (box_w.saturating_sub(byline.len() as u16)) / 2;
set_str(buf, bx, y0 + 2, byline, byline_s, box_w - 2);
struct HelpEntry {
key: &'static str,
desc: &'static str,
val_fn: fn(&App) -> String,
is_section: bool,
}
fn empty_val(_: &App) -> String {
String::new()
}
let col1 = vec![
HelpEntry {
key: "GENERAL",
desc: "",
val_fn: empty_val,
is_section: true,
},
HelpEntry {
key: "q/Q",
desc: "Quit",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "h/H/?",
desc: "Toggle help",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "p/P",
desc: "Pause/resume",
val_fn: |a| format!("[{}]", if a.paused { "on" } else { "off" }),
is_section: false,
},
HelpEntry {
key: "f/F",
desc: "Cycle refresh",
val_fn: |a| format!("[{}s]", a.prefs.refresh_rate),
is_section: false,
},
HelpEntry {
key: "l/L",
desc: "Local only",
val_fn: |a| format!("[{}]", if a.prefs.show_local { "on" } else { "off" }),
is_section: false,
},
HelpEntry {
key: "a/A",
desc: "All filesystems",
val_fn: |a| format!("[{}]", if a.prefs.show_all { "on" } else { "off" }),
is_section: false,
},
HelpEntry {
key: "SORT",
desc: "",
val_fn: empty_val,
is_section: true,
},
HelpEntry {
key: "n/N",
desc: "By name",
val_fn: |a| {
if a.prefs.sort_mode == SortMode::Name {
"[\u{2713}]".into()
} else {
String::new()
}
},
is_section: false,
},
HelpEntry {
key: "u/U",
desc: "By usage %",
val_fn: |a| {
if a.prefs.sort_mode == SortMode::Pct {
"[\u{2713}]".into()
} else {
String::new()
}
},
is_section: false,
},
HelpEntry {
key: "s/S",
desc: "By size",
val_fn: |a| {
if a.prefs.sort_mode == SortMode::Size {
"[\u{2713}]".into()
} else {
String::new()
}
},
is_section: false,
},
HelpEntry {
key: "r/R",
desc: "Reverse",
val_fn: |a| {
format!(
"[{}]",
if a.prefs.sort_rev {
"\u{25BC}"
} else {
"\u{25B2}"
}
)
},
is_section: false,
},
HelpEntry {
key: "FILTER",
desc: "",
val_fn: empty_val,
is_section: true,
},
HelpEntry {
key: "/",
desc: "Filter mode",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "0",
desc: "Clear filter",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "NAV",
desc: "",
val_fn: empty_val,
is_section: true,
},
HelpEntry {
key: "j/k",
desc: "Select next/prev",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "G/End",
desc: "Jump to last",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "^G/Home",
desc: "Jump to first",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "^D/^U",
desc: "Half-page dn/up",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "Esc",
desc: "Deselect",
val_fn: empty_val,
is_section: false,
},
];
let col2 = vec![
HelpEntry {
key: "DISPLAY",
desc: "",
val_fn: empty_val,
is_section: true,
},
HelpEntry {
key: "b",
desc: "Bar style",
val_fn: |a| {
format!(
"[{}]",
match a.prefs.bar_style {
BarStyle::Gradient => "grad",
BarStyle::Solid => "solid",
BarStyle::Thin => "thin",
BarStyle::Ascii => "ascii",
}
)
},
is_section: false,
},
HelpEntry {
key: "c",
desc: "Theme chooser",
val_fn: |a| {
let n = if let Some(ref t) = a.prefs.active_theme {
t.clone()
} else {
a.prefs.color_mode.name().into()
};
format!("[{}]", n)
},
is_section: false,
},
HelpEntry {
key: "C",
desc: "Theme editor",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "v/V",
desc: "Toggle bars",
val_fn: |a| format!("[{}]", if a.prefs.show_bars { "on" } else { "off" }),
is_section: false,
},
HelpEntry {
key: "d/D",
desc: "Used/size cols",
val_fn: |a| format!("[{}]", if a.prefs.show_used { "on" } else { "off" }),
is_section: false,
},
HelpEntry {
key: "g",
desc: "Col headers",
val_fn: |a| format!("[{}]", if a.prefs.show_header { "on" } else { "off" }),
is_section: false,
},
HelpEntry {
key: "x/X",
desc: "Border",
val_fn: |a| format!("[{}]", if a.prefs.show_border { "on" } else { "off" }),
is_section: false,
},
HelpEntry {
key: "m/M",
desc: "Compact mounts",
val_fn: |a| format!("[{}]", if a.prefs.compact { "on" } else { "off" }),
is_section: false,
},
HelpEntry {
key: "w/W",
desc: "Full paths",
val_fn: |a| format!("[{}]", if a.prefs.full_mount { "on" } else { "off" }),
is_section: false,
},
HelpEntry {
key: "i/I",
desc: "Cycle units",
val_fn: |a| {
format!(
"[{}]",
match a.prefs.unit_mode {
UnitMode::Human => "human",
UnitMode::GiB => "GiB",
UnitMode::MiB => "MiB",
UnitMode::Bytes => "B",
}
)
},
is_section: false,
},
HelpEntry {
key: "t",
desc: "Warn threshold",
val_fn: |a| format!("[{}%]", a.prefs.thresh_warn),
is_section: false,
},
HelpEntry {
key: "T",
desc: "Crit threshold",
val_fn: |a| format!("[{}%]", a.prefs.thresh_crit),
is_section: false,
},
];
let col3 = vec![
HelpEntry {
key: "ACTIONS",
desc: "",
val_fn: empty_val,
is_section: true,
},
HelpEntry {
key: "Enter",
desc: "Drill down",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "o/O",
desc: "Open in finder",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "y/Y",
desc: "Copy path",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "e/E",
desc: "Export to file",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "B",
desc: "Bookmark \u{2605}",
val_fn: |a| format!("[{}]", a.prefs.bookmarks.len()),
is_section: false,
},
HelpEntry {
key: "DRILL DOWN",
desc: "",
val_fn: empty_val,
is_section: true,
},
HelpEntry {
key: "Enter",
desc: "Into subdir",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "Bksp",
desc: "Up one level",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "Esc",
desc: "Back to disks",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "s/n",
desc: "Sort size/name",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "r",
desc: "Reverse sort",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "o/O",
desc: "Open directory",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "MOUSE",
desc: "",
val_fn: empty_val,
is_section: true,
},
HelpEntry {
key: "Click",
desc: "Select row",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "Click\u{00D7}2",
desc: "Drill into",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "Drag",
desc: "Resize cols",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "R-Click",
desc: "Mount tooltip",
val_fn: empty_val,
is_section: false,
},
HelpEntry {
key: "Hover",
desc: "Bar tooltip",
val_fn: empty_val,
is_section: false,
},
];
let columns = [col1, col2, col3];
let col_w = ((box_w as usize).saturating_sub(4)) / 3;
for (ci, col_entries) in columns.iter().enumerate() {
let cx = x0 + 2 + (ci as u16) * col_w as u16;
for (ri, entry) in col_entries.iter().enumerate() {
let ey = y0 + 4 + ri as u16;
if ey >= y0 + box_h - 1 {
break;
}
if entry.is_section {
set_str(buf, cx, ey, entry.key, section_s, col_w as u16);
} else {
set_str(buf, cx, ey, entry.key, key_s, 8);
set_str(buf, cx + 9, ey, entry.desc, bg_s, 15);
let val = (entry.val_fn)(app);
if !val.is_empty() {
let vx = cx + 25;
let vw = col_w.saturating_sub(26);
set_str(buf, vx, ey, &val, val_s, vw as u16);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::layout::Rect;
#[test]
fn palette_returns_six_colors() {
for &mode in ColorMode::ALL {
let (a, b, c, d, e, f) = palette(mode);
assert_ne!(a, Color::Reset);
assert_ne!(b, Color::Reset);
assert_ne!(c, Color::Reset);
assert_ne!(d, Color::Reset);
assert_ne!(e, Color::Reset);
assert_ne!(f, Color::Reset);
}
}
#[test]
fn palette_modes_differ() {
let default = palette(ColorMode::Default);
let green = palette(ColorMode::Green);
assert_ne!(default.0, green.0);
}
#[test]
fn set_cell_within_bounds() {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 5));
set_cell(&mut buf, 3, 2, "X", Style::default());
assert_eq!(buf[(3, 2)].symbol(), "X");
}
#[test]
fn set_cell_out_of_bounds_no_panic() {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 5));
set_cell(&mut buf, 100, 100, "X", Style::default());
}
#[test]
fn set_str_writes_string() {
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 5));
set_str(&mut buf, 0, 0, "Hello", Style::default(), 20);
assert_eq!(buf[(0, 0)].symbol(), "H");
assert_eq!(buf[(1, 0)].symbol(), "e");
assert_eq!(buf[(4, 0)].symbol(), "o");
}
#[test]
fn set_str_truncates_at_max_w() {
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 5));
set_str(&mut buf, 0, 0, "Hello World", Style::default(), 5);
assert_eq!(buf[(4, 0)].symbol(), "o");
assert_eq!(buf[(5, 0)].symbol(), " "); }
#[test]
fn gradient_color_at_returns_colors() {
for &mode in ColorMode::ALL {
let c0 = gradient_color_at(0.0, mode);
let c50 = gradient_color_at(0.5, mode);
let c90 = gradient_color_at(0.9, mode);
assert_ne!(c0, Color::Reset);
assert_ne!(c50, Color::Reset);
assert_ne!(c90, Color::Reset);
}
}
#[test]
fn border_color_paused_vs_normal() {
use crate::app::App;
use std::sync::{Arc, Mutex};
let shared = Arc::new(Mutex::new((SysStats::default(), vec![])));
let mut app = App::new_default(shared);
let normal = border_color(&app);
app.paused = true;
let paused = border_color(&app);
assert_ne!(normal, paused);
assert_eq!(paused, DIM_BORDER);
}
#[test]
fn thresh_color_levels() {
use crate::app::App;
use std::sync::{Arc, Mutex};
let shared = Arc::new(Mutex::new((SysStats::default(), vec![])));
let app = App::new_default(shared);
let (_, bg_low, icon_low) = thresh_color(30.0, &app);
assert!(bg_low.is_none());
assert_eq!(icon_low, "\u{25C8}");
let (_, bg_warn, icon_warn) = thresh_color(75.0, &app);
assert!(bg_warn.is_some());
assert_eq!(icon_warn, "\u{26A0}");
let (_, bg_crit, icon_crit) = thresh_color(95.0, &app);
assert!(bg_crit.is_some());
assert_eq!(icon_crit, "\u{2716}");
}
#[test]
fn gradient_color_at_zero() {
for &mode in ColorMode::ALL {
let c = gradient_color_at(0.0, mode);
assert_ne!(c, Color::Reset);
}
}
#[test]
fn gradient_color_at_one() {
for &mode in ColorMode::ALL {
let c = gradient_color_at(1.0, mode);
assert_ne!(c, Color::Reset);
}
}
#[test]
fn gradient_color_at_half() {
for &mode in ColorMode::ALL {
let c = gradient_color_at(0.5, mode);
assert_ne!(c, Color::Reset);
}
}
#[test]
fn gradient_color_at_near_zero() {
let c = gradient_color_at(0.001, ColorMode::Default);
assert_ne!(c, Color::Reset);
}
#[test]
fn gradient_color_at_near_one() {
let c = gradient_color_at(0.999, ColorMode::Default);
assert_ne!(c, Color::Reset);
}
#[test]
fn all_palettes_return_six() {
for &mode in ColorMode::ALL {
let (a, b, c, d, e, f) = palette(mode);
for color in [a, b, c, d, e, f] {
assert_ne!(color, Color::Reset, "palette({:?}) returned Reset", mode);
}
}
}
#[test]
fn set_cell_at_origin() {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 5));
set_cell(&mut buf, 0, 0, "A", Style::default());
assert_eq!(buf[(0, 0)].symbol(), "A");
}
#[test]
fn set_cell_at_max_valid() {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 5));
set_cell(&mut buf, 9, 4, "Z", Style::default());
assert_eq!(buf[(9, 4)].symbol(), "Z");
}
#[test]
fn set_cell_just_out_of_bounds_no_panic() {
let mut buf = Buffer::empty(Rect::new(0, 0, 10, 5));
set_cell(&mut buf, 10, 5, "X", Style::default()); }
#[test]
fn set_str_empty_string() {
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 5));
set_str(&mut buf, 0, 0, "", Style::default(), 20);
assert_eq!(buf[(0, 0)].symbol(), " "); }
#[test]
fn set_str_max_w_zero() {
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 5));
set_str(&mut buf, 0, 0, "Hello", Style::default(), 0);
assert_eq!(buf[(0, 0)].symbol(), " "); }
#[test]
fn set_str_at_buffer_edge() {
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
set_str(&mut buf, 3, 0, "Hello", Style::default(), 10);
assert_eq!(buf[(3, 0)].symbol(), "H");
assert_eq!(buf[(4, 0)].symbol(), "e");
}
#[test]
fn thresh_color_at_exact_warn() {
use crate::app::App;
use std::sync::{Arc, Mutex};
let shared = Arc::new(Mutex::new((SysStats::default(), vec![])));
let mut app = App::new_default(shared);
app.prefs.thresh_warn = 70;
let (_, bg, icon) = thresh_color(70.0, &app);
assert!(bg.is_some());
assert_eq!(icon, "\u{26A0}");
}
#[test]
fn thresh_color_at_exact_crit() {
use crate::app::App;
use std::sync::{Arc, Mutex};
let shared = Arc::new(Mutex::new((SysStats::default(), vec![])));
let mut app = App::new_default(shared);
app.prefs.thresh_crit = 90;
let (_, bg, icon) = thresh_color(90.0, &app);
assert!(bg.is_some());
assert_eq!(icon, "\u{2716}");
}
#[test]
fn thresh_color_just_below_warn() {
use crate::app::App;
use std::sync::{Arc, Mutex};
let shared = Arc::new(Mutex::new((SysStats::default(), vec![])));
let mut app = App::new_default(shared);
app.prefs.thresh_warn = 70;
let (_, bg, icon) = thresh_color(69.9, &app);
assert!(bg.is_none());
assert_eq!(icon, "\u{25C8}");
}
#[test]
fn thresh_color_at_zero() {
use crate::app::App;
use std::sync::{Arc, Mutex};
let shared = Arc::new(Mutex::new((SysStats::default(), vec![])));
let app = App::new_default(shared);
let (_, bg, icon) = thresh_color(0.0, &app);
assert!(bg.is_none());
assert_eq!(icon, "\u{25C8}");
}
#[test]
fn thresh_color_at_hundred() {
use crate::app::App;
use std::sync::{Arc, Mutex};
let shared = Arc::new(Mutex::new((SysStats::default(), vec![])));
let app = App::new_default(shared);
let (_, bg, icon) = thresh_color(100.0, &app);
assert!(bg.is_some());
assert_eq!(icon, "\u{2716}");
}
#[test]
fn border_color_different_modes() {
use crate::app::App;
use std::sync::{Arc, Mutex};
let shared = Arc::new(Mutex::new((SysStats::default(), vec![])));
let mut app = App::new_default(shared);
for &mode in ColorMode::ALL {
app.prefs.color_mode = mode;
app.paused = false;
let c = border_color(&app);
assert_ne!(
c,
Color::Reset,
"border_color with {:?} returned Reset",
mode
);
}
}
}