use chrono::Timelike; use ncurses::*; use rtop_dev::components::listview::Ordering; use rtop_dev::{components, widget}; use rtop_rs::window; use serde::Deserialize; use sysinfo::{ProcessExt, CpuExt, SystemExt}; type WidgetInitializer = fn() -> (Box, bool); type WidgetInitializerResult<'a> = std::result::Result, libloading::Error>; fn default_pages() -> Vec> { vec![vec![ String::from("cpu_chart"), String::from("memory_chart"), String::from("process_list"), ]] } #[derive(Deserialize)] struct Option { #[serde(default = "default_pages")] pages: Vec>, #[serde(default)] plugins: Vec, } #[derive(Deserialize)] struct LibOption { #[serde(default)] path: String, #[serde(default)] name: String, } struct MemoryUsage { sysinfo: sysinfo::System, data: Vec, chart: components::chart::Chart, } struct PluginError { message: String, } struct CpuUsage { sysinfo: sysinfo::System, data: Vec, chart: components::chart::Chart, last_cpu_usage: f32, } struct ProcessList { sysinfo: sysinfo::System, data: Vec, chart: components::listview::ListView, refresh_progress: usize, kill_process_security: bool, } impl widget::Widget for ProcessList { fn on_update(&mut self) { self.refresh_progress += 1; if self.refresh_progress == 7 { let mut process_done = vec![]; let mut new_process_list = vec![]; let physical_core_count = self.sysinfo.physical_core_count().unwrap(); for process in self.sysinfo.processes().values() { if process_done.contains(&process.name()) { continue; } process_done.push(process.name()); let mut process_data = std::collections::HashMap::new(); let mut total_cpu = 0.; let mut total_memory: u64 = 0; let mut count: i32 = 0; for sub_proc in self.sysinfo.processes_by_exact_name(process.name()) { count += 1; total_memory += sub_proc.memory(); total_cpu += sub_proc.cpu_usage(); } process_data.insert( String::from("CPU %"), format!("{:.1}", (total_cpu / physical_core_count as f32)), ); process_data.insert(String::from("Count"), format!("{}", count)); process_data.insert( String::from("Memory %"), format!( "{:.1}", (total_memory as f32 * 100. / self.sysinfo.total_memory() as f32) ), ); new_process_list.push(components::listview::ListItem::new( process.name(), &process_data, )); } self.data = new_process_list; self.sysinfo.refresh_processes(); self.refresh_progress = 0; } } fn display(&mut self, h: i32, w: i32) -> String { self.chart.resize(h, w); self.chart.update_items(&self.data); self.chart.display() } fn on_input(&mut self, key: String) { if key == "KEY_DOWN" || key == "k" { self.chart.next(); } else if key == "KEY_UP" || key == "j" { self.chart.previous(); } else if key == "g" { self.chart.to_first(); } else if key == "G" { self.chart.to_last(); } else if key == "m" { self.chart.sort_by( std::option::Option::from(String::from("Memory %")), std::option::Option::from(Ordering::Default), ); } else if key == "c" { self.chart.sort_by( std::option::Option::from(String::from("CPU %")), std::option::Option::from(Ordering::Default), ); } else if key == "n" { self.chart.sort_by( std::option::Option::from(String::from("Name")), std::option::Option::from(Ordering::Inversed), ); } else if key == "C" { self.chart.sort_by( std::option::Option::from(String::from("Count")), std::option::Option::from(Ordering::Default), ); } else if key == "d" { if self.kill_process_security { let item = self.chart.select(); for proc in self.sysinfo.processes_by_exact_name(&item.name) { proc.kill(); } } self.kill_process_security = !self.kill_process_security; } if key != "d" && self.kill_process_security { self.kill_process_security = false; } } fn title(&mut self) -> std::option::Option { Some(format!("[{}] Processes", self.data.len())) } } impl widget::Widget for CpuUsage { fn on_update(&mut self) { self.sysinfo.refresh_cpu(); self.data.push( ((self.sysinfo.global_cpu_info().cpu_usage() + self.last_cpu_usage) / 2.) as i32, ); } fn display(&mut self, h: i32, w: i32) -> String { self.chart.resize(w, h); self.chart.display(&self.data) } fn title(&mut self) -> std::option::Option { Some(String::from("CPU Usage")) } } impl widget::Widget for MemoryUsage { fn display(&mut self, h: i32, w: i32) -> String { self.chart.resize(w, h); self.chart.display(&self.data) } fn on_update(&mut self) { self.sysinfo.refresh_memory(); self.data .push((self.sysinfo.used_memory() * 100 / self.sysinfo.total_memory()) as i32); } fn title(&mut self) -> std::option::Option { Some(String::from("Memory")) } } impl widget::Widget for PluginError { fn display(&mut self, h: i32, w: i32) -> String { let error_message = format!("An error occured: {}", self.message); format!( "{}{}{}", String::from("\n").repeat((h / 2) as usize), String::from(" ").repeat((w / 2) as usize - error_message.len() / 2), error_message ) } } struct ScreenWidget { plugin: Box, name: String, } struct Page { widgets: Vec, focusable_widgets: Vec, } unsafe impl Send for Page {} #[tokio::main] async fn main() { let default_panic = std::panic::take_hook(); // Handle error, work only with builtin-plugin std::panic::set_hook(Box::new(move |info| { // TODO: Make this work for plugin curs_set(ncurses::CURSOR_VISIBILITY::CURSOR_VISIBLE); endwin(); default_panic(info); std::process::exit(1); })); let option: Option = serde_json::from_str( &std::fs::read_to_string(format!( "{}/.config/rtop/config.json", home::home_dir().unwrap().display() )) .unwrap_or_else(|_| String::from("{}")), ) .unwrap(); let mut plugins = std::collections::HashMap::new(); // 0: name 1: dylib for plugin in option.plugins { unsafe { let plugin_in_load = libloading::Library::new(String::from(&plugin.path)); if let Ok(plugin_in_load) = plugin_in_load { plugins.insert(plugin.name, plugin_in_load); } else { continue; } } } let mut builtin_addon: std::collections::HashMap = std::collections::HashMap::new(); builtin_addon.insert(String::from("memory_chart"), init_memory_plugin); builtin_addon.insert(String::from("cpu_chart"), init_cpuusage_plugin); builtin_addon.insert(String::from("process_list"), init_process_plugin); let mut current_page_number = 1; let sysinfo = sysinfo::System::new_all(); let mut current_widget = 1; let locale = setlocale(LcCategory::all, ""); if !locale.contains("UTF-8") { println!("You need to have a terminal that support UTF-8"); return; } //Initialize Ncurses let term = initscr(); keypad(term, true); start_color(); use_default_colors(); let mut height = 0; let mut width = 0; getmaxyx(term, &mut height, &mut width); curs_set(ncurses::CURSOR_VISIBILITY::CURSOR_INVISIBLE); timeout(334); noecho(); init_pair(1, COLOR_RED, -1); init_pair(2, COLOR_GREEN, -1); init_pair(3, COLOR_YELLOW, -1); init_pair(4, COLOR_BLUE, -1); init_pair(5, COLOR_MAGENTA, -1); init_pair(6, COLOR_CYAN, -1); init_pair(7, COLOR_WHITE, -1); init_pair(8, COLOR_BLACK, -1); let pages: std::sync::Arc>> = std::sync::Arc::new(tokio::sync::Mutex::new(vec![])); let pages_mutex = std::sync::Arc::clone(&pages); for page in option.pages { if page.len() > 4 { pages.lock().await.push(Page { widgets: vec![ScreenWidget { name: String::from("Error"), plugin: Box::new(PluginError { message: String::from("You cannot have more than 4 widgets per pages"), }), }], focusable_widgets: vec![], }); } else if page.is_empty() { pages.lock().await.push(Page { widgets: vec![ScreenWidget { name: String::from("Error"), plugin: Box::new(PluginError { message: String::from("You must add a widget to this page"), }), }], focusable_widgets: vec![], }); } else { let mut i = 0; let mut pages_widgets = vec![]; let mut focusable_widgets = vec![]; for widget in page { i += 1; if builtin_addon.contains_key(&widget) { let mut tmp = builtin_addon[&widget](); if tmp.1 { focusable_widgets.push(i); } tmp.0.init(); pages_widgets.push(ScreenWidget { name: String::from(&widget), plugin: tmp.0, }); } else { let tmp = &widget.split('.').collect::>(); let plugin = plugins.get(tmp[0]); if let Some(plugin) = plugin { let initializer: WidgetInitializerResult = unsafe { plugin.get(format!("init_{}", tmp[1]).as_bytes()) }; if let Ok(initializer) = initializer { let mut created_widget = initializer(); if created_widget.1 { focusable_widgets.push(i); } created_widget.0.init(); pages_widgets.push(ScreenWidget { name: widget, plugin: created_widget.0, }); } else { pages_widgets.push(ScreenWidget { name: String::from("Error"), plugin: Box::new(PluginError { message: format!( "Unknow widget {} in plugin {}", tmp[1], tmp[0] ), }), }); } } else { pages_widgets.push(ScreenWidget { name: String::from("Error"), plugin: Box::new(PluginError { message: format!("Unable to find plugin {}", tmp[0]), }), }); } } } pages.lock().await.push(Page { widgets: pages_widgets, focusable_widgets, }); } } tokio::spawn(async move { loop { for page in &mut *pages_mutex.lock().await { for el in &mut page.widgets { el.plugin.on_update(); } } std::thread::sleep(std::time::Duration::from_millis(333)); } }); let mut widgets; { let locked_pages = pages.lock().await; widgets = create_widget_window( height - 2, width, locked_pages[current_page_number - 1].widgets.len() as i32, ); } let current_os = sysinfo.name().unwrap_or_else(|| String::from("You")); attron(ncurses::A_BOLD()); attron(COLOR_PAIR(4)); addstr(" rtop "); attrset(ncurses::A_NORMAL()); addstr(&format!("for {}", current_os)); refresh(); display_help(height); loop { let current_page_widget_count; let current_page_focusable_widget_count; { let locked_pages = &mut pages.lock().await; let current_page = &mut locked_pages[current_page_number - 1]; current_page_widget_count = current_page.widgets.len(); current_page_focusable_widget_count = current_page.focusable_widgets.len(); if current_page_focusable_widget_count == 0 { current_widget = 0; } for (i, item) in widgets .iter_mut() .enumerate() .take(current_page_widget_count) { let widget = &mut current_page.widgets[i]; item.write(&widget.plugin.display(item.height - 2, item.width - 4)); let title = &widget.plugin.title(); if let Some(title) = title { item.set_title(String::from(title)); } else { item.set_title(String::from(&widget.name)); } item.set_border_color(COLOR_PAIR(2)); } if current_page_focusable_widget_count > 1 { let tmp = current_page.focusable_widgets[current_widget - 1] as usize; widgets[(tmp) - 1].set_border_color(COLOR_PAIR(4)); widgets[(tmp) - 1].refresh(); } for widget in widgets.iter().take(current_page_widget_count) { widget.refresh(); } } // Update TopBar and BottomBar Infos let now = chrono::Local::now(); let load_average = sysinfo.load_average(); let load_average_string = format!( " Load Average: {:.2} {:.2} {:.2} ", load_average.one, load_average.five, load_average.fifteen ); mvaddstr( 0, width - 9, &format!("{:02}:{:02}:{:02}", now.hour(), now.minute(), now.second()), ); let page_indicator = format!("[{}/{}]", current_page_number, pages.lock().await.len()); mvaddstr( height - 1, width - 1 - page_indicator.len() as i32, &page_indicator, ); mvaddstr( 0, width / 2 - (load_average_string.len() / 2) as i32, &load_average_string, ); let key = getch(); match key { ncurses::KEY_RIGHT => { let locked_pages = pages.lock().await; current_page_number += 1; if current_page_number > locked_pages.len() { current_page_number = locked_pages.len(); } widgets = create_widget_window( height - 2, width, locked_pages[current_page_number - 1].widgets.len() as i32, ); current_widget = 1; } ncurses::KEY_LEFT => { let locked_pages = pages.lock().await; current_page_number -= 1; if current_page_number < 1 { current_page_number = 1; } widgets = create_widget_window( height - 2, width, locked_pages[current_page_number - 1].widgets.len() as i32, ); current_widget = 1; } ncurses::KEY_RESIZE => { erase(); refresh(); getmaxyx(term, &mut height, &mut width); resizeterm(0, 0); attron(ncurses::A_BOLD()); attron(COLOR_PAIR(4)); addstr(" rtop "); attrset(ncurses::A_NORMAL()); addstr(&format!("for {}", current_os)); display_help(height); widgets = create_widget_window(height - 2, width, current_page_widget_count as i32); } 9 => { // TAB Key current_widget += 1; if current_widget > current_page_focusable_widget_count { current_widget = current_page_focusable_widget_count; } } 353 => { // BackTAB Key current_widget -= 1; if current_widget < 1 { current_widget = 1; } } 90 => { // BackTAB Key current_widget -= 1; if current_widget < 1 { current_widget = 1; } } 113 => exit(), _ => { if current_widget != 0 { let locked_pages = &mut pages.lock().await; let current_page = &mut locked_pages[current_page_number - 1]; current_page.widgets [(current_page.focusable_widgets[current_widget - 1] as usize) - 1] .plugin .on_input(ncurses::keyname(key).unwrap_or_default()); } } } } } fn display_help(win_height: i32) { let mut help: std::collections::HashMap<&str, &str> = std::collections::HashMap::new(); help.insert("Q", "Quit"); help.insert("J", "Down"); help.insert("K", "Up"); help.insert("g", "Jump to top"); help.insert("G", "Jump to bottom"); help.insert("dd", "Kill process"); help.insert("m", "Sort by memory"); help.insert("n", "Sort by name"); help.insert("c", "Sort by CPU"); help.insert("C", "Sort by count"); mv(win_height - 1, 0); for (key, value) in help { attron(ncurses::A_BOLD()); attron(COLOR_PAIR(4)); addstr(&format!(" {} ", key)); attroff(ncurses::A_BOLD()); attroff(COLOR_PAIR(4)); addstr(&format!("{} ", value)); } } fn exit() { curs_set(ncurses::CURSOR_VISIBILITY::CURSOR_VISIBLE); endwin(); std::process::exit(0); } fn create_widget_window(height: i32, width: i32, widget_count: i32) -> Vec { let mut win_height = height; let mut win_width = width; if widget_count >= 2 { win_height = height / 2; } if widget_count == 4 { win_width = width / 2; } let widget1 = window::Window::new( win_height, win_width, 0, 1, COLOR_PAIR(2), COLOR_PAIR(4), String::from("1"), ); let widget2 = if widget_count == 3 { window::Window::new( height - win_height, (width as f32 / 2.).ceil() as i32, 0, 1 + win_height, COLOR_PAIR(2), COLOR_PAIR(4), String::from("2"), ) } else if widget_count == 4 { window::Window::new( win_height, width - win_width, win_width, 1, COLOR_PAIR(2), COLOR_PAIR(4), String::from("2"), ) } else { window::Window::new( height - win_height, win_width, 0, 1 + win_height, COLOR_PAIR(2), COLOR_PAIR(4), String::from("2"), ) }; let widget3 = if widget_count == 4 { window::Window::new( height - win_height, win_width, 0, 1 + win_height, COLOR_PAIR(2), COLOR_PAIR(4), String::from("3"), ) } else { window::Window::new( height - win_height, width - (width as f32 / 2.).ceil() as i32, width - (width / 2), 1 + win_height, COLOR_PAIR(2), COLOR_PAIR(4), String::from("3"), ) }; let widget4 = window::Window::new( height - win_height, width - win_width, win_width, 1 + win_height, COLOR_PAIR(2), COLOR_PAIR(4), String::from("4"), ); vec![widget1, widget2, widget3, widget4] } fn init_cpuusage_plugin() -> (Box, bool) { ( Box::new(CpuUsage { data: Vec::new(), chart: components::chart::Chart::new(0, 0, None, Some(true), None), sysinfo: sysinfo::System::new_all(), last_cpu_usage: 0., }), false, ) } fn init_memory_plugin() -> (Box, bool) { ( Box::new(MemoryUsage { sysinfo: sysinfo::System::new_all(), data: vec![], chart: components::chart::Chart::new(0, 0, None, Some(true), None), }), false, ) } fn init_process_plugin() -> (Box, bool) { ( Box::new(ProcessList { sysinfo: sysinfo::System::new_all(), data: vec![], chart: components::listview::ListView::new( 0, 0, &Vec::new(), String::from("Name"), vec![ String::from("CPU %"), String::from("Count"), String::from("Memory %"), ], std::option::Option::from(String::from("Name")), std::option::Option::from(Ordering::Inversed), ), refresh_progress: 6, kill_process_security: false, }), true, ) }