temp work

This commit is contained in:
ClementTsang 2025-03-27 02:28:50 -04:00
parent db214a73c1
commit 2c0ab7b979
No known key found for this signature in database
GPG Key ID: DC3B7867D8D97095
6 changed files with 718 additions and 325 deletions

@ -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.24", 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" },

@ -4,12 +4,8 @@ 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 +14,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},
@ -99,11 +95,9 @@ cfg_if::cfg_if! {
pub struct App {
awaiting_second_char: bool,
second_char: Option<char>,
pub dd_err: Option<String>, // FIXME: The way we do deletes is really gross.
to_delete_process_list: Option<(String, Vec<Pid>)>,
pub data_store: DataStore,
last_key_press: Instant,
pub delete_dialog_state: AppDeleteDialogState,
pub process_kill_dialog: ProcessKillDialog,
pub help_dialog_state: AppHelpDialogState,
pub is_expanded: bool,
pub is_force_redraw: bool,
@ -127,11 +121,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,
@ -184,7 +176,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
@ -195,10 +187,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
@ -211,24 +199,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 {
@ -297,7 +276,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 {
@ -475,30 +454,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
@ -516,7 +474,9 @@ impl App {
}
pub fn on_delete(&mut self) {
if let BottomWidgetType::ProcSearch = self.current_widget.widget_type {
if self.process_kill_dialog.is_open() {
self.process_kill_dialog.on_delete();
} else if let BottomWidgetType::ProcSearch = self.current_widget.widget_type {
let is_in_search_widget = self.is_in_search_widget();
if let Some(proc_widget_state) = self
.states
@ -555,8 +515,6 @@ impl App {
proc_widget_state.update_query();
}
} else {
self.start_killing_process()
}
}
}
@ -607,78 +565,35 @@ 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;
if self.process_kill_dialog.is_open() {
if let Some(value) = number_char.to_digit(10) {
self.process_kill_dialog.on_number(value);
}
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) {
@ -717,29 +632,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();
}
}
@ -784,45 +678,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;
@ -841,15 +704,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;
@ -1036,34 +892,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() {
@ -1134,35 +962,8 @@ 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();
}
}
if is_first_g {
self.awaiting_second_char = true;
self.second_char = Some('g');
}
}
'G' => self.skip_to_last(),
_ => {}
}
} else if self.process_kill_dialog.is_open() {
self.process_kill_dialog.on_char(caught_char);
}
}
@ -1181,7 +982,45 @@ impl App {
self.awaiting_second_char = false;
self.second_char = None;
self.start_killing_process();
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);
let simple_selection = {
#[cfg(target_os = "windows")]
{
true
}
#[cfg(not(target_os = "windows"))]
{
self.app_config_fields.is_advanced_kill
}
};
self.process_kill_dialog.start_process_kill(
current_process.0,
current_process.1,
simple_selection,
);
// TODO: I don't think most of this is needed.
self.is_determining_widget_boundary = true;
}
}
}
}
}
@ -1396,36 +1235,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<Pid>)> {
self.to_delete_process_list.clone()
}
fn toggle_expand_widget(&mut self) {
if self.is_expanded {
self.is_expanded = false;
@ -1964,8 +1773,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.scroll_to_first();
}
}
@ -2025,8 +1834,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.scroll_to_last();
}
}
@ -2135,14 +1944,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();
@ -2152,14 +1956,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();
@ -2497,24 +2296,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() {
return;
}

