diff --git a/Cargo.toml b/Cargo.toml index 55f1f0bf..b438d447 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.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" }, diff --git a/src/app.rs b/src/app.rs index 87e11664..d0a5f006 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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, - 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 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)> { - 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; } diff --git a/src/canvas.rs b/src/canvas.rs index c86703f1..06345d84 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -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 { diff --git a/src/canvas/dialogs.rs b/src/canvas/dialogs.rs index fe40156e..0f278916 100644 --- a/src/canvas/dialogs.rs +++ b/src/canvas/dialogs.rs @@ -1,2 +1,3 @@ pub mod dd_dialog; pub mod help_dialog; +pub mod process_kill_dialog; diff --git a/src/canvas/dialogs/dd_dialog.rs b/src/canvas/dialogs/dd_dialog.rs index 8cf8b3ca..0740dfcd 100644 --- a/src/canvas/dialogs/dd_dialog.rs +++ b/src/canvas/dialogs/dd_dialog.rs @@ -157,7 +157,7 @@ cfg_if::cfg_if! { } impl Painter { - pub fn get_dd_spans(&self, app_state: &App) -> Option> { + pub fn get_dd_spans(&self, app_state: &App, text_width: u16) -> Option> { 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::>>(); 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() diff --git a/src/canvas/dialogs/process_kill_dialog.rs b/src/canvas/dialogs/process_kill_dialog.rs new file mode 100644 index 00000000..8a5a4a57 --- /dev/null +++ b/src/canvas/dialogs/process_kill_dialog.rs @@ -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, ButtonState), + Killing(Vec), + 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, 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); + } + } + } +}