diff --git a/Cargo.lock b/Cargo.lock index 62308eaa..3a98e8fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -121,9 +121,9 @@ checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11" dependencies = [ "addr2line", "cc", @@ -235,9 +235,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.95" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" +checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" [[package]] name = "cfg-if" @@ -597,9 +597,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "hashbrown" @@ -888,9 +888,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" dependencies = [ "memchr", ] @@ -1114,9 +1114,9 @@ checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" @@ -1488,9 +1488,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.13" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ "indexmap", "serde", diff --git a/Cargo.toml b/Cargo.toml index 75bc8984..7e4dd767 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,10 +40,7 @@ rust-version = "1.74.0" # The oldest version I've tested that should still build [[bin]] name = "btm" -path = "src/bin/main.rs" -doc = false - -[lib] +path = "src/main.rs" test = true doctest = true doc = true @@ -78,7 +75,7 @@ default = ["deploy"] [dependencies] anyhow = "1.0.86" -backtrace = "0.3.71" +backtrace = "0.3.72" cfg-if = "1.0.0" clap = { version = "4.5.4", features = ["default", "cargo", "wrap_help", "derive"] } concat-string = "1.0.1" @@ -99,7 +96,7 @@ starship-battery = { version = "0.8.3", optional = true } sysinfo = "=0.30.12" thiserror = "1.0.61" time = { version = "0.3.36", features = ["formatting", "macros"] } -toml_edit = { version = "0.22.13", features = ["serde"] } +toml_edit = { version = "0.22.14", features = ["serde"] } tui = { version = "0.26.3", package = "ratatui" } unicode-ellipsis = "0.1.4" unicode-segmentation = "1.11.0" diff --git a/src/app.rs b/src/app.rs index 11cd358b..44976346 100644 --- a/src/app.rs +++ b/src/app.rs @@ -22,15 +22,15 @@ use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation}; use crate::{ canvas::components::time_chart::LegendPosition, - constants, - data_collection::temperature, + constants, convert_mem_data_points, convert_swap_data_points, + data_collection::{processes::Pid, temperature}, data_conversion::ConvertedData, + get_network_points, utils::{ data_units::DataUnit, error::{BottomError, Result}, }, widgets::{ProcWidgetColumn, ProcWidgetMode}, - Pid, }; #[derive(Debug, Clone, Eq, PartialEq, Default)] @@ -128,6 +128,7 @@ pub struct App { } impl App { + /// Create a new [`App`]. pub fn new( app_config_fields: AppConfigFields, states: AppWidgetStates, widget_map: HashMap, current_widget: BottomWidget, @@ -159,6 +160,87 @@ impl App { } } + /// Update the data in the [`App`]. + pub fn update_data(&mut self) { + let data_source = match &self.frozen_state { + FrozenState::NotFrozen => &self.data_collection, + FrozenState::Frozen(data) => data, + }; + + for proc in self.states.proc_state.widget_states.values_mut() { + if proc.force_update_data { + proc.set_table_data(data_source); + proc.force_update_data = false; + } + } + + // FIXME: Make this CPU force update less terrible. + if self.states.cpu_state.force_update.is_some() { + self.converted_data.convert_cpu_data(data_source); + self.converted_data.load_avg_data = data_source.load_avg_harvest; + + self.states.cpu_state.force_update = None; + } + + // FIXME: This is a bit of a temp hack to move data over. + { + let data = &self.converted_data.cpu_data; + for cpu in self.states.cpu_state.widget_states.values_mut() { + cpu.update_table(data); + } + } + { + let data = &self.converted_data.temp_data; + for temp in self.states.temp_state.widget_states.values_mut() { + if temp.force_update_data { + temp.set_table_data(data); + temp.force_update_data = false; + } + } + } + { + let data = &self.converted_data.disk_data; + for disk in self.states.disk_state.widget_states.values_mut() { + if disk.force_update_data { + disk.set_table_data(data); + disk.force_update_data = false; + } + } + } + + // TODO: [OPT] Prefer reassignment over new vectors? + if self.states.mem_state.force_update.is_some() { + self.converted_data.mem_data = convert_mem_data_points(data_source); + #[cfg(not(target_os = "windows"))] + { + self.converted_data.cache_data = crate::convert_cache_data_points(data_source); + } + self.converted_data.swap_data = convert_swap_data_points(data_source); + #[cfg(feature = "zfs")] + { + self.converted_data.arc_data = crate::convert_arc_data_points(data_source); + } + + #[cfg(feature = "gpu")] + { + self.converted_data.gpu_data = crate::convert_gpu_data(data_source); + } + self.states.mem_state.force_update = None; + } + + if self.states.net_state.force_update.is_some() { + let (rx, tx) = get_network_points( + data_source, + &self.app_config_fields.network_scale_type, + &self.app_config_fields.network_unit_type, + self.app_config_fields.network_use_binary_prefix, + ); + self.converted_data.network_data_rx = rx; + self.converted_data.network_data_tx = tx; + self.states.net_state.force_update = None; + } + } + pub fn reset(&mut self) { // Reset multi self.reset_multi_tap_keys(); diff --git a/src/app/data_farmer.rs b/src/app/data_farmer.rs index f04408eb..104778d4 100644 --- a/src/app/data_farmer.rs +++ b/src/app/data_farmer.rs @@ -20,9 +20,12 @@ use hashbrown::HashMap; #[cfg(feature = "battery")] use crate::data_collection::batteries; use crate::{ - data_collection::{cpu, disks, memory, network, processes::ProcessHarvest, temperature, Data}, + data_collection::{ + cpu, disks, memory, network, + processes::{Pid, ProcessHarvest}, + temperature, Data, + }, utils::data_prefixes::*, - Pid, }; pub type TimeOffset = f64; diff --git a/src/app/process_killer.rs b/src/app/process_killer.rs index c68b81ab..b3b198ee 100644 --- a/src/app/process_killer.rs +++ b/src/app/process_killer.rs @@ -8,9 +8,9 @@ use windows::Win32::{ }, }; +use crate::data_collection::processes::Pid; #[cfg(target_family = "unix")] use crate::utils::error::BottomError; -use crate::Pid; /// Based from [this SO answer](https://stackoverflow.com/a/55231715). #[cfg(target_os = "windows")] @@ -58,10 +58,11 @@ pub fn kill_process_given_pid(pid: Pid) -> crate::utils::error::Result<()> { Ok(()) } -/// Kills a process, given a PID, for unix. +/// Kills a process, given a PID, for UNIX. #[cfg(target_family = "unix")] pub fn kill_process_given_pid(pid: Pid, signal: usize) -> crate::utils::error::Result<()> { // SAFETY: the signal should be valid, and we act properly on an error (exit code not 0). + let output = unsafe { libc::kill(pid, signal as i32) }; if output != 0 { // We had an error... diff --git a/src/canvas/components/tui_widget/time_chart.rs b/src/canvas/components/tui_widget/time_chart.rs index 04f491bc..05ec4374 100644 --- a/src/canvas/components/tui_widget/time_chart.rs +++ b/src/canvas/components/tui_widget/time_chart.rs @@ -287,8 +287,7 @@ impl<'a> Dataset<'a> { /// You can use dots (`•`), blocks (`█`), bars (`▄`), braille (`⠓`, `⣇`, `⣿`) or half-blocks /// (`█`, `▄`, and `▀`). See [symbols::Marker] for more details. /// - /// Note [`Marker::Braille`](symbols::Marker::Braille) requires a font that supports Unicode - /// Braille Patterns. + /// Note [`Marker::Braille`] requires a font that supports Unicode Braille Patterns. /// /// This is a fluent setter method which must be chained or used as it consumes self #[must_use = "method moves the value of self and returns the modified value"] diff --git a/src/constants.rs b/src/constants.rs index 19873250..e3750a4a 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -514,7 +514,6 @@ pub const DEFAULT_BATTERY_LAYOUT: &str = r#" "#; // Config and flags -pub const DEFAULT_CONFIG_FILE_PATH: &str = "bottom/bottom.toml"; // TODO: Eventually deprecate this, or grab from a file. pub const CONFIG_TEXT: &str = r#"# This is a default config file for bottom. All of the settings are commented diff --git a/src/data_collection.rs b/src/data_collection.rs index e97b34bc..3095405a 100644 --- a/src/data_collection.rs +++ b/src/data_collection.rs @@ -17,6 +17,8 @@ use std::time::{Duration, Instant}; #[cfg(any(target_os = "linux", feature = "gpu"))] use hashbrown::HashMap; +#[cfg(not(target_os = "windows"))] +use processes::Pid; #[cfg(feature = "battery")] use starship_battery::{Battery, Manager}; @@ -147,7 +149,7 @@ pub struct DataCollector { filters: DataFilters, #[cfg(target_os = "linux")] - pid_mapping: HashMap, + pid_mapping: HashMap, #[cfg(target_os = "linux")] prev_idle: f64, #[cfg(target_os = "linux")] @@ -471,12 +473,12 @@ impl DataCollector { } } -/// We set a sleep duration between 10ms and 250ms, ideally sysinfo's [`System::MINIMUM_CPU_UPDATE_INTERVAL`] + 1. +/// We set a sleep duration between 10ms and 250ms, ideally sysinfo's [`sysinfo::MINIMUM_CPU_UPDATE_INTERVAL`] + 1. /// /// We bound the upper end to avoid waiting too long (e.g. FreeBSD is 1s, which I'm fine with losing /// accuracy on for the first refresh), and we bound the lower end just to avoid the off-chance that /// refreshing too quickly causes problems. This second case should only happen on unsupported -/// systems via sysinfo, in which case [`System::MINIMUM_CPU_UPDATE_INTERVAL`] is defined as 0. +/// systems via sysinfo, in which case [`sysinfo::MINIMUM_CPU_UPDATE_INTERVAL`] is defined as 0. /// /// We also do `INTERVAL + 1` for some wiggle room, just in case. const fn get_sleep_duration() -> Duration { diff --git a/src/data_collection/disks/unix/file_systems.rs b/src/data_collection/disks/unix/file_systems.rs index c9056826..713754e2 100644 --- a/src/data_collection/disks/unix/file_systems.rs +++ b/src/data_collection/disks/unix/file_systems.rs @@ -10,64 +10,64 @@ use crate::multi_eq_ignore_ascii_case; #[derive(Debug, Eq, PartialEq, Hash, Clone)] #[non_exhaustive] pub enum FileSystem { - /// ext2 (https://en.wikipedia.org/wiki/Ext2) + /// ext2 () Ext2, - /// ext3 (https://en.wikipedia.org/wiki/Ext3) + /// ext3 () Ext3, - /// ext4 (https://en.wikipedia.org/wiki/Ext4) + /// ext4 () Ext4, - /// FAT (https://en.wikipedia.org/wiki/File_Allocation_Table) + /// FAT () VFat, - /// exFAT (https://en.wikipedia.org/wiki/ExFAT) + /// exFAT () ExFat, - /// F2FS (https://en.wikipedia.org/wiki/F2FS) + /// F2FS () F2fs, - /// NTFS (https://en.wikipedia.org/wiki/NTFS) + /// NTFS () Ntfs, - /// ZFS (https://en.wikipedia.org/wiki/ZFS) + /// ZFS () Zfs, - /// HFS (https://en.wikipedia.org/wiki/Hierarchical_File_System) + /// HFS () Hfs, - /// HFS+ (https://en.wikipedia.org/wiki/HFS_Plus) + /// HFS+ () HfsPlus, - /// JFS (https://en.wikipedia.org/wiki/JFS_(file_system)) + /// JFS () Jfs, - /// ReiserFS 3 (https://en.wikipedia.org/wiki/ReiserFS) + /// ReiserFS 3 () Reiser3, - /// ReiserFS 4 (https://en.wikipedia.org/wiki/Reiser4) + /// ReiserFS 4 () Reiser4, - /// Btrfs (https://en.wikipedia.org/wiki/Btrfs) + /// Btrfs () Btrfs, - /// Bcachefs (https://en.wikipedia.org/wiki/Bcachefs) + /// Bcachefs () Bcachefs, - /// MINIX FS (https://en.wikipedia.org/wiki/MINIX_file_system) + /// MINIX FS () Minix, - /// NILFS (https://en.wikipedia.org/wiki/NILFS) + /// NILFS () Nilfs, - /// XFS (https://en.wikipedia.org/wiki/XFS) + /// XFS () Xfs, - /// APFS (https://en.wikipedia.org/wiki/Apple_File_System) + /// APFS () Apfs, - /// FUSE (https://en.wikipedia.org/wiki/Filesystem_in_Userspace) + /// FUSE () FuseBlk, /// Some unspecified filesystem. diff --git a/src/data_collection/disks/unix/usage.rs b/src/data_collection/disks/unix/usage.rs index d9e75323..3ee70bc4 100644 --- a/src/data_collection/disks/unix/usage.rs +++ b/src/data_collection/disks/unix/usage.rs @@ -13,7 +13,7 @@ impl Usage { u64::from(self.0.f_blocks) * u64::from(self.0.f_frsize) } - /// Returns the available number of bytes used. Note this is not necessarily the same as [`free`]. + /// Returns the available number of bytes used. Note this is not necessarily the same as [`Usage::free`]. pub fn available(&self) -> u64 { u64::from(self.0.f_bfree) * u64::from(self.0.f_frsize) } @@ -25,7 +25,7 @@ impl Usage { self.total() - avail_to_root } - /// Returns the total number of bytes free. Note this is not necessarily the same as [`available`]. + /// Returns the total number of bytes free. Note this is not necessarily the same as [`Usage::available`]. pub fn free(&self) -> u64 { u64::from(self.0.f_bavail) * u64::from(self.0.f_frsize) } diff --git a/src/data_collection/processes.rs b/src/data_collection/processes.rs index ef96c818..52caabb7 100644 --- a/src/data_collection/processes.rs +++ b/src/data_collection/processes.rs @@ -34,7 +34,17 @@ cfg_if! { use std::{borrow::Cow, time::Duration}; use super::DataCollector; -use crate::{utils::error, Pid}; +use crate::utils::error; + +cfg_if! { + if #[cfg(target_family = "windows")] { + /// A Windows process ID. + pub type Pid = usize; + } else if #[cfg(target_family = "unix")] { + /// A UNIX process ID. + pub type Pid = libc::pid_t; + } +} #[derive(Debug, Clone, Default)] pub struct ProcessHarvest { diff --git a/src/data_collection/processes/freebsd.rs b/src/data_collection/processes/freebsd.rs index f0a8262a..5ae31ec9 100644 --- a/src/data_collection/processes/freebsd.rs +++ b/src/data_collection/processes/freebsd.rs @@ -5,10 +5,7 @@ use std::{io, process::Command}; use hashbrown::HashMap; use serde::{Deserialize, Deserializer}; -use crate::{ - data_collection::{deserialize_xo, processes::UnixProcessExt}, - Pid, -}; +use crate::data_collection::{deserialize_xo, processes::UnixProcessExt, Pid}; #[derive(Deserialize, Debug, Default)] #[serde(rename_all = "kebab-case")] diff --git a/src/data_collection/processes/linux.rs b/src/data_collection/processes/linux.rs index 5d99fbbc..899963d7 100644 --- a/src/data_collection/processes/linux.rs +++ b/src/data_collection/processes/linux.rs @@ -12,14 +12,13 @@ use hashbrown::HashSet; use process::*; use sysinfo::ProcessStatus; -use super::{ProcessHarvest, UserTable}; +use super::{Pid, ProcessHarvest, UserTable}; use crate::{ data_collection::DataCollector, utils::error::{self, BottomError}, - Pid, }; -/// Maximum character length of a /proc//stat process name. +/// Maximum character length of a `/proc//stat`` process name. /// If it's equal or greater, then we instead refer to the command for the name. const MAX_STAT_NAME_LEN: usize = 15; diff --git a/src/data_collection/processes/linux/process.rs b/src/data_collection/processes/linux/process.rs index bb1d07a6..d77ff1f7 100644 --- a/src/data_collection/processes/linux/process.rs +++ b/src/data_collection/processes/linux/process.rs @@ -16,7 +16,7 @@ use rustix::{ path::Arg, }; -use crate::Pid; +use crate::data_collection::processes::Pid; static PAGESIZE: OnceLock = OnceLock::new(); diff --git a/src/data_collection/processes/macos.rs b/src/data_collection/processes/macos.rs index 8f3a09a8..e04b6ae8 100644 --- a/src/data_collection/processes/macos.rs +++ b/src/data_collection/processes/macos.rs @@ -8,7 +8,7 @@ use hashbrown::HashMap; use itertools::Itertools; use super::UnixProcessExt; -use crate::Pid; +use crate::data_collection::Pid; pub(crate) struct MacOSProcessExt; diff --git a/src/data_collection/processes/macos/sysctl_bindings.rs b/src/data_collection/processes/macos/sysctl_bindings.rs index 420c6a2e..f9ff358a 100644 --- a/src/data_collection/processes/macos/sysctl_bindings.rs +++ b/src/data_collection/processes/macos/sysctl_bindings.rs @@ -10,7 +10,7 @@ use libc::{ }; use mach2::vm_types::user_addr_t; -use crate::Pid; +use crate::data_collection::Pid; #[allow(non_camel_case_types)] #[repr(C)] diff --git a/src/data_collection/processes/unix/process_ext.rs b/src/data_collection/processes/unix/process_ext.rs index feabfea1..07077ee2 100644 --- a/src/data_collection/processes/unix/process_ext.rs +++ b/src/data_collection/processes/unix/process_ext.rs @@ -6,7 +6,7 @@ use hashbrown::HashMap; use sysinfo::{ProcessStatus, System}; use super::ProcessHarvest; -use crate::{data_collection::processes::UserTable, utils::error, Pid}; +use crate::{data_collection::processes::UserTable, data_collection::Pid, utils::error}; pub(crate) trait UnixProcessExt { fn sysinfo_process_data( diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 00000000..9b45a8c8 --- /dev/null +++ b/src/event.rs @@ -0,0 +1,142 @@ +//! Some code around handling events. + +use std::sync::mpsc::Sender; + +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind}; + +use crate::{ + app::{layout_manager::WidgetDirection, App}, + data_collection::Data, +}; + +/// Events sent to the main thread. +#[derive(Debug)] +pub enum BottomEvent { + Resize, + KeyInput(KeyEvent), + MouseInput(MouseEvent), + PasteEvent(String), + Update(Box), + Clean, + Terminate, +} + +/// Events sent to the collection thread. +#[derive(Debug)] +pub enum CollectionThreadEvent { + Reset, +} + +/// Handle a [`MouseEvent`]. +pub fn handle_mouse_event(event: MouseEvent, app: &mut App) { + match event.kind { + MouseEventKind::ScrollUp => app.handle_scroll_up(), + MouseEventKind::ScrollDown => app.handle_scroll_down(), + MouseEventKind::Down(button) => { + let (x, y) = (event.column, event.row); + if !app.app_config_fields.disable_click { + match button { + crossterm::event::MouseButton::Left => { + // Trigger left click widget activity + app.on_left_mouse_up(x, y); + } + crossterm::event::MouseButton::Right => {} + _ => {} + } + } + } + _ => {} + }; +} + +/// Handle a [`KeyEvent`]. +pub fn handle_key_event_or_break( + event: KeyEvent, app: &mut App, reset_sender: &Sender, +) -> bool { + // c_debug!("KeyEvent: {event:?}"); + + if event.modifiers.is_empty() { + // Required catch for searching - otherwise you couldn't search with q. + if event.code == KeyCode::Char('q') && !app.is_in_search_widget() { + return true; + } + match event.code { + KeyCode::End => app.skip_to_last(), + KeyCode::Home => app.skip_to_first(), + KeyCode::Up => app.on_up_key(), + KeyCode::Down => app.on_down_key(), + KeyCode::Left => app.on_left_key(), + KeyCode::Right => app.on_right_key(), + KeyCode::Char(caught_char) => app.on_char_key(caught_char), + KeyCode::Esc => app.on_esc(), + KeyCode::Enter => app.on_enter(), + KeyCode::Tab => app.on_tab(), + KeyCode::Backspace => app.on_backspace(), + KeyCode::Delete => app.on_delete(), + KeyCode::F(1) => app.toggle_ignore_case(), + KeyCode::F(2) => app.toggle_search_whole_word(), + KeyCode::F(3) => app.toggle_search_regex(), + KeyCode::F(5) => app.toggle_tree_mode(), + KeyCode::F(6) => app.toggle_sort_menu(), + KeyCode::F(9) => app.start_killing_process(), + KeyCode::PageDown => app.on_page_down(), + KeyCode::PageUp => app.on_page_up(), + _ => {} + } + } else { + // Otherwise, track the modifier as well... + if let KeyModifiers::ALT = event.modifiers { + match event.code { + KeyCode::Char('c') | KeyCode::Char('C') => app.toggle_ignore_case(), + KeyCode::Char('w') | KeyCode::Char('W') => app.toggle_search_whole_word(), + KeyCode::Char('r') | KeyCode::Char('R') => app.toggle_search_regex(), + KeyCode::Char('h') => app.on_left_key(), + KeyCode::Char('l') => app.on_right_key(), + _ => {} + } + } else if let KeyModifiers::CONTROL = event.modifiers { + if event.code == KeyCode::Char('c') { + return true; + } + + match event.code { + KeyCode::Char('f') => app.on_slash(), + KeyCode::Left => app.move_widget_selection(&WidgetDirection::Left), + KeyCode::Right => app.move_widget_selection(&WidgetDirection::Right), + KeyCode::Up => app.move_widget_selection(&WidgetDirection::Up), + KeyCode::Down => app.move_widget_selection(&WidgetDirection::Down), + KeyCode::Char('r') => { + if reset_sender.send(CollectionThreadEvent::Reset).is_ok() { + app.reset(); + } + } + KeyCode::Char('a') => app.skip_cursor_beginning(), + KeyCode::Char('e') => app.skip_cursor_end(), + KeyCode::Char('u') if app.is_in_search_widget() => app.clear_search(), + KeyCode::Char('w') => app.clear_previous_word(), + KeyCode::Char('h') => app.on_backspace(), + KeyCode::Char('d') => app.scroll_half_page_down(), + KeyCode::Char('u') => app.scroll_half_page_up(), + // KeyCode::Char('j') => {}, // Move down + // KeyCode::Char('k') => {}, // Move up + // KeyCode::Char('h') => {}, // Move right + // KeyCode::Char('l') => {}, // Move left + // Can't do now, CTRL+BACKSPACE doesn't work and graphemes + // are hard to iter while truncating last (eloquently). + // KeyCode::Backspace => app.skip_word_backspace(), + _ => {} + } + } else if let KeyModifiers::SHIFT = event.modifiers { + match event.code { + KeyCode::Left => app.move_widget_selection(&WidgetDirection::Left), + KeyCode::Right => app.move_widget_selection(&WidgetDirection::Right), + KeyCode::Up => app.move_widget_selection(&WidgetDirection::Up), + KeyCode::Down => app.move_widget_selection(&WidgetDirection::Down), + KeyCode::Char(caught_char) => app.on_char_key(caught_char), + _ => {} + } + } + } + + false +} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index f80317e1..00000000 --- a/src/lib.rs +++ /dev/null @@ -1,547 +0,0 @@ -//! A customizable cross-platform graphical process/system monitor for the terminal. -//! Supports Linux, macOS, and Windows. Inspired by gtop, gotop, and htop. -//! -//! **Note:** The following documentation is primarily intended for people to refer to for development purposes rather -//! than the actual usage of the application. If you are instead looking for documentation regarding the *usage* of -//! bottom, refer to [here](https://clementtsang.github.io/bottom/stable/). - -#![deny(rust_2018_idioms)] -// #![deny(missing_docs)] -#![deny(unused_extern_crates)] -#![deny(rustdoc::broken_intra_doc_links)] -#![deny(rustdoc::missing_crate_level_docs)] -#![deny(clippy::todo)] -#![deny(clippy::unimplemented)] -#![deny(clippy::missing_safety_doc)] - -pub mod app; -pub mod utils { - pub mod data_prefixes; - pub mod data_units; - pub mod error; - pub mod general; - pub mod logging; - pub mod strings; -} -pub mod canvas; -pub mod constants; -pub mod data_collection; -pub mod data_conversion; -pub mod options; -pub mod widgets; - -use std::{ - boxed::Box, - fs, - io::{stderr, stdout, Write}, - panic::PanicInfo, - path::{Path, PathBuf}, - sync::{ - mpsc::{Receiver, Sender}, - Arc, Condvar, Mutex, - }, - thread::{self, JoinHandle}, - time::{Duration, Instant}, -}; - -use app::{ - frozen_state::FrozenState, - layout_manager::{UsedWidgets, WidgetDirection}, - App, AppConfigFields, DataFilters, -}; -use constants::*; -use crossterm::{ - event::{ - poll, read, DisableBracketedPaste, DisableMouseCapture, Event, KeyCode, KeyEvent, - KeyEventKind, KeyModifiers, MouseEvent, MouseEventKind, - }, - execute, - style::Print, - terminal::{disable_raw_mode, LeaveAlternateScreen}, -}; -use data_conversion::*; -pub use options::args; -use options::ConfigV1; -use utils::error; -#[allow(unused_imports)] -pub use utils::logging::*; - -#[cfg(target_family = "windows")] -pub type Pid = usize; - -#[cfg(target_family = "unix")] -pub type Pid = libc::pid_t; - -/// Events sent to the main thread. -#[derive(Debug)] -pub enum BottomEvent { - Resize, - KeyInput(KeyEvent), - MouseInput(MouseEvent), - PasteEvent(String), - Update(Box), - Clean, - Terminate, -} - -/// Events sent to the collection thread. -#[derive(Debug)] -pub enum CollectionThreadEvent { - Reset, -} - -pub fn handle_mouse_event(event: MouseEvent, app: &mut App) { - match event.kind { - MouseEventKind::ScrollUp => app.handle_scroll_up(), - MouseEventKind::ScrollDown => app.handle_scroll_down(), - MouseEventKind::Down(button) => { - let (x, y) = (event.column, event.row); - if !app.app_config_fields.disable_click { - match button { - crossterm::event::MouseButton::Left => { - // Trigger left click widget activity - app.on_left_mouse_up(x, y); - } - crossterm::event::MouseButton::Right => {} - _ => {} - } - } - } - _ => {} - }; -} - -pub fn handle_key_event_or_break( - event: KeyEvent, app: &mut App, reset_sender: &Sender, -) -> bool { - // c_debug!("KeyEvent: {event:?}"); - - if event.modifiers.is_empty() { - // Required catch for searching - otherwise you couldn't search with q. - if event.code == KeyCode::Char('q') && !app.is_in_search_widget() { - return true; - } - match event.code { - KeyCode::End => app.skip_to_last(), - KeyCode::Home => app.skip_to_first(), - KeyCode::Up => app.on_up_key(), - KeyCode::Down => app.on_down_key(), - KeyCode::Left => app.on_left_key(), - KeyCode::Right => app.on_right_key(), - KeyCode::Char(caught_char) => app.on_char_key(caught_char), - KeyCode::Esc => app.on_esc(), - KeyCode::Enter => app.on_enter(), - KeyCode::Tab => app.on_tab(), - KeyCode::Backspace => app.on_backspace(), - KeyCode::Delete => app.on_delete(), - KeyCode::F(1) => app.toggle_ignore_case(), - KeyCode::F(2) => app.toggle_search_whole_word(), - KeyCode::F(3) => app.toggle_search_regex(), - KeyCode::F(5) => app.toggle_tree_mode(), - KeyCode::F(6) => app.toggle_sort_menu(), - KeyCode::F(9) => app.start_killing_process(), - KeyCode::PageDown => app.on_page_down(), - KeyCode::PageUp => app.on_page_up(), - _ => {} - } - } else { - // Otherwise, track the modifier as well... - if let KeyModifiers::ALT = event.modifiers { - match event.code { - KeyCode::Char('c') | KeyCode::Char('C') => app.toggle_ignore_case(), - KeyCode::Char('w') | KeyCode::Char('W') => app.toggle_search_whole_word(), - KeyCode::Char('r') | KeyCode::Char('R') => app.toggle_search_regex(), - KeyCode::Char('h') => app.on_left_key(), - KeyCode::Char('l') => app.on_right_key(), - _ => {} - } - } else if let KeyModifiers::CONTROL = event.modifiers { - if event.code == KeyCode::Char('c') { - return true; - } - - match event.code { - KeyCode::Char('f') => app.on_slash(), - KeyCode::Left => app.move_widget_selection(&WidgetDirection::Left), - KeyCode::Right => app.move_widget_selection(&WidgetDirection::Right), - KeyCode::Up => app.move_widget_selection(&WidgetDirection::Up), - KeyCode::Down => app.move_widget_selection(&WidgetDirection::Down), - KeyCode::Char('r') => { - if reset_sender.send(CollectionThreadEvent::Reset).is_ok() { - app.reset(); - } - } - KeyCode::Char('a') => app.skip_cursor_beginning(), - KeyCode::Char('e') => app.skip_cursor_end(), - KeyCode::Char('u') if app.is_in_search_widget() => app.clear_search(), - KeyCode::Char('w') => app.clear_previous_word(), - KeyCode::Char('h') => app.on_backspace(), - KeyCode::Char('d') => app.scroll_half_page_down(), - KeyCode::Char('u') => app.scroll_half_page_up(), - // KeyCode::Char('j') => {}, // Move down - // KeyCode::Char('k') => {}, // Move up - // KeyCode::Char('h') => {}, // Move right - // KeyCode::Char('l') => {}, // Move left - // Can't do now, CTRL+BACKSPACE doesn't work and graphemes - // are hard to iter while truncating last (eloquently). - // KeyCode::Backspace => app.skip_word_backspace(), - _ => {} - } - } else if let KeyModifiers::SHIFT = event.modifiers { - match event.code { - KeyCode::Left => app.move_widget_selection(&WidgetDirection::Left), - KeyCode::Right => app.move_widget_selection(&WidgetDirection::Right), - KeyCode::Up => app.move_widget_selection(&WidgetDirection::Up), - KeyCode::Down => app.move_widget_selection(&WidgetDirection::Down), - KeyCode::Char(caught_char) => app.on_char_key(caught_char), - _ => {} - } - } - } - - false -} - -pub fn get_config_path(override_config_path: Option<&Path>) -> Option { - if let Some(conf_loc) = override_config_path { - Some(conf_loc.to_path_buf()) - } else if cfg!(target_os = "windows") { - if let Some(home_path) = dirs::config_dir() { - let mut path = home_path; - path.push(DEFAULT_CONFIG_FILE_PATH); - Some(path) - } else { - None - } - } else if let Some(home_path) = dirs::home_dir() { - let mut path = home_path; - path.push(".config/"); - path.push(DEFAULT_CONFIG_FILE_PATH); - if path.exists() { - // If it already exists, use the old one. - Some(path) - } else { - // If it does not, use the new one! - if let Some(config_path) = dirs::config_dir() { - let mut path = config_path; - path.push(DEFAULT_CONFIG_FILE_PATH); - Some(path) - } else { - None - } - } - } else { - None - } -} - -pub fn get_or_create_config(override_config_path: Option<&Path>) -> error::Result { - let config_path = get_config_path(override_config_path); - - if let Some(path) = &config_path { - if let Ok(config_string) = fs::read_to_string(path) { - Ok(toml_edit::de::from_str(config_string.as_str())?) - } else { - if let Some(parent_path) = path.parent() { - fs::create_dir_all(parent_path)?; - } - - fs::File::create(path)?.write_all(CONFIG_TEXT.as_bytes())?; - Ok(ConfigV1::default()) - } - } else { - // If we somehow don't have any config path, then just assume the default config but don't write to any file. - Ok(ConfigV1::default()) - } -} - -pub fn try_drawing( - terminal: &mut tui::terminal::Terminal>, - app: &mut App, painter: &mut canvas::Painter, -) -> error::Result<()> { - if let Err(err) = painter.draw_data(terminal, app) { - cleanup_terminal(terminal)?; - Err(err) - } else { - Ok(()) - } -} - -pub fn cleanup_terminal( - terminal: &mut tui::terminal::Terminal>, -) -> error::Result<()> { - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - DisableBracketedPaste, - DisableMouseCapture, - LeaveAlternateScreen - )?; - terminal.show_cursor()?; - - Ok(()) -} - -/// Check and report to the user if the current environment is not a terminal. -pub fn check_if_terminal() { - use crossterm::tty::IsTty; - - if !stdout().is_tty() { - eprintln!( - "Warning: bottom is not being output to a terminal. Things might not work properly." - ); - eprintln!("If you're stuck, press 'q' or 'Ctrl-c' to quit the program."); - stderr().flush().unwrap(); - thread::sleep(Duration::from_secs(1)); - } -} - -/// A panic hook to properly restore the terminal in the case of a panic. -/// Originally based on [spotify-tui's implementation](https://github.com/Rigellute/spotify-tui/blob/master/src/main.rs). -pub fn panic_hook(panic_info: &PanicInfo<'_>) { - let mut stdout = stdout(); - - let msg = match panic_info.payload().downcast_ref::<&'static str>() { - Some(s) => *s, - None => match panic_info.payload().downcast_ref::() { - Some(s) => &s[..], - None => "Box", - }, - }; - - let backtrace = format!("{:?}", backtrace::Backtrace::new()); - - let _ = disable_raw_mode(); - let _ = execute!( - stdout, - DisableBracketedPaste, - DisableMouseCapture, - LeaveAlternateScreen - ); - - // Print stack trace. Must be done after! - if let Some(panic_info) = panic_info.location() { - let _ = execute!( - stdout, - Print(format!( - "thread '' panicked at '{msg}', {panic_info}\n\r{backtrace}", - )), - ); - } -} - -pub fn update_data(app: &mut App) { - let data_source = match &app.frozen_state { - FrozenState::NotFrozen => &app.data_collection, - FrozenState::Frozen(data) => data, - }; - - for proc in app.states.proc_state.widget_states.values_mut() { - if proc.force_update_data { - proc.set_table_data(data_source); - proc.force_update_data = false; - } - } - - // FIXME: Make this CPU force update less terrible. - if app.states.cpu_state.force_update.is_some() { - app.converted_data.convert_cpu_data(data_source); - app.converted_data.load_avg_data = data_source.load_avg_harvest; - - app.states.cpu_state.force_update = None; - } - - // FIXME: This is a bit of a temp hack to move data over. - { - let data = &app.converted_data.cpu_data; - for cpu in app.states.cpu_state.widget_states.values_mut() { - cpu.update_table(data); - } - } - { - let data = &app.converted_data.temp_data; - for temp in app.states.temp_state.widget_states.values_mut() { - if temp.force_update_data { - temp.set_table_data(data); - temp.force_update_data = false; - } - } - } - { - let data = &app.converted_data.disk_data; - for disk in app.states.disk_state.widget_states.values_mut() { - if disk.force_update_data { - disk.set_table_data(data); - disk.force_update_data = false; - } - } - } - - // TODO: [OPT] Prefer reassignment over new vectors? - if app.states.mem_state.force_update.is_some() { - app.converted_data.mem_data = convert_mem_data_points(data_source); - #[cfg(not(target_os = "windows"))] - { - app.converted_data.cache_data = convert_cache_data_points(data_source); - } - app.converted_data.swap_data = convert_swap_data_points(data_source); - #[cfg(feature = "zfs")] - { - app.converted_data.arc_data = convert_arc_data_points(data_source); - } - - #[cfg(feature = "gpu")] - { - app.converted_data.gpu_data = convert_gpu_data(data_source); - } - app.states.mem_state.force_update = None; - } - - if app.states.net_state.force_update.is_some() { - let (rx, tx) = get_network_points( - data_source, - &app.app_config_fields.network_scale_type, - &app.app_config_fields.network_unit_type, - app.app_config_fields.network_use_binary_prefix, - ); - app.converted_data.network_data_rx = rx; - app.converted_data.network_data_tx = tx; - app.states.net_state.force_update = None; - } -} - -pub fn create_input_thread( - sender: Sender, termination_ctrl_lock: Arc>, -) -> JoinHandle<()> { - thread::spawn(move || { - let mut mouse_timer = Instant::now(); - - loop { - if let Ok(is_terminated) = termination_ctrl_lock.try_lock() { - // We don't block. - if *is_terminated { - drop(is_terminated); - break; - } - } - if let Ok(poll) = poll(Duration::from_millis(20)) { - if poll { - if let Ok(event) = read() { - match event { - Event::Resize(_, _) => { - // TODO: Might want to debounce this in the future, or take into account the actual resize - // values. Maybe we want to keep the current implementation in case the resize event might - // not fire... not sure. - - if sender.send(BottomEvent::Resize).is_err() { - break; - } - } - Event::Paste(paste) => { - if sender.send(BottomEvent::PasteEvent(paste)).is_err() { - break; - } - } - Event::Key(key) if key.kind == KeyEventKind::Press => { - // For now, we only care about key down events. This may change in the future. - if sender.send(BottomEvent::KeyInput(key)).is_err() { - break; - } - } - Event::Mouse(mouse) => match mouse.kind { - MouseEventKind::Moved | MouseEventKind::Drag(..) => {} - MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => { - if Instant::now().duration_since(mouse_timer).as_millis() >= 20 - { - if sender.send(BottomEvent::MouseInput(mouse)).is_err() { - break; - } - mouse_timer = Instant::now(); - } - } - _ => { - if sender.send(BottomEvent::MouseInput(mouse)).is_err() { - break; - } - } - }, - Event::Key(_) => {} - Event::FocusGained => {} - Event::FocusLost => {} - } - } - } - } - } - }) -} - -pub fn create_collection_thread( - sender: Sender, control_receiver: Receiver, - termination_lock: Arc>, termination_cvar: Arc, - app_config_fields: &AppConfigFields, filters: DataFilters, used_widget_set: UsedWidgets, -) -> JoinHandle<()> { - let temp_type = app_config_fields.temperature_type; - let use_current_cpu_total = app_config_fields.use_current_cpu_total; - let unnormalized_cpu = app_config_fields.unnormalized_cpu; - let show_average_cpu = app_config_fields.show_average_cpu; - let update_time = app_config_fields.update_rate; - - thread::spawn(move || { - let mut data_state = data_collection::DataCollector::new(filters); - - data_state.set_data_collection(used_widget_set); - data_state.set_temperature_type(temp_type); - data_state.set_use_current_cpu_total(use_current_cpu_total); - data_state.set_unnormalized_cpu(unnormalized_cpu); - data_state.set_show_average_cpu(show_average_cpu); - - data_state.init(); - - loop { - // Check once at the very top... don't block though. - if let Ok(is_terminated) = termination_lock.try_lock() { - if *is_terminated { - drop(is_terminated); - break; - } - } - - if let Ok(message) = control_receiver.try_recv() { - // trace!("Received message in collection thread: {message:?}"); - match message { - CollectionThreadEvent::Reset => { - data_state.data.cleanup(); - } - } - } - - data_state.update_data(); - - // Yet another check to bail if needed... do not block! - if let Ok(is_terminated) = termination_lock.try_lock() { - if *is_terminated { - drop(is_terminated); - break; - } - } - - let event = BottomEvent::Update(Box::from(data_state.data)); - data_state.data = data_collection::Data::default(); - if sender.send(event).is_err() { - break; - } - - // This is actually used as a "sleep" that can be interrupted by another thread. - if let Ok((is_terminated, _)) = termination_cvar.wait_timeout( - termination_lock.lock().unwrap(), - Duration::from_millis(update_time), - ) { - if *is_terminated { - drop(is_terminated); - break; - } - } - } - }) -} diff --git a/src/bin/main.rs b/src/main.rs similarity index 52% rename from src/bin/main.rs rename to src/main.rs index f22e828b..ca4656e1 100644 --- a/src/bin/main.rs +++ b/src/main.rs @@ -1,46 +1,287 @@ -#![deny(rust_2018_idioms)] -#![deny(clippy::todo)] -#![deny(clippy::unimplemented)] -#![deny(clippy::missing_safety_doc)] +//! A customizable cross-platform graphical process/system monitor for the terminal. +//! Supports Linux, macOS, and Windows. Inspired by gtop, gotop, and htop. +//! +//! **Note:** The following documentation is primarily intended for people to refer to for development purposes rather +//! than the actual usage of the application. If you are instead looking for documentation regarding the *usage* of +//! bottom, refer to [here](https://clementtsang.github.io/bottom/stable/). + +pub mod app; +pub mod utils { + pub mod data_prefixes; + pub mod data_units; + pub mod error; + pub mod general; + pub mod logging; + pub mod strings; +} +pub mod canvas; +pub mod constants; +pub mod data_collection; +pub mod data_conversion; +pub mod event; +pub mod options; +pub mod widgets; use std::{ boxed::Box, - io::stdout, - panic, - sync::{mpsc, Arc, Condvar, Mutex}, - thread, - time::Duration, + io::{stderr, stdout, Write}, + panic::{self, PanicInfo}, + sync::{ + mpsc::{self, Receiver, Sender}, + Arc, Condvar, Mutex, + }, + thread::{self, JoinHandle}, + time::{Duration, Instant}, }; -use anyhow::{Context, Result}; -use bottom::{ - args, - canvas::{self, styling::CanvasStyling}, - check_if_terminal, cleanup_terminal, create_collection_thread, create_input_thread, - data_conversion::*, - get_or_create_config, handle_key_event_or_break, handle_mouse_event, - options::{get_color_scheme, init_app}, - panic_hook, try_drawing, update_data, BottomEvent, -}; +use anyhow::Context; +use app::{layout_manager::UsedWidgets, App, AppConfigFields, DataFilters}; +use canvas::styling::CanvasStyling; use crossterm::{ - event::{EnableBracketedPaste, EnableMouseCapture}, + event::{ + poll, read, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, + EnableMouseCapture, Event, KeyEventKind, MouseEventKind, + }, execute, - terminal::{enable_raw_mode, EnterAlternateScreen}, + style::Print, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; +use data_conversion::*; +use event::{handle_key_event_or_break, handle_mouse_event, BottomEvent, CollectionThreadEvent}; +use options::args; +use options::{get_color_scheme, get_config_path, get_or_create_config, init_app}; use tui::{backend::CrosstermBackend, Terminal}; +use utils::error; +#[allow(unused_imports)] +use utils::logging::*; // Used for heap allocation debugging purposes. // #[global_allocator] // static ALLOC: dhat::Alloc = dhat::Alloc; -fn main() -> Result<()> { +/// Try drawing. If not, clean up the terminal and return an error. +fn try_drawing( + terminal: &mut Terminal>, app: &mut App, + painter: &mut canvas::Painter, +) -> error::Result<()> { + if let Err(err) = painter.draw_data(terminal, app) { + cleanup_terminal(terminal)?; + Err(err) + } else { + Ok(()) + } +} + +/// Clean up the terminal before returning it to the user. +fn cleanup_terminal( + terminal: &mut Terminal>, +) -> error::Result<()> { + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + DisableBracketedPaste, + DisableMouseCapture, + LeaveAlternateScreen + )?; + terminal.show_cursor()?; + + Ok(()) +} + +/// Check and report to the user if the current environment is not a terminal. +fn check_if_terminal() { + use crossterm::tty::IsTty; + + if !stdout().is_tty() { + eprintln!( + "Warning: bottom is not being output to a terminal. Things might not work properly." + ); + eprintln!("If you're stuck, press 'q' or 'Ctrl-c' to quit the program."); + stderr().flush().unwrap(); + thread::sleep(Duration::from_secs(1)); + } +} + +/// A panic hook to properly restore the terminal in the case of a panic. +/// Originally based on [spotify-tui's implementation](https://github.com/Rigellute/spotify-tui/blob/master/src/main.rs). +fn panic_hook(panic_info: &PanicInfo<'_>) { + let mut stdout = stdout(); + + let msg = match panic_info.payload().downcast_ref::<&'static str>() { + Some(s) => *s, + None => match panic_info.payload().downcast_ref::() { + Some(s) => &s[..], + None => "Box", + }, + }; + + let backtrace = format!("{:?}", backtrace::Backtrace::new()); + + let _ = disable_raw_mode(); + let _ = execute!( + stdout, + DisableBracketedPaste, + DisableMouseCapture, + LeaveAlternateScreen + ); + + // Print stack trace. Must be done after! + if let Some(panic_info) = panic_info.location() { + let _ = execute!( + stdout, + Print(format!( + "thread '' panicked at '{msg}', {panic_info}\n\r{backtrace}", + )), + ); + } +} + +/// Create a thread to poll for user inputs and forward them to the main thread. +fn create_input_thread( + sender: Sender, termination_ctrl_lock: Arc>, +) -> JoinHandle<()> { + thread::spawn(move || { + let mut mouse_timer = Instant::now(); + + loop { + if let Ok(is_terminated) = termination_ctrl_lock.try_lock() { + // We don't block. + if *is_terminated { + drop(is_terminated); + break; + } + } + if let Ok(poll) = poll(Duration::from_millis(20)) { + if poll { + if let Ok(event) = read() { + match event { + Event::Resize(_, _) => { + // TODO: Might want to debounce this in the future, or take into account the actual resize + // values. Maybe we want to keep the current implementation in case the resize event might + // not fire... not sure. + + if sender.send(BottomEvent::Resize).is_err() { + break; + } + } + Event::Paste(paste) => { + if sender.send(BottomEvent::PasteEvent(paste)).is_err() { + break; + } + } + Event::Key(key) if key.kind == KeyEventKind::Press => { + // For now, we only care about key down events. This may change in the future. + if sender.send(BottomEvent::KeyInput(key)).is_err() { + break; + } + } + Event::Mouse(mouse) => match mouse.kind { + MouseEventKind::Moved | MouseEventKind::Drag(..) => {} + MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => { + if Instant::now().duration_since(mouse_timer).as_millis() >= 20 + { + if sender.send(BottomEvent::MouseInput(mouse)).is_err() { + break; + } + mouse_timer = Instant::now(); + } + } + _ => { + if sender.send(BottomEvent::MouseInput(mouse)).is_err() { + break; + } + } + }, + Event::Key(_) => {} + Event::FocusGained => {} + Event::FocusLost => {} + } + } + } + } + } + }) +} + +/// Create a thread to handle data collection. +fn create_collection_thread( + sender: Sender, control_receiver: Receiver, + termination_lock: Arc>, termination_cvar: Arc, + app_config_fields: &AppConfigFields, filters: DataFilters, used_widget_set: UsedWidgets, +) -> JoinHandle<()> { + let temp_type = app_config_fields.temperature_type; + let use_current_cpu_total = app_config_fields.use_current_cpu_total; + let unnormalized_cpu = app_config_fields.unnormalized_cpu; + let show_average_cpu = app_config_fields.show_average_cpu; + let update_time = app_config_fields.update_rate; + + thread::spawn(move || { + let mut data_state = data_collection::DataCollector::new(filters); + + data_state.set_data_collection(used_widget_set); + data_state.set_temperature_type(temp_type); + data_state.set_use_current_cpu_total(use_current_cpu_total); + data_state.set_unnormalized_cpu(unnormalized_cpu); + data_state.set_show_average_cpu(show_average_cpu); + + data_state.init(); + + loop { + // Check once at the very top... don't block though. + if let Ok(is_terminated) = termination_lock.try_lock() { + if *is_terminated { + drop(is_terminated); + break; + } + } + + if let Ok(message) = control_receiver.try_recv() { + // trace!("Received message in collection thread: {message:?}"); + match message { + CollectionThreadEvent::Reset => { + data_state.data.cleanup(); + } + } + } + + data_state.update_data(); + + // Yet another check to bail if needed... do not block! + if let Ok(is_terminated) = termination_lock.try_lock() { + if *is_terminated { + drop(is_terminated); + break; + } + } + + let event = BottomEvent::Update(Box::from(data_state.data)); + data_state.data = data_collection::Data::default(); + if sender.send(event).is_err() { + break; + } + + // This is actually used as a "sleep" that can be interrupted by another thread. + if let Ok((is_terminated, _)) = termination_cvar.wait_timeout( + termination_lock.lock().unwrap(), + Duration::from_millis(update_time), + ) { + if *is_terminated { + drop(is_terminated); + break; + } + } + } + }) +} + +fn main() -> anyhow::Result<()> { // let _profiler = dhat::Profiler::new_heap(); let args = args::get_args(); #[cfg(feature = "logging")] { - if let Err(err) = bottom::init_logger( + if let Err(err) = init_logger( log::LevelFilter::Debug, Some(std::ffi::OsStr::new("debug.log")), ) { @@ -49,8 +290,11 @@ fn main() -> Result<()> { } // Read from config file. - let config = get_or_create_config(args.general.config_location.as_deref()) - .context("Unable to parse or create the config file.")?; + let config = { + let config_path = get_config_path(args.general.config_location.as_deref()); + get_or_create_config(config_path.as_deref()) + .context("Unable to parse or create the config file.")? + }; // FIXME: Should move this into build app or config let styling = { @@ -166,17 +410,17 @@ fn main() -> Result<()> { if handle_key_event_or_break(event, &mut app, &collection_thread_ctrl_sender) { break; } - update_data(&mut app); + app.update_data(); try_drawing(&mut terminal, &mut app, &mut painter)?; } BottomEvent::MouseInput(event) => { handle_mouse_event(event, &mut app); - update_data(&mut app); + app.update_data(); try_drawing(&mut terminal, &mut app, &mut painter)?; } BottomEvent::PasteEvent(paste) => { app.handle_paste(paste); - update_data(&mut app); + app.update_data(); try_drawing(&mut terminal, &mut app, &mut painter)?; } BottomEvent::Update(data) => { @@ -295,7 +539,7 @@ fn main() -> Result<()> { } } - update_data(&mut app); + app.update_data(); try_drawing(&mut terminal, &mut app, &mut painter)?; } } diff --git a/src/options.rs b/src/options.rs index c98618b2..998cc285 100644 --- a/src/options.rs +++ b/src/options.rs @@ -8,6 +8,9 @@ pub mod config; use std::{ convert::TryInto, + fs, + io::Write, + path::{Path, PathBuf}, str::FromStr, time::{Duration, Instant}, }; @@ -59,6 +62,63 @@ macro_rules! is_flag_enabled { }; } +/// Returns the config path to use. If `override_config_path` is specified, then we will use +/// that. If not, then return the "default" config path, which is: +/// - If a path already exists at `/bottom/bottom.toml`, then use that for legacy reasons. +/// - Otherwise, use `/bottom/bottom.toml`. +/// +/// For more details on this, see [dirs](https://docs.rs/dirs/latest/dirs/fn.config_dir.html)' +/// documentation. +pub fn get_config_path(override_config_path: Option<&Path>) -> Option { + const DEFAULT_CONFIG_FILE_PATH: &str = "bottom/bottom.toml"; + + if let Some(conf_loc) = override_config_path { + return Some(conf_loc.to_path_buf()); + } else if let Some(home_path) = dirs::home_dir() { + let mut old_home_path = home_path; + old_home_path.push(".config/"); + old_home_path.push(DEFAULT_CONFIG_FILE_PATH); + if old_home_path.exists() { + // We used to create it at `/DEFAULT_CONFIG_FILE_PATH`, but changed it + // to be more correct later. However, for legacy reasons, if it already exists, + // use the old one. + return Some(old_home_path); + } + } + + // Otherwise, return the "correct" path based on the config dir. + dirs::config_dir().map(|mut path| { + path.push(DEFAULT_CONFIG_FILE_PATH); + path + }) +} + +/// Get the config at `config_path`. If there is no config file at the specified path, it will +/// try to create a new file with the default settings, and return the default config. If bottom +/// fails to write a new config, it will silently just return the default config. +pub fn get_or_create_config(config_path: Option<&Path>) -> error::Result { + match &config_path { + Some(path) => { + if let Ok(config_string) = fs::read_to_string(path) { + Ok(toml_edit::de::from_str(config_string.as_str())?) + } else { + if let Some(parent_path) = path.parent() { + fs::create_dir_all(parent_path)?; + } + + fs::File::create(path)?.write_all(CONFIG_TEXT.as_bytes())?; + Ok(ConfigV1::default()) + } + } + None => { + // If we somehow don't have any config path, then just assume the default config but don't write to any file. + // + // TODO: Maybe make this "show" an error, but don't crash. + Ok(ConfigV1::default()) + } + } +} + pub fn init_app( args: BottomArgs, config: ConfigV1, styling: &CanvasStyling, ) -> Result<(App, BottomLayout)> { diff --git a/src/options/config/ignore_list.rs b/src/options/config/ignore_list.rs index d70224c1..472f7fa6 100644 --- a/src/options/config/ignore_list.rs +++ b/src/options/config/ignore_list.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -/// Workaround as per https://github.com/serde-rs/serde/issues/1030 +/// Workaround as per . fn default_as_true() -> bool { true } diff --git a/src/widgets/process_table.rs b/src/widgets/process_table.rs index 8d2c5ba1..0b081dec 100644 --- a/src/widgets/process_table.rs +++ b/src/widgets/process_table.rs @@ -25,8 +25,7 @@ use crate::{ }, styling::CanvasStyling, }, - data_collection::processes::ProcessHarvest, - Pid, + data_collection::processes::{Pid, ProcessHarvest}, }; /// ProcessSearchState only deals with process' search's current settings and state. @@ -1012,7 +1011,7 @@ impl ProcWidgetState { self.table.columns.iter().filter(|c| !c.is_hidden).count() } - /// Sets the [`ProcWidget`]'s current sort index to whatever was in the sort table if possible, then closes the + /// Sets the [`ProcWidgetState`]'s current sort index to whatever was in the sort table if possible, then closes the /// sort table. pub(crate) fn use_sort_table_value(&mut self) { self.table.set_sort_index(self.sort_table.current_index()); diff --git a/src/widgets/process_table/proc_widget_data.rs b/src/widgets/process_table/proc_widget_data.rs index 656be892..034c36e5 100644 --- a/src/widgets/process_table/proc_widget_data.rs +++ b/src/widgets/process_table/proc_widget_data.rs @@ -15,9 +15,8 @@ use crate::{ components::data_table::{DataTableColumn, DataToCell}, Painter, }, - data_collection::processes::ProcessHarvest, + data_collection::processes::{Pid, ProcessHarvest}, data_conversion::{binary_byte_string, dec_bytes_per_second_string, dec_bytes_string}, - Pid, }; #[derive(Clone, Debug)]