use std::io::Write;
use crossterm::{queue, style::Color, style::Print};
use crate::device::ProcessInfo;
use crate::ui::text::{print_colored_text, truncate_to_width};
#[allow(clippy::too_many_arguments)]
pub fn print_process_info<W: Write>(
stdout: &mut W,
processes: &[ProcessInfo],
selected_index: usize,
start_index: usize,
available_rows: u16,
cols: u16,
horizontal_scroll_offset: usize,
current_user: &str,
sort_criteria: &crate::app_state::SortCriteria,
sort_direction: &crate::app_state::SortDirection,
) {
queue!(stdout, Print("Processes:\r\n")).unwrap();
let width = cols as usize;
let fixed_widths = [7, 12, 3, 3, 6, 6, 1, 5, 5, 5, 7, 8];
let num_gaps = fixed_widths.len(); let fixed_total: usize = fixed_widths.iter().sum::<usize>() + num_gaps;
let _command_w = if width > fixed_total + 20 {
width - fixed_total
} else {
20
};
let (pid_w, user_w, pri_w, ni_w, virt_w, res_w, s_w, cpu_w, mem_w, gpu_w, gpu_mem_w, time_w) = (
fixed_widths[0], fixed_widths[1], fixed_widths[2], fixed_widths[3], fixed_widths[4], fixed_widths[5], fixed_widths[6], fixed_widths[7], fixed_widths[8], fixed_widths[9], fixed_widths[10], fixed_widths[11], );
let get_sort_arrow = |criteria: crate::app_state::SortCriteria| -> &'static str {
if sort_criteria == &criteria {
match sort_direction {
crate::app_state::SortDirection::Ascending => "↑",
crate::app_state::SortDirection::Descending => "↓",
}
} else {
""
}
};
#[allow(clippy::format_in_format_args)]
let header_format = format!(
"{:>pid_w$} {:<user_w$} {:>pri_w$} {:>ni_w$} {:>virt_w$} {:>res_w$} {:<s_w$} {:>cpu_w$} {:>mem_w$} {:>gpu_w$} {:>gpu_mem_w$} {:>time_w$} {}",
format!("PID{}", get_sort_arrow(crate::app_state::SortCriteria::Pid)),
format!("USER{}", get_sort_arrow(crate::app_state::SortCriteria::User)),
format!("PRI{}", get_sort_arrow(crate::app_state::SortCriteria::Priority)),
format!("NI{}", get_sort_arrow(crate::app_state::SortCriteria::Nice)),
format!("VIRT{}", get_sort_arrow(crate::app_state::SortCriteria::VirtualMemory)),
format!("RES{}", get_sort_arrow(crate::app_state::SortCriteria::ResidentMemory)),
format!("S{}", get_sort_arrow(crate::app_state::SortCriteria::State)),
format!("CPU%{}", get_sort_arrow(crate::app_state::SortCriteria::CpuPercent)),
format!("MEM%{}", get_sort_arrow(crate::app_state::SortCriteria::MemoryPercent)),
format!("GPU%{}", get_sort_arrow(crate::app_state::SortCriteria::GpuPercent)),
format!("VRAM{}", get_sort_arrow(crate::app_state::SortCriteria::GpuMemoryUsage)),
format!("TIME+{}", get_sort_arrow(crate::app_state::SortCriteria::CpuTime)),
format!("Command{}", get_sort_arrow(crate::app_state::SortCriteria::Command)),
);
let visible_header = if horizontal_scroll_offset < header_format.len() {
let scrolled = &header_format[horizontal_scroll_offset..];
format!("{:<width$}", truncate_to_width(scrolled, width))
} else {
" ".repeat(width)
};
print_colored_text(stdout, &visible_header, Color::White, None, None);
queue!(stdout, Print("\r\n")).unwrap();
let separator = "─".repeat(width.min(120));
print_colored_text(stdout, &separator, Color::DarkGrey, None, None);
queue!(stdout, Print("\r\n")).unwrap();
let footer_rows = 2usize;
const RESERVED_HEADER_ROWS: usize = 4;
let available_rows_for_processes =
(available_rows as usize).saturating_sub(RESERVED_HEADER_ROWS + footer_rows);
let end_index = (start_index + available_rows_for_processes).min(processes.len());
for i in start_index..end_index {
if let Some(process) = processes.get(i) {
let is_selected = i == selected_index;
let pid = format!("{}", process.pid);
let user = truncate_to_width(&process.user, user_w);
let priority = format!("{}", process.priority);
let nice = format!("{:+}", process.nice_value);
let virt = format_memory_size(process.memory_vms);
let res = format_memory_size(process.memory_rss);
let state = truncate_to_width(&process.state, s_w);
let cpu_percent = format!("{:.1}", process.cpu_percent);
let mem_percent = format!("{:.1}", process.memory_percent);
let gpu_percent = if process.uses_gpu && process.gpu_utilization > 0.0 {
format!("{:.1}", process.gpu_utilization)
} else if process.uses_gpu {
"-".to_string()
} else {
"".to_string()
};
let gpu_mem = if process.used_memory > 0 {
let gpu_mem_mb = process.used_memory as f64 / (1024.0 * 1024.0);
if gpu_mem_mb >= 1024.0 {
format!("{:.1}G", gpu_mem_mb / 1024.0)
} else {
format!("{gpu_mem_mb:.0}M")
}
} else if process.uses_gpu {
"-".to_string()
} else {
"".to_string()
};
let time_plus = format_cpu_time(process.cpu_time);
let command = process.command.clone();
let row_format = format!(
"{pid:>pid_w$} {:<user_w$} {priority:>pri_w$} {nice:>ni_w$} {virt:>virt_w$} {res:>res_w$} {state:<s_w$} {cpu_percent:>cpu_w$} {mem_percent:>mem_w$} {gpu_percent:>gpu_w$} {gpu_mem:>gpu_mem_w$} {time_plus:>time_w$} {command}",
truncate_to_width(&user, user_w),
);
let visible_row = if horizontal_scroll_offset < row_format.len() {
let scrolled = &row_format[horizontal_scroll_offset..];
format!("{:<width$}", truncate_to_width(scrolled, width))
} else {
" ".repeat(width)
};
if is_selected {
print_colored_text(stdout, &visible_row, Color::Black, Some(Color::White), None);
} else {
print_process_row_colored(
stdout,
process,
current_user,
&pid,
&user,
&priority,
&nice,
&virt,
&res,
&state,
&cpu_percent,
&mem_percent,
&gpu_percent,
&gpu_mem,
&time_plus,
&command,
horizontal_scroll_offset,
width,
&fixed_widths,
);
}
queue!(stdout, Print("\r\n")).unwrap();
}
}
let mut lines_used = 3; lines_used += end_index.saturating_sub(start_index);
let total_lines_before_footer = (available_rows as usize).saturating_sub(footer_rows);
while lines_used < total_lines_before_footer {
let clear_line = " ".repeat(width);
queue!(stdout, Print(&clear_line)).unwrap();
queue!(stdout, Print("\r\n")).unwrap();
lines_used += 1;
}
if processes.len() > available_rows_for_processes {
let nav_info = format!(
"Showing {}-{end_index} of {} processes (Use ↑↓ to navigate, PgUp/PgDn for pages)",
start_index + 1,
processes.len()
);
let padded_nav_info = format!("{nav_info:<width$}");
print_colored_text(stdout, &padded_nav_info, Color::DarkGrey, None, None);
queue!(stdout, Print("\r\n")).unwrap();
lines_used += 1;
} else if !processes.is_empty() {
let nav_info = format!("Showing all {} processes", processes.len());
let padded_nav_info = format!("{nav_info:<width$}");
print_colored_text(stdout, &padded_nav_info, Color::DarkGrey, None, None);
queue!(stdout, Print("\r\n")).unwrap();
lines_used += 1;
}
if !processes.is_empty() {
let total_gpu_mem: u64 = processes.iter().map(|p| p.used_memory).sum();
let gpu_mem_gb = total_gpu_mem as f64 / (1024.0 * 1024.0 * 1024.0);
let active_processes = processes.iter().filter(|p| p.cpu_percent > 0.1).count();
let gpu_processes = processes.iter().filter(|p| p.used_memory > 0).count();
let stats = format!(
"Active: {active_processes} | GPU: {gpu_processes} | Total GPU Memory: {gpu_mem_gb:.1}GB"
);
let padded_stats = format!("{stats:<width$}");
print_colored_text(stdout, &padded_stats, Color::Cyan, None, None);
queue!(stdout, Print("\r\n")).unwrap();
lines_used += 1;
}
while lines_used < available_rows as usize {
let clear_line = " ".repeat(width);
queue!(stdout, Print(&clear_line)).unwrap();
queue!(stdout, Print("\r\n")).unwrap();
lines_used += 1;
}
}
fn format_memory_size(bytes: u64) -> String {
if bytes == 0 {
return "0".to_string();
}
let gb = bytes as f64 / (1024.0 * 1024.0 * 1024.0);
let mb = bytes as f64 / (1024.0 * 1024.0);
let kb = bytes as f64 / 1024.0;
if gb >= 1000.0 {
let tb = gb / 1024.0;
format!("{tb:.0}T")
} else if gb >= 1.0 {
format!("{gb:.0}G")
} else if mb >= 1.0 {
format!("{mb:.0}M")
} else if kb >= 1.0 {
format!("{kb:.0}K")
} else {
format!("{bytes}")
}
}
#[allow(clippy::too_many_arguments)]
fn print_process_row_colored<W: Write>(
stdout: &mut W,
process: &ProcessInfo,
current_user: &str,
pid: &str,
user: &str,
priority: &str,
nice: &str,
virt: &str,
res: &str,
state: &str,
cpu_percent: &str,
mem_percent: &str,
gpu_percent: &str,
gpu_mem: &str,
time_plus: &str,
command: &str,
horizontal_scroll_offset: usize,
width: usize,
fixed_widths: &[usize; 12],
) {
let values = vec![
pid,
user,
priority,
nice,
virt,
res,
state,
cpu_percent,
mem_percent,
gpu_percent,
gpu_mem,
time_plus,
command,
];
let mut current_pos = 0;
let mut output_pos = 0;
let is_current_user = process.user == current_user;
let default_color = if process.cpu_percent >= 90.0 || process.memory_percent >= 90.0 {
Color::Red
} else if process.cpu_percent >= 80.0 || process.memory_percent >= 80.0 {
Color::Rgb {
r: 255,
g: 100,
b: 100,
}
} else if process.cpu_percent >= 70.0 || process.memory_percent >= 70.0 {
Color::Yellow
} else if process.cpu_percent >= 50.0 || process.memory_percent >= 50.0 {
Color::Rgb {
r: 255,
g: 200,
b: 0,
}
} else if process.uses_gpu && (process.cpu_percent >= 30.0 || process.memory_percent >= 30.0) {
Color::Cyan
} else if process.uses_gpu {
Color::Green
} else if is_current_user {
Color::White
} else {
Color::DarkGrey
};
for (idx, value) in values.iter().enumerate() {
let col_width = if idx < fixed_widths.len() {
fixed_widths[idx]
} else {
width
.saturating_sub(current_pos)
.saturating_sub(horizontal_scroll_offset)
};
let col_start = current_pos;
let col_end = if idx < fixed_widths.len() {
current_pos + col_width + 1 } else {
current_pos + value.len() };
if col_end > horizontal_scroll_offset && output_pos < width {
let color = match idx {
4 => {
if process.memory_vms == 0 {
Color::White
} else {
Color::Green
}
}
0 => {
if process.pid > 0 {
Color::White
} else {
default_color
}
}
2 => {
if process.priority != 20 {
Color::White
} else {
default_color
}
}
3 => {
if process.nice_value != 0 {
Color::White
} else {
Color::DarkGrey
}
}
5 => {
if process.memory_rss > 0 {
Color::White
} else {
default_color
}
}
7 => {
if process.cpu_percent > 0.0 {
Color::White
} else {
default_color
}
}
8 => {
if process.memory_percent > 0.0 {
Color::White
} else {
default_color
}
}
9 => {
if process.gpu_utilization > 0.0 {
Color::White
} else {
default_color
}
}
10 => {
if process.used_memory > 0 {
Color::White
} else {
default_color
}
}
11 => {
if time_plus != "0:00:00" {
Color::White
} else {
default_color
}
}
_ => default_color, };
let skip = horizontal_scroll_offset.saturating_sub(col_start);
let formatted = if idx < fixed_widths.len() {
match idx {
0 => format!("{value:>col_width$}"), 1 => format!("{:<col_width$}", truncate_to_width(value, col_width)), 2..=11 => format!("{value:>col_width$}"), _ => value.to_string(),
}
} else {
value.to_string() };
if skip < formatted.len() {
let visible_part = &formatted[skip..];
let remaining_width = width.saturating_sub(output_pos);
let to_print = truncate_to_width(visible_part, remaining_width);
print_colored_text(stdout, &to_print, color, None, None);
output_pos += to_print.len();
}
if idx < values.len() - 1 && output_pos < width && col_end > horizontal_scroll_offset {
print_colored_text(stdout, " ", default_color, None, None);
output_pos += 1;
}
}
current_pos = col_end;
}
if output_pos < width {
print_colored_text(
stdout,
&" ".repeat(width - output_pos),
Color::Black,
None,
None,
);
}
}
fn format_cpu_time(seconds: u64) -> String {
if seconds == 0 {
return "0:00:00".to_string();
}
if seconds > 365 * 24 * 3600 {
return "0:00:00".to_string();
}
let hours = seconds / 3600;
let minutes = (seconds % 3600) / 60;
let secs = seconds % 60;
if hours > 0 {
format!("{hours}:{minutes:02}:{secs:02}")
} else {
format!("{}:{:02}:{secs:02}", minutes / 60, minutes % 60)
}
}