@ -1,5 +1,5 @@
pub mod components;
mod dialogs;
pub mod dialogs;
mod drawing_utils;
mod widgets;
@ -244,14 +244,14 @@ impl Painter {
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);
let text_width = if terminal_width < 100 {
terminal_width * 90 / 100
} else {
terminal_width * 50 / 100
};
let dd_text = self.get_dd_spans(app_state, text_width);
let text_height = if cfg!(target_os = "windows")
|| !app_state.app_config_fields.is_advanced_kill
{

@ -1,2 +1,3 @@
pub mod dd_dialog;
pub mod help_dialog;
pub mod process_kill_dialog;

@ -157,7 +157,7 @@ cfg_if::cfg_if! {
}
impl Painter {
pub fn get_dd_spans(&self, app_state: &App) -> Option<Text<'_>> {
pub fn get_dd_spans(&self, app_state: &App, text_width: u16) -> Option<Text<'_>> {
if let Some(dd_err) = &app_state.dd_err {
return Some(Text::from(vec![
Line::default(),
@ -166,6 +166,9 @@ impl Painter {
Line::from("Please press ENTER or ESC to close this dialog."),
]));
} else if let Some(to_kill_processes) = app_state.get_to_delete_processes() {
let truncated_process_name =
unicode_ellipsis::truncate_str(&to_kill_processes.0, text_width.into());
if let Some(first_pid) = to_kill_processes.1.first() {
return Some(Text::from(vec![
Line::from(""),
@ -179,20 +182,20 @@ impl Painter {
{
if to_kill_processes.1.len() != 1 {
Line::from(format!(
"Kill {} processes with the name '{}'? Press ENTER to confirm.",
"Kill {} processes with the name '{}'? Press ENTER to confirm.",
to_kill_processes.1.len(),
to_kill_processes.0
truncated_process_name
))
} else {
Line::from(format!(
"Kill 1 process with the name '{}'? Press ENTER to confirm.",
to_kill_processes.0
"Kill 1 process with the name '{}'? Press ENTER to confirm.",
truncated_process_name
))
}
} else {
Line::from(format!(
"Kill process '{}' with PID {}? Press ENTER to confirm.",
to_kill_processes.0, first_pid
"Kill process '{}' with PID {}? Press ENTER to confirm.",
truncated_process_name, first_pid
))
},
]));
@ -205,6 +208,7 @@ impl Painter {
fn draw_dd_confirm_buttons(
&self, f: &mut Frame<'_>, button_draw_loc: &Rect, app_state: &mut App,
) {
// TODO: CHECK HEIGHT
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(_) => (
@ -258,7 +262,7 @@ impl Painter {
app_state.delete_dialog_state.button_positions = vec![
// Yes
(
button_layout[0].x + button_layout[0].width - 4,
(button_layout[0].x + button_layout[0].width).saturating_sub(4),
button_layout[0].y,
button_layout[0].x + button_layout[0].width,
button_layout[0].y,
@ -266,7 +270,7 @@ impl Painter {
),
// No
(
button_layout[2].x - 1,
button_layout[2].x.saturating_sub(1),
button_layout[2].y,
button_layout[2].x + 2,
button_layout[2].y,
@ -282,9 +286,9 @@ impl Painter {
.margin(1)
.constraints(
[
Constraint::Length((button_draw_loc.width - 14) / 2),
Constraint::Length((button_draw_loc.width.saturating_sub(14)) / 2),
Constraint::Min(0),
Constraint::Length((button_draw_loc.width - 14) / 2),
Constraint::Length((button_draw_loc.width.saturating_sub(14)) / 2),
]
.as_ref(),
)
@ -299,6 +303,7 @@ impl Painter {
selected -= 2;
}
// FIXME: THIS IS BROKEN! USE A LIST!
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Min(1); button_rect.height.into()])
@ -322,8 +327,9 @@ impl Painter {
.map(|text| Span::styled(*text, self.styles.text_style))
.collect::<Vec<Span<'_>>>();
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);
if let Some(button) = buttons.get_mut(selected - scroll_offset) {
*button = Span::styled(SIGNAL_TEXT[selected], self.styles.selected_text_style);
}
app_state.delete_dialog_state.button_positions = layout
.iter()

@ -0,0 +1,605 @@
//! A dialog box to handle killing processes.
use cfg_if::cfg_if;
use tui::{
Frame,
layout::{Alignment, Constraint, Flex, Layout, Rect},
text::{Line, Span, Text},
widgets::{List, ListState, Padding, Paragraph, Wrap},
};
use crate::{
canvas::drawing_utils::dialog_block, collection::processes::Pid, options::config::style::Styles,
};
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",
];
}
}
/// 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.
pub(crate) enum ButtonState {
#[cfg(not(target_os = "windows"))]
Signals(ListState),
Simple {
yes: bool,
},
}
/// The current state of the process kill dialog.
#[derive(Default)]
pub(crate) enum ProcessKillDialogState {
#[default]
NotEnabled,
Selecting(String, Vec<Pid>, ButtonState),
Killing(Vec<Pid>),
Error(String),
}
/// Process kill dialog.
#[derive(Default)]
pub(crate) struct ProcessKillDialog {
state: ProcessKillDialogState,
}
impl ProcessKillDialog {
pub fn reset(&mut self) {
self.state = ProcessKillDialogState::default();
}
#[inline]
pub fn is_open(&self) -> bool {
!(matches!(self.state, ProcessKillDialogState::NotEnabled))
}
pub fn on_esc(&mut self) {}
pub fn on_delete(&mut self) {}
pub fn on_enter(&mut self) {
let mut current = ProcessKillDialogState::NotEnabled;
std::mem::swap(&mut self.state, &mut current);
match &self.state {
ProcessKillDialogState::NotEnabled => {} // Do nothing
ProcessKillDialogState::Selecting(name, pids, button_state) => match button_state {
ButtonState::Signals(list_state) => {}
ButtonState::Simple { yes } => {
if *yes {
} else {
}
}
},
ProcessKillDialogState::Killing(items) => {}
ProcessKillDialogState::Error(_) => {}
}
}
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(),
// #[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();
// }
// }
//
// if is_first_g {
// self.awaiting_second_char = true;
// self.second_char = Some('g');
// }
// }
// 'G' => self.skip_to_last(),
_ => {}
}
}
pub fn on_number(&mut self, value: u32) {
// 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_click(&mut self) -> bool {
// 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)
// }
// }
// _ => {}
// }
// return;
// }
false
}
/// Scroll up in the signal list.
pub fn on_scroll_up(&mut self) {
if let ProcessKillDialogState::Selecting(_, _, button_state) = &mut self.state {
if let ButtonState::Signals(list_state) = button_state {
if let Some(selected) = list_state.selected() {
if selected > 0 {
list_state.select(Some(selected - 1));
}
}
}
}
}
/// Scroll down in the signal list.
pub fn on_scroll_down(&mut self) {
if let ProcessKillDialogState::Selecting(_, _, button_state) = &mut self.state {
if let ButtonState::Signals(list_state) = button_state {
if let Some(selected) = list_state.selected() {
if selected < SIGNAL_TEXT.len() - 1 {
list_state.select(Some(selected + 1));
}
}
}
}
}
/// Handle a left key press.
pub fn on_left_key(&mut self) {}
/// Handle a right key press.
pub fn on_right_key(&mut self) {}
/// Handle an up key press.
pub fn on_up_key(&mut self) {}
/// Handle a down key press.
pub fn on_down_key(&mut self) {}
// Handle page up.
pub fn on_page_up(&mut self) {
// 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),
// };
}
/// Handle page down.
pub fn on_page_down(&mut self) {
// 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);
}
pub fn scroll_to_first(&mut self) {}
pub fn scroll_to_last(&mut self) {}
/// Enable the process kill process.
pub fn start_process_kill(
&mut self, process_name: String, pids: Vec<Pid>, simple_selection: bool,
) {
self.state = ProcessKillDialogState::Selecting(
process_name,
pids,
if simple_selection {
ButtonState::Simple { yes: false }
} else {
ButtonState::Signals(ListState::default().with_selected(Some(0)))
},
)
}
#[inline]
fn draw_selecting(
f: &mut Frame<'_>, draw_loc: Rect, styles: &Styles, name: &str, pids: &[Pid],
button_state: &mut ButtonState,
) {
let text = {
const MAX_PROCESS_NAME_WIDTH: usize = 20;
if let Some(first_pid) = pids.first() {
let truncated_process_name =
unicode_ellipsis::truncate_str(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::new(text)
.style(styles.text_style)
.alignment(Alignment::Center)
.wrap(Wrap { trim: true });
let title = match button_state {
#[cfg(not(target_os = "windows"))]
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_loc).width) as u16;
match button_state {
#[cfg(not(target_os = "windows"))]
ButtonState::Signals(list_state) => {
// 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).
let draw_loc = Layout::vertical([Constraint::Max(num_lines + SIGNAL_TEXT_LEN + 2)])
.flex(Flex::Center)
.areas::<1>(draw_loc)[0];
// // If there's enough room, add padding to the top.
// if draw_loc.height > num_lines + 2 + 2 {
// block = block.padding(Padding::top(1));
// }
// Now we need to divide the block into one area for the paragraph,
// and one for the buttons.
let draw_locs = Layout::vertical([
Constraint::Max(num_lines),
Constraint::Max(SIGNAL_TEXT_LEN),
])
.flex(Flex::SpaceAround)
.areas::<2>(draw_loc);
// Now render the text + block...
f.render_widget(text.block(block), draw_locs[0]);
// And the tricky part, rendering the buttons.
let selected = list_state.selected().unwrap_or(0);
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)
}));
// FIXME: I have no idea what will happen here...
f.render_stateful_widget(buttons, draw_locs[0], list_state);
}
ButtonState::Simple { yes } => {
// Just a yes/no, horizontally.
// 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).
let draw_loc = Layout::vertical([Constraint::Max(num_lines + 1 + 3)])
.flex(Flex::Center)
.areas::<1>(draw_loc)[0];
// // If there's enough room, add padding.
// if draw_loc.height > num_lines + 2 + 2 {
// block = block.padding(Padding::vertical(1));
// }
// Now we need to divide the block into one area for the paragraph,
// and one for the buttons.
let draw_locs =
Layout::vertical([Constraint::Max(num_lines), Constraint::Length(1)])
.flex(Flex::SpaceAround)
.areas::<2>(draw_loc);
f.render_widget(text.block(block), draw_locs[0]);
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 button_locs = Layout::horizontal([Constraint::Length(3 + 2); 2])
.flex(Flex::SpaceAround)
.areas::<2>(draw_locs[1]);
f.render_widget(yes, button_locs[0]);
f.render_widget(no, button_locs[1]);
}
}
}
#[inline]
fn draw_no_button_dialog(
&self, f: &mut Frame<'_>, draw_loc: Rect, styles: &Styles, text: Text<'_>, title: Line<'_>,
) {
let text = Paragraph::new(text)
.style(styles.text_style)
.alignment(Alignment::Center)
.wrap(Wrap { trim: true });
let mut 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_loc).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_loc = Layout::vertical([Constraint::Max(num_lines + 2 + 2)])
.flex(Flex::Center)
.areas::<1>(draw_loc)[0];
// If there's enough room, add padding. I think this is also faster than doing another Layout
// for this case since there's just one object anyway.
if draw_loc.height > num_lines + 2 + 2 {
block = block.padding(Padding::vertical(1));
}
f.render_widget(text.block(block), draw_loc);
}
/// Draw the [`ProcessKillDialog`].
pub fn draw(&mut self, f: &mut Frame<'_>, draw_loc: 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.
match &mut self.state {
ProcessKillDialogState::NotEnabled => {}
ProcessKillDialogState::Selecting(name, pids, button_state) => {
// Draw a text box. If buttons are yes/no, fit it, otherwise, use max space.
Self::draw_selecting(f, draw_loc, styles, name, pids, button_state);
}
ProcessKillDialogState::Killing(pids) => {
// Only draw a text box the size of the text + any margins if possible
let text = Text::from(format!("Killing {} processes...", pids.len()));
let title = Line::styled(" Killing Process ", styles.widget_title_style);
self.draw_no_button_dialog(f, draw_loc, styles, text, title);
}
ProcessKillDialogState::Error(err) => {
let text = Text::from(vec![
"Failed to kill process:".into(),
err.clone().into(),
"Please press ENTER or ESC to close this dialog.".into(),
]);
let title = Line::styled(" Error ", styles.widget_title_style);
self.draw_no_button_dialog(f, draw_loc, styles, text, title);
}
}
}
}