#![feature(let_chains)] #![forbid(clippy::all)] use std::{ collections::HashMap, io, sync::Mutex, time::{Duration, Instant}, }; use backend::{EnumCount, IntoEnumIterator}; use crossterm::{ event::{DisableMouseCapture, EnableMouseCapture, Event, KeyCode, ModifierKeyCode, MouseEventKind}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use itertools::Itertools; use tui::{ backend::CrosstermBackend, layout::{Alignment, Constraint, Direction, Layout}, style::{Color, Modifier, Style}, symbols::Marker, text::{Span, Spans}, widgets::{Axis, Block, Borders, Chart, Dataset, GraphType, List, ListItem, ListState, Paragraph, Tabs, Wrap}, Frame, Terminal, }; type DataPoint = (f64, f64); type DataPoints = Vec; #[derive(Copy, Clone, Debug)] enum Ordering { Ascending, Descending, } impl Ordering { fn sort_by(&self) -> impl Fn(T, T) -> std::cmp::Ordering + '_ where T: std::cmp::PartialOrd, { move |a, b| match self { Self::Ascending => a.partial_cmp(&b).unwrap(), Self::Descending => b.partial_cmp(&a).unwrap(), } } } #[derive(Copy, Clone, Debug)] enum SortByProcess { CpuUsage(Ordering), MemoryUsage(Ordering), SwapUsage(Ordering), Runtime(Ordering), } #[derive(Copy, Clone, Debug)] enum SortByComponent { Temperature(Ordering), Critical(Ordering), } static NETWORK_INFO: Mutex> = Mutex::new(None); fn run_app(terminal: &mut Terminal, manager: &mut backend::Manager) { let (sender, receiver) = std::sync::mpsc::channel(); let thread = std::thread::spawn(move || { let mut parallel_manager = backend::Manager::new(); loop { if receiver.try_recv().is_ok() { break; } let network_info_temp = Some(parallel_manager.network_information()); // This temporary must be used otherwise // network_tab blocks on NETWORK_INFO.lock let mut network_info = NETWORK_INFO.lock().unwrap(); *network_info = network_info_temp; } }); let mut current_tab = 0usize; let mut current_line: u16 = 0; let starting_time = Instant::now(); let mut process_ordering = SortByProcess::CpuUsage(Ordering::Descending); let mut component_ordering = SortByComponent::Temperature(Ordering::Descending); let mut shift_pressed = false; let mut kill_current_process = false; let mut cpu_dataset: HashMap = HashMap::new(); loop { let _ = terminal.draw(|f| { ui( f, manager, current_line, current_tab, starting_time, process_ordering, component_ordering, shift_pressed, kill_current_process, &mut cpu_dataset, ) }); shift_pressed = false; kill_current_process = false; if crossterm::event::poll(Duration::from_millis(0)).unwrap() { match crossterm::event::read() { Ok(Event::Key(event)) => match event.code { KeyCode::Char('q') | KeyCode::Esc => { sender.send(()).unwrap(); thread.join().unwrap(); return; } KeyCode::Char(chr) => match chr { 'c' => match current_tab { 6 => process_ordering = SortByProcess::CpuUsage(Ordering::Ascending), 7 => component_ordering = SortByComponent::Critical(Ordering::Ascending), _ => (), }, 'C' => match current_tab { 6 => process_ordering = SortByProcess::CpuUsage(Ordering::Descending), 7 => component_ordering = SortByComponent::Critical(Ordering::Descending), _ => (), }, 'm' => { process_ordering = SortByProcess::MemoryUsage(Ordering::Ascending); } 'M' => { process_ordering = SortByProcess::MemoryUsage(Ordering::Descending); } 's' => { process_ordering = SortByProcess::SwapUsage(Ordering::Ascending); } 'S' => { process_ordering = SortByProcess::SwapUsage(Ordering::Descending); } 'r' => { process_ordering = SortByProcess::Runtime(Ordering::Ascending); } 'R' => { process_ordering = SortByProcess::Runtime(Ordering::Descending); } 't' => { component_ordering = SortByComponent::Temperature(Ordering::Ascending); } 'T' => { component_ordering = SortByComponent::Temperature(Ordering::Descending); } 'k' => { kill_current_process = true; } _ => (), }, KeyCode::Modifier(ModifierKeyCode::LeftShift) | KeyCode::Modifier(ModifierKeyCode::RightShift) => { // This just straight up doesn't work (on MacOS) shift_pressed = true; } KeyCode::Up => current_line = current_line.saturating_sub(1), KeyCode::Down => current_line = current_line.saturating_add(1), KeyCode::Left => { current_tab = current_tab.saturating_sub(1); current_line = 0; } KeyCode::Right => { if current_tab < backend::Tab::COUNT - 1 { current_tab += 1 } current_line = 0; } _ => (), }, Ok(Event::Mouse(event)) => match event.kind { // TODO: Limit scrolling MouseEventKind::ScrollDown => current_line = current_line.saturating_add(1), MouseEventKind::ScrollUp => current_line = current_line.saturating_sub(1), _ => (), }, _ => (), } } } } fn format_duration(duration: &Duration) -> String { format!("{:0>2}:{:0>2}:{:0>2}", duration.as_secs() / 3600, (duration.as_secs() / 60) % 60, duration.as_secs() % 60) } fn ui( f: &mut Frame, manager: &mut backend::Manager, current_line: u16, current_tab: usize, starting_time: Instant, process_ordering: SortByProcess, component_ordering: SortByComponent, shift_pressed: bool, kill_current_process: bool, cpu_dataset: &mut HashMap, ) { let titles = backend::Tab::iter().map(|tab| Spans::from(tab.to_string())).collect::>(); let size = f.size(); let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) .split(size); let cpu_vertical_chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Percentage(20), Constraint::Percentage(80)]) .split(chunks[1]); let network_chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Percentage(33), Constraint::Percentage(33), Constraint::Percentage(34)]) .split(chunks[1]); let block = Block::default().style(Style::default().bg(Color::Black).fg(Color::White)); f.render_widget(block, size); let tabs = Tabs::new(titles) .block(Block::default().borders(Borders::ALL)) .select(current_tab) .highlight_style(Style::default().add_modifier(Modifier::BOLD).bg(Color::White).fg(Color::Black)); f.render_widget(tabs, chunks[0]); let mut list_state = ListState::default(); list_state.select(Some(current_line as usize)); match current_tab { 0 => f.render_widget(system_tab(manager, current_line), chunks[1]), 1 => { let cpu_tab_widgets = cpu_tab(manager, starting_time, cpu_dataset); let cpu_list_chunks = Layout::default() .direction(Direction::Horizontal) .constraints(vec![Constraint::Percentage(100 / cpu_tab_widgets.len() as u16); cpu_tab_widgets.len()]) .split(cpu_vertical_chunks[0]); let cpu_chart_chunks = Layout::default() .direction(Direction::Horizontal) .constraints(vec![Constraint::Percentage(100 / cpu_tab_widgets.len() as u16); cpu_tab_widgets.len()]) .split(cpu_vertical_chunks[1]); for (index, (list, chart)) in cpu_tab_widgets.iter().enumerate() { f.render_stateful_widget(list.clone(), cpu_list_chunks[index], &mut list_state); f.render_widget(chart.clone(), cpu_chart_chunks[index]); } } 2 => f.render_widget(memory_tab(manager, starting_time), chunks[1]), 3 => f.render_widget(disk_tab(manager, current_line), chunks[1]), 4 => f.render_widget(battery_tab(manager, current_line), chunks[1]), 5 => { let network_tab_widgets = network_tab(); f.render_widget(network_tab_widgets.0, network_chunks[0]); f.render_stateful_widget(network_tab_widgets.1, network_chunks[1], &mut list_state); f.render_stateful_widget(network_tab_widgets.2, network_chunks[2], &mut list_state); } 6 => f.render_stateful_widget(process_tab(manager, process_ordering, shift_pressed, kill_current_process), chunks[1], &mut list_state), 7 => f.render_stateful_widget(component_tab(manager, component_ordering, shift_pressed), chunks[1], &mut list_state), _ => unreachable!(), }; } fn system_tab(manager: &mut backend::Manager, scroll: u16) -> Paragraph { if let Some(system_info) = manager.system_information() { let text = [ vec![ Spans::from(vec![Span::raw("Operating System: "), Span::raw(system_info.os.clone().unwrap_or("unknown".to_string()))]), Spans::from(vec![ Span::raw("Operating System Version: "), Span::raw(system_info.os_version.clone().unwrap_or("unknown".to_string())), ]), Spans::from(vec![Span::raw("Kernel Version: "), Span::raw(system_info.kernel_version.clone().unwrap_or("unknown".to_string()))]), Spans::from(vec![Span::raw("Uptime: "), Span::raw(format_duration(&system_info.uptime))]), Spans::from(Span::raw("Users: ")), ], system_info.users.iter().map(|user| Spans::from(Span::raw(format!(" {user}\n")))).collect(), ] .into_iter() .flatten() .collect::>(); Paragraph::new(text).scroll((scroll, 0)) } else { Paragraph::new("No information available!") } .block(Block::default().title("System").borders(Borders::ALL)) .style(Style::default().fg(Color::White).bg(Color::Black)) .alignment(Alignment::Left) .wrap(Wrap { trim: false }) } const COLORS: [Color; 15] = [ Color::Red, Color::Green, Color::Yellow, Color::Blue, Color::Magenta, Color::Cyan, Color::Gray, Color::DarkGray, Color::LightRed, Color::LightGreen, Color::LightYellow, Color::LightBlue, Color::LightMagenta, Color::LightCyan, Color::White, ]; // TODO: make this with charts fn cpu_tab<'a>(manager: &'a mut backend::Manager, starting_time: Instant, cpu_dataset: &'a mut HashMap) -> Vec<(List<'a>, Chart<'a>)> { static LATEST_UPDATE: Mutex<(Option>, Option)> = Mutex::new((None, None)); static LATEST_CHART: Mutex<(Option, Option)> = Mutex::new((None, None)); let interval = Duration::from_secs(1); let elapsed = starting_time.elapsed(); let mut latest_update = LATEST_UPDATE.lock().unwrap(); if latest_update.1.is_none() || latest_update.1.unwrap().elapsed() > interval { *latest_update = (manager.cpu_information(), Some(Instant::now())); } let mut res = if let Some(mut cpu_info) = latest_update.0.clone() { if cpu_dataset.is_empty() { cpu_info.iter().for_each(|cpu_core| { cpu_dataset.insert(cpu_core.clone(), vec![(cpu_core.usage as f64, elapsed.as_secs_f64())]); }); } else if latest_update.1.unwrap().elapsed() > interval { cpu_info .iter() .for_each(|cpu_core| cpu_dataset.get_mut(cpu_core).expect("The core should exist").push((cpu_core.usage as f64, elapsed.as_secs_f64()))); } cpu_info.sort_unstable_by(|a, b| a.manufacturer.cmp(&b.manufacturer)); let sorted_cpu_info = cpu_info .iter() .group_by(|cpu_core| cpu_core.manufacturer.clone()) .into_iter() .map(|(_key, info)| info.cloned().collect()) .collect::>>(); // This is only ever necessary in multi CPU // setups, but I don't want a issue six years down // the line when multi CPU has become the norm sorted_cpu_info .iter() .map(|cpu| { ( { let usage_label = "Usage"; let model_label = "Model/Core Nr."; let manufacturer_label = "Manufacturer"; let frequency_label = "Frequency (GHz)"; let mut usage_width = usage_label.len(); let mut model_width = model_label.len(); let mut manufacturer_width = manufacturer_label.len(); let mut frequency_width = frequency_label.len(); for cpu_core in cpu.iter() { let usage_candidate = format!("{:.2}", cpu_core.usage).len(); if usage_width < usage_candidate { usage_width = usage_candidate; } if model_width < cpu_core.model.len() { model_width = cpu_core.model.len(); } if manufacturer_width < cpu_core.manufacturer.len() { manufacturer_width = cpu_core.manufacturer.len(); } let frequency_candidate = format!("{:.2}", cpu_core.frequency_ghz).len(); if frequency_width < frequency_candidate { frequency_width = frequency_candidate; } } List::new( cpu.iter() .map(|cpu_core| { ListItem::new(format!( "{:manufacturer_width$} {:model_width$} {:frequency_width$.2} {:usage_width$.2}%", "", cpu_core.model.clone(), cpu_core.frequency_ghz, cpu_core.usage )) }) .collect::>(), ) .block( Block::default() .title(format!( "{:manufacturer_width$} {model_label:model_width$} {frequency_label:frequency_width$} {usage_label:usage_width$}", cpu[0].manufacturer.clone() )) .borders(Borders::ALL), ) }, { let mut latest_chart = LATEST_CHART.lock().unwrap(); if latest_chart.1.is_none() || latest_chart.1.unwrap().elapsed() > interval { *latest_chart = ( Some(Chart::new( cpu.iter() .take(2) .enumerate() .map(|(index, cpu_core)| { Dataset::default() .name(cpu_core.model.clone()) .marker(Marker::Braille) .graph_type(GraphType::Line) .style(Style::default().fg(if index < COLORS.len() { COLORS[index] } else { Color::Rgb(((index as u8 * 100) % 255) as u8, ((index * 50) % 255) as u8, ((index * 75) % 255) as u8) })) .data({ eprintln!("{:?}", cpu_dataset[cpu_core]); cpu_dataset[cpu_core].clone() }) }) .collect(), )), Some(Instant::now()), ); } latest_chart.0.clone().unwrap() }, ) }) .collect() } else { vec![(List::new(vec![]), Chart::new(vec![]))] }; for (list, chart) in &mut res { *list = list .clone() .style(Style::default().fg(Color::White).bg(Color::Black)) .highlight_style(Style::default().fg(Color::Black).bg(Color::White)); *chart = chart .clone() .style(Style::default().bg(Color::Black).fg(Color::White)) .x_axis( Axis::default() .title(Span::raw("Seconds Elapsed")) .style(Style::default().fg(Color::White).bg(Color::Black)) .bounds([0.0, elapsed.as_secs_f64()]) .labels( ["0".to_string(), (elapsed / 2).as_secs().to_string(), elapsed.as_secs().to_string()] .iter() .cloned() .map(Span::from) .collect(), ), ) .y_axis( Axis::default() .title(Span::raw("CPU usage")) .style(Style::default().fg(Color::White).bg(Color::Black)) .bounds([0.0, 100.0]) .labels(["0%", "50%", "100%"].iter().cloned().map(Span::raw).collect()), ); } res } fn memory_tab(manager: &mut backend::Manager, starting_time: Instant) -> Chart { // Replacing these datasets with Mutexes would // remove all unsafe code but it leads to many // complications. If you can solve this, feel free // to submit a PR static mut RAM_DATASET: DataPoints = vec![]; static mut SWAP_DATASET: DataPoints = vec![]; let formatter = humansize::make_format(humansize::DECIMAL); let interval = Duration::from_millis(500); let elapsed = starting_time.elapsed(); if let Some(memory_info) = manager.memory_information() { let total_memory = memory_info.total_memory; let total_swap = memory_info.total_swap; let mut ram_important_digits = total_memory as f64; while ram_important_digits > 1000.0 { // TODO: precompute these values ram_important_digits /= 1000.0; } ram_important_digits = ram_important_digits.floor(); let mut swap_important_digits = total_swap as f64; while swap_important_digits > 1000.0 { swap_important_digits /= 1000.0; } swap_important_digits = swap_important_digits.floor(); unsafe { if RAM_DATASET.is_empty() || elapsed.as_secs_f64() - RAM_DATASET.iter().last().unwrap().0 > interval.as_secs_f64() { RAM_DATASET.push((elapsed.as_secs_f64(), match total_memory { // This is highly unlikely to ever trigger as computers tend to have memory 0 => 0.0, _ => (memory_info.used_memory as f64 / total_memory as f64) * ram_important_digits, })); SWAP_DATASET.push((elapsed.as_secs_f64(), match total_swap { 0 => 0.0, _ => (memory_info.used_swap as f64 / total_swap as f64) * swap_important_digits, })); } } let max_y_axis_bound = ram_important_digits.max(swap_important_digits); let max_y_axis_label = total_memory.max(total_swap); let datasets = vec![ Dataset::default() .name("RAM used") .marker(Marker::Braille) .graph_type(GraphType::Line) .style(Style::default().fg(Color::Cyan)) .data(unsafe { RAM_DATASET.clone() }), Dataset::default() .name("SWAP used") .marker(Marker::Braille) .graph_type(GraphType::Line) .style(Style::default().fg(Color::Green)) .data(unsafe { SWAP_DATASET.clone() }), ]; return Chart::new(datasets) .block(Block::default().title(format!( "Memory: {}/{}, SWAP: {}/{}", formatter(memory_info.used_memory), formatter(total_memory), formatter(memory_info.used_swap), formatter(total_swap) ))) .style(Style::default().bg(Color::Black).fg(Color::White)) .x_axis( Axis::default() .title(Span::raw("Seconds Elapsed")) .style(Style::default().fg(Color::White).bg(Color::Black)) .bounds([0.0, elapsed.as_secs_f64()]) .labels( ["0".to_string(), (elapsed / 2).as_secs().to_string(), elapsed.as_secs().to_string()] .iter() .cloned() .map(Span::from) .collect(), ), ) .y_axis( Axis::default() .title(Span::raw("Used Memory/SWAP")) .style(Style::default().fg(Color::White).bg(Color::Black)) .bounds([0.0, max_y_axis_bound]) .labels([formatter(0), formatter(max_y_axis_label / 2), formatter(max_y_axis_label)].iter().cloned().map(Span::from).collect()), ); } else { return Chart::new(vec![Dataset::default()]).block(Block::default().title("No memory/SWAP information was able to be obtained!")); } } fn disk_tab(manager: &mut backend::Manager, scroll: u16) -> Paragraph { let formatter = humansize::make_format(humansize::DECIMAL); if let Some(disk_info) = manager.disk_information() { let text = disk_info .iter() .flat_map(|disk| { vec![ Spans::from(Span::styled(disk.name.clone(), Style::default().add_modifier(Modifier::BOLD))), Spans::from(vec![Span::raw("Used Space: "), Span::raw(formatter(disk.used))]), Spans::from(vec![Span::raw("Total Space: "), Span::raw(formatter(disk.total))]), Spans::from(vec![Span::raw("Mount Point: "), Span::raw(disk.mount_point.clone())]), Spans::from(vec![Span::raw("Filesystem: "), Span::raw(disk.file_system.clone().unwrap_or("unknown".to_string()))]), Spans::from(Span::raw("\n")), ] }) .collect::>(); Paragraph::new(text).scroll((scroll, 0)) } else { Paragraph::new("No information available!") } .block(Block::default().title("Disks").borders(Borders::ALL)) .style(Style::default().fg(Color::White).bg(Color::Black)) .alignment(Alignment::Left) .wrap(Wrap { trim: false }) } fn battery_tab(manager: &mut backend::Manager, scroll: u16) -> Paragraph { if let Some(battery_info) = manager.battery_information() { let batteries = battery_info .iter() .flat_map(|battery| { vec![ Spans::from(Span::styled(battery.model.clone().unwrap_or("unknown".to_string()), Style::default().add_modifier(Modifier::BOLD))), Spans::from(vec![Span::raw("Manufacturer: "), Span::raw(battery.manufacturer.clone().unwrap_or("unknown".to_string()))]), Spans::from(vec![Span::raw("Charge: "), Span::raw((battery.charge * 100.0).floor().to_string()), Span::raw("%")]), Spans::from(vec![Span::raw("Status: "), Span::raw(battery.state.to_string())]), Spans::from(vec![Span::raw("Capacity: "), Span::raw(format!("{:.2}", battery.capacity_wh)), Span::raw("kWh")]), Spans::from(vec![Span::raw("Intended Capacity: "), Span::raw(format!("{:.2}", battery.capacity_new_wh)), Span::raw("kWh")]), Spans::from(vec![Span::raw("Health: "), Span::raw(format!("{:.2}", battery.health)), Span::raw("%")]), Spans::from(vec![Span::raw("Voltage: "), Span::raw(format!("{:.2}", battery.voltage)), Span::raw("V")]), Spans::from(vec![Span::raw("Technology: "), Span::raw(format!("{:.2}", battery.technology))]), Spans::from(vec![ Span::raw("Cycle Count: "), Span::raw(battery.cycle_count.map(|cycle_count| cycle_count.to_string()).unwrap_or("unknown".to_string())), ]), Spans::from(Span::raw("\n".repeat(3))), ] }) .collect::>(); Paragraph::new(batteries).scroll((scroll, 0)) } else { Paragraph::new("No battery information was able to be obtained!") } .block(Block::default().title("Batteries").borders(Borders::ALL)) .style(Style::default().fg(Color::White).bg(Color::Black)) .alignment(Alignment::Left) .wrap(Wrap { trim: false }) } // TODO: Make all "find max width" type statements // into one per iterator fn network_tab<'a>() -> (Paragraph<'a>, List<'a>, List<'a>) { // let formatter = // humansize::make_format(humansize::DECIMAL); // // This is needed for popup (which you should // totally implement) let mut res = if let Some(network_info) = (*NETWORK_INFO.lock().unwrap()).clone() { let text = vec![ Spans::from(vec![Span::raw("Connected to the internet: "), Span::raw(network_info.connected.to_string())]), Spans::from(vec![ Span::raw("IP Address (IPv4): "), Span::raw(network_info.ip_address_v4.map(|addr| addr.to_string()).unwrap_or("unknown".to_string())), ]), Spans::from(vec![ Span::raw("IP Address (IPv6): "), Span::raw(network_info.ip_address_v6.map(|addr| addr.to_string()).unwrap_or("unknown".to_string())), ]), ]; let (wifis, wifi_title) = if let Some(wifis) = network_info.wifis { let wifi_name_label = "Name"; let wifi_mac_label = "MAC Address"; let wifi_channel_label = "Channel"; let wifi_security_label = "Security"; let wifi_signal_label = "Signal Level"; let mut wifi_name_width = wifi_name_label.len(); let mut wifi_mac_width = wifi_mac_label.len(); let mut wifi_channel_width = wifi_channel_label.len(); let mut wifi_security_width = wifi_security_label.len(); let mut wifi_signal_width = wifi_signal_label.len(); for wifi in wifis.iter() { if wifi_name_width < wifi.ssid.len() { wifi_name_width = wifi.ssid.len(); } if wifi_mac_width < wifi.mac.len() { wifi_mac_width = wifi.mac.len(); } if wifi_channel_width < wifi.channel.len() { wifi_channel_width = wifi.channel.len(); } if wifi_security_width < wifi.security.len() { wifi_security_width = wifi.security.len(); } if wifi_signal_width < wifi.signal_level.len() { wifi_signal_width = wifi.signal_level.len(); } } ( wifis .iter() .map(|wifi| { ListItem::new(format!( "{:wifi_name_width$} {:wifi_mac_width$} {:wifi_channel_width$} {:wifi_security_width$} {:wifi_signal_width$}", wifi.ssid.clone(), match !wifi.mac.is_empty() { true => wifi.mac.clone(), false => "unknown".to_string(), }, wifi.channel.clone(), wifi.security.clone(), wifi.signal_level.clone() )) }) .collect(), format!( "{wifi_name_label:wifi_name_width$} {wifi_mac_label:wifi_mac_width$} {wifi_channel_label:wifi_channel_width$} {wifi_security_label:wifi_security_width$} \ {wifi_signal_label:wifi_signal_width$}" ), ) } else { (vec![ListItem::new("No WiFi information available!")], "WiFi networks".to_string()) }; let (networks, network_title) = if let Some(networks) = network_info.networks { // TODO: Add a popup with more information (return // an Option in the tuple) let network_name_label = "Name"; let network_index_label = "Index"; let network_mac_label = "MAC Address"; let network_flags_label = "Flags"; let mut network_name_width = network_name_label.len(); let mut network_index_width = network_index_label.len(); let mut network_mac_width = network_mac_label.len(); let mut network_flags_width = network_flags_label.len(); for network in networks.iter() { if network_name_width < network.name.len() { network_name_width = network.name.len(); } let index_width_candidate = network.index.map(|index| index.to_string()).unwrap_or("unknown".to_string()).len(); if network_index_width < index_width_candidate { network_index_width = index_width_candidate; } let mac_width_candidate = network.mac_address.map(|mac| mac.to_string()).unwrap_or("unknown".to_string()).len(); if network_mac_width < mac_width_candidate { network_mac_width = mac_width_candidate; } let flags_width_candidate = match network.flags { Some(flags) => format!("{flags:b}"), None => "unknown".to_string(), } .len(); if network_flags_width < flags_width_candidate { network_flags_width = flags_width_candidate; } } ( networks .iter() .map(|network| { ListItem::new(format!( "{:network_name_width$} {:network_index_width$} {:network_mac_width$} {:network_flags_width$}", network.name, /* TODO: Convert this to a more human readable format * on MacOS (and maybe others) */ network.index.map(|index| index.to_string()).unwrap_or("unknown".to_string()), network.mac_address.map(|mac| mac.to_string()).unwrap_or("unknown".to_string()), match network.flags { // TODO: present this a lil better Some(flags) => format!("{flags:b}"), None => "unknown".to_string(), } )) }) .collect(), format!("{network_name_label:network_name_width$} {network_index_label:network_index_width$} {network_mac_label:network_mac_width$} {network_flags_label:network_flags_width$}"), ) } else { (vec![ListItem::new("No network/interface information available!")], "Networks/Interfaces".to_string()) }; ( Paragraph::new(text), List::new(wifis).block(Block::default().title(wifi_title).borders(Borders::ALL)), List::new(networks).block(Block::default().title(network_title).borders(Borders::ALL)), ) } else { ( Paragraph::new("Loading..."), List::new(vec![ListItem::new("Loading...")]).block(Block::default().title("WiFi Networks").borders(Borders::ALL)), List::new(vec![ListItem::new("Loading...")]).block(Block::default().title("Networks/Interfaces").borders(Borders::ALL)), ) }; res.0 = res .0 .block(Block::default().title("Networks").borders(Borders::ALL)) .style(Style::default().fg(Color::White).bg(Color::Black)) .alignment(Alignment::Left) .wrap(Wrap { trim: false }); res.1 = res .1 .style(Style::default().fg(Color::White).bg(Color::Black)) .highlight_style(Style::default().fg(Color::Black).bg(Color::White)); res.2 = res .2 .style(Style::default().fg(Color::White).bg(Color::Black)) .highlight_style(Style::default().fg(Color::Black).bg(Color::White)); res } // TODO: make a popup with more information // TODO: implement process killing fn process_tab(manager: &mut backend::Manager, ordering: SortByProcess, shift_pressed: bool, #[allow(unused_variables)] kill_current_process: bool) -> List { static LATEST_INFO: Mutex<(Option>, Option)> = Mutex::new((None, None)); let formatter = humansize::make_format(humansize::DECIMAL); let interval = Duration::from_secs(2); let mut latest_info = LATEST_INFO.lock().unwrap(); if latest_info.1.is_none() || latest_info.1.unwrap().elapsed() >= interval { *latest_info = (manager.process_information(), Some(Instant::now())); } if let Some(ref mut process_info) = &mut latest_info.0 && !process_info.is_empty() { let selected_label = "Kill [k] "; let name_label = "Name"; let cpu_label = format!("CPU usage [{}]", if shift_pressed { 'C' } else { 'c' }); let memory_label = format!("Memory usage [{}]", if shift_pressed { 'M' } else { 'm' }); let swap_label = format!("SWAP usage [{}]", if shift_pressed { 'S' } else { 's' }); let runtime_label = format!("Runtime [{}]", if shift_pressed { 'R' } else { 'r' }); let selected_width = selected_label.len(); let name_width = std::cmp::max( process_info .iter() .map(|process| process.name.len()) .max() .unwrap(), name_label.len(), ); let cpu_width = cpu_label.len(); let memory_width = std::cmp::max( process_info .iter() .map(|process| formatter(process.memory_usage).len()) .max() .unwrap(), memory_label.len(), ); let swap_width = std::cmp::max( process_info .iter() .map(|process| formatter(process.swap_usage).len()) .max() .unwrap(), swap_label.len(), ); let runtime_width = std::cmp::max( process_info .iter() .map(|process| format_duration(&process.run_time).len()) .max() .unwrap(), runtime_label.len(), ); let sort_fn = |a: &backend::ProcessInfo, b: &backend::ProcessInfo| match ordering { SortByProcess::CpuUsage(ord) => ord.sort_by()(a.cpu_usage, b.cpu_usage), SortByProcess::MemoryUsage(ord) => ord.sort_by()(a.memory_usage, b.memory_usage), SortByProcess::SwapUsage(ord) => ord.sort_by()(a.swap_usage, b.swap_usage), SortByProcess::Runtime(ord) => ord.sort_by()(a.run_time, b.run_time), }; process_info.sort_by(sort_fn); let items = process_info .iter() .map(|process| { ListItem::new(format!( "{:name_width$} {:cpu_width$.2}% {:memory_width$} {:swap_width$} {:runtime_width$}", process.name, process.cpu_usage, formatter(process.memory_usage), formatter(process.swap_usage), format_duration(&process.run_time) )) }) .collect::>(); List::new(items) .block(Block::default().title(format!( "{:selected_width$}{:name_width$} {:cpu_width$} {:memory_width$} {:swap_width$} {:runtime_width$}", "", name_label, cpu_label, memory_label, swap_label, runtime_label )) .borders(Borders::ALL) ) .highlight_symbol(selected_label) } else { List::new(vec![ListItem::new("No information available!")]) .block(Block::default().title("Processes") .borders(Borders::ALL) ) } .style(Style::default().fg(Color::White).bg(Color::Black)).highlight_style(Style::default().fg(Color::Black).bg(Color::White)) } fn component_tab(manager: &mut backend::Manager, ordering: SortByComponent, shift_pressed: bool) -> List { if let Some(mut component_info) = manager.component_information() && !component_info.is_empty() { let selected_label = ">"; let name_label = "Name"; let temperature_label = format!("Temperature [{}]", if shift_pressed { 'T' } else { 't' }); let critical_label = format!("Critical Temperature [{}]", if shift_pressed { 'C' } else { 'c' }); let selected_width = selected_label.len(); let name_width = std::cmp::max( component_info .iter() .map(|component| component.name.len()) .max() .unwrap(), name_label.len(), ); let temperature_width = temperature_label.len(); // This is a bit of a gamble as it assumes that the label will always be longer than a temperature reading let critical_width = critical_label.len(); let sort_fn = |a: &backend::ComponentInfo, b: &backend::ComponentInfo| match ordering { SortByComponent::Temperature(ord) => ord.sort_by()(a.temperature, b.temperature), SortByComponent::Critical(ord) => ord.sort_by()(a.critical_temperature.unwrap_or(0.0), b.critical_temperature.unwrap_or(0.0)) }; component_info.sort_by(sort_fn); let items = component_info.iter().map(|component| ListItem::new( format!("{:name_width$} {:temperature_width$.2}°C {:critical_width$}", component.name, component.temperature, match component.critical_temperature { Some(critical_temp) => format!("{critical_temp:.2}°C"), None => "None".to_string() }))).collect::>(); List::new(items) .block(Block::default().title(format!( "{:selected_width$}{:name_width$} {:temperature_width$} {:critical_width$}", "", name_label, temperature_label, critical_label )) .borders(Borders::ALL) ) .highlight_symbol(selected_label) } else { List::new(vec![ListItem::new("No information available!")]) } .style(Style::default().fg(Color::White).bg(Color::Black)).highlight_style(Style::default().fg(Color::Black).bg(Color::White)) } fn main() -> Result<(), io::Error> { enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; let mut manager = backend::Manager::new(); run_app(&mut terminal, &mut manager); disable_raw_mode()?; execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?; terminal.show_cursor()?; Ok(()) }