use std::io::Write;
use crossterm::{queue, style::Color, style::Print};
use crate::device::MemoryInfo;
use crate::ui::text::print_colored_text;
use crate::ui::widgets::{BarSegment, draw_bar_multi};
#[allow(dead_code)]
pub struct MemoryRenderer;
impl Default for MemoryRenderer {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
impl MemoryRenderer {
pub fn new() -> Self {
Self
}
}
fn format_hostname_with_scroll(hostname: &str, scroll_offset: usize) -> String {
if hostname.len() > 9 {
let scroll_len = hostname.len() + 3;
let start_pos = scroll_offset % scroll_len;
let extended_hostname = format!("{hostname} {hostname}");
extended_hostname
.chars()
.skip(start_pos)
.take(9)
.collect::<String>()
} else {
format!("{hostname:<9}")
}
}
pub fn print_memory_info<W: Write>(
stdout: &mut W,
_index: usize,
info: &MemoryInfo,
width: usize,
hostname_scroll_offset: usize,
show_hostname: bool,
) {
let total_gb = info.total_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
let used_gb = info.used_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
let available_gb = info.available_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
print_colored_text(stdout, "Host Memory ", Color::Cyan, None, None);
if show_hostname {
let hostname_display = format_hostname_with_scroll(&info.hostname, hostname_scroll_offset);
print_colored_text(stdout, " @ ", Color::DarkGreen, None, None);
print_colored_text(stdout, &hostname_display, Color::White, None, None);
}
print_colored_text(stdout, " Total:", Color::Green, None, None);
print_colored_text(
stdout,
&format!("{total_gb:>6.0}GB"),
Color::White,
None,
None,
);
print_colored_text(stdout, " Used:", Color::Red, None, None);
print_colored_text(
stdout,
&format!("{used_gb:>6.1}GB"),
Color::White,
None,
None,
);
print_colored_text(stdout, " Avail:", Color::Green, None, None);
print_colored_text(
stdout,
&format!("{available_gb:>6.1}GB"),
Color::White,
None,
None,
);
print_colored_text(stdout, " Util:", Color::Magenta, None, None);
print_colored_text(
stdout,
&format!("{:>5.1}%", info.utilization),
Color::White,
None,
None,
);
queue!(stdout, Print("\r\n")).unwrap();
let available_width = width.saturating_sub(10); let gauge_width = available_width;
let total_gauge_width = gauge_width;
let left_padding = 5;
let right_padding = width
.saturating_sub(left_padding)
.saturating_sub(total_gauge_width);
print_colored_text(stdout, " ", Color::White, None, None);
let mut segments = Vec::new();
let actual_used_bytes = info
.used_bytes
.saturating_sub(info.buffers_bytes + info.cached_bytes);
let actual_used_gb = actual_used_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
let buffers_gb = info.buffers_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
let cached_gb = info.cached_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
if actual_used_bytes > 0 {
segments.push(BarSegment::memory_used(actual_used_gb));
}
if info.buffers_bytes > 0 {
segments.push(BarSegment::memory_buffers(buffers_gb));
}
if info.cached_bytes > 0 {
segments.push(BarSegment::memory_cache(cached_gb));
}
let total_used_gb = actual_used_gb + buffers_gb + cached_gb;
let display_text = format!("{total_used_gb:.1}GB");
draw_bar_multi(
stdout,
"Mem",
&segments,
total_gb,
gauge_width,
Some(display_text),
);
print_colored_text(stdout, &" ".repeat(right_padding), Color::White, None, None);
queue!(stdout, Print("\r\n")).unwrap();
if info.swap_total_bytes > 0 {
print_swap_bar(stdout, info, width, gauge_width, left_padding);
}
}
fn print_swap_bar<W: Write>(
stdout: &mut W,
info: &MemoryInfo,
width: usize,
gauge_width: usize,
left_padding: usize,
) {
let swap_total_gb = info.swap_total_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
let swap_used_gb = info.swap_used_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
let right_padding = width
.saturating_sub(left_padding)
.saturating_sub(gauge_width);
print_colored_text(stdout, &" ".repeat(left_padding), Color::White, None, None);
let mut segments = Vec::new();
if info.swap_used_bytes > 0 {
segments.push(BarSegment::swap_used_active(swap_used_gb));
}
let display_text = format!("{swap_used_gb:.1}GB");
draw_bar_multi(
stdout,
"Swap",
&segments,
swap_total_gb,
gauge_width,
Some(display_text),
);
print_colored_text(stdout, &" ".repeat(right_padding), Color::White, None, None);
queue!(stdout, Print("\r\n")).unwrap();
}
#[cfg(test)]
mod tests {
use super::*;
use crate::device::MemoryInfo;
fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\x1b' {
if chars.next() == Some('[') {
for nc in chars.by_ref() {
if nc.is_ascii_alphabetic() {
break;
}
}
}
} else {
out.push(c);
}
}
out
}
fn foreground_sgr(color: Color) -> String {
let mut buf: Vec<u8> = Vec::new();
queue!(buf, crossterm::style::SetForegroundColor(color)).unwrap();
String::from_utf8(buf).unwrap()
}
fn make_memory_info(hostname: &str) -> MemoryInfo {
MemoryInfo {
index: 0,
host_id: "localhost".to_string(),
hostname: hostname.to_string(),
instance: String::new(),
total_bytes: 16 * 1024 * 1024 * 1024,
used_bytes: 8 * 1024 * 1024 * 1024,
available_bytes: 8 * 1024 * 1024 * 1024,
free_bytes: 4 * 1024 * 1024 * 1024,
buffers_bytes: 1024 * 1024 * 1024,
cached_bytes: 3 * 1024 * 1024 * 1024,
swap_total_bytes: 4 * 1024 * 1024 * 1024,
swap_used_bytes: 512 * 1024 * 1024,
swap_free_bytes: 3584 * 1024 * 1024,
utilization: 50.0,
time: String::new(),
}
}
#[test]
fn test_print_memory_info_with_hostname() {
let info = make_memory_info("myhost");
let mut buf: Vec<u8> = Vec::new();
print_memory_info(&mut buf, 0, &info, 120, 0, true);
let output = String::from_utf8_lossy(&buf);
assert!(output.contains("myhost"));
assert!(!buf.is_empty());
}
#[test]
fn test_print_memory_info_without_hostname() {
let info = make_memory_info("myhost");
let mut buf: Vec<u8> = Vec::new();
print_memory_info(&mut buf, 0, &info, 120, 0, false);
let output = String::from_utf8_lossy(&buf);
assert!(!output.contains("@ myhost"));
assert!(!buf.is_empty());
}
#[test]
fn test_print_memory_info_long_hostname_scrolls() {
let info = make_memory_info("very-long-hostname-value");
let mut buf: Vec<u8> = Vec::new();
print_memory_info(&mut buf, 0, &info, 120, 3, true);
assert!(!buf.is_empty());
}
#[test]
fn test_print_memory_info_narrow_width() {
let info = make_memory_info("host");
let mut buf: Vec<u8> = Vec::new();
print_memory_info(&mut buf, 0, &info, 30, 0, false);
assert!(!buf.is_empty());
}
#[test]
fn test_swap_row_renders_when_swap_total_nonzero() {
let info = make_memory_info("host");
let mut buf: Vec<u8> = Vec::new();
print_memory_info(&mut buf, 0, &info, 120, 0, false);
let visible = strip_ansi(&String::from_utf8_lossy(&buf));
assert!(
visible.contains("Swap"),
"Swap row should appear when swap_total_bytes > 0; got: {visible:?}"
);
let row_count = visible.matches("\r\n").count();
assert!(
row_count >= 3,
"Expected at least 3 newlines (info + Mem + Swap), got {row_count} in: {visible:?}"
);
}
#[test]
fn test_swap_row_hidden_when_swap_total_zero() {
let mut info = make_memory_info("host");
info.swap_total_bytes = 0;
info.swap_used_bytes = 0;
info.swap_free_bytes = 0;
let mut buf: Vec<u8> = Vec::new();
print_memory_info(&mut buf, 0, &info, 120, 0, false);
let visible = strip_ansi(&String::from_utf8_lossy(&buf));
assert!(
!visible.contains("Swap"),
"Swap row should NOT appear when swap_total_bytes == 0; got: {visible:?}"
);
}
#[test]
fn test_swap_row_renders_when_total_present_but_unused() {
let mut info = make_memory_info("host");
info.swap_used_bytes = 0;
info.swap_free_bytes = info.swap_total_bytes;
let mut buf: Vec<u8> = Vec::new();
print_memory_info(&mut buf, 0, &info, 120, 0, false);
let visible = strip_ansi(&String::from_utf8_lossy(&buf));
assert!(
visible.contains("Swap"),
"Swap row should still appear when swap_used_bytes == 0 but swap_total_bytes > 0; got: {visible:?}"
);
assert!(
visible.contains("0.0GB"),
"Idle swap row should show '0.0GB' overlay text; got: {visible:?}"
);
}
#[test]
fn test_swap_row_active_uses_red_color() {
let info = make_memory_info("host");
assert!(info.swap_used_bytes > 0, "fixture precondition");
let mut buf: Vec<u8> = Vec::new();
print_memory_info(&mut buf, 0, &info, 120, 0, false);
let raw = String::from_utf8_lossy(&buf);
let red_sgr = foreground_sgr(Color::Red);
assert!(
raw.contains(&red_sgr),
"Active swap row should be colored red; raw output did not contain {red_sgr:?}"
);
}
#[test]
fn test_swap_row_renders_at_narrow_width() {
let info = make_memory_info("host");
let mut buf: Vec<u8> = Vec::new();
print_memory_info(&mut buf, 0, &info, 30, 0, false);
let visible = strip_ansi(&String::from_utf8_lossy(&buf));
assert!(
visible.contains("Swap"),
"Swap row should appear even at narrow width; got: {visible:?}"
);
}
}