diff --git a/Cargo.toml b/Cargo.toml index 2fa5989a..f58cbfd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,7 +92,7 @@ starship-battery = { version = "0.10.1", optional = true } sysinfo = "=0.30.13" timeless = "0.0.14-alpha" toml_edit = { version = "0.22.26", features = ["serde"] } -tui = { version = "0.29.0", package = "ratatui" } +tui = { version = "0.29.0", package = "ratatui", features = ["unstable-rendered-line-info"] } unicode-ellipsis = "0.3.0" unicode-segmentation = "1.12.0" unicode-width = "0.2.0" @@ -221,7 +221,6 @@ depends = "libc6:armhf (>= 2.28)" [package.metadata.wix] output = "bottom_x86_64_installer.msi" - [package.metadata.generate-rpm] assets = [ { source = "target/release/btm", dest = "/usr/bin/", mode = "755" }, diff --git a/docs/content/configuration/command-line-options.md b/docs/content/configuration/command-line-options.md index 8e4286b3..ce989b2f 100644 --- a/docs/content/configuration/command-line-options.md +++ b/docs/content/configuration/command-line-options.md @@ -25,18 +25,18 @@ see information on these options by running `btm -h`, or run `btm --help` to dis ## Process Options -| Option | Behaviour | -| --------------------------- | -------------------------------------------------------------------------------------- | -| `-S, --case_sensitive` | Enables case sensitivity by default. | -| `-u, --current_usage` | Calculates process CPU usage as a percentage of current usage rather than total usage. | -| `--disable_advanced_kill` | Hides additional stopping options Unix-like systems. | -| `-g, --group_processes` | Groups processes with the same name by default. No effect if `--tree` is set. | -| `--process_memory_as_value` | Defaults to showing process memory usage by value. | -| `--process_command` | Shows the full command name instead of the process name by default. | -| `-R, --regex` | Enables regex by default while searching. | -| `-T, --tree` | Makes the process widget use tree mode by default. | -| `-n, --unnormalized_cpu` | Show process CPU% usage without averaging over the number of CPU cores. | -| `-W, --whole_word` | Enables whole-word matching by default while searching. | +| Option | Behaviour | +| --------------------------- | --------------------------------------------------------------------------------------------- | +| `-S, --case_sensitive` | Enables case sensitivity by default. | +| `-u, --current_usage` | Calculates process CPU usage as a percentage of current usage rather than total usage. | +| `--disable_advanced_kill` | Disable being able to send signals to processes. Only available on Linux, macOS, and FreeBSD. | +| `-g, --group_processes` | Groups processes with the same name by default. No effect if `--tree` is set. | +| `--process_memory_as_value` | Defaults to showing process memory usage by value. | +| `--process_command` | Shows the full command name instead of the process name by default. | +| `-R, --regex` | Enables regex by default while searching. | +| `-T, --tree` | Makes the process widget use tree mode by default. | +| `-n, --unnormalized_cpu` | Show process CPU% usage without averaging over the number of CPU cores. | +| `-W, --whole_word` | Enables whole-word matching by default while searching. | ## Temperature Options diff --git a/docs/content/configuration/config-file/flags.md b/docs/content/configuration/config-file/flags.md index f4838a53..0350d964 100644 --- a/docs/content/configuration/config-file/flags.md +++ b/docs/content/configuration/config-file/flags.md @@ -14,40 +14,40 @@ hide_avg_cpu = true Most of the [command line flags](../command-line-options.md) have config file equivalents to avoid having to type them out each time: -| Field | Type | Functionality | -| ---------------------------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- | -| `hide_avg_cpu` | Boolean | Hides the average CPU usage. | -| `dot_marker` | Boolean | Uses a dot marker for graphs. | -| `cpu_left_legend` | Boolean | Puts the CPU chart legend to the left side. | -| `current_usage` | Boolean | Sets process CPU% to be based on current CPU%. | -| `group_processes` | Boolean | Groups processes with the same name by default. | -| `case_sensitive` | Boolean | Enables case sensitivity by default. | -| `whole_word` | Boolean | Enables whole-word matching by default. | -| `regex` | Boolean | Enables regex by default. | -| `basic` | Boolean | Hides graphs and uses a more basic look. | -| `use_old_network_legend` | Boolean | DEPRECATED - uses the older network legend. | -| `battery` | Boolean | Shows the battery widget. | -| `rate` | Unsigned Int (represents milliseconds) or String (represents human time) | Sets a refresh rate in ms. | -| `default_time_value` | Unsigned Int (represents milliseconds) or String (represents human time) | Default time value for graphs in ms. | -| `time_delta` | Unsigned Int (represents milliseconds) or String (represents human time) | The amount in ms changed upon zooming. | -| `hide_time` | Boolean | Hides the time scale. | -| `temperature_type` | String (one of ["k", "f", "c", "kelvin", "fahrenheit", "celsius"]) | Sets the temperature unit type. | -| `default_widget_type` | String (one of ["cpu", "proc", "net", "temp", "mem", "disk"], same as layout options) | Sets the default widget type, use --help for more info. | -| `default_widget_count` | Unsigned Int (represents which `default_widget_type`) | Sets the n'th selected widget type as the default. | -| `disable_click` | Boolean | Disables mouse clicks. | -| `enable_cache_memory` | Boolean | Enable cache and buffer memory stats (not available on Windows). | -| `process_memory_as_value` | Boolean | Defaults to showing process memory usage by value. | -| `tree` | Boolean | Defaults to showing the process widget in tree mode. | -| `show_table_scroll_position` | Boolean | Shows the scroll position tracker in table widgets. | -| `process_command` | Boolean | Show processes as their commands by default. | -| `disable_advanced_kill` | Boolean | Hides advanced options to stop a process on Unix-like systems. | -| `network_use_binary_prefix` | Boolean | Displays the network widget with binary prefixes. | -| `network_use_bytes` | Boolean | Displays the network widget using bytes. | -| `network_use_log` | Boolean | Displays the network widget with a log scale. | -| `disable_gpu` | Boolean | Disable NVIDIA and AMD GPU data collection. | -| `retention` | String (human readable time, such as "10m", "1h", etc.) | How much data is stored at once in terms of time. | -| `unnormalized_cpu` | Boolean | Show process CPU% without normalizing over the number of cores. | -| `expanded` | Boolean | Expand the default widget upon starting the app. | -| `memory_legend` | String (one of ["none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right"]) | Where to place the legend for the memory widget. | -| `network_legend` | String (one of ["none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right"]) | Where to place the legend for the network widget. | -| `average_cpu_row` | Boolean | Moves the average CPU usage entry to its own row when using basic mode. | +| Field | Type | Functionality | +| ---------------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | +| `hide_avg_cpu` | Boolean | Hides the average CPU usage. | +| `dot_marker` | Boolean | Uses a dot marker for graphs. | +| `cpu_left_legend` | Boolean | Puts the CPU chart legend to the left side. | +| `current_usage` | Boolean | Sets process CPU% to be based on current CPU%. | +| `group_processes` | Boolean | Groups processes with the same name by default. | +| `case_sensitive` | Boolean | Enables case sensitivity by default. | +| `whole_word` | Boolean | Enables whole-word matching by default. | +| `regex` | Boolean | Enables regex by default. | +| `basic` | Boolean | Hides graphs and uses a more basic look. | +| `use_old_network_legend` | Boolean | DEPRECATED - uses the older network legend. | +| `battery` | Boolean | Shows the battery widget. | +| `rate` | Unsigned Int (represents milliseconds) or String (represents human time) | Sets a refresh rate in ms. | +| `default_time_value` | Unsigned Int (represents milliseconds) or String (represents human time) | Default time value for graphs in ms. | +| `time_delta` | Unsigned Int (represents milliseconds) or String (represents human time) | The amount in ms changed upon zooming. | +| `hide_time` | Boolean | Hides the time scale. | +| `temperature_type` | String (one of ["k", "f", "c", "kelvin", "fahrenheit", "celsius"]) | Sets the temperature unit type. | +| `default_widget_type` | String (one of ["cpu", "proc", "net", "temp", "mem", "disk"], same as layout options) | Sets the default widget type, use --help for more info. | +| `default_widget_count` | Unsigned Int (represents which `default_widget_type`) | Sets the n'th selected widget type as the default. | +| `disable_click` | Boolean | Disables mouse clicks. | +| `enable_cache_memory` | Boolean | Enable cache and buffer memory stats (not available on Windows). | +| `process_memory_as_value` | Boolean | Defaults to showing process memory usage by value. | +| `tree` | Boolean | Defaults to showing the process widget in tree mode. | +| `show_table_scroll_position` | Boolean | Shows the scroll position tracker in table widgets. | +| `process_command` | Boolean | Show processes as their commands by default. | +| `disable_advanced_kill` | Boolean | Disable being able to send signals to processes on supported Unix-like systems. Only available on Linux, macOS, and FreeBSD. | +| `network_use_binary_prefix` | Boolean | Displays the network widget with binary prefixes. | +| `network_use_bytes` | Boolean | Displays the network widget using bytes. | +| `network_use_log` | Boolean | Displays the network widget with a log scale. | +| `disable_gpu` | Boolean | Disable NVIDIA and AMD GPU data collection. | +| `retention` | String (human readable time, such as "10m", "1h", etc.) | How much data is stored at once in terms of time. | +| `unnormalized_cpu` | Boolean | Show process CPU% without normalizing over the number of cores. | +| `expanded` | Boolean | Expand the default widget upon starting the app. | +| `memory_legend` | String (one of ["none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right"]) | Where to place the legend for the memory widget. | +| `network_legend` | String (one of ["none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right"]) | Where to place the legend for the network widget. | +| `average_cpu_row` | Boolean | Moves the average CPU usage entry to its own row when using basic mode. | diff --git a/docs/content/usage/widgets/process.md b/docs/content/usage/widgets/process.md index c1f7b921..9b5f4ec9 100644 --- a/docs/content/usage/widgets/process.md +++ b/docs/content/usage/widgets/process.md @@ -83,8 +83,8 @@ operating systems, you are also able to control which specific signals to send (
The process termination menu on Linux
-If you're on Windows, or if the `disable_advanced_kill` flag is set in the options or command-line, then a simpler termination -screen will be shown to confirm whether you want to kill that process/process group. +If you're on Windows, or if the `disable_advanced_kill` flag is set in the options or command-line (only available on +Linux, macOS, and FreeBSD), then a simpler termination screen with just yes or no options will be shown.
A picture of the process kill menu on Windows. diff --git a/sample_configs/default_config.toml b/sample_configs/default_config.toml index aa450c41..1cb6614c 100644 --- a/sample_configs/default_config.toml +++ b/sample_configs/default_config.toml @@ -97,7 +97,7 @@ # Displays the network widget with a log scale. #network_use_log = false -# Hides advanced options to stop a process on Unix-like systems. +# Hides advanced options to stop a process on Unix-like systems. Only available on Linux, macOS, and FreeBSD #disable_advanced_kill = false # Hide GPU(s) information @@ -115,20 +115,17 @@ # Where to place the legend for the network widget. One of "none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right". #network_legend = "top-right" - # Processes widget configuration #[processes] # The columns shown by the process widget. The following columns are supported (the GPU columns are only available if the GPU feature is enabled when built): # PID, Name, CPU%, Mem%, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU% #columns = ["PID", "Name", "CPU%", "Mem%", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMem%", "GPU%"] - # CPU widget configuration #[cpu] # One of "all" (default), "average"/"avg" #default = "average" - # Disk widget configuration #[disk] # The columns shown by the process widget. The following columns are supported: @@ -170,7 +167,6 @@ # Whether to be require matching the whole word. Defaults to false. #whole_word = false - # Temperature widget configuration #[temperature] # By default, there are no temperature sensor filters enabled. An example use case is provided below. @@ -190,7 +186,6 @@ # Whether to be require matching the whole word. Defaults to false. #whole_word = false - # Network widget configuration #[network] # By default, there are no network interface filters enabled. An example use case is provided below. @@ -210,7 +205,6 @@ # Whether to be require matching the whole word. Defaults to false. #whole_word = false - # These are all the components that support custom theming. Note that colour support # will depend on terminal support. #[styles] # Uncomment if you want to use custom styling diff --git a/src/app.rs b/src/app.rs index 3c18b310..541f3872 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,15 +1,10 @@ pub mod data; pub mod filter; pub mod layout_manager; -mod process_killer; pub mod states; -use std::{ - cmp::{max, min}, - time::Instant, -}; +use std::time::Instant; -use anyhow::bail; use concat_string::concat_string; use data::*; use filter::*; @@ -18,9 +13,9 @@ use layout_manager::*; pub use states::*; use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation}; +use crate::canvas::dialogs::process_kill_dialog::ProcessKillDialog; use crate::{ canvas::components::time_graph::LegendPosition, - collection::processes::Pid, constants, utils::data_units::DataUnit, widgets::{ProcWidgetColumn, ProcWidgetMode}, @@ -57,6 +52,7 @@ pub struct AppConfigFields { pub enable_gpu: bool, pub enable_cache_memory: bool, pub show_table_scroll_position: bool, + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] pub is_advanced_kill: bool, pub memory_legend_position: Option, // TODO: Remove these, move network details state-side. @@ -77,35 +73,12 @@ pub struct DataFilters { pub net_filter: Option, } -cfg_if::cfg_if! { - if #[cfg(target_os = "linux")] { - /// The max signal we can send to a process on Linux. - pub const MAX_PROCESS_SIGNAL: usize = 64; - } else if #[cfg(target_os = "macos")] { - /// The max signal we can send to a process on macOS. - pub const MAX_PROCESS_SIGNAL: usize = 31; - } else if #[cfg(target_os = "freebsd")] { - /// The max signal we can send to a process on FreeBSD. - /// See [https://www.freebsd.org/cgi/man.cgi?query=signal&apropos=0&sektion=3&manpath=FreeBSD+13.1-RELEASE+and+Ports&arch=default&format=html] - /// for more details. - pub const MAX_PROCESS_SIGNAL: usize = 33; - } else if #[cfg(target_os = "windows")] { - /// The max signal we can send to a process. For Windows, we only have support for one signal (kill). - pub const MAX_PROCESS_SIGNAL: usize = 1; - } else { - /// The max signal we can send to a process. As a fallback, we only support one signal (kill). - pub const MAX_PROCESS_SIGNAL: usize = 1; - } -} - pub struct App { awaiting_second_char: bool, second_char: Option, - pub dd_err: Option, // FIXME: The way we do deletes is really gross. - to_delete_process_list: Option<(String, Vec)>, pub data_store: DataStore, last_key_press: Instant, - pub delete_dialog_state: AppDeleteDialogState, + pub(crate) process_kill_dialog: ProcessKillDialog, pub help_dialog_state: AppHelpDialogState, pub is_expanded: bool, pub is_force_redraw: bool, @@ -129,11 +102,9 @@ impl App { Self { awaiting_second_char: false, second_char: None, - dd_err: None, - to_delete_process_list: None, data_store: DataStore::default(), last_key_press: Instant::now(), - delete_dialog_state: AppDeleteDialogState::default(), + process_kill_dialog: ProcessKillDialog::default(), help_dialog_state: AppHelpDialogState::default(), is_expanded, is_force_redraw: false, @@ -186,7 +157,7 @@ impl App { // Reset dialog state self.help_dialog_state.is_showing_help = false; - self.delete_dialog_state.is_showing_dd = false; + self.process_kill_dialog.reset(); // Close all searches and reset it self.states @@ -197,10 +168,6 @@ impl App { state.proc_search.search_state.reset(); }); - // Clear current delete list - self.to_delete_process_list = None; - self.dd_err = None; - self.data_store.reset(); // Reset zoom @@ -213,24 +180,15 @@ impl App { self.is_force_redraw || self.is_determining_widget_boundary } - fn close_dd(&mut self) { - self.delete_dialog_state.is_showing_dd = false; - self.delete_dialog_state.selected_signal = KillSignal::default(); - self.delete_dialog_state.scroll_pos = 0; - self.to_delete_process_list = None; - self.dd_err = None; - } - pub fn on_esc(&mut self) { self.reset_multi_tap_keys(); - if self.is_in_dialog() { - if self.help_dialog_state.is_showing_help { - self.help_dialog_state.is_showing_help = false; - self.help_dialog_state.scroll_state.current_scroll_index = 0; - } else { - self.close_dd(); - } + if self.process_kill_dialog.is_open() { + self.process_kill_dialog.on_esc(); + self.is_force_redraw = true; + } else if self.help_dialog_state.is_showing_help { + self.help_dialog_state.is_showing_help = false; + self.help_dialog_state.scroll_state.current_scroll_index = 0; self.is_force_redraw = true; } else { match self.current_widget.widget_type { @@ -299,7 +257,7 @@ impl App { } fn is_in_dialog(&self) -> bool { - self.help_dialog_state.is_showing_help || self.delete_dialog_state.is_showing_dd + self.help_dialog_state.is_showing_help || self.process_kill_dialog.is_open() } fn ignore_normal_keybinds(&self) -> bool { @@ -477,30 +435,9 @@ impl App { /// One of two functions allowed to run while in a dialog... pub fn on_enter(&mut self) { - if self.delete_dialog_state.is_showing_dd { - if self.dd_err.is_some() { - self.close_dd(); - } else if self.delete_dialog_state.selected_signal != KillSignal::Cancel { - // If within dd... - if self.dd_err.is_none() { - // Also ensure that we didn't just fail a dd... - let dd_result = self.kill_highlighted_process(); - self.delete_dialog_state.scroll_pos = 0; - self.delete_dialog_state.selected_signal = KillSignal::default(); - - // Check if there was an issue... if so, inform the user. - if let Err(dd_err) = dd_result { - self.dd_err = Some(dd_err.to_string()); - } else { - self.delete_dialog_state.is_showing_dd = false; - } - } - } else { - self.delete_dialog_state.scroll_pos = 0; - self.delete_dialog_state.selected_signal = KillSignal::default(); - self.delete_dialog_state.is_showing_dd = false; - } - self.is_force_redraw = true; + if self.process_kill_dialog.is_open() { + // Not the best way of doing things for now but works as glue. + self.process_kill_dialog.on_enter(); } else if !self.is_in_dialog() { if let BottomWidgetType::ProcSort = self.current_widget.widget_type { if let Some(proc_widget_state) = self @@ -561,7 +498,7 @@ impl App { } } BottomWidgetType::Proc => { - self.start_killing_process(); + self.kill_current_process(); } _ => {} } @@ -610,80 +547,28 @@ impl App { } } - #[cfg(target_family = "unix")] - pub fn on_number(&mut self, number_char: char) { - if self.delete_dialog_state.is_showing_dd { - if self - .delete_dialog_state - .last_number_press - .map_or(100, |ins| ins.elapsed().as_millis()) - >= 400 - { - self.delete_dialog_state.keyboard_signal_select = 0; - } - let mut kbd_signal = self.delete_dialog_state.keyboard_signal_select * 10; - kbd_signal += number_char.to_digit(10).unwrap() as usize; - if kbd_signal > 64 { - kbd_signal %= 100; - } - #[cfg(target_os = "linux")] - if kbd_signal > 64 || kbd_signal == 32 || kbd_signal == 33 { - kbd_signal %= 10; - } - #[cfg(target_os = "macos")] - if kbd_signal > 31 { - kbd_signal %= 10; - } - self.delete_dialog_state.selected_signal = KillSignal::Kill(kbd_signal); - if kbd_signal < 10 { - self.delete_dialog_state.keyboard_signal_select = kbd_signal; - } else { - self.delete_dialog_state.keyboard_signal_select = 0; - } - self.delete_dialog_state.last_number_press = Some(Instant::now()); - } - } - pub fn on_up_key(&mut self) { if !self.is_in_dialog() { self.decrement_position_count(); + self.reset_multi_tap_keys(); } else if self.help_dialog_state.is_showing_help { self.help_scroll_up(); - } else if self.delete_dialog_state.is_showing_dd { - #[cfg(target_os = "windows")] - self.on_right_key(); - #[cfg(target_family = "unix")] - { - if self.app_config_fields.is_advanced_kill { - self.on_left_key(); - } else { - self.on_right_key(); - } - } - return; + self.reset_multi_tap_keys(); + } else if self.process_kill_dialog.is_open() { + self.process_kill_dialog.on_up_key(); } - self.reset_multi_tap_keys(); } pub fn on_down_key(&mut self) { if !self.is_in_dialog() { self.increment_position_count(); + self.reset_multi_tap_keys(); } else if self.help_dialog_state.is_showing_help { self.help_scroll_down(); - } else if self.delete_dialog_state.is_showing_dd { - #[cfg(target_os = "windows")] - self.on_left_key(); - #[cfg(target_family = "unix")] - { - if self.app_config_fields.is_advanced_kill { - self.on_right_key(); - } else { - self.on_left_key(); - } - } - return; + self.reset_multi_tap_keys(); + } else if self.process_kill_dialog.is_open() { + self.process_kill_dialog.on_down_key(); } - self.reset_multi_tap_keys(); } pub fn on_left_key(&mut self) { @@ -731,29 +616,8 @@ impl App { } _ => {} } - } else if self.delete_dialog_state.is_showing_dd { - #[cfg(target_family = "unix")] - { - if self.app_config_fields.is_advanced_kill { - match self.delete_dialog_state.selected_signal { - KillSignal::Kill(prev_signal) => { - self.delete_dialog_state.selected_signal = match prev_signal - 1 { - 0 => KillSignal::Cancel, - // 32 + 33 are skipped - 33 => KillSignal::Kill(31), - signal => KillSignal::Kill(signal), - }; - } - KillSignal::Cancel => {} - }; - } else { - self.delete_dialog_state.selected_signal = KillSignal::default(); - } - } - #[cfg(target_os = "windows")] - { - self.delete_dialog_state.selected_signal = KillSignal::Kill(1); - } + } else if self.process_kill_dialog.is_open() { + self.process_kill_dialog.on_left_key(); } } @@ -807,45 +671,14 @@ impl App { } _ => {} } - } else if self.delete_dialog_state.is_showing_dd { - #[cfg(target_family = "unix")] - { - if self.app_config_fields.is_advanced_kill { - let new_signal = match self.delete_dialog_state.selected_signal { - KillSignal::Cancel => 1, - // 32+33 are skipped - #[cfg(target_os = "linux")] - KillSignal::Kill(31) => 34, - #[cfg(target_os = "macos")] - KillSignal::Kill(31) => 31, - KillSignal::Kill(64) => 64, - KillSignal::Kill(signal) => signal + 1, - }; - self.delete_dialog_state.selected_signal = KillSignal::Kill(new_signal); - } else { - self.delete_dialog_state.selected_signal = KillSignal::Cancel; - } - } - #[cfg(target_os = "windows")] - { - self.delete_dialog_state.selected_signal = KillSignal::Cancel; - } + } else if self.process_kill_dialog.is_open() { + self.process_kill_dialog.on_right_key(); } } pub fn on_page_up(&mut self) { - if self.delete_dialog_state.is_showing_dd { - let mut new_signal = match self.delete_dialog_state.selected_signal { - KillSignal::Cancel => 0, - KillSignal::Kill(signal) => max(signal, 8) - 8, - }; - if new_signal > 23 && new_signal < 33 { - new_signal -= 2; - } - self.delete_dialog_state.selected_signal = match new_signal { - 0 => KillSignal::Cancel, - sig => KillSignal::Kill(sig), - }; + if self.process_kill_dialog.is_open() { + self.process_kill_dialog.on_page_up(); } else if self.help_dialog_state.is_showing_help { let current = &mut self.help_dialog_state.scroll_state.current_scroll_index; let amount = self.help_dialog_state.height; @@ -864,15 +697,8 @@ impl App { } pub fn on_page_down(&mut self) { - if self.delete_dialog_state.is_showing_dd { - let mut new_signal = match self.delete_dialog_state.selected_signal { - KillSignal::Cancel => 8, - KillSignal::Kill(signal) => min(signal + 8, MAX_PROCESS_SIGNAL), - }; - if new_signal > 31 && new_signal < 42 { - new_signal += 2; - } - self.delete_dialog_state.selected_signal = KillSignal::Kill(new_signal); + if self.process_kill_dialog.is_open() { + self.process_kill_dialog.on_page_down(); } else if self.help_dialog_state.is_showing_help { let current = self.help_dialog_state.scroll_state.current_scroll_index; let amount = self.help_dialog_state.height; @@ -1059,34 +885,6 @@ impl App { } } - pub fn start_killing_process(&mut self) { - self.reset_multi_tap_keys(); - - if let Some(pws) = self - .states - .proc_state - .widget_states - .get(&self.current_widget.widget_id) - { - if let Some(current) = pws.table.current_item() { - let id = current.id.to_string(); - if let Some(pids) = pws - .id_pid_map - .get(&id) - .cloned() - .or_else(|| Some(vec![current.pid])) - { - let current_process = (id, pids); - - self.to_delete_process_list = Some(current_process); - self.delete_dialog_state.is_showing_dd = true; - self.is_determining_widget_boundary = true; - } - } - } - // FIXME: This should handle errors. - } - pub fn on_char_key(&mut self, caught_char: char) { // Skip control code chars if caught_char.is_control() { @@ -1159,34 +957,50 @@ impl App { 'j' | 'k' | 'g' | 'G' => self.handle_char(caught_char), _ => {} } - } else if self.delete_dialog_state.is_showing_dd { - match caught_char { - 'h' => self.on_left_key(), - 'j' => self.on_down_key(), - 'k' => self.on_up_key(), - 'l' => self.on_right_key(), - #[cfg(target_family = "unix")] - '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => { - self.on_number(caught_char) - } - 'g' => { - let mut is_first_g = true; - if let Some(second_char) = self.second_char { - if self.awaiting_second_char && second_char == 'g' { - is_first_g = false; - self.awaiting_second_char = false; - self.second_char = None; - self.skip_to_first(); - } - } + } else if self.process_kill_dialog.is_open() { + self.process_kill_dialog.on_char(caught_char); + } + } - if is_first_g { - self.awaiting_second_char = true; - self.second_char = Some('g'); - } + /// Kill the currently selected process if we are in the process widget. + /// + /// TODO: This ideally gets abstracted out into a separate widget. + pub(crate) fn kill_current_process(&mut self) { + if let Some(pws) = self + .states + .proc_state + .widget_states + .get(&self.current_widget.widget_id) + { + if let Some(current) = pws.table.current_item() { + let id = current.id.to_string(); + if let Some(pids) = pws + .id_pid_map + .get(&id) + .cloned() + .or_else(|| Some(vec![current.pid])) + { + let current_process = (id, pids); + + let use_simple_selection = { + cfg_if::cfg_if! { + if #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] { + !self.app_config_fields.is_advanced_kill + } else { + true + } + } + }; + + self.process_kill_dialog.start_process_kill( + current_process.0, + current_process.1, + use_simple_selection, + ); + + // TODO: I don't think most of this is needed. + self.is_determining_widget_boundary = true; } - 'G' => self.skip_to_last(), - _ => {} } } } @@ -1206,7 +1020,9 @@ impl App { self.awaiting_second_char = false; self.second_char = None; - self.start_killing_process(); + self.reset_multi_tap_keys(); + + self.kill_current_process(); } } @@ -1421,36 +1237,6 @@ impl App { } } - pub fn kill_highlighted_process(&mut self) -> anyhow::Result<()> { - if let BottomWidgetType::Proc = self.current_widget.widget_type { - if let Some((_, pids)) = &self.to_delete_process_list { - #[cfg(target_family = "unix")] - let signal = match self.delete_dialog_state.selected_signal { - KillSignal::Kill(sig) => sig, - KillSignal::Cancel => 15, // should never happen, so just TERM - }; - for pid in pids { - #[cfg(target_family = "unix")] - { - process_killer::kill_process_given_pid(*pid, signal)?; - } - #[cfg(target_os = "windows")] - { - process_killer::kill_process_given_pid(*pid)?; - } - } - } - self.to_delete_process_list = None; - Ok(()) - } else { - bail!("Cannot kill processes if the current widget is not the Process widget!"); - } - } - - pub fn get_to_delete_processes(&self) -> Option<(String, Vec)> { - self.to_delete_process_list.clone() - } - fn toggle_expand_widget(&mut self) { if self.is_expanded { self.is_expanded = false; @@ -1989,8 +1775,8 @@ impl App { self.reset_multi_tap_keys(); } else if self.help_dialog_state.is_showing_help { self.help_dialog_state.scroll_state.current_scroll_index = 0; - } else if self.delete_dialog_state.is_showing_dd { - self.delete_dialog_state.selected_signal = KillSignal::Cancel; + } else if self.process_kill_dialog.is_open() { + self.process_kill_dialog.go_to_first(); } } @@ -2050,8 +1836,8 @@ impl App { } else if self.help_dialog_state.is_showing_help { self.help_dialog_state.scroll_state.current_scroll_index = self.help_dialog_state.scroll_state.max_scroll_index; - } else if self.delete_dialog_state.is_showing_dd { - self.delete_dialog_state.selected_signal = KillSignal::Kill(MAX_PROCESS_SIGNAL); + } else if self.process_kill_dialog.is_open() { + self.process_kill_dialog.go_to_last(); } } @@ -2160,14 +1946,9 @@ impl App { } pub fn handle_scroll_up(&mut self) { - if self.delete_dialog_state.is_showing_dd { - #[cfg(target_family = "unix")] - { - self.on_up_key(); - return; - } - } - if self.help_dialog_state.is_showing_help { + if self.process_kill_dialog.is_open() { + self.process_kill_dialog.on_scroll_up(); + } else if self.help_dialog_state.is_showing_help { self.help_scroll_up(); } else if self.current_widget.widget_type.is_widget_graph() { self.zoom_in(); @@ -2177,14 +1958,9 @@ impl App { } pub fn handle_scroll_down(&mut self) { - if self.delete_dialog_state.is_showing_dd { - #[cfg(target_family = "unix")] - { - self.on_down_key(); - return; - } - } - if self.help_dialog_state.is_showing_help { + if self.process_kill_dialog.is_open() { + self.process_kill_dialog.on_scroll_down(); + } else if self.help_dialog_state.is_showing_help { self.help_scroll_down(); } else if self.current_widget.widget_type.is_widget_graph() { self.zoom_out(); @@ -2516,24 +2292,7 @@ impl App { // Second short circuit --- are we in the dd dialog state? If so, only check // yes/no/signals and bail after. - if self.is_in_dialog() { - match self.delete_dialog_state.button_positions.iter().find( - |(tl_x, tl_y, br_x, br_y, _idx)| { - (x >= *tl_x && y >= *tl_y) && (x <= *br_x && y <= *br_y) - }, - ) { - Some((_, _, _, _, 0)) => { - self.delete_dialog_state.selected_signal = KillSignal::Cancel - } - Some((_, _, _, _, idx)) => { - if *idx > 31 { - self.delete_dialog_state.selected_signal = KillSignal::Kill(*idx + 2) - } else { - self.delete_dialog_state.selected_signal = KillSignal::Kill(*idx) - } - } - _ => {} - } + if self.process_kill_dialog.is_open() && self.process_kill_dialog.on_click(x, y) { return; } diff --git a/src/app/data/time_series.rs b/src/app/data/time_series.rs index a3557d6f..aac1b1dc 100644 --- a/src/app/data/time_series.rs +++ b/src/app/data/time_series.rs @@ -184,12 +184,12 @@ impl TimeSeriesData { partition_point - 1 } else { // If the partition point was 0, then it means all values are too new to be pruned. - crate::info!("Skipping prune."); + // crate::info!("Skipping prune."); return; } }; - crate::info!("Pruning up to index {end}."); + // crate::info!("Pruning up to index {end}."); // Note that end here is _inclusive_. self.time.drain(0..=end); diff --git a/src/app/states.rs b/src/app/states.rs index 0c55919e..be7ad254 100644 --- a/src/app/states.rs +++ b/src/app/states.rs @@ -1,4 +1,4 @@ -use std::{ops::Range, time::Instant}; +use std::ops::Range; use hashbrown::HashMap; use indexmap::IndexMap; @@ -31,34 +31,6 @@ pub enum CursorDirection { Right, } -#[derive(PartialEq, Eq)] -pub enum KillSignal { - Cancel, - Kill(usize), -} - -impl Default for KillSignal { - #[cfg(target_family = "unix")] - fn default() -> Self { - KillSignal::Kill(15) - } - #[cfg(target_os = "windows")] - fn default() -> Self { - KillSignal::Kill(1) - } -} - -#[derive(Default)] -pub struct AppDeleteDialogState { - pub is_showing_dd: bool, - pub selected_signal: KillSignal, - /// tl x, tl y, br x, br y, index/signal - pub button_positions: Vec<(u16, u16, u16, u16, usize)>, - pub keyboard_signal_select: usize, - pub last_number_press: Option, - pub scroll_pos: usize, -} - pub struct AppHelpDialogState { pub is_showing_help: bool, pub height: u16, diff --git a/src/canvas.rs b/src/canvas.rs index 7ce47290..0a93f5a5 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -4,7 +4,7 @@ //! or components. pub mod components; -mod dialogs; +pub mod dialogs; mod drawing_utils; mod widgets; @@ -200,6 +200,7 @@ impl Painter { self.previous_width = terminal_width; } + // TODO: We should probably remove this or make it done elsewhere, not the responsibility of the app. if app_state.should_get_widget_bounds() { // If we're force drawing, reset ALL mouse boundaries. for widget in app_state.widget_map.values_mut() { @@ -207,15 +208,16 @@ impl Painter { widget.bottom_right_corner = None; } - // Reset dd_dialog... - app_state.delete_dialog_state.button_positions = vec![]; + // Reset process kill dialog button locations... + app_state.process_kill_dialog.handle_redraw(); - // Reset battery dialog... + // Reset battery dialog button locations... for battery_widget in app_state.states.battery_state.widget_states.values_mut() { battery_widget.tab_click_locs = None; } } + // TODO: Make drawing dialog generic. if app_state.help_dialog_state.is_showing_help { let gen_help_len = GENERAL_HELP_TEXT.len() as u16 + 3; let border_len = terminal_height.saturating_sub(gen_help_len) / 2; @@ -248,46 +250,32 @@ impl Painter { .split(vertical_dialog_chunk[1]); self.draw_help_dialog(f, app_state, middle_dialog_chunk[1]); - } else if app_state.delete_dialog_state.is_showing_dd { - let dd_text = self.get_dd_spans(app_state); + } else if app_state.process_kill_dialog.is_open() { + // FIXME: For width, just limit to a max size or full width. For height, not sure. Maybe pass max and let child handle? + let horizontal_padding = if terminal_width < 100 { 0 } else { 5 }; + let vertical_padding = if terminal_height < 100 { 0 } else { 5 }; - let text_width = if terminal_width < 100 { - terminal_width * 90 / 100 - } else { - terminal_width * 50 / 100 - }; - - let text_height = if cfg!(target_os = "windows") - || !app_state.app_config_fields.is_advanced_kill - { - 7 - } else { - 22 - }; - - let vertical_bordering = terminal_height.saturating_sub(text_height) / 2; let vertical_dialog_chunk = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(vertical_bordering), - Constraint::Length(text_height), - Constraint::Length(vertical_bordering), + Constraint::Length(vertical_padding), + Constraint::Fill(1), + Constraint::Length(vertical_padding), ]) - .split(terminal_size); + .areas::<3>(terminal_size)[1]; - let horizontal_bordering = terminal_width.saturating_sub(text_width) / 2; - let middle_dialog_chunk = Layout::default() + let dialog_draw_area = Layout::default() .direction(Direction::Horizontal) .constraints([ - Constraint::Length(horizontal_bordering), - Constraint::Length(text_width), - Constraint::Length(horizontal_bordering), + Constraint::Length(horizontal_padding), + Constraint::Fill(1), + Constraint::Length(horizontal_padding), ]) - .split(vertical_dialog_chunk[1]); + .areas::<3>(vertical_dialog_chunk)[1]; - // This is a bit nasty, but it works well... I guess. - app_state.delete_dialog_state.is_showing_dd = - self.draw_dd_dialog(f, dd_text, app_state, middle_dialog_chunk[1]); + app_state + .process_kill_dialog + .draw(f, dialog_draw_area, &self.styles); } else if app_state.is_expanded { if let Some(frozen_draw_loc) = frozen_draw_loc { self.draw_frozen_indicator(f, frozen_draw_loc); diff --git a/src/canvas/dialogs/dd_dialog.rs b/src/canvas/dialogs/dd_dialog.rs deleted file mode 100644 index 8cf8b3ca..00000000 --- a/src/canvas/dialogs/dd_dialog.rs +++ /dev/null @@ -1,418 +0,0 @@ -#[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "macos"))] -use std::cmp::min; - -use tui::{ - Frame, - layout::{Alignment, Constraint, Direction, Layout, Rect}, - text::{Line, Span, Text}, - widgets::{Block, Paragraph, Wrap}, -}; - -use crate::{ - app::{App, KillSignal, MAX_PROCESS_SIGNAL}, - canvas::{Painter, drawing_utils::dialog_block}, - widgets::ProcWidgetMode, -}; - -cfg_if::cfg_if! { - if #[cfg(target_os = "linux")] { - const SIGNAL_TEXT: [&str; 63] = [ - "0: Cancel", - "1: HUP", - "2: INT", - "3: QUIT", - "4: ILL", - "5: TRAP", - "6: ABRT", - "7: BUS", - "8: FPE", - "9: KILL", - "10: USR1", - "11: SEGV", - "12: USR2", - "13: PIPE", - "14: ALRM", - "15: TERM", - "16: STKFLT", - "17: CHLD", - "18: CONT", - "19: STOP", - "20: TSTP", - "21: TTIN", - "22: TTOU", - "23: URG", - "24: XCPU", - "25: XFSZ", - "26: VTALRM", - "27: PROF", - "28: WINCH", - "29: IO", - "30: PWR", - "31: SYS", - "34: RTMIN", - "35: RTMIN+1", - "36: RTMIN+2", - "37: RTMIN+3", - "38: RTMIN+4", - "39: RTMIN+5", - "40: RTMIN+6", - "41: RTMIN+7", - "42: RTMIN+8", - "43: RTMIN+9", - "44: RTMIN+10", - "45: RTMIN+11", - "46: RTMIN+12", - "47: RTMIN+13", - "48: RTMIN+14", - "49: RTMIN+15", - "50: RTMAX-14", - "51: RTMAX-13", - "52: RTMAX-12", - "53: RTMAX-11", - "54: RTMAX-10", - "55: RTMAX-9", - "56: RTMAX-8", - "57: RTMAX-7", - "58: RTMAX-6", - "59: RTMAX-5", - "60: RTMAX-4", - "61: RTMAX-3", - "62: RTMAX-2", - "63: RTMAX-1", - "64: RTMAX", - ]; - } else if #[cfg(target_os = "macos")] { - const SIGNAL_TEXT: [&str; 32] = [ - "0: Cancel", - "1: HUP", - "2: INT", - "3: QUIT", - "4: ILL", - "5: TRAP", - "6: ABRT", - "7: EMT", - "8: FPE", - "9: KILL", - "10: BUS", - "11: SEGV", - "12: SYS", - "13: PIPE", - "14: ALRM", - "15: TERM", - "16: URG", - "17: STOP", - "18: TSTP", - "19: CONT", - "20: CHLD", - "21: TTIN", - "22: TTOU", - "23: IO", - "24: XCPU", - "25: XFSZ", - "26: VTALRM", - "27: PROF", - "28: WINCH", - "29: INFO", - "30: USR1", - "31: USR2", - ]; - } else if #[cfg(target_os = "freebsd")] { - const SIGNAL_TEXT: [&str; 34] = [ - "0: Cancel", - "1: HUP", - "2: INT", - "3: QUIT", - "4: ILL", - "5: TRAP", - "6: ABRT", - "7: EMT", - "8: FPE", - "9: KILL", - "10: BUS", - "11: SEGV", - "12: SYS", - "13: PIPE", - "14: ALRM", - "15: TERM", - "16: URG", - "17: STOP", - "18: TSTP", - "19: CONT", - "20: CHLD", - "21: TTIN", - "22: TTOU", - "23: IO", - "24: XCPU", - "25: XFSZ", - "26: VTALRM", - "27: PROF", - "28: WINCH", - "29: INFO", - "30: USR1", - "31: USR2", - "32: THR", - "33: LIBRT", - ]; - } -} - -impl Painter { - pub fn get_dd_spans(&self, app_state: &App) -> Option> { - if let Some(dd_err) = &app_state.dd_err { - return Some(Text::from(vec![ - Line::default(), - Line::from("Failed to kill process."), - Line::from(dd_err.clone()), - Line::from("Please press ENTER or ESC to close this dialog."), - ])); - } else if let Some(to_kill_processes) = app_state.get_to_delete_processes() { - if let Some(first_pid) = to_kill_processes.1.first() { - return Some(Text::from(vec![ - Line::from(""), - if app_state - .states - .proc_state - .widget_states - .get(&app_state.current_widget.widget_id) - .map(|p| matches!(p.mode, ProcWidgetMode::Grouped)) - .unwrap_or(false) - { - if to_kill_processes.1.len() != 1 { - Line::from(format!( - "Kill {} processes with the name '{}'? Press ENTER to confirm.", - to_kill_processes.1.len(), - to_kill_processes.0 - )) - } else { - Line::from(format!( - "Kill 1 process with the name '{}'? Press ENTER to confirm.", - to_kill_processes.0 - )) - } - } else { - Line::from(format!( - "Kill process '{}' with PID {}? Press ENTER to confirm.", - to_kill_processes.0, first_pid - )) - }, - ])); - } - } - - None - } - - fn draw_dd_confirm_buttons( - &self, f: &mut Frame<'_>, button_draw_loc: &Rect, app_state: &mut App, - ) { - if MAX_PROCESS_SIGNAL == 1 || !app_state.app_config_fields.is_advanced_kill { - let (yes_button, no_button) = match app_state.delete_dialog_state.selected_signal { - KillSignal::Kill(_) => ( - Span::styled("Yes", self.styles.selected_text_style), - Span::styled("No", self.styles.text_style), - ), - KillSignal::Cancel => ( - Span::styled("Yes", self.styles.text_style), - Span::styled("No", self.styles.selected_text_style), - ), - }; - - let button_layout = Layout::default() - .direction(Direction::Horizontal) - .constraints( - [ - Constraint::Percentage(35), - Constraint::Percentage(30), - Constraint::Percentage(35), - ] - .as_ref(), - ) - .split(*button_draw_loc); - - f.render_widget( - Paragraph::new(yes_button) - .block(Block::default()) - .alignment(Alignment::Right), - button_layout[0], - ); - f.render_widget( - Paragraph::new(no_button) - .block(Block::default()) - .alignment(Alignment::Left), - button_layout[2], - ); - - if app_state.should_get_widget_bounds() { - const SIGNAL: usize = if cfg!(target_os = "windows") { 1 } else { 15 }; - - // This is kinda weird, but the gist is: - // - We have three sections; we put our mouse bounding box for the "yes" button - // at the very right edge of the left section and 3 characters back. We then - // give it a buffer size of 1 on the x-coordinate. - // - Same for the "no" button, except it is the right section and we do it from - // the start of the right section. - // - // Lastly, note that mouse detection for the dd buttons assume correct widths. - // As such, we correct them here and check with >= and <= mouse - // bound checks, as opposed to how we do it elsewhere with >= and <. See https://github.com/ClementTsang/bottom/pull/459 for details. - app_state.delete_dialog_state.button_positions = vec![ - // Yes - ( - button_layout[0].x + button_layout[0].width - 4, - button_layout[0].y, - button_layout[0].x + button_layout[0].width, - button_layout[0].y, - SIGNAL, - ), - // No - ( - button_layout[2].x - 1, - button_layout[2].y, - button_layout[2].x + 2, - button_layout[2].y, - 0, - ), - ]; - } - } else { - #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "macos"))] - { - let button_rect = Layout::default() - .direction(Direction::Horizontal) - .margin(1) - .constraints( - [ - Constraint::Length((button_draw_loc.width - 14) / 2), - Constraint::Min(0), - Constraint::Length((button_draw_loc.width - 14) / 2), - ] - .as_ref(), - ) - .split(*button_draw_loc)[1]; - - let mut selected = match app_state.delete_dialog_state.selected_signal { - KillSignal::Cancel => 0, - KillSignal::Kill(signal) => signal, - }; - // 32+33 are skipped - if selected > 31 { - selected -= 2; - } - - let layout = Layout::default() - .direction(Direction::Vertical) - .constraints(vec![Constraint::Min(1); button_rect.height.into()]) - .split(button_rect); - - let prev_offset: usize = app_state.delete_dialog_state.scroll_pos; - app_state.delete_dialog_state.scroll_pos = if selected == 0 { - 0 - } else if selected < prev_offset + 1 { - selected - 1 - } else if selected > prev_offset + layout.len() - 1 { - selected - layout.len() + 1 - } else { - prev_offset - }; - let scroll_offset: usize = app_state.delete_dialog_state.scroll_pos; - - let mut buttons = SIGNAL_TEXT - [scroll_offset + 1..min((layout.len()) + scroll_offset, SIGNAL_TEXT.len())] - .iter() - .map(|text| Span::styled(*text, self.styles.text_style)) - .collect::>>(); - buttons.insert(0, Span::styled(SIGNAL_TEXT[0], self.styles.text_style)); - buttons[selected - scroll_offset] = - Span::styled(SIGNAL_TEXT[selected], self.styles.selected_text_style); - - app_state.delete_dialog_state.button_positions = layout - .iter() - .enumerate() - .map(|(i, pos)| { - ( - pos.x, - pos.y, - pos.x + pos.width - 1, - pos.y + pos.height - 1, - if i == 0 { 0 } else { scroll_offset } + i, - ) - }) - .collect::>(); - - for (btn, pos) in buttons.into_iter().zip(layout.iter()) { - f.render_widget(Paragraph::new(btn).alignment(Alignment::Left), *pos); - } - } - } - } - - pub fn draw_dd_dialog( - &self, f: &mut Frame<'_>, dd_text: Option>, app_state: &mut App, draw_loc: Rect, - ) -> bool { - if let Some(dd_text) = dd_text { - let dd_title = if app_state.dd_err.is_some() { - Line::styled(" Error ", self.styles.widget_title_style) - } else { - Line::styled(" Confirm Kill Process ", self.styles.widget_title_style) - }; - - f.render_widget( - Paragraph::new(dd_text) - .block( - dialog_block(self.styles.border_type) - .title_top(dd_title) - .title_top( - Line::styled(" Esc to close ", self.styles.widget_title_style) - .right_aligned(), - ) - .style(self.styles.border_style) - .border_style(self.styles.border_style), - ) - .style(self.styles.text_style) - .alignment(Alignment::Center) - .wrap(Wrap { trim: true }), - draw_loc, - ); - - let btn_height = { - cfg_if::cfg_if! { - if #[cfg(target_os = "windows")] { - 3 - } else { - if !app_state.app_config_fields.is_advanced_kill { - 3 - } else { - 20 - } - } - } - }; - - // Now draw buttons if needed... - let split_draw_loc = Layout::default() - .direction(Direction::Vertical) - .constraints(if app_state.dd_err.is_some() { - vec![Constraint::Percentage(100)] - } else { - vec![Constraint::Min(3), Constraint::Length(btn_height)] - }) - .split(draw_loc); - - // This being true implies that dd_err is none. - if let Some(button_draw_loc) = split_draw_loc.get(1) { - self.draw_dd_confirm_buttons(f, button_draw_loc, app_state); - } - - if app_state.dd_err.is_some() { - return app_state.delete_dialog_state.is_showing_dd; - } else { - return true; - } - } - - // Currently we just return "false" if things go wrong finding - // the process or a first PID (if an error arises it should be caught). - // I don't really like this, and I find it ugly, but it works for now. - false - } -} diff --git a/src/canvas/dialogs/mod.rs b/src/canvas/dialogs/mod.rs index fe40156e..d940d778 100644 --- a/src/canvas/dialogs/mod.rs +++ b/src/canvas/dialogs/mod.rs @@ -1,2 +1,2 @@ -pub mod dd_dialog; pub mod help_dialog; +pub mod process_kill_dialog; diff --git a/src/canvas/dialogs/process_kill_dialog.rs b/src/canvas/dialogs/process_kill_dialog.rs new file mode 100644 index 00000000..f001244e --- /dev/null +++ b/src/canvas/dialogs/process_kill_dialog.rs @@ -0,0 +1,826 @@ +//! A dialog box to handle killing processes. + +use cfg_if::cfg_if; +use tui::{ + Frame, + layout::{Alignment, Constraint, Flex, Layout, Position, Rect}, + text::{Line, Span, Text}, + widgets::{Paragraph, Wrap}, +}; + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] +use tui::widgets::ListState; + +use crate::{ + canvas::drawing_utils::dialog_block, collection::processes::Pid, options::config::style::Styles, +}; + +// Configure signal text based on the target OS. +cfg_if! { + if #[cfg(target_os = "linux")] { + const DEFAULT_KILL_SIGNAL: usize = 15; + const SIGNAL_TEXT: [&str; 63] = [ + "0: Cancel", + "1: HUP", + "2: INT", + "3: QUIT", + "4: ILL", + "5: TRAP", + "6: ABRT", + "7: BUS", + "8: FPE", + "9: KILL", + "10: USR1", + "11: SEGV", + "12: USR2", + "13: PIPE", + "14: ALRM", + "15: TERM", + "16: STKFLT", + "17: CHLD", + "18: CONT", + "19: STOP", + "20: TSTP", + "21: TTIN", + "22: TTOU", + "23: URG", + "24: XCPU", + "25: XFSZ", + "26: VTALRM", + "27: PROF", + "28: WINCH", + "29: IO", + "30: PWR", + "31: SYS", + "34: RTMIN", + "35: RTMIN+1", + "36: RTMIN+2", + "37: RTMIN+3", + "38: RTMIN+4", + "39: RTMIN+5", + "40: RTMIN+6", + "41: RTMIN+7", + "42: RTMIN+8", + "43: RTMIN+9", + "44: RTMIN+10", + "45: RTMIN+11", + "46: RTMIN+12", + "47: RTMIN+13", + "48: RTMIN+14", + "49: RTMIN+15", + "50: RTMAX-14", + "51: RTMAX-13", + "52: RTMAX-12", + "53: RTMAX-11", + "54: RTMAX-10", + "55: RTMAX-9", + "56: RTMAX-8", + "57: RTMAX-7", + "58: RTMAX-6", + "59: RTMAX-5", + "60: RTMAX-4", + "61: RTMAX-3", + "62: RTMAX-2", + "63: RTMAX-1", + "64: RTMAX", + ]; + } else if #[cfg(target_os = "macos")] { + const DEFAULT_KILL_SIGNAL: usize = 15; + const SIGNAL_TEXT: [&str; 32] = [ + "0: Cancel", + "1: HUP", + "2: INT", + "3: QUIT", + "4: ILL", + "5: TRAP", + "6: ABRT", + "7: EMT", + "8: FPE", + "9: KILL", + "10: BUS", + "11: SEGV", + "12: SYS", + "13: PIPE", + "14: ALRM", + "15: TERM", + "16: URG", + "17: STOP", + "18: TSTP", + "19: CONT", + "20: CHLD", + "21: TTIN", + "22: TTOU", + "23: IO", + "24: XCPU", + "25: XFSZ", + "26: VTALRM", + "27: PROF", + "28: WINCH", + "29: INFO", + "30: USR1", + "31: USR2", + ]; + } else if #[cfg(target_os = "freebsd")] { + const DEFAULT_KILL_SIGNAL: usize = 15; + const SIGNAL_TEXT: [&str; 34] = [ + "0: Cancel", + "1: HUP", + "2: INT", + "3: QUIT", + "4: ILL", + "5: TRAP", + "6: ABRT", + "7: EMT", + "8: FPE", + "9: KILL", + "10: BUS", + "11: SEGV", + "12: SYS", + "13: PIPE", + "14: ALRM", + "15: TERM", + "16: URG", + "17: STOP", + "18: TSTP", + "19: CONT", + "20: CHLD", + "21: TTIN", + "22: TTOU", + "23: IO", + "24: XCPU", + "25: XFSZ", + "26: VTALRM", + "27: PROF", + "28: WINCH", + "29: INFO", + "30: USR1", + "31: USR2", + "32: THR", + "33: LIBRT", + ]; + } +} + +/// Button state type for a [`ProcessKillDialog`]. +/// +/// Simple only has two buttons (yes/no), while signals (AKA advanced) are +/// a list of signals to send. +/// +/// Note that signals are not available for Windows. +#[derive(Debug)] +pub(crate) enum ButtonState { + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] + Signals { + state: ListState, + last_button_draw_area: Rect, + }, + Simple { + yes: bool, + last_yes_button_area: Rect, + last_no_button_area: Rect, + }, +} + +#[derive(Debug)] +struct ProcessKillSelectingInner { + process_name: String, + pids: Vec, + button_state: ButtonState, +} + +/// The current state of the process kill dialog. +#[derive(Default, Debug)] +enum ProcessKillDialogState { + #[default] + NotEnabled, + Selecting(ProcessKillSelectingInner), + Error { + process_name: String, + pid: Option, + err: String, + }, +} + +/// Process kill dialog. +#[derive(Default, Debug)] +pub(crate) struct ProcessKillDialog { + state: ProcessKillDialogState, + last_char: Option, +} + +impl ProcessKillDialog { + pub fn reset(&mut self) { + *self = Self::default(); + } + + #[inline] + pub fn is_open(&self) -> bool { + !(matches!(self.state, ProcessKillDialogState::NotEnabled)) + } + + pub fn on_esc(&mut self) { + self.reset(); + } + + pub fn on_enter(&mut self) { + // We do this to get around borrow issues. + let mut current = ProcessKillDialogState::NotEnabled; + std::mem::swap(&mut self.state, &mut current); + + if let ProcessKillDialogState::Selecting(state) = current { + let process_name = state.process_name; + let button_state = state.button_state; + let pids = state.pids; + + match button_state { + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] + ButtonState::Signals { state, .. } => { + use crate::utils::process_killer; + + if let Some(signal) = state.selected() { + if signal != 0 { + for pid in pids { + if let Err(err) = + process_killer::kill_process_given_pid(pid, signal) + { + self.state = ProcessKillDialogState::Error { + process_name, + pid: Some(pid), + err: err.to_string(), + }; + return; + } + } + } + } + } + ButtonState::Simple { yes, .. } => { + if yes { + cfg_if! { + if #[cfg(target_os = "windows")] { + use crate::utils::process_killer; + + for pid in pids { + if let Err(err) = process_killer::kill_process_given_pid(pid) { + self.state = ProcessKillDialogState::Error { process_name, pid: Some(pid), err: err.to_string() }; + break; + } + } + } else if #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] { + use crate::utils::process_killer; + + for pid in pids { + // Send a SIGTERM by default. + if let Err(err) = process_killer::kill_process_given_pid(pid, DEFAULT_KILL_SIGNAL) { + self.state = ProcessKillDialogState::Error { process_name, pid: Some(pid), err: err.to_string() }; + break; + } + } + } else { + self.state = ProcessKillDialogState::Error { process_name, pid: None, err: "Killing processes is not supported on this platform.".into() }; + + } + } + } + } + } + } + + // Fall through behaviour is just to close the dialog. + self.last_char = None; + } + + pub fn on_char(&mut self, c: char) { + match c { + 'h' => self.on_left_key(), + 'j' => self.on_down_key(), + 'k' => self.on_up_key(), + 'l' => self.on_right_key(), + '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => { + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] + if let Some(value) = c.to_digit(10) { + if let ProcessKillDialogState::Selecting(ProcessKillSelectingInner { + button_state: ButtonState::Signals { state, .. }, + .. + }) = &mut self.state + { + let current = state.selected().unwrap_or(0); + let new = current * 10 + value as usize; + + // If the new value is too large, then just assume we instead want the value itself. + if new >= SIGNAL_TEXT.len() { + state.select(Some(value as usize)); + } else { + state.select(Some(new)) + } + } + } + } + 'g' => { + if let Some('g') = self.last_char { + self.go_to_first(); + } + } + 'G' => { + self.go_to_last(); + } + _ => {} + } + + self.last_char = Some(c); + } + + /// Handle a click at the given coordinates. Returns true if the click was + /// handled, false otherwise. + pub fn on_click(&mut self, x: u16, y: u16) -> bool { + if let ProcessKillDialogState::Selecting(state) = &mut self.state { + match &mut state.button_state { + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] + ButtonState::Signals { + state, + last_button_draw_area, + } => { + if last_button_draw_area.contains(Position { x, y }) { + let relative_y = + y.saturating_sub(last_button_draw_area.y) as usize + state.offset(); + if relative_y < SIGNAL_TEXT.len() { + state.select(Some(relative_y)); + } + } + } + ButtonState::Simple { + yes, + last_yes_button_area, + last_no_button_area, + } => { + if last_yes_button_area.contains(Position { x, y }) { + *yes = true; + } else if last_no_button_area.contains(Position { x, y }) { + *yes = false; + } + } + } + } + + false + } + + /// Scroll up in the signal list. + pub fn on_scroll_up(&mut self) { + self.on_up_key(); + } + + /// Scroll down in the signal list. + pub fn on_scroll_down(&mut self) { + self.on_down_key(); + } + + /// Handle a left key press. + pub fn on_left_key(&mut self) { + self.last_char = None; + + if let ProcessKillDialogState::Selecting(ProcessKillSelectingInner { + button_state: ButtonState::Simple { yes, .. }, + .. + }) = &mut self.state + { + *yes = true; + } + } + + /// Handle a right key press. + pub fn on_right_key(&mut self) { + self.last_char = None; + + if let ProcessKillDialogState::Selecting(ProcessKillSelectingInner { + button_state: ButtonState::Simple { yes, .. }, + .. + }) = &mut self.state + { + *yes = false; + } + } + + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] + fn scroll_up_by(state: &mut ListState, amount: usize) { + if let Some(selected) = state.selected() { + if let Some(new_position) = selected.checked_sub(amount) { + state.select(Some(new_position)); + } else { + state.select(Some(0)); + } + } + } + + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] + fn scroll_down_by(state: &mut ListState, amount: usize) { + if let Some(selected) = state.selected() { + let new_position = selected + amount; + if new_position < SIGNAL_TEXT.len() { + state.select(Some(new_position)); + } else { + state.select(Some(SIGNAL_TEXT.len() - 1)); + } + } + } + + /// Handle an up key press. + pub fn on_up_key(&mut self) { + self.last_char = None; + + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] + if let ProcessKillDialogState::Selecting(ProcessKillSelectingInner { + button_state: ButtonState::Signals { state, .. }, + .. + }) = &mut self.state + { + Self::scroll_up_by(state, 1); + } + } + + /// Handle a down key press. + pub fn on_down_key(&mut self) { + self.last_char = None; + + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] + if let ProcessKillDialogState::Selecting(ProcessKillSelectingInner { + button_state: ButtonState::Signals { state, .. }, + .. + }) = &mut self.state + { + Self::scroll_down_by(state, 1); + } + } + + // Handle page up. + pub fn on_page_up(&mut self) { + self.last_char = None; + + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] + if let ProcessKillDialogState::Selecting(ProcessKillSelectingInner { + button_state: + ButtonState::Signals { + state, + last_button_draw_area, + .. + }, + .. + }) = &mut self.state + { + Self::scroll_up_by(state, last_button_draw_area.height as usize); + } + } + + /// Handle page down. + pub fn on_page_down(&mut self) { + self.last_char = None; + + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] + if let ProcessKillDialogState::Selecting(ProcessKillSelectingInner { + button_state: + ButtonState::Signals { + state, + last_button_draw_area, + .. + }, + .. + }) = &mut self.state + { + Self::scroll_down_by(state, last_button_draw_area.height as usize); + } + } + + pub fn go_to_first(&mut self) { + self.last_char = None; + + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] + if let ProcessKillDialogState::Selecting(ProcessKillSelectingInner { + button_state: ButtonState::Signals { state, .. }, + .. + }) = &mut self.state + { + state.select(Some(0)); + } + } + + pub fn go_to_last(&mut self) { + self.last_char = None; + + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] + if let ProcessKillDialogState::Selecting(ProcessKillSelectingInner { + button_state: ButtonState::Signals { state, .. }, + .. + }) = &mut self.state + { + state.select(Some(SIGNAL_TEXT.len() - 1)); + } + } + + /// Enable the process kill process. + pub fn start_process_kill( + &mut self, process_name: String, pids: Vec, use_simple_selection: bool, + ) { + let button_state = if use_simple_selection { + ButtonState::Simple { + yes: false, + last_yes_button_area: Rect::default(), + last_no_button_area: Rect::default(), + } + } else { + cfg_if! { + if #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] { + ButtonState::Signals { state: ListState::default().with_selected(Some(DEFAULT_KILL_SIGNAL)), last_button_draw_area: Rect::default() } + } else { + ButtonState::Simple { yes: false, last_yes_button_area: Rect::default(), last_no_button_area: Rect::default()} + } + } + }; + + if pids.is_empty() { + self.state = ProcessKillDialogState::Error { + process_name, + pid: None, + err: "No PIDs found for the given process name.".into(), + }; + return; + } + + self.state = ProcessKillDialogState::Selecting(ProcessKillSelectingInner { + process_name, + pids, + button_state, + }); + } + + pub fn handle_redraw(&mut self) { + // FIXME: Not sure if we need this. We can probably handle this better in the draw function later. + + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] + { + if let ProcessKillDialogState::Selecting(ProcessKillSelectingInner { + button_state: ButtonState::Signals { state, .. }, + .. + }) = &mut self.state + { + // Fix the button offset state when we do things like resize. + *state.offset_mut() = 0; + } + } + } + + #[inline] + fn draw_selecting( + f: &mut Frame<'_>, draw_area: Rect, styles: &Styles, state: &mut ProcessKillSelectingInner, + ) { + let ProcessKillSelectingInner { + process_name, + pids, + button_state, + .. + } = state; + + // FIXME: Add some colour to this! + let text = { + const MAX_PROCESS_NAME_WIDTH: usize = 20; + + if let Some(first_pid) = pids.first() { + let truncated_process_name = + unicode_ellipsis::truncate_str(process_name, MAX_PROCESS_NAME_WIDTH); + + let text = if pids.len() > 1 { + Line::from(format!( + "Kill {} processes with the name '{}'? Press ENTER to confirm.", + pids.len(), + truncated_process_name + )) + } else { + Line::from(format!( + "Kill process '{truncated_process_name}' with PID {first_pid}? Press ENTER to confirm." + )) + }; + + Text::from(vec![text]) + } else { + Text::from(vec![ + "Could not find process to kill.".into(), + "Please press ENTER or ESC to close this dialog.".into(), + ]) + } + }; + + let text: Paragraph<'_> = Paragraph::new(text) + .style(styles.text_style) + .alignment(Alignment::Center) + .wrap(Wrap { trim: true }); + + let title = match button_state { + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] + ButtonState::Signals { .. } => { + Line::styled(" Select Signal ", styles.widget_title_style) + } + ButtonState::Simple { .. } => { + Line::styled(" Confirm Kill Process ", styles.widget_title_style) + } + }; + + let block = dialog_block(styles.border_type) + .title_top(title) + .title_top(Line::styled(" Esc to close ", styles.widget_title_style).right_aligned()) + .style(styles.border_style) + .border_style(styles.border_style); + + let num_lines = text.line_count(block.inner(draw_area).width) as u16; + + match button_state { + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] + ButtonState::Signals { + state, + last_button_draw_area, + } => { + use tui::widgets::List; + + // A list of options, displayed vertically. + const SIGNAL_TEXT_LEN: u16 = SIGNAL_TEXT.len() as u16; + + // Make the rect only as big as it needs to be, which is the height of the text, + // the buttons, and up to 2 spaces (margin and space between), and the size of the block. + let [draw_area] = + Layout::vertical([Constraint::Max(num_lines + SIGNAL_TEXT_LEN + 2 + 3)]) + .flex(Flex::Center) + .areas(draw_area); + + // Now we need to divide the block into one area for the paragraph, + // and one for the buttons. + let [text_draw_area, button_draw_area] = Layout::vertical([ + Constraint::Max(num_lines), + Constraint::Max(SIGNAL_TEXT_LEN), + ]) + .flex(Flex::SpaceAround) + .areas(block.inner(draw_area)); + + // Render the block. + f.render_widget(block, draw_area); + + // Now render the text. + f.render_widget(text, text_draw_area); + + // And the tricky part, rendering the buttons. + let selected = state + .selected() + .expect("the list state should always be initialized with a selection!"); + + let buttons = List::new(SIGNAL_TEXT.iter().enumerate().map(|(index, &signal)| { + let style = if index == selected { + styles.selected_text_style + } else { + styles.text_style + }; + + Span::styled(signal, style) + })); + + // This is kinda dumb how you have to set the constraint, but ok. + const LONGEST_SIGNAL_TEXT_LENGTH: u16 = const { + let mut i = 0; + let mut max = 0; + while i < SIGNAL_TEXT.len() { + if SIGNAL_TEXT[i].len() > max { + max = SIGNAL_TEXT[i].len(); + } + i += 1; + } + + max as u16 + }; + let [button_draw_area] = + Layout::horizontal([Constraint::Length(LONGEST_SIGNAL_TEXT_LENGTH)]) + .flex(Flex::Center) + .areas(button_draw_area); + + *last_button_draw_area = button_draw_area; + f.render_stateful_widget(buttons, button_draw_area, state); + } + ButtonState::Simple { + yes, + last_yes_button_area, + last_no_button_area, + } => { + // Make the rect only as big as it needs to be, which is the height of the text, + // the buttons, and up to 3 spaces (margin and space between) + 2 for block. + let [draw_area] = Layout::vertical([Constraint::Max(num_lines + 1 + 3 + 2)]) + .flex(Flex::Center) + .areas(draw_area); + + // Now we need to divide the block into one area for the paragraph, + // and one for the buttons. + let [text_area, button_area] = + Layout::vertical([Constraint::Max(num_lines), Constraint::Length(1)]) + .flex(Flex::SpaceAround) + .areas(block.inner(draw_area)); + + // Render things, starting from the block. + f.render_widget(block, draw_area); + f.render_widget(text, text_area); + + let (yes, no) = { + let (yes_style, no_style) = if *yes { + (styles.selected_text_style, styles.text_style) + } else { + (styles.text_style, styles.selected_text_style) + }; + + ( + Paragraph::new(Span::styled("Yes", yes_style)), + Paragraph::new(Span::styled("No", no_style)), + ) + }; + + let [yes_area, no_area] = Layout::horizontal([Constraint::Length(3); 2]) + .flex(Flex::SpaceAround) + .areas(button_area); + + *last_yes_button_area = yes_area; + *last_no_button_area = no_area; + + f.render_widget(yes, yes_area); + f.render_widget(no, no_area); + } + } + } + + #[inline] + fn draw_no_button_dialog( + &self, f: &mut Frame<'_>, draw_area: Rect, styles: &Styles, text: Text<'_>, title: Line<'_>, + ) { + let text = Paragraph::new(text) + .style(styles.text_style) + .alignment(Alignment::Center) + .wrap(Wrap { trim: true }); + + let block = dialog_block(styles.border_type) + .title_top(title) + .title_top(Line::styled(" Esc to close ", styles.widget_title_style).right_aligned()) + .style(styles.border_style) + .border_style(styles.border_style); + + let num_lines = text.line_count(block.inner(draw_area).width) as u16; + + // Also calculate how big of a draw loc we actually need. For this + // one, we want it to be shorter if possible. + // + // Note the +2 is for the margin, and another +2 for border. + let [draw_area] = Layout::vertical([Constraint::Max(num_lines + 2 + 2)]) + .flex(Flex::Center) + .areas(draw_area); + + let [text_draw_area] = Layout::vertical([Constraint::Length(num_lines)]) + .flex(Flex::Center) + .areas(block.inner(draw_area)); + + f.render_widget(block, draw_area); + f.render_widget(text, text_draw_area); + } + + /// Draw the [`ProcessKillDialog`]. + pub fn draw(&mut self, f: &mut Frame<'_>, draw_area: Rect, styles: &Styles) { + // The idea is: + // - Use as big of a dialog box as needed (within the maximal draw loc) + // - So the non-button ones are going to be smaller... probably + // whatever the height of the text is. + // - Meanwhile for the button one, it'll likely be full height if it's + // "advanced" kill. + + const MAX_DIALOG_WIDTH: u16 = 100; + let [draw_area] = Layout::horizontal([Constraint::Max(MAX_DIALOG_WIDTH)]) + .flex(Flex::Center) + .areas(draw_area); + + // FIXME: Add some colour to this! + match &mut self.state { + ProcessKillDialogState::NotEnabled => {} + ProcessKillDialogState::Selecting(state) => { + // Draw a text box. If buttons are yes/no, fit it, otherwise, use max space. + Self::draw_selecting(f, draw_area, styles, state); + } + ProcessKillDialogState::Error { + process_name, + pid, + err, + } => { + let text = Text::from(vec![ + if let Some(pid) = pid { + format!("Failed to kill process {process_name} ({pid}):").into() + } else { + format!("Failed to kill process '{process_name}':").into() + }, + err.to_owned().into(), + "Please press ENTER or ESC to close this dialog.".into(), + ]) + .alignment(Alignment::Center); + let title = Line::styled(" Error ", styles.widget_title_style); + + self.draw_no_button_dialog(f, draw_area, styles, text, title); + } + } + } +} diff --git a/src/event.rs b/src/event.rs index 75e20da7..44afe576 100644 --- a/src/event.rs +++ b/src/event.rs @@ -78,7 +78,7 @@ pub fn handle_key_event_or_break( 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::F(9) => app.kill_current_process(), KeyCode::PageDown => app.on_page_down(), KeyCode::PageUp => app.on_page_up(), _ => {} diff --git a/src/lib.rs b/src/lib.rs index 93f985ae..208201a1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ mod utils { pub(crate) mod data_units; pub(crate) mod general; pub(crate) mod logging; + pub(crate) mod process_killer; pub(crate) mod strings; } pub(crate) mod canvas; diff --git a/src/options.rs b/src/options.rs index e9537a59..17ccdf2f 100644 --- a/src/options.rs +++ b/src/options.rs @@ -224,6 +224,7 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL let is_use_regex = is_flag_enabled!(regex, args.process, config); let is_default_tree = is_flag_enabled!(tree, args.process, config); let is_default_command = is_flag_enabled!(process_command, args.process, config); + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] let is_advanced_kill = !(is_flag_enabled!(disable_advanced_kill, args.process, config)); let process_memory_as_value = is_flag_enabled!(process_memory_as_value, args.process, config); @@ -296,6 +297,7 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL args.general, config ), + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] is_advanced_kill, memory_legend_position, network_legend_position, diff --git a/src/options/args.rs b/src/options/args.rs index d1689e60..93dcb8f4 100644 --- a/src/options/args.rs +++ b/src/options/args.rs @@ -303,7 +303,7 @@ pub struct ProcessArgs { )] pub current_usage: bool, - // TODO: Disable this on Windows? + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] #[arg( long, action = ArgAction::SetTrue, diff --git a/src/options/config/flags.rs b/src/options/config/flags.rs index f46829de..815f67eb 100644 --- a/src/options/config/flags.rs +++ b/src/options/config/flags.rs @@ -36,7 +36,8 @@ pub(crate) struct FlagConfig { pub(crate) tree: Option, pub(crate) show_table_scroll_position: Option, pub(crate) process_command: Option, - pub(crate) disable_advanced_kill: Option, + // #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))] + pub(crate) disable_advanced_kill: Option, // This does nothing on Windows, but we leave it enabled to make the config file consistent across platforms. pub(crate) network_use_bytes: Option, pub(crate) network_use_log: Option, pub(crate) network_use_binary_prefix: Option, diff --git a/src/utils/general.rs b/src/utils/general.rs index 351329b5..c2a9e352 100644 --- a/src/utils/general.rs +++ b/src/utils/general.rs @@ -1,7 +1,7 @@ use std::cmp::Ordering; #[inline] -pub const fn sort_partial_fn(is_descending: bool) -> fn(T, T) -> Ordering { +pub(crate) const fn sort_partial_fn(is_descending: bool) -> fn(T, T) -> Ordering { if is_descending { partial_ordering_desc } else { @@ -11,7 +11,7 @@ pub const fn sort_partial_fn(is_descending: bool) -> fn(T, T) -> /// Returns an [`Ordering`] between two [`PartialOrd`]s. #[inline] -pub fn partial_ordering(a: T, b: T) -> Ordering { +pub(crate) fn partial_ordering(a: T, b: T) -> Ordering { a.partial_cmp(&b).unwrap_or(Ordering::Equal) } @@ -20,12 +20,12 @@ pub fn partial_ordering(a: T, b: T) -> Ordering { /// This is simply a wrapper function around [`partial_ordering`] that reverses /// the result. #[inline] -pub fn partial_ordering_desc(a: T, b: T) -> Ordering { +pub(crate) fn partial_ordering_desc(a: T, b: T) -> Ordering { partial_ordering(a, b).reverse() } /// A trait for additional clamping functions on numeric types. -pub trait ClampExt { +pub(crate) trait ClampExt { /// Restrict a value by a lower bound. If the current value is _lower_ than /// `lower_bound`, it will be set to `_lower_bound`. #[cfg_attr(not(test), expect(dead_code))] @@ -63,12 +63,12 @@ macro_rules! clamp_num_impl { clamp_num_impl!(u8, u16, u32, u64, usize); /// Checked log2. -pub fn saturating_log2(value: f64) -> f64 { +pub(crate) fn saturating_log2(value: f64) -> f64 { if value > 0.0 { value.log2() } else { 0.0 } } /// Checked log10. -pub fn saturating_log10(value: f64) -> f64 { +pub(crate) fn saturating_log10(value: f64) -> f64 { if value > 0.0 { value.log10() } else { 0.0 } } diff --git a/src/app/process_killer.rs b/src/utils/process_killer.rs similarity index 98% rename from src/app/process_killer.rs rename to src/utils/process_killer.rs index 84b95242..73a68e82 100644 --- a/src/app/process_killer.rs +++ b/src/utils/process_killer.rs @@ -59,8 +59,7 @@ pub fn kill_process_given_pid(pid: Pid) -> anyhow::Result<()> { /// Kills a process, given a PID, for UNIX. #[cfg(target_family = "unix")] pub fn kill_process_given_pid(pid: Pid, signal: usize) -> anyhow::Result<()> { - // SAFETY: the signal should be valid, and we act properly on an error (exit - // code not 0). + // 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 {