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.
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 {