use crate::app_state::AppState;
use crossterm::style::{Color, Stylize};
pub fn generate_help_popup_content(
cols: u16,
rows: u16,
state: &AppState,
is_remote: bool,
) -> String {
let width = cols as usize;
let height = rows as usize;
let mut content = String::new();
for row in 0..height {
let line = match row {
0 => format!("╔{}╗", "═".repeat(width.saturating_sub(2))),
r if r == height - 1 => format!("╚{}╝", "═".repeat(width.saturating_sub(2))),
_ => {
let inner_content = get_row_content(row, width, height, state, is_remote);
let padded_content = pad_content_to_width(inner_content, width.saturating_sub(2));
format!("║{padded_content}║")
}
};
content.push_str(&line);
if row < height - 1 {
content.push('\n');
}
}
content
}
fn pad_content_to_width(content: String, target_width: usize) -> String {
let display_width = calculate_display_width(&content);
if display_width >= target_width {
content
} else {
format!("{content}{}", " ".repeat(target_width - display_width))
}
}
fn calculate_display_width(text: &str) -> usize {
let clean_text = strip_ansi_codes(text);
let mut width = 0;
for ch in clean_text.chars() {
width += char_display_width(ch);
}
width
}
fn char_display_width(ch: char) -> usize {
match ch {
'█' | '╔' | '╗' | '╚' | '╝' | '║' | '═' | '╭' | '╮' | '╰' | '╯' | '│' | '─' | '┌' | '┐'
| '└' | '┘' | '├' | '┤' | '┬' | '┴' | '┼' => 1,
'←' | '→' | '↑' | '↓' => 1,
c if c.is_ascii() => 1,
_ => 1,
}
}
fn get_row_content(
row: usize,
width: usize,
height: usize,
state: &AppState,
is_remote: bool,
) -> String {
let content_width = width.saturating_sub(2);
let title_start = 2;
let title_end = 12;
let shortcuts_start = 14;
let shortcuts_end = height.saturating_sub(16); let terminal_start = shortcuts_end + 1;
if row >= title_start && row <= title_end {
render_title_section(row - title_start, content_width)
} else if row >= shortcuts_start && row < shortcuts_end {
render_shortcuts_section(row - shortcuts_start, content_width, state, is_remote)
} else if row >= terminal_start && row < height - 1 {
render_terminal_section(row - terminal_start, content_width)
} else {
" ".repeat(content_width)
}
}
fn render_title_section(line_idx: usize, width: usize) -> String {
let title_lines = [
"",
" █████╗ ██╗ ██╗ ███████╗███╗ ███╗██╗",
" ██╔══██╗██║ ██║ ██╔════╝████╗ ████║██║",
" ███████║██║ ██║ █████╗███████╗██╔████╔██║██║",
" ██╔══██║██║ ██║ ╚════╝╚════██║██║╚██╔╝██║██║",
" ██║ ██║███████╗███████╗ ███████║██║ ╚═╝ ██║██║",
" ╚═╝ ╚═╝╚══════╝╚══════╝ ╚══════╝╚═╝ ╚═╝╚═╝",
"",
"GPU Monitoring and Management Tool",
"",
];
let description_lines = ["Developed and maintained as part of the Backend.AI project."];
if line_idx < title_lines.len() {
center_text_colored(title_lines[line_idx], width, Color::Cyan)
} else if line_idx < title_lines.len() + description_lines.len() {
center_text_colored(
description_lines[line_idx - title_lines.len()],
width,
Color::Green,
)
} else {
" ".repeat(width)
}
}
fn render_shortcuts_section(
line_idx: usize,
width: usize,
state: &AppState,
is_remote: bool,
) -> String {
let mut left_column = vec![
("Navigation Keys:", "", "header"),
(
" ← →",
"Switch tabs (remote) / Scroll process list (local)",
"shortcut",
),
(" ↑ ↓", "Scroll up/down in lists", "shortcut"),
(" PgUp PgDn", "Page up/down navigation", "shortcut"),
(" Home End", "Jump to top/bottom", "shortcut"),
("", "", ""),
("Display Control:", "", "header"),
(" H", "Toggle this help screen", "shortcut"),
(" F", "Toggle GPU process filter", "shortcut"),
(
" /",
"Open filter query bar (temp>85, host~=dgx, ...)",
"shortcut",
),
(
" Ctrl-R",
"Recall a previous filter query (while editing)",
"shortcut",
),
(" A", "Toggle alert history panel", "shortcut"),
(
" R",
"Reset energy session counter (keeps Prometheus total)",
"shortcut",
),
(" V", "Jump to cluster-wide Users tab (remote)", "shortcut"),
(" T", "Jump to Topology tab (remote/replay)", "shortcut"),
(" Q", "Exit application", "shortcut"),
(" ESC", "Close help / clear filter / exit", "shortcut"),
("", "", ""),
("Data Sorting:", "", "header"),
(" D", "Sort by default (hostname+index)", "shortcut"),
(" U", "Sort by GPU utilization", "shortcut"),
(" G", "Sort by GPU memory usage", "shortcut"),
];
if !is_remote {
left_column.extend(vec![
(" P", "Sort processes by PID", "shortcut"),
(" C", "Sort processes by CPU%", "shortcut"),
(" M", "Sort processes by memory", "shortcut"),
]);
}
if is_remote || state.replay.is_some() {
left_column.extend(vec![
("", "", ""),
("Users tab (V):", "", "header"),
(" u", "Sort by username (default)", "shortcut"),
(" m", "Sort by total GPU memory", "shortcut"),
(" p", "Sort by total power (derived)", "shortcut"),
(" n", "Sort by node count", "shortcut"),
(" t", "Sort by oldest process start time", "shortcut"),
(" Enter", "Drill down on the selected user", "shortcut"),
(
" ESC",
"Exit drill-down (or close if top-level)",
"shortcut",
),
(" f", "Toggle system-account filter (uid<1000)", "shortcut"),
(" e", "Export visible table to CSV", "shortcut"),
]);
left_column.extend(vec![
("", "", ""),
("Topology tab (T):", "", "header"),
(
" M",
"Toggle between graph and matrix render modes",
"shortcut",
),
]);
}
if state.replay.is_some() {
left_column.extend(vec![
("", "", ""),
("Replay Controls (--replay):", "", "header"),
(" SPACE", "Play / pause", "shortcut"),
(" ] / [", "Step one frame forward / back", "shortcut"),
(" + / -", "Cycle speed (0.25x–8x)", "shortcut"),
(" j / k", "Seek -10s / +10s", "shortcut"),
(" g", "Open timecode editor (HH:MM:SS)", "shortcut"),
(" L", "Toggle loop playback", "shortcut"),
]);
}
left_column.extend(vec![
("", "", ""),
("Process View Columns:", "", "header"),
(" PID", "Process ID", "legend"),
(" USER", "Process owner", "legend"),
(" PRI", "Priority (0-139, lower is higher)", "legend"),
(" NI", "Nice value (-20 to 19)", "legend"),
(" VIRT", "Virtual memory size", "legend"),
(" RES", "Resident memory size", "legend"),
(" S", "Process state (R/S/D/Z/T)", "legend"),
(" CPU%", "CPU utilization", "legend"),
(" MEM%", "Memory utilization", "legend"),
(" GPU%", "GPU utilization (if available)", "legend"),
(" VRAM", "GPU memory usage", "legend"),
(" TIME+", "Total CPU time used", "legend"),
(" Command", "Command line (← → to scroll)", "legend"),
]);
let mut right_column = vec![
("Process Color Legend:", "", "header"),
(" Your processes", "White text", "legend"),
(" Root/unknown", "Dark grey text", "legend"),
(" High usage", "Red/Yellow based on CPU/Memory %", "legend"),
(
" GPU processes",
"Green/Cyan based on system load",
"legend",
),
("", "", ""),
("Resource Gauge Legend:", "", "header"),
(
" Memory gauge:",
"[used/buffers/cache used%]",
"membar",
),
(
" Swap gauge:",
"[swap GB] (red = active)",
"swapbar",
),
("", "", ""),
("Energy Session:", "", "header"),
(
" Shows",
"kWh accumulated since session start, avg W, est. cost",
"legend",
),
(" R key", "Resets the kWh/cost session counter", "legend"),
(
" Price",
"$/kWh from [energy] TOML or ALL_SMI_ENERGY_PRICE",
"legend",
),
(
" Currency",
"ALL_SMI_ENERGY_CURRENCY (default USD)",
"legend",
),
(
" Hide cost",
"ALL_SMI_ENERGY_NO_COST=1 (kWh still shown)",
"legend",
),
("", "", ""),
("Current Status:", "", "header"),
];
let sort_status = get_current_sort_status(&state.sort_criteria);
right_column.push((" Sort mode:", &sort_status, "status"));
let filter_status = if state.gpu_filter_enabled {
"GPU Only"
} else {
"All Processes"
};
right_column.push((" Filter:", filter_status, "status"));
match line_idx {
0 => center_text_colored("KEYBOARD SHORTCUTS & NAVIGATION", width, Color::Yellow),
1 => "═".repeat(width).with(Color::DarkGrey).to_string(),
2 => " ".repeat(width),
_ => {
let content_line = line_idx - 3; let column_width = (width - 4) / 2;
let left_content = if content_line < left_column.len() {
let (key, desc, style) = &left_column[content_line];
format_shortcut_line(key, desc, style, column_width)
} else {
" ".repeat(column_width)
};
let right_content = if content_line < right_column.len() {
let (key, desc, style) = &right_column[content_line];
format_shortcut_line(key, desc, style, column_width)
} else {
" ".repeat(column_width)
};
format!("{left_content} │ {right_content}")
}
}
}
fn render_terminal_section(line_idx: usize, width: usize) -> String {
let terminal_lines = vec![
("", "TERMINAL USAGE OPTIONS", "title"),
("", "", "separator"),
("", "", ""),
("Local Monitoring:", "", "header"),
(" all-smi", "Monitor local GPUs (default mode)", "command"),
("", "", ""),
("Remote Monitoring:", "", "header"),
(
" all-smi view --hosts http://node1:9090",
"Monitor specific remote hosts",
"command",
),
(
" all-smi view --hostfile hosts.csv",
"Monitor hosts from CSV file",
"command",
),
("", "", ""),
("Agentless SSH:", "", "header"),
(
" all-smi view --ssh user@dgx-01,user@dgx-02",
"Connect over SSH (no agent install)",
"command",
),
(
" all-smi view --ssh-hostfile hosts-ssh.txt",
"SSH targets from a hostfile",
"command",
),
(
" --ssh-fallback nvidia-smi,rocm-smi",
"Fallback probes when all-smi not present",
"command",
),
("", "", ""),
("API Server Mode:", "", "header"),
(
" all-smi api --port 9090",
"Run as Prometheus metrics server",
"command",
),
(
" curl http://localhost:9090/metrics",
"Fetch Prometheus metrics via HTTP",
"command",
),
];
if line_idx < terminal_lines.len() {
let (cmd, desc, style) = &terminal_lines[line_idx];
format_terminal_line(cmd, desc, style, width)
} else {
" ".repeat(width)
}
}
fn format_shortcut_line(key: &str, desc: &str, style: &str, width: usize) -> String {
let content = match style {
"title" => center_text_colored(desc, width, Color::Yellow),
"separator" => "═".repeat(width).with(Color::DarkGrey).to_string(),
"header" => format!(" {}", key.green()),
"shortcut" => {
if key.is_empty() {
String::new()
} else {
let key_str = key.white().bold().to_string();
let desc_str = desc.white().to_string();
let key_display_width = calculate_display_width(&key_str) + 1; let available_desc_width = width.saturating_sub(key_display_width + 2); let truncated_desc = if calculate_display_width(&desc_str) > available_desc_width {
let mut truncated = String::new();
let mut current_width = 0;
for ch in desc.chars() {
let ch_width = char_display_width(ch);
if current_width + ch_width + 3 > available_desc_width {
truncated.push_str("...");
break;
}
truncated.push(ch);
current_width += ch_width;
}
truncated
} else {
desc.to_string()
};
format!(" {key_str:<10} {}", truncated_desc.white())
}
}
"legend" => {
let available_desc_width = width.saturating_sub(12); let truncated_desc = if desc.len() > available_desc_width {
format!("{}...", &desc[..available_desc_width.saturating_sub(3)])
} else {
desc.to_string()
};
format!(" {key:<10} {}", truncated_desc.white())
}
"status" => {
let key_str = key.cyan().to_string();
let desc_str = desc.yellow().to_string();
format!(" {key_str:<10} {desc_str}")
}
"membar" => {
let colored_desc = desc
.replace("used", &"used".green().to_string())
.replace("buffers", &"buffers".blue().to_string())
.replace("cache", &"cache".yellow().to_string());
format!(" {key:<10} {colored_desc}")
}
"swapbar" => {
let colored_desc = desc.replace("swap", &"swap".red().to_string());
format!(" {key:<10} {colored_desc}")
}
_ => String::new(),
};
pad_content_to_width(content, width)
}
fn format_terminal_line(cmd: &str, desc: &str, style: &str, width: usize) -> String {
let content = match style {
"title" => center_text_colored(desc, width, Color::Magenta),
"separator" => "═".repeat(width).with(Color::DarkGrey).to_string(),
"header" => format!(" {}", cmd.green()),
"command" => {
if cmd.is_empty() {
String::new()
} else {
let formatted_cmd = format!(" {:<35}", cmd.white().bold().to_string());
let formatted_seperator = "#".with(Color::DarkGrey).to_string();
let formatted_desc = desc.blue().to_string();
format!("{formatted_cmd} {formatted_seperator} {formatted_desc}")
}
}
_ => String::new(),
};
pad_content_to_width(content, width)
}
fn center_text_colored(text: &str, width: usize, color: Color) -> String {
let display_width = calculate_display_width(text);
if display_width >= width {
text.to_string()
} else {
let total_padding = width - display_width;
let left_padding = total_padding / 2;
let right_padding = total_padding - left_padding;
format!(
"{}{}{}",
" ".repeat(left_padding),
text.with(color),
" ".repeat(right_padding)
)
}
}
fn strip_ansi_codes(text: &str) -> String {
let mut result = String::new();
let mut chars = text.chars();
while let Some(ch) = chars.next() {
if ch == '\x1b' {
if chars.next() == Some('[') {
for c in chars.by_ref() {
if c.is_ascii_alphabetic() {
break;
}
}
}
} else {
result.push(ch);
}
}
result
}
fn get_current_sort_status(sort_criteria: &crate::app_state::SortCriteria) -> String {
match sort_criteria {
crate::app_state::SortCriteria::Default => "Default (hostname+index)",
crate::app_state::SortCriteria::Pid => "Process PID",
crate::app_state::SortCriteria::User => "User",
crate::app_state::SortCriteria::Priority => "Priority",
crate::app_state::SortCriteria::Nice => "Nice Value",
crate::app_state::SortCriteria::VirtualMemory => "Virtual Memory",
crate::app_state::SortCriteria::ResidentMemory => "Resident Memory",
crate::app_state::SortCriteria::State => "Process State",
crate::app_state::SortCriteria::CpuPercent => "CPU Usage %",
crate::app_state::SortCriteria::MemoryPercent => "Memory Usage %",
crate::app_state::SortCriteria::GpuPercent => "GPU Usage %",
crate::app_state::SortCriteria::GpuMemoryUsage => "GPU Memory Usage",
crate::app_state::SortCriteria::CpuTime => "CPU Time",
crate::app_state::SortCriteria::Command => "Command",
crate::app_state::SortCriteria::Utilization => "GPU Utilization",
crate::app_state::SortCriteria::GpuMemory => "GPU Memory",
crate::app_state::SortCriteria::Power => "Power Consumption",
crate::app_state::SortCriteria::Temperature => "Temperature",
}
.to_string()
}
#[cfg(test)]
mod tests {
use super::{render_shortcuts_section, render_terminal_section};
use crate::app_state::AppState;
#[test]
fn local_help_advertises_cpu_sort_shortcut() {
let state = AppState::new();
let shortcuts: String = (0..40)
.map(|line| render_shortcuts_section(line, 118, &state, false))
.collect::<Vec<_>>()
.join("\n");
assert!(
shortcuts.contains("Sort processes by CPU%"),
"local shortcuts must advertise the CPU sort shortcut.\n--- shortcuts ---\n{shortcuts}"
);
}
#[test]
fn remote_help_hides_local_cpu_sort_shortcut() {
let state = AppState::new();
let shortcuts: String = (0..40)
.map(|line| render_shortcuts_section(line, 118, &state, true))
.collect::<Vec<_>>()
.join("\n");
assert!(
!shortcuts.contains("Sort processes by CPU%"),
"remote shortcuts must not advertise the local-only CPU sort shortcut.\n--- shortcuts ---\n{shortcuts}"
);
}
#[test]
fn local_help_omits_obsolete_macos_sudo_command() {
let output = render_terminal_section(0, 120)
+ "\n"
+ &render_terminal_section(1, 120)
+ "\n"
+ &render_terminal_section(2, 120)
+ "\n"
+ &render_terminal_section(3, 120)
+ "\n"
+ &render_terminal_section(4, 120)
+ "\n"
+ &render_terminal_section(5, 120);
assert!(output.contains("Local Monitoring:"));
assert!(output.contains("all-smi"));
assert!(!output.contains("sudo all-smi local"));
assert!(!output.contains("requires sudo on macOS"));
}
}