use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FilesViewMode {
Tree,
Flat,
#[default]
Size,
}
impl FilesViewMode {
#[must_use]
pub fn next(self) -> Self {
match self {
Self::Tree => Self::Flat,
Self::Flat => Self::Size,
Self::Size => Self::Tree,
}
}
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Tree => "tree",
Self::Flat => "flat",
Self::Size => "size",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PanelType {
Cpu,
Memory,
Disk,
Network,
Process,
Gpu,
Battery,
Sensors,
Files,
Connections,
Psi,
Containers,
}
impl PanelType {
pub fn all() -> &'static [Self] {
&[
Self::Cpu,
Self::Memory,
Self::Disk,
Self::Network,
Self::Process,
Self::Gpu,
Self::Battery,
Self::Sensors,
Self::Files,
Self::Connections,
Self::Psi,
Self::Containers,
]
}
pub fn next(self) -> Self {
let all = Self::all();
let idx = all.iter().position(|&p| p == self).unwrap_or(0);
all[(idx + 1) % all.len()]
}
pub fn prev(self) -> Self {
let all = Self::all();
let idx = all.iter().position(|&p| p == self).unwrap_or(0);
if idx == 0 {
all[all.len() - 1]
} else {
all[idx - 1]
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignalType {
Term,
Kill,
Hup,
Int,
Usr1,
Usr2,
Stop,
Cont,
}
impl SignalType {
#[cfg(unix)]
pub fn number(&self) -> i32 {
match self {
SignalType::Term => 15,
SignalType::Kill => 9,
SignalType::Hup => 1,
SignalType::Int => 2,
SignalType::Usr1 => 10,
SignalType::Usr2 => 12,
SignalType::Stop => 19,
SignalType::Cont => 18,
}
}
#[cfg(not(unix))]
pub fn number(&self) -> i32 {
0
}
pub fn name(&self) -> &'static str {
match self {
SignalType::Term => "TERM",
SignalType::Kill => "KILL",
SignalType::Hup => "HUP",
SignalType::Int => "INT",
SignalType::Usr1 => "USR1",
SignalType::Usr2 => "USR2",
SignalType::Stop => "STOP",
SignalType::Cont => "CONT",
}
}
pub fn key(&self) -> char {
match self {
SignalType::Term => 'x',
SignalType::Kill => 'K',
SignalType::Hup => 'H',
SignalType::Int => 'i',
SignalType::Usr1 => '1',
SignalType::Usr2 => '2',
SignalType::Stop => 'p',
SignalType::Cont => 'c',
}
}
pub fn description(&self) -> &'static str {
match self {
SignalType::Term => "Graceful shutdown",
SignalType::Kill => "Force kill (cannot be caught)",
SignalType::Hup => "Reload config / hangup",
SignalType::Int => "Interrupt (like Ctrl+C)",
SignalType::Usr1 => "User signal 1",
SignalType::Usr2 => "User signal 2",
SignalType::Stop => "Pause process",
SignalType::Cont => "Resume paused process",
}
}
pub fn all() -> &'static [SignalType] {
&[
SignalType::Term,
SignalType::Kill,
SignalType::Hup,
SignalType::Int,
SignalType::Usr1,
SignalType::Usr2,
SignalType::Stop,
SignalType::Cont,
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum DetailLevel {
Minimal,
Compact,
Normal,
Expanded,
Exploded,
}
impl DetailLevel {
pub fn for_height(height: u16) -> Self {
match height {
0..=5 => Self::Minimal,
6..=8 => Self::Minimal,
9..=14 => Self::Compact,
15..=19 => Self::Normal,
20..=39 => Self::Expanded,
_ => Self::Exploded, }
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum LayoutType {
#[default]
AdaptiveGrid,
FixedGrid,
Flexbox,
Constraint,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum FocusStyle {
#[default]
DoubleBorder,
HighlightBorder,
Pulse,
BoldTitle,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum HistogramStyle {
#[default]
Braille,
Block,
Ascii,
}
#[derive(Debug, Clone)]
pub struct PanelConfig {
pub enabled: bool,
pub auto_detect: bool,
pub position: Option<(u16, u16)>,
pub span: u16,
pub auto_expand: bool,
pub min_detail: DetailLevel,
pub expansion_priority: u8,
pub histogram: HistogramStyle,
pub show_temperature: bool,
pub show_frequency: bool,
pub max_processes: usize,
pub process_columns: Vec<String>,
pub sparkline_history: u32,
}
impl Default for PanelConfig {
fn default() -> Self {
Self {
enabled: true,
auto_detect: false,
position: None,
span: 1,
auto_expand: true,
min_detail: DetailLevel::Compact,
expansion_priority: 5,
histogram: HistogramStyle::default(),
show_temperature: true,
show_frequency: true,
max_processes: 5,
process_columns: vec![
"pid".into(),
"user".into(),
"cpu".into(),
"mem".into(),
"cmd".into(),
],
sparkline_history: 60,
}
}
}
#[derive(Debug, Clone)]
pub struct LayoutConfig {
pub layout_type: LayoutType,
pub snap_to_grid: bool,
pub grid_size: u16,
pub min_panel_width: u16,
pub min_panel_height: u16,
}
impl Default for LayoutConfig {
fn default() -> Self {
Self {
layout_type: LayoutType::AdaptiveGrid,
snap_to_grid: true,
grid_size: 1, min_panel_width: 20,
min_panel_height: 6,
}
}
}
#[derive(Debug, Clone)]
pub struct ThemeConfig {
pub borders: HashMap<String, String>,
pub background: String,
pub focus_indicator: FocusStyle,
}
impl Default for ThemeConfig {
fn default() -> Self {
let mut borders = HashMap::new();
borders.insert("cpu".into(), "#64C8FF".into());
borders.insert("memory".into(), "#B478FF".into());
borders.insert("disk".into(), "#64B4FF".into());
borders.insert("network".into(), "#FF9664".into());
borders.insert("process".into(), "#DCC464".into());
borders.insert("gpu".into(), "#64FF96".into());
borders.insert("battery".into(), "#FFDC64".into());
borders.insert("sensors".into(), "#FF6496".into());
Self {
borders,
background: "default".into(),
focus_indicator: FocusStyle::DoubleBorder,
}
}
}
#[derive(Debug, Clone)]
pub struct KeybindingConfig {
pub quit: String,
pub help: String,
pub toggle_fps: String,
pub filter: String,
pub sort_cpu: String,
pub sort_mem: String,
pub sort_pid: String,
pub kill_process: String,
pub explode: String,
pub collapse: String,
pub navigate: String,
pub toggle_panel: String,
}
impl Default for KeybindingConfig {
fn default() -> Self {
Self {
quit: "q".into(),
help: "?".into(),
toggle_fps: "f".into(),
filter: "/".into(),
sort_cpu: "c".into(),
sort_mem: "m".into(),
sort_pid: "p".into(),
kill_process: "k".into(),
explode: "Enter".into(),
collapse: "Escape".into(),
navigate: "Tab".into(),
toggle_panel: "1-9".into(),
}
}
}
#[derive(Debug, Clone)]
pub struct PtopConfig {
pub version: String,
pub refresh_ms: u64,
pub layout: LayoutConfig,
pub panels: HashMap<PanelType, PanelConfig>,
pub theme: ThemeConfig,
pub keybindings: KeybindingConfig,
pub last_modified: std::time::SystemTime,
}
impl Default for PtopConfig {
fn default() -> Self {
let mut panels = HashMap::new();
panels.insert(PanelType::Cpu, PanelConfig::default());
panels.insert(PanelType::Memory, PanelConfig::default());
panels.insert(PanelType::Disk, PanelConfig::default());
panels.insert(PanelType::Network, PanelConfig::default());
panels.insert(PanelType::Process, PanelConfig::default());
panels.insert(
PanelType::Gpu,
PanelConfig {
auto_detect: true,
enabled: false, process_columns: vec![
"type".into(), "pid".into(),
"sm".into(),
"mem".into(),
"enc".into(),
"dec".into(),
"cmd".into(),
],
..Default::default()
},
);
panels.insert(
PanelType::Sensors,
PanelConfig {
auto_detect: true,
enabled: false,
..Default::default()
},
);
panels.insert(
PanelType::Connections,
PanelConfig {
auto_detect: true,
enabled: false,
..Default::default()
},
);
panels.insert(
PanelType::Psi,
PanelConfig {
auto_detect: true,
enabled: false,
..Default::default()
},
);
panels.insert(
PanelType::Battery,
PanelConfig {
auto_detect: true,
enabled: false,
..Default::default()
},
);
panels.insert(
PanelType::Files,
PanelConfig {
auto_detect: true,
enabled: false,
..Default::default()
},
);
panels.insert(
PanelType::Containers,
PanelConfig {
auto_detect: true,
enabled: false,
..Default::default()
},
);
Self {
version: "1.0".into(),
refresh_ms: 1000,
layout: LayoutConfig::default(),
panels,
theme: ThemeConfig::default(),
keybindings: KeybindingConfig::default(),
last_modified: std::time::SystemTime::UNIX_EPOCH,
}
}
}
impl PtopConfig {
pub fn config_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
paths.push(PathBuf::from(xdg_config).join("ptop/config.yaml"));
}
if let Ok(home) = std::env::var("HOME") {
paths.push(PathBuf::from(home).join(".config/ptop/config.yaml"));
}
paths
}
pub fn load() -> Self {
for path in Self::config_paths() {
if path.exists() {
if let Ok(contents) = fs::read_to_string(&path) {
if let Some(config) = Self::parse_yaml(&contents) {
return config;
}
}
}
}
Self::default()
}
pub fn load_from_file(path: &std::path::Path) -> Option<Self> {
if path.exists() {
if let Ok(contents) = fs::read_to_string(path) {
return Self::parse_yaml(&contents);
}
}
None
}
pub fn default_yaml() -> String {
r#"# ptop configuration file
# Location: ~/.config/ptop/config.yaml
# Documentation: https://github.com/anthropics/presentar/blob/main/docs/ptop-config.md
# Refresh interval in milliseconds
refresh_ms: 1000
# Layout configuration
layout:
snap_to_grid: true
grid_size: 4
min_panel_width: 30
min_panel_height: 6
# Panel configuration
panels:
cpu:
enabled: true
histogram: braille # braille | block | ascii
show_temperature: true
show_frequency: true
sparkline_history: 60 # seconds
memory:
enabled: true
histogram: braille
disk:
enabled: true
network:
enabled: true
sparkline_history: 60
process:
enabled: true
max_processes: 20
columns:
- pid
- user
- cpu
- mem
- cmd
gpu:
enabled: auto # auto-detect availability
show_temperature: true
show_frequency: true
sensors:
enabled: auto
battery:
enabled: auto
connections:
enabled: true
files:
enabled: true
psi:
enabled: auto # Pressure Stall Information
containers:
enabled: auto # Docker/Podman
# Keybindings (default values shown)
keybindings:
quit: q
help: "?"
toggle_fps: f
filter: "/"
sort_cpu: c
sort_mem: m
sort_pid: p
kill_process: k
explode: Enter
collapse: Escape
# Theme (future - not yet implemented)
# theme:
# cpu_color: "64C8FF"
# memory_color: "B478FF"
"#
.to_string()
}
fn parse_yaml(contents: &str) -> Option<Self> {
let mut config = Self::default();
let mut warnings: Vec<String> = Vec::new();
for line in contents.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once(':') {
let key = key.trim();
let value = value.trim();
match key {
"refresh_ms" => {
if let Ok(ms) = value.parse::<u64>() {
config.refresh_ms = ms;
} else {
warnings.push(format!("Invalid refresh_ms: {value}"));
}
}
"snap_to_grid" => {
config.layout.snap_to_grid = value == "true";
}
"grid_size" => {
if let Ok(size) = value.parse::<u16>() {
config.layout.grid_size = size;
} else {
warnings.push(format!("Invalid grid_size: {value}"));
}
}
"min_panel_width" => {
if let Ok(width) = value.parse::<u16>() {
config.layout.min_panel_width = width;
} else {
warnings.push(format!("Invalid min_panel_width: {value}"));
}
}
"min_panel_height" => {
if let Ok(height) = value.parse::<u16>() {
config.layout.min_panel_height = height;
} else {
warnings.push(format!("Invalid min_panel_height: {value}"));
}
}
"quit" => {
if !value.is_empty() {
config.keybindings.quit = value.to_string();
}
}
"help" => {
if !value.is_empty() {
config.keybindings.help = value.trim_matches('"').to_string();
}
}
"toggle_fps" => {
if !value.is_empty() {
config.keybindings.toggle_fps = value.to_string();
}
}
"filter" => {
if !value.is_empty() {
config.keybindings.filter = value.trim_matches('"').to_string();
}
}
"sort_cpu" => {
if !value.is_empty() {
config.keybindings.sort_cpu = value.to_string();
}
}
"sort_mem" => {
if !value.is_empty() {
config.keybindings.sort_mem = value.to_string();
}
}
"sort_pid" => {
if !value.is_empty() {
config.keybindings.sort_pid = value.to_string();
}
}
"kill_process" => {
if !value.is_empty() {
config.keybindings.kill_process = value.to_string();
}
}
"explode" => {
if !value.is_empty() {
config.keybindings.explode = value.to_string();
}
}
"collapse" => {
if !value.is_empty() {
config.keybindings.collapse = value.to_string();
}
}
"navigate" => {
if !value.is_empty() {
config.keybindings.navigate = value.to_string();
}
}
"toggle_panel" => {
if !value.is_empty() {
config.keybindings.toggle_panel = value.to_string();
}
}
"layout" | "panels" | "keybindings" | "theme" | "version" => {}
_ => {
if !value.is_empty() {
warnings.push(format!("Unknown config field: {key}"));
}
}
}
}
}
for warning in warnings {
eprintln!("[ptop config] warning: {warning}");
}
Some(config)
}
pub fn check_reload(&self) -> Option<Self> {
for path in Self::config_paths() {
if path.exists() {
if let Ok(metadata) = fs::metadata(&path) {
if let Ok(modified) = metadata.modified() {
if modified > self.last_modified {
return Self::load_from_path(&path);
}
}
}
}
}
None
}
fn load_from_path(path: &std::path::Path) -> Option<Self> {
if let Ok(contents) = fs::read_to_string(path) {
let mut config = Self::parse_yaml(&contents)?;
if let Ok(metadata) = fs::metadata(path) {
if let Ok(modified) = metadata.modified() {
config.last_modified = modified;
}
}
Some(config)
} else {
None
}
}
pub fn panel(&self, panel_type: PanelType) -> &PanelConfig {
self.panels.get(&panel_type).unwrap_or_else(|| {
static DEFAULT: PanelConfig = PanelConfig {
enabled: true,
auto_detect: false,
position: None,
span: 1,
auto_expand: true,
min_detail: DetailLevel::Compact,
expansion_priority: 5,
histogram: HistogramStyle::Braille,
show_temperature: true,
show_frequency: true,
max_processes: 5,
process_columns: Vec::new(),
sparkline_history: 60,
};
&DEFAULT
})
}
}
pub fn snap_to_grid(value: u16, grid_size: u16) -> u16 {
if grid_size == 0 || grid_size == 1 {
return value;
}
((value + grid_size / 2) / grid_size) * grid_size
}
pub fn calculate_grid_layout(
panel_count: u32,
width: u16,
height: u16,
config: &LayoutConfig,
) -> Vec<PanelRect> {
if panel_count == 0 {
return Vec::new();
}
let (rows, first_row_count, second_row_count) = if panel_count <= 4 {
(1u32, panel_count as usize, 0usize)
} else {
let first = (panel_count as usize).div_ceil(2);
let second = panel_count as usize - first;
(2u32, first, second)
};
let row_height = height / rows as u16;
let mut rects = Vec::with_capacity(panel_count as usize);
let first_col_width = width / first_row_count as u16;
for i in 0..first_row_count {
let x = snap_to_grid(i as u16 * first_col_width, config.grid_size);
let w = if i == first_row_count - 1 {
width - x } else {
snap_to_grid(first_col_width, config.grid_size)
};
rects.push(PanelRect {
x,
y: 0,
width: w.max(config.min_panel_width),
height: if rows == 1 {
height
} else {
row_height.max(config.min_panel_height)
},
});
}
if second_row_count > 0 {
let second_col_width = width / second_row_count as u16;
for i in 0..second_row_count {
let x = snap_to_grid(i as u16 * second_col_width, config.grid_size);
let w = if i == second_row_count - 1 {
width - x
} else {
snap_to_grid(second_col_width, config.grid_size)
};
rects.push(PanelRect {
x,
y: row_height,
width: w.max(config.min_panel_width),
height: (height - row_height).max(config.min_panel_height),
});
}
}
rects
}
#[derive(Debug, Clone, Copy)]
pub struct PanelRect {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_snap_to_grid() {
assert_eq!(snap_to_grid(10, 1), 10);
assert_eq!(snap_to_grid(10, 8), 8);
assert_eq!(snap_to_grid(12, 8), 16);
assert_eq!(snap_to_grid(15, 8), 16);
assert_eq!(snap_to_grid(0, 8), 0);
}
#[test]
fn test_calculate_grid_layout_single() {
let config = LayoutConfig::default();
let rects = calculate_grid_layout(1, 120, 40, &config);
assert_eq!(rects.len(), 1);
assert_eq!(rects[0].width, 120);
assert_eq!(rects[0].height, 40);
}
#[test]
fn test_calculate_grid_layout_two() {
let config = LayoutConfig::default();
let rects = calculate_grid_layout(2, 120, 40, &config);
assert_eq!(rects.len(), 2);
assert_eq!(rects[0].width, 60);
assert_eq!(rects[1].width, 60);
}
#[test]
fn test_calculate_grid_layout_seven() {
let config = LayoutConfig::default();
let rects = calculate_grid_layout(7, 120, 40, &config);
assert_eq!(rects.len(), 7);
assert_eq!(rects[0].y, 0);
assert_eq!(rects[3].y, 0);
assert_eq!(rects[4].y, 20);
assert_eq!(rects[6].y, 20);
}
#[test]
fn test_panel_type_cycle() {
let cpu = PanelType::Cpu;
assert_eq!(cpu.next(), PanelType::Memory);
assert_eq!(PanelType::Containers.next(), PanelType::Cpu);
assert_eq!(PanelType::Cpu.prev(), PanelType::Containers);
}
#[test]
fn test_detail_level_for_height() {
assert_eq!(DetailLevel::for_height(5), DetailLevel::Minimal);
assert_eq!(DetailLevel::for_height(6), DetailLevel::Minimal);
assert_eq!(DetailLevel::for_height(10), DetailLevel::Compact);
assert_eq!(DetailLevel::for_height(16), DetailLevel::Normal);
assert_eq!(DetailLevel::for_height(25), DetailLevel::Expanded);
assert_eq!(DetailLevel::for_height(39), DetailLevel::Expanded);
assert_eq!(DetailLevel::for_height(40), DetailLevel::Exploded);
assert_eq!(DetailLevel::for_height(80), DetailLevel::Exploded);
}
#[test]
fn test_default_config() {
let config = PtopConfig::default();
assert_eq!(config.refresh_ms, 1000);
assert!(config.layout.snap_to_grid);
assert!(config.panels.get(&PanelType::Cpu).unwrap().enabled);
}
#[test]
fn test_parse_yaml_all_layout_fields() {
let yaml = r#"
refresh_ms: 2000
snap_to_grid: false
grid_size: 4
min_panel_width: 100
min_panel_height: 12
"#;
let config = PtopConfig::parse_yaml(yaml).unwrap();
assert_eq!(config.refresh_ms, 2000);
assert!(!config.layout.snap_to_grid);
assert_eq!(config.layout.grid_size, 4);
assert_eq!(config.layout.min_panel_width, 100);
assert_eq!(config.layout.min_panel_height, 12);
}
#[test]
fn test_parse_yaml_partial_config() {
let yaml = "min_panel_width: 50";
let config = PtopConfig::parse_yaml(yaml).unwrap();
assert_eq!(config.layout.min_panel_width, 50);
assert_eq!(config.refresh_ms, 1000);
assert!(config.layout.snap_to_grid);
}
#[test]
fn test_parse_yaml_invalid_values() {
let yaml = r#"
refresh_ms: not_a_number
min_panel_width: 100
"#;
let config = PtopConfig::parse_yaml(yaml).unwrap();
assert_eq!(config.refresh_ms, 1000);
assert_eq!(config.layout.min_panel_width, 100);
}
#[test]
fn test_parse_yaml_comments_ignored() {
let yaml = r#"
# This is a comment
refresh_ms: 500
# Another comment
min_panel_width: 30
"#;
let config = PtopConfig::parse_yaml(yaml).unwrap();
assert_eq!(config.refresh_ms, 500);
assert_eq!(config.layout.min_panel_width, 30);
}
#[test]
fn test_config_check_reload_returns_none_when_unchanged() {
let config = PtopConfig::default();
assert!(config.check_reload().is_none());
}
#[test]
fn test_signal_type_term() {
assert_eq!(SignalType::Term.name(), "TERM");
assert_eq!(SignalType::Term.key(), 'x');
assert!(SignalType::Term.description().contains("Graceful"));
#[cfg(unix)]
assert_eq!(SignalType::Term.number(), 15);
}
#[test]
fn test_signal_type_kill() {
assert_eq!(SignalType::Kill.name(), "KILL");
assert_eq!(SignalType::Kill.key(), 'K');
assert!(SignalType::Kill.description().contains("Force"));
#[cfg(unix)]
assert_eq!(SignalType::Kill.number(), 9);
}
#[test]
fn test_signal_type_hup() {
assert_eq!(SignalType::Hup.name(), "HUP");
assert_eq!(SignalType::Hup.key(), 'H');
#[cfg(unix)]
assert_eq!(SignalType::Hup.number(), 1);
}
#[test]
fn test_signal_type_int() {
assert_eq!(SignalType::Int.name(), "INT");
assert_eq!(SignalType::Int.key(), 'i');
#[cfg(unix)]
assert_eq!(SignalType::Int.number(), 2);
}
#[test]
fn test_signal_type_usr1() {
assert_eq!(SignalType::Usr1.name(), "USR1");
assert_eq!(SignalType::Usr1.key(), '1');
#[cfg(unix)]
assert_eq!(SignalType::Usr1.number(), 10);
}
#[test]
fn test_signal_type_usr2() {
assert_eq!(SignalType::Usr2.name(), "USR2");
assert_eq!(SignalType::Usr2.key(), '2');
#[cfg(unix)]
assert_eq!(SignalType::Usr2.number(), 12);
}
#[test]
fn test_signal_type_stop() {
assert_eq!(SignalType::Stop.name(), "STOP");
assert_eq!(SignalType::Stop.key(), 'p');
#[cfg(unix)]
assert_eq!(SignalType::Stop.number(), 19);
}
#[test]
fn test_signal_type_cont() {
assert_eq!(SignalType::Cont.name(), "CONT");
assert_eq!(SignalType::Cont.key(), 'c');
#[cfg(unix)]
assert_eq!(SignalType::Cont.number(), 18);
}
#[test]
fn test_signal_type_all() {
let all = SignalType::all();
assert_eq!(all.len(), 8);
assert_eq!(all[0], SignalType::Term);
assert_eq!(all[7], SignalType::Cont);
}
#[test]
fn test_signal_type_debug() {
let sig = SignalType::Kill;
let debug = format!("{:?}", sig);
assert!(debug.contains("Kill"));
}
#[test]
fn test_signal_type_clone() {
let sig = SignalType::Stop;
let cloned = sig.clone();
assert_eq!(sig, cloned);
}
#[test]
fn test_panel_type_all() {
let all = PanelType::all();
assert_eq!(all.len(), 12);
assert_eq!(all[0], PanelType::Cpu);
assert_eq!(all[11], PanelType::Containers);
}
#[test]
fn test_panel_type_debug() {
let panel = PanelType::Memory;
let debug = format!("{:?}", panel);
assert!(debug.contains("Memory"));
}
#[test]
fn test_panel_type_clone() {
let panel = PanelType::Disk;
let cloned = panel.clone();
assert_eq!(panel, cloned);
}
#[test]
fn test_panel_type_hash() {
let mut map = HashMap::new();
map.insert(PanelType::Cpu, "CPU");
map.insert(PanelType::Memory, "MEM");
assert_eq!(map.get(&PanelType::Cpu), Some(&"CPU"));
}
#[test]
fn test_detail_level_ordering() {
assert!(DetailLevel::Minimal < DetailLevel::Compact);
assert!(DetailLevel::Compact < DetailLevel::Normal);
assert!(DetailLevel::Normal < DetailLevel::Expanded);
assert!(DetailLevel::Expanded < DetailLevel::Exploded);
}
#[test]
fn test_detail_level_debug() {
let level = DetailLevel::Normal;
let debug = format!("{:?}", level);
assert!(debug.contains("Normal"));
}
#[test]
fn test_detail_level_clone() {
let level = DetailLevel::Expanded;
let cloned = level.clone();
assert_eq!(level, cloned);
}
#[test]
fn test_layout_type_default() {
let layout = LayoutType::default();
assert_eq!(layout, LayoutType::AdaptiveGrid);
}
#[test]
fn test_layout_type_debug() {
let layout = LayoutType::Flexbox;
let debug = format!("{:?}", layout);
assert!(debug.contains("Flexbox"));
}
#[test]
fn test_focus_style_default() {
let style = FocusStyle::default();
assert_eq!(style, FocusStyle::DoubleBorder);
}
#[test]
fn test_focus_style_debug() {
let style = FocusStyle::Pulse;
let debug = format!("{:?}", style);
assert!(debug.contains("Pulse"));
}
#[test]
fn test_histogram_style_default() {
let style = HistogramStyle::default();
assert_eq!(style, HistogramStyle::Braille);
}
#[test]
fn test_histogram_style_debug() {
let style = HistogramStyle::Block;
let debug = format!("{:?}", style);
assert!(debug.contains("Block"));
}
#[test]
fn test_panel_config_default() {
let config = PanelConfig::default();
assert!(config.enabled);
assert!(!config.auto_detect);
assert_eq!(config.span, 1);
assert!(config.auto_expand);
assert!(config.show_temperature);
assert!(config.show_frequency);
assert_eq!(config.max_processes, 5);
assert_eq!(config.sparkline_history, 60);
}
#[test]
fn test_panel_config_debug() {
let config = PanelConfig::default();
let debug = format!("{:?}", config);
assert!(debug.contains("PanelConfig"));
}
#[test]
fn test_panel_config_clone() {
let config = PanelConfig {
enabled: false,
max_processes: 10,
..Default::default()
};
let cloned = config.clone();
assert!(!cloned.enabled);
assert_eq!(cloned.max_processes, 10);
}
#[test]
fn test_layout_config_default() {
let config = LayoutConfig::default();
assert!(config.snap_to_grid);
assert_eq!(config.grid_size, 1);
assert_eq!(config.min_panel_width, 20);
assert_eq!(config.min_panel_height, 6);
}
#[test]
fn test_layout_config_debug() {
let config = LayoutConfig::default();
let debug = format!("{:?}", config);
assert!(debug.contains("LayoutConfig"));
}
#[test]
fn test_theme_config_default() {
let config = ThemeConfig::default();
assert!(config.borders.contains_key("cpu"));
assert!(config.borders.contains_key("memory"));
assert_eq!(config.background, "default");
assert_eq!(config.focus_indicator, FocusStyle::DoubleBorder);
}
#[test]
fn test_theme_config_debug() {
let config = ThemeConfig::default();
let debug = format!("{:?}", config);
assert!(debug.contains("ThemeConfig"));
}
#[test]
fn test_keybinding_config_default() {
let config = KeybindingConfig::default();
assert_eq!(config.toggle_panel, "1-9");
assert_eq!(config.quit, "q");
}
#[test]
fn test_keybinding_config_debug() {
let config = KeybindingConfig::default();
let debug = format!("{:?}", config);
assert!(debug.contains("KeybindingConfig"));
}
#[test]
fn test_keybinding_has_help_field() {
let config = KeybindingConfig::default();
assert_eq!(config.help, "?");
}
#[test]
fn test_keybinding_has_toggle_fps_field() {
let config = KeybindingConfig::default();
assert_eq!(config.toggle_fps, "f");
}
#[test]
fn test_keybinding_has_filter_field() {
let config = KeybindingConfig::default();
assert_eq!(config.filter, "/");
}
#[test]
fn test_keybinding_has_sort_cpu_field() {
let config = KeybindingConfig::default();
assert_eq!(config.sort_cpu, "c");
}
#[test]
fn test_keybinding_has_sort_mem_field() {
let config = KeybindingConfig::default();
assert_eq!(config.sort_mem, "m");
}
#[test]
fn test_keybinding_has_sort_pid_field() {
let config = KeybindingConfig::default();
assert_eq!(config.sort_pid, "p");
}
#[test]
fn test_keybinding_has_kill_process_field() {
let config = KeybindingConfig::default();
assert_eq!(config.kill_process, "k");
}
#[test]
fn test_keybinding_has_explode_field() {
let config = KeybindingConfig::default();
assert_eq!(config.explode, "Enter");
}
#[test]
fn test_keybinding_has_collapse_field() {
let config = KeybindingConfig::default();
assert_eq!(config.collapse, "Escape");
}
#[test]
fn test_keybinding_yaml_parsing() {
let yaml = r#"
keybindings:
quit: Q
help: F1
filter: s
"#;
let config = PtopConfig::parse_yaml(yaml).unwrap();
assert_eq!(config.keybindings.quit, "Q");
assert_eq!(config.keybindings.help, "F1");
assert_eq!(config.keybindings.filter, "s");
}
#[test]
fn test_ptop_config_panel() {
let config = PtopConfig::default();
let cpu = config.panel(PanelType::Cpu);
assert!(cpu.enabled);
}
#[test]
fn test_ptop_config_panel_unknown() {
let mut config = PtopConfig::default();
config.panels.clear();
let panel = config.panel(PanelType::Cpu);
assert!(panel.enabled); }
#[test]
fn test_ptop_config_debug() {
let config = PtopConfig::default();
let debug = format!("{:?}", config);
assert!(debug.contains("PtopConfig"));
}
#[test]
fn test_ptop_config_default_yaml() {
let yaml = PtopConfig::default_yaml();
assert!(yaml.contains("refresh_ms"));
assert!(yaml.contains("layout"));
assert!(yaml.contains("panels"));
assert!(yaml.contains("keybindings"));
}
#[test]
fn test_ptop_config_paths() {
let paths = PtopConfig::config_paths();
for path in &paths {
assert!(path.to_string_lossy().contains("ptop"));
}
}
#[test]
fn test_ptop_config_load_from_file_nonexistent() {
let result = PtopConfig::load_from_file(std::path::Path::new("/nonexistent/path.yaml"));
assert!(result.is_none());
}
#[test]
fn test_ptop_config_load_defaults_on_missing_file() {
let config = PtopConfig::load();
assert_eq!(config.refresh_ms, 1000);
}
#[test]
fn test_panel_rect_debug() {
let rect = PanelRect {
x: 0,
y: 0,
width: 100,
height: 50,
};
let debug = format!("{:?}", rect);
assert!(debug.contains("PanelRect"));
}
#[test]
fn test_panel_rect_clone() {
let rect = PanelRect {
x: 10,
y: 20,
width: 30,
height: 40,
};
let cloned = rect.clone();
assert_eq!(cloned.x, 10);
assert_eq!(cloned.y, 20);
}
#[test]
fn test_calculate_grid_layout_zero_panels() {
let config = LayoutConfig::default();
let rects = calculate_grid_layout(0, 120, 40, &config);
assert!(rects.is_empty());
}
#[test]
fn test_calculate_grid_layout_five_panels() {
let config = LayoutConfig::default();
let rects = calculate_grid_layout(5, 120, 40, &config);
assert_eq!(rects.len(), 5);
}
#[test]
fn test_snap_to_grid_zero() {
assert_eq!(snap_to_grid(0, 0), 0);
assert_eq!(snap_to_grid(10, 0), 10);
}
#[test]
fn test_parse_yaml_empty() {
let config = PtopConfig::parse_yaml("").unwrap();
assert_eq!(config.refresh_ms, 1000); }
#[test]
fn test_parse_yaml_only_comments() {
let yaml = "# comment\n# another comment\n";
let config = PtopConfig::parse_yaml(yaml).unwrap();
assert_eq!(config.refresh_ms, 1000); }
}