Merge pull request #60 from ClementTsang/simple_mode

Simple mode
This commit is contained in:
Clement Tsang 2020-03-05 00:11:04 -05:00 committed by GitHub
commit 22cdc005bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 5814 additions and 5032 deletions

View File

@ -30,6 +30,8 @@ Features of bottom include:
- Maximizing of widgets of interest to take up the entire window. - Maximizing of widgets of interest to take up the entire window.
- Basic mode
More details about each widget and compatibility can be found [here](./docs/widgets.md). More details about each widget and compatibility can be found [here](./docs/widgets.md).
## Config files ## Config files
@ -75,7 +77,7 @@ sudo dpkg -i bottom_0.2.2_amd64.deb
### Windows ### Windows
You can get release versions via [Chocolatey](https://chocolatey.org/packages/bottom/): You can get release versions via [Chocolatey](https://chocolatey.org/packages/bottom/) (note it may take a while to be available due to moderation/review):
```bash ```bash
choco install bottom --version=0.2.1 choco install bottom --version=0.2.1
@ -86,10 +88,10 @@ choco install bottom --version=0.2.1
You can get release versions using Homebrew: You can get release versions using Homebrew:
```bash ```bash
$ brew tap clementtsang/bottom brew tap clementtsang/bottom
$ brew install bottom brew install bottom
# Or # Or
$ brew install clementtsang/bottom/bottom brew install clementtsang/bottom/bottom
``` ```
## Usage ## Usage
@ -134,6 +136,8 @@ Run using `btm`.
- `-C`, `--config` takes in a file path leading to a TOML file. If doesn't exist, creates one. - `-C`, `--config` takes in a file path leading to a TOML file. If doesn't exist, creates one.
- `-b`, `--basic` will enable basic mode, removing all graphs from the main interface and condensing data.
### Keybindings ### Keybindings
#### General #### General

View File

@ -3,7 +3,6 @@ max_width = 100
newline_style = "Unix" newline_style = "Unix"
reorder_imports = true reorder_imports = true
fn_args_layout = "Compressed" fn_args_layout = "Compressed"
hard_tabs = true
merge_derives = true merge_derives = true
reorder_modules = true reorder_modules = true
tab_spaces = 4 tab_spaces = 4

View File

@ -34,6 +34,9 @@
#default_widget = "network_default" #default_widget = "network_default"
#default_widget = "process_default" #default_widget = "process_default"
# Basic mode
#basic = true
# These are all the components that support custom theming. Currently, it only # These are all the components that support custom theming. Currently, it only
# supports taking in a string representing a hex colour. Note that colour support # supports taking in a string representing a hex colour. Note that colour support

View File

@ -23,6 +23,33 @@ pub enum WidgetPosition {
Network, Network,
Process, Process,
ProcessSearch, ProcessSearch,
BasicCpu,
BasicMem,
BasicNet,
}
impl WidgetPosition {
pub fn is_widget_table(self) -> bool {
match self {
WidgetPosition::Disk
| WidgetPosition::Process
| WidgetPosition::ProcessSearch
| WidgetPosition::Temp => true,
_ => false,
}
}
pub fn get_pretty_name(self) -> String {
match self {
WidgetPosition::Cpu | WidgetPosition::BasicCpu => "CPU",
WidgetPosition::Mem | WidgetPosition::BasicMem => "Memory",
WidgetPosition::Disk => "Disks",
WidgetPosition::Temp => "Temperature",
WidgetPosition::Network | WidgetPosition::BasicNet => "Network",
WidgetPosition::Process | WidgetPosition::ProcessSearch => "Processes",
}
.to_string()
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -180,6 +207,7 @@ pub struct AppConfigFields {
pub show_average_cpu: bool, pub show_average_cpu: bool,
pub use_current_cpu_total: bool, pub use_current_cpu_total: bool,
pub show_disabled_data: bool, pub show_disabled_data: bool,
pub use_basic_mode: bool,
} }
/// Network specific /// Network specific
@ -243,7 +271,7 @@ pub struct App {
pub update_process_gui: bool, pub update_process_gui: bool,
pub app_scroll_positions: AppScrollState, pub app_scroll_positions: AppScrollState,
pub current_widget_selected: WidgetPosition, pub current_widget_selected: WidgetPosition,
pub data: data_harvester::Data, pub previous_basic_table_selected: WidgetPosition,
awaiting_second_char: bool, awaiting_second_char: bool,
second_char: Option<char>, second_char: Option<char>,
pub dd_err: Option<String>, pub dd_err: Option<String>,
@ -270,15 +298,28 @@ impl App {
show_average_cpu: bool, temperature_type: temperature::TemperatureType, show_average_cpu: bool, temperature_type: temperature::TemperatureType,
update_rate_in_milliseconds: u64, use_dot: bool, left_legend: bool, update_rate_in_milliseconds: u64, use_dot: bool, left_legend: bool,
use_current_cpu_total: bool, current_widget_selected: WidgetPosition, use_current_cpu_total: bool, current_widget_selected: WidgetPosition,
show_disabled_data: bool, show_disabled_data: bool, use_basic_mode: bool,
) -> App { ) -> App {
App { App {
process_sorting_type: processes::ProcessSorting::CPU, process_sorting_type: processes::ProcessSorting::CPU,
process_sorting_reverse: true, process_sorting_reverse: true,
update_process_gui: false, update_process_gui: false,
current_widget_selected, current_widget_selected: if use_basic_mode {
match current_widget_selected {
WidgetPosition::Cpu => WidgetPosition::BasicCpu,
WidgetPosition::Network => WidgetPosition::BasicNet,
WidgetPosition::Mem => WidgetPosition::BasicMem,
_ => current_widget_selected,
}
} else {
current_widget_selected
},
previous_basic_table_selected: if current_widget_selected.is_widget_table() {
current_widget_selected
} else {
WidgetPosition::Process
},
app_scroll_positions: AppScrollState::default(), app_scroll_positions: AppScrollState::default(),
data: data_harvester::Data::default(),
awaiting_second_char: false, awaiting_second_char: false,
second_char: None, second_char: None,
dd_err: None, dd_err: None,
@ -299,6 +340,7 @@ impl App {
left_legend, left_legend,
use_current_cpu_total, use_current_cpu_total,
show_disabled_data, show_disabled_data,
use_basic_mode,
}, },
is_expanded: false, is_expanded: false,
is_resized: false, is_resized: false,
@ -333,6 +375,12 @@ impl App {
self.dd_err = None; self.dd_err = None;
} else if self.is_filtering_or_searching() { } else if self.is_filtering_or_searching() {
match self.current_widget_selected { match self.current_widget_selected {
WidgetPosition::Cpu
if self.is_expanded && self.app_config_fields.use_basic_mode =>
{
self.current_widget_selected = WidgetPosition::BasicCpu;
self.cpu_state.is_showing_tray = false;
}
WidgetPosition::Process | WidgetPosition::ProcessSearch => { WidgetPosition::Process | WidgetPosition::ProcessSearch => {
if self.process_search_state.search_state.is_enabled { if self.process_search_state.search_state.is_enabled {
self.current_widget_selected = WidgetPosition::Process; self.current_widget_selected = WidgetPosition::Process;
@ -436,6 +484,10 @@ impl App {
pub fn on_slash(&mut self) { pub fn on_slash(&mut self) {
if !self.is_in_dialog() { if !self.is_in_dialog() {
match self.current_widget_selected { match self.current_widget_selected {
WidgetPosition::BasicCpu if self.is_expanded => {
self.current_widget_selected = WidgetPosition::Cpu;
self.cpu_state.is_showing_tray = true;
}
WidgetPosition::Process | WidgetPosition::ProcessSearch => { WidgetPosition::Process | WidgetPosition::ProcessSearch => {
// Toggle on // Toggle on
self.process_search_state.search_state.is_enabled = true; self.process_search_state.search_state.is_enabled = true;
@ -858,7 +910,8 @@ impl App {
let current_key_press_inst = Instant::now(); let current_key_press_inst = Instant::now();
if current_key_press_inst if current_key_press_inst
.duration_since(self.last_key_press) .duration_since(self.last_key_press)
.as_millis() > constants::MAX_KEY_TIMEOUT_IN_MILLISECONDS .as_millis()
> constants::MAX_KEY_TIMEOUT_IN_MILLISECONDS
{ {
self.reset_multi_tap_keys(); self.reset_multi_tap_keys();
} }
@ -1046,7 +1099,7 @@ impl App {
self.to_delete_process_list.clone() self.to_delete_process_list.clone()
} }
// For now, these are hard coded --- in the future, they shouldn't be! // TODO: [MODULARITY] Do NOT hard code this in thu future!
// //
// General idea for now: // General idea for now:
// CPU -(down)> MEM // CPU -(down)> MEM
@ -1058,6 +1111,16 @@ impl App {
// PROC_SEARCH -(up)> PROC, -(left)> Network // PROC_SEARCH -(up)> PROC, -(left)> Network
pub fn move_widget_selection_left(&mut self) { pub fn move_widget_selection_left(&mut self) {
if !self.is_in_dialog() && !self.is_expanded { if !self.is_in_dialog() && !self.is_expanded {
if self.app_config_fields.use_basic_mode {
self.current_widget_selected = match self.current_widget_selected {
WidgetPosition::BasicNet => WidgetPosition::BasicMem,
WidgetPosition::Process => WidgetPosition::Disk,
WidgetPosition::ProcessSearch => WidgetPosition::Disk,
WidgetPosition::Disk => WidgetPosition::Temp,
WidgetPosition::Temp => WidgetPosition::Process,
_ => self.current_widget_selected,
};
} else {
self.current_widget_selected = match self.current_widget_selected { self.current_widget_selected = match self.current_widget_selected {
WidgetPosition::Process => WidgetPosition::Network, WidgetPosition::Process => WidgetPosition::Network,
WidgetPosition::ProcessSearch => WidgetPosition::Network, WidgetPosition::ProcessSearch => WidgetPosition::Network,
@ -1066,24 +1129,50 @@ impl App {
_ => self.current_widget_selected, _ => self.current_widget_selected,
}; };
} }
}
self.reset_multi_tap_keys(); self.reset_multi_tap_keys();
} }
pub fn move_widget_selection_right(&mut self) { pub fn move_widget_selection_right(&mut self) {
if !self.is_in_dialog() && !self.is_expanded { if !self.is_in_dialog() && !self.is_expanded {
if self.app_config_fields.use_basic_mode {
self.current_widget_selected = match self.current_widget_selected {
WidgetPosition::BasicMem => WidgetPosition::BasicNet,
WidgetPosition::Process => WidgetPosition::Temp,
WidgetPosition::ProcessSearch => WidgetPosition::Temp,
WidgetPosition::Disk => WidgetPosition::Process,
WidgetPosition::Temp => WidgetPosition::Disk,
_ => self.current_widget_selected,
};
} else {
self.current_widget_selected = match self.current_widget_selected { self.current_widget_selected = match self.current_widget_selected {
WidgetPosition::Mem => WidgetPosition::Temp, WidgetPosition::Mem => WidgetPosition::Temp,
WidgetPosition::Network => WidgetPosition::Process, WidgetPosition::Network => WidgetPosition::Process,
_ => self.current_widget_selected, _ => self.current_widget_selected,
}; };
} }
}
self.reset_multi_tap_keys(); self.reset_multi_tap_keys();
} }
pub fn move_widget_selection_up(&mut self) { pub fn move_widget_selection_up(&mut self) {
if !self.is_in_dialog() && !self.is_expanded { if !self.is_in_dialog() && !self.is_expanded {
if self.app_config_fields.use_basic_mode {
if self.current_widget_selected.is_widget_table() {
self.previous_basic_table_selected = self.current_widget_selected;
}
self.current_widget_selected = match self.current_widget_selected {
WidgetPosition::BasicMem => WidgetPosition::BasicCpu,
WidgetPosition::BasicNet => WidgetPosition::BasicCpu,
WidgetPosition::Process => WidgetPosition::BasicMem,
WidgetPosition::ProcessSearch => WidgetPosition::Process,
WidgetPosition::Temp => WidgetPosition::BasicMem,
WidgetPosition::Disk => WidgetPosition::BasicMem,
_ => self.current_widget_selected,
};
} else {
self.current_widget_selected = match self.current_widget_selected { self.current_widget_selected = match self.current_widget_selected {
WidgetPosition::Mem => WidgetPosition::Cpu, WidgetPosition::Mem => WidgetPosition::Cpu,
WidgetPosition::Network => WidgetPosition::Mem, WidgetPosition::Network => WidgetPosition::Mem,
@ -1093,6 +1182,7 @@ impl App {
WidgetPosition::Disk => WidgetPosition::Temp, WidgetPosition::Disk => WidgetPosition::Temp,
_ => self.current_widget_selected, _ => self.current_widget_selected,
}; };
}
} else if self.is_expanded { } else if self.is_expanded {
self.current_widget_selected = match self.current_widget_selected { self.current_widget_selected = match self.current_widget_selected {
WidgetPosition::ProcessSearch => WidgetPosition::Process, WidgetPosition::ProcessSearch => WidgetPosition::Process,
@ -1105,6 +1195,21 @@ impl App {
pub fn move_widget_selection_down(&mut self) { pub fn move_widget_selection_down(&mut self) {
if !self.is_in_dialog() && !self.is_expanded { if !self.is_in_dialog() && !self.is_expanded {
if self.app_config_fields.use_basic_mode {
self.current_widget_selected = match self.current_widget_selected {
WidgetPosition::BasicMem => self.previous_basic_table_selected,
WidgetPosition::BasicNet => self.previous_basic_table_selected,
WidgetPosition::BasicCpu => WidgetPosition::BasicMem,
WidgetPosition::Process => {
if self.is_searching() {
WidgetPosition::ProcessSearch
} else {
WidgetPosition::Process
}
}
_ => self.current_widget_selected,
};
} else {
self.current_widget_selected = match self.current_widget_selected { self.current_widget_selected = match self.current_widget_selected {
WidgetPosition::Cpu => WidgetPosition::Mem, WidgetPosition::Cpu => WidgetPosition::Mem,
WidgetPosition::Mem => WidgetPosition::Network, WidgetPosition::Mem => WidgetPosition::Network,
@ -1119,6 +1224,7 @@ impl App {
} }
_ => self.current_widget_selected, _ => self.current_widget_selected,
}; };
}
} else if self.is_expanded { } else if self.is_expanded {
self.current_widget_selected = match self.current_widget_selected { self.current_widget_selected = match self.current_widget_selected {
WidgetPosition::Process => { WidgetPosition::Process => {
@ -1172,12 +1278,14 @@ impl App {
WidgetPosition::Process => { WidgetPosition::Process => {
self.app_scroll_positions self.app_scroll_positions
.process_scroll_state .process_scroll_state
.current_scroll_position = self.canvas_data.finalized_process_data.len() as u64 - 1 .current_scroll_position =
self.canvas_data.finalized_process_data.len() as u64 - 1
} }
WidgetPosition::Temp => { WidgetPosition::Temp => {
self.app_scroll_positions self.app_scroll_positions
.temp_scroll_state .temp_scroll_state
.current_scroll_position = self.canvas_data.temp_sensor_data.len() as u64 - 1 .current_scroll_position =
self.canvas_data.temp_sensor_data.len() as u64 - 1
} }
WidgetPosition::Disk => { WidgetPosition::Disk => {
self.app_scroll_positions self.app_scroll_positions

View File

@ -1,8 +1,8 @@
use std::cmp::max; use std::cmp::{max, min};
use std::collections::HashMap; use std::collections::HashMap;
use tui::{ use tui::{
backend, backend::Backend,
layout::{Alignment, Constraint, Direction, Layout, Rect}, layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Style}, style::{Color, Style},
terminal::Frame, terminal::Frame,
@ -35,6 +35,7 @@ const NETWORK_HEADERS: [&str; 4] = ["RX", "TX", "Total RX", "Total TX"];
const FORCE_MIN_THRESHOLD: usize = 5; const FORCE_MIN_THRESHOLD: usize = 5;
lazy_static! { lazy_static! {
static ref SIDE_BORDERS: Borders = Borders::from_bits_truncate(20);
static ref DEFAULT_TEXT_STYLE: Style = Style::default().fg(Color::Gray); static ref DEFAULT_TEXT_STYLE: Style = Style::default().fg(Color::Gray);
static ref DEFAULT_HEADER_STYLE: Style = Style::default().fg(Color::LightBlue); static ref DEFAULT_HEADER_STYLE: Style = Style::default().fg(Color::LightBlue);
static ref DISK_HEADERS_LENS: Vec<usize> = DISK_HEADERS static ref DISK_HEADERS_LENS: Vec<usize> = DISK_HEADERS
@ -73,9 +74,12 @@ pub struct DisplayableData {
pub network_data_tx: Vec<(f64, f64)>, pub network_data_tx: Vec<(f64, f64)>,
pub disk_data: Vec<Vec<String>>, pub disk_data: Vec<Vec<String>>,
pub temp_sensor_data: Vec<Vec<String>>, pub temp_sensor_data: Vec<Vec<String>>,
pub process_data: HashMap<u32, ProcessHarvest>, // Not the final value pub process_data: HashMap<u32, ProcessHarvest>,
pub grouped_process_data: Vec<ConvertedProcessData>, // Not the final value // Not the final value
pub finalized_process_data: Vec<ConvertedProcessData>, // What's actually displayed pub grouped_process_data: Vec<ConvertedProcessData>,
// Not the final value
pub finalized_process_data: Vec<ConvertedProcessData>,
// What's actually displayed
pub mem_label: String, pub mem_label: String,
pub swap_label: String, pub swap_label: String,
pub mem_data: Vec<(f64, f64)>, pub mem_data: Vec<(f64, f64)>,
@ -147,16 +151,33 @@ impl Painter {
); );
} }
pub fn draw_specific_table<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool,
widget_selected: WidgetPosition,
) {
match widget_selected {
WidgetPosition::Process | WidgetPosition::ProcessSearch => {
self.draw_process_and_search(f, app_state, draw_loc, draw_border)
}
WidgetPosition::Temp => self.draw_temp_table(f, app_state, draw_loc, draw_border),
WidgetPosition::Disk => self.draw_disk_table(f, app_state, draw_loc, draw_border),
_ => {}
}
}
// TODO: [REFACTOR] We should clean this up tbh // TODO: [REFACTOR] We should clean this up tbh
// TODO: [FEATURE] Auto-resizing dialog sizes. // TODO: [FEATURE] Auto-resizing dialog sizes.
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
pub fn draw_data<B: backend::Backend>( pub fn draw_data<B: Backend>(
&mut self, terminal: &mut Terminal<B>, app_state: &mut app::App, &mut self, terminal: &mut Terminal<B>, app_state: &mut app::App,
) -> error::Result<()> { ) -> error::Result<()> {
let terminal_size = terminal.size()?; let terminal_size = terminal.size()?;
let current_height = terminal_size.height; let current_height = terminal_size.height;
let current_width = terminal_size.width; let current_width = terminal_size.width;
// TODO: [OPT] we might be able to add an argument s.t. if there is
// no resize AND it's not a data update (or process refresh/search/etc.)
// then just... don't draw again!
if self.height == 0 && self.width == 0 { if self.height == 0 && self.width == 0 {
self.height = current_height; self.height = current_height;
self.width = current_width; self.width = current_width;
@ -354,7 +375,7 @@ impl Painter {
.constraints([Constraint::Percentage(100)].as_ref()) .constraints([Constraint::Percentage(100)].as_ref())
.split(f.size()); .split(f.size());
match &app_state.current_widget_selected { match &app_state.current_widget_selected {
WidgetPosition::Cpu => { WidgetPosition::Cpu | WidgetPosition::BasicCpu => {
let cpu_chunk = Layout::default() let cpu_chunk = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.margin(0) .margin(0)
@ -382,35 +403,60 @@ impl Painter {
self.draw_cpu_graph(&mut f, &app_state, cpu_chunk[graph_index]); self.draw_cpu_graph(&mut f, &app_state, cpu_chunk[graph_index]);
self.draw_cpu_legend(&mut f, app_state, cpu_chunk[legend_index]); self.draw_cpu_legend(&mut f, app_state, cpu_chunk[legend_index]);
} }
WidgetPosition::Mem => { WidgetPosition::Mem | WidgetPosition::BasicMem => {
self.draw_memory_graph(&mut f, &app_state, rect[0]); self.draw_memory_graph(&mut f, &app_state, rect[0]);
} }
WidgetPosition::Disk => { WidgetPosition::Disk => {
self.draw_disk_table(&mut f, app_state, rect[0]); self.draw_disk_table(&mut f, app_state, rect[0], true);
} }
WidgetPosition::Temp => { WidgetPosition::Temp => {
self.draw_temp_table(&mut f, app_state, rect[0]); self.draw_temp_table(&mut f, app_state, rect[0], true);
} }
WidgetPosition::Network => { WidgetPosition::Network | WidgetPosition::BasicNet => {
self.draw_network_graph(&mut f, &app_state, rect[0]); self.draw_network_graph(&mut f, &app_state, rect[0]);
} }
WidgetPosition::Process | WidgetPosition::ProcessSearch => { WidgetPosition::Process | WidgetPosition::ProcessSearch => {
if app_state.is_searching() { self.draw_process_and_search(&mut f, app_state, rect[0], true);
let processes_chunk = Layout::default() }
.direction(Direction::Vertical) }
.margin(0) } else if app_state.app_config_fields.use_basic_mode {
.constraints( // Basic mode. This basically removes all graphs but otherwise
[Constraint::Percentage(85), Constraint::Percentage(15)] // the same info.
.as_ref(),
)
.split(rect[0]);
self.draw_processes_table(&mut f, app_state, processes_chunk[0]); let cpu_height = (app_state.canvas_data.cpu_data.len() / 4) as u16
self.draw_search_field(&mut f, app_state, processes_chunk[1]); + (
if app_state.canvas_data.cpu_data.len() % 4 == 0 {
0
} else { } else {
self.draw_processes_table(&mut f, app_state, rect[0]); 1
}
} }
);
let vertical_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(cpu_height),
Constraint::Length(1),
Constraint::Length(2),
Constraint::Length(2),
Constraint::Min(5),
].as_ref())
.split(f.size());
let middle_chunks= Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(50),
Constraint::Percentage(50),
].as_ref())
.split(vertical_chunks[2]);
self.draw_basic_cpu(&mut f, app_state, vertical_chunks[0]);
self.draw_basic_memory(&mut f, app_state, middle_chunks[0]);
self.draw_basic_network(&mut f, app_state, middle_chunks[1]);
self.draw_basic_table_arrows(&mut f, app_state, vertical_chunks[3]);
if app_state.current_widget_selected.is_widget_table() {
self.draw_specific_table(&mut f, app_state, vertical_chunks[4], false, app_state.current_widget_selected);
} else {
self.draw_specific_table(&mut f, app_state, vertical_chunks[4], false, app_state.previous_basic_table_selected);
} }
} else { } else {
// TODO: [TUI] Change this back to a more even 33/33/34 when TUI releases // TODO: [TUI] Change this back to a more even 33/33/34 when TUI releases
@ -490,50 +536,14 @@ impl Painter {
0 0
}; };
// Set up blocks and their components
// CPU graph + legend
self.draw_cpu_graph(&mut f, &app_state, cpu_chunk[graph_index]); self.draw_cpu_graph(&mut f, &app_state, cpu_chunk[graph_index]);
self.draw_cpu_legend(&mut f, app_state, cpu_chunk[legend_index]); self.draw_cpu_legend(&mut f, app_state, cpu_chunk[legend_index]);
//Memory usage graph
self.draw_memory_graph(&mut f, &app_state, middle_chunks[0]); self.draw_memory_graph(&mut f, &app_state, middle_chunks[0]);
// Network graph
self.draw_network_graph(&mut f, &app_state, network_chunk[0]); self.draw_network_graph(&mut f, &app_state, network_chunk[0]);
self.draw_network_labels(&mut f, app_state, network_chunk[1]); self.draw_network_labels(&mut f, app_state, network_chunk[1]);
self.draw_temp_table(&mut f, app_state, middle_divided_chunk_2[0], true);
// Temperature table self.draw_disk_table(&mut f, app_state, middle_divided_chunk_2[1], true);
self.draw_temp_table(&mut f, app_state, middle_divided_chunk_2[0]); self.draw_process_and_search(&mut f, app_state, bottom_chunks[1], true);
// Disk usage table
self.draw_disk_table(&mut f, app_state, middle_divided_chunk_2[1]);
// Processes table
if app_state.is_searching() {
let processes_chunk = Layout::default()
.direction(Direction::Vertical)
.margin(0)
.constraints(
if (bottom_chunks[1].height as f64 * 0.25) as u16 >= 4 {
[Constraint::Percentage(75), Constraint::Percentage(25)]
} else {
let required = if bottom_chunks[1].height < 10 {
bottom_chunks[1].height / 2
} else {
5
};
let remaining = bottom_chunks[1].height - required;
[Constraint::Length(remaining), Constraint::Length(required)]
}
.as_ref(),
)
.split(bottom_chunks[1]);
self.draw_processes_table(&mut f, app_state, processes_chunk[0]);
self.draw_search_field(&mut f, app_state, processes_chunk[1]);
} else {
self.draw_processes_table(&mut f, app_state, bottom_chunks[1]);
}
} }
})?; })?;
@ -542,7 +552,25 @@ impl Painter {
Ok(()) Ok(())
} }
fn draw_cpu_graph<B: backend::Backend>( fn draw_process_and_search<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool,
) {
let search_width = if draw_border { 5 } else { 3 };
if app_state.is_searching() {
let processes_chunk = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(0), Constraint::Length(search_width)].as_ref())
.split(draw_loc);
self.draw_processes_table(f, app_state, processes_chunk[0], draw_border);
self.draw_search_field(f, app_state, processes_chunk[1], draw_border);
} else {
self.draw_processes_table(f, app_state, draw_loc, draw_border);
}
}
fn draw_cpu_graph<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &app::App, draw_loc: Rect, &self, f: &mut Frame<'_, B>, app_state: &app::App, draw_loc: Rect,
) { ) {
let cpu_data: &[ConvertedCpuData] = &app_state.canvas_data.cpu_data; let cpu_data: &[ConvertedCpuData] = &app_state.canvas_data.cpu_data;
@ -609,7 +637,9 @@ impl Painter {
}) })
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(match app_state.current_widget_selected { .border_style(match app_state.current_widget_selected {
app::WidgetPosition::Cpu => self.colours.highlighted_border_style, WidgetPosition::Cpu | WidgetPosition::BasicCpu => {
self.colours.highlighted_border_style
}
_ => self.colours.border_style, _ => self.colours.border_style,
}), }),
) )
@ -619,7 +649,7 @@ impl Painter {
.render(f, draw_loc); .render(f, draw_loc);
} }
fn draw_cpu_legend<B: backend::Backend>( fn draw_cpu_legend<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect,
) { ) {
let cpu_data: &[ConvertedCpuData] = &app_state.canvas_data.cpu_data; let cpu_data: &[ConvertedCpuData] = &app_state.canvas_data.cpu_data;
@ -671,12 +701,13 @@ impl Painter {
Row::StyledData( Row::StyledData(
cpu_string_row.iter(), cpu_string_row.iter(),
match app_state.current_widget_selected { match app_state.current_widget_selected {
app::WidgetPosition::Cpu => { WidgetPosition::Cpu => {
if itx as u64 if itx as u64
== app_state == app_state
.app_scroll_positions .app_scroll_positions
.cpu_scroll_state .cpu_scroll_state
.current_scroll_position - start_position .current_scroll_position
- start_position
{ {
self.colours.currently_selected_text_style self.colours.currently_selected_text_style
} else if app_state.app_config_fields.show_average_cpu && itx == 0 { } else if app_state.app_config_fields.show_average_cpu && itx == 0 {
@ -745,13 +776,13 @@ impl Painter {
self.colours.highlighted_border_style self.colours.highlighted_border_style
} else { } else {
match app_state.current_widget_selected { match app_state.current_widget_selected {
app::WidgetPosition::Cpu => self.colours.highlighted_border_style, WidgetPosition::Cpu => self.colours.highlighted_border_style,
_ => self.colours.border_style, _ => self.colours.border_style,
} }
}) })
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(match app_state.current_widget_selected { .border_style(match app_state.current_widget_selected {
app::WidgetPosition::Cpu => self.colours.highlighted_border_style, WidgetPosition::Cpu => self.colours.highlighted_border_style,
_ => self.colours.border_style, _ => self.colours.border_style,
}), }),
) )
@ -765,7 +796,7 @@ impl Painter {
.render(f, draw_loc); .render(f, draw_loc);
} }
fn draw_memory_graph<B: backend::Backend>( fn draw_memory_graph<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &app::App, draw_loc: Rect, &self, f: &mut Frame<'_, B>, app_state: &app::App, draw_loc: Rect,
) { ) {
let mem_data: &[(f64, f64)] = &app_state.canvas_data.mem_data; let mem_data: &[(f64, f64)] = &app_state.canvas_data.mem_data;
@ -828,7 +859,9 @@ impl Painter {
}) })
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(match app_state.current_widget_selected { .border_style(match app_state.current_widget_selected {
app::WidgetPosition::Mem => self.colours.highlighted_border_style, WidgetPosition::Mem | WidgetPosition::BasicMem => {
self.colours.highlighted_border_style
}
_ => self.colours.border_style, _ => self.colours.border_style,
}), }),
) )
@ -838,7 +871,7 @@ impl Painter {
.render(f, draw_loc); .render(f, draw_loc);
} }
fn draw_network_graph<B: backend::Backend>( fn draw_network_graph<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &app::App, draw_loc: Rect, &self, f: &mut Frame<'_, B>, app_state: &app::App, draw_loc: Rect,
) { ) {
let network_data_rx: &[(f64, f64)] = &app_state.canvas_data.network_data_rx; let network_data_rx: &[(f64, f64)] = &app_state.canvas_data.network_data_rx;
@ -878,7 +911,9 @@ impl Painter {
}) })
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(match app_state.current_widget_selected { .border_style(match app_state.current_widget_selected {
app::WidgetPosition::Network => self.colours.highlighted_border_style, WidgetPosition::Network | WidgetPosition::BasicNet => {
self.colours.highlighted_border_style
}
_ => self.colours.border_style, _ => self.colours.border_style,
}), }),
) )
@ -908,18 +943,18 @@ impl Painter {
"Total RX: {:7}", "Total RX: {:7}",
app_state.canvas_data.total_rx_display app_state.canvas_data.total_rx_display
)) ))
.style(self.colours.rx_total_style), .style(self.colours.total_rx_style),
Dataset::default() Dataset::default()
.name(&format!( .name(&format!(
"Total TX: {:7}", "Total TX: {:7}",
app_state.canvas_data.total_tx_display app_state.canvas_data.total_tx_display
)) ))
.style(self.colours.tx_total_style), .style(self.colours.total_tx_style),
]) ])
.render(f, draw_loc); .render(f, draw_loc);
} }
fn draw_network_labels<B: backend::Backend>( fn draw_network_labels<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect,
) { ) {
let rx_display = &app_state.canvas_data.rx_display; let rx_display = &app_state.canvas_data.rx_display;
@ -951,7 +986,7 @@ impl Painter {
Table::new(NETWORK_HEADERS.iter(), mapped_network) Table::new(NETWORK_HEADERS.iter(), mapped_network)
.block(Block::default().borders(Borders::ALL).border_style( .block(Block::default().borders(Borders::ALL).border_style(
match app_state.current_widget_selected { match app_state.current_widget_selected {
app::WidgetPosition::Network => self.colours.highlighted_border_style, WidgetPosition::Network => self.colours.highlighted_border_style,
_ => self.colours.border_style, _ => self.colours.border_style,
}, },
)) ))
@ -966,8 +1001,8 @@ impl Painter {
.render(f, draw_loc); .render(f, draw_loc);
} }
fn draw_temp_table<B: backend::Backend>( fn draw_temp_table<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool,
) { ) {
let temp_sensor_data: &[Vec<String>] = &app_state.canvas_data.temp_sensor_data; let temp_sensor_data: &[Vec<String>] = &app_state.canvas_data.temp_sensor_data;
@ -993,12 +1028,13 @@ impl Painter {
Row::StyledData( Row::StyledData(
temp_row.iter(), temp_row.iter(),
match app_state.current_widget_selected { match app_state.current_widget_selected {
app::WidgetPosition::Temp => { WidgetPosition::Temp => {
if temp_row_counter as u64 if temp_row_counter as u64
== app_state == app_state
.app_scroll_positions .app_scroll_positions
.temp_scroll_state .temp_scroll_state
.current_scroll_position - start_position .current_scroll_position
- start_position
{ {
temp_row_counter = -1; temp_row_counter = -1;
self.colours.currently_selected_text_style self.colours.currently_selected_text_style
@ -1033,26 +1069,50 @@ impl Painter {
); );
result_title result_title
} else if app_state.app_config_fields.use_basic_mode {
String::new()
} else { } else {
" Temperatures ".to_string() " Temperatures ".to_string()
}; };
// Draw let temp_block = if draw_border {
Table::new(TEMP_HEADERS.iter(), temperature_rows)
.block(
Block::default() Block::default()
.title(&title) .title(&title)
.title_style(if app_state.is_expanded { .title_style(if app_state.is_expanded {
self.colours.highlighted_border_style match app_state.current_widget_selected {
WidgetPosition::Temp => self.colours.highlighted_border_style,
_ => self.colours.border_style,
}
} else { } else {
self.colours.widget_title_style self.colours.widget_title_style
}) })
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(match app_state.current_widget_selected { .border_style(match app_state.current_widget_selected {
app::WidgetPosition::Temp => self.colours.highlighted_border_style, WidgetPosition::Temp => self.colours.highlighted_border_style,
_ => self.colours.border_style, _ => self.colours.border_style,
}), })
) } else {
match app_state.current_widget_selected {
WidgetPosition::Temp => Block::default()
.borders(*SIDE_BORDERS)
.border_style(self.colours.highlighted_border_style),
_ => Block::default().borders(Borders::NONE),
}
};
let margined_draw_loc = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref())
.horizontal_margin(match app_state.current_widget_selected {
WidgetPosition::Temp => 0,
_ if !draw_border => 1,
_ => 0,
})
.direction(Direction::Horizontal)
.split(draw_loc);
// Draw
Table::new(TEMP_HEADERS.iter(), temperature_rows)
.block(temp_block)
.header_style(self.colours.table_header_style) .header_style(self.colours.table_header_style)
.widths( .widths(
&(intrinsic_widths &(intrinsic_widths
@ -1060,11 +1120,11 @@ impl Painter {
.map(|calculated_width| Constraint::Length(*calculated_width as u16)) .map(|calculated_width| Constraint::Length(*calculated_width as u16))
.collect::<Vec<_>>()), .collect::<Vec<_>>()),
) )
.render(f, draw_loc); .render(f, margined_draw_loc[0]);
} }
fn draw_disk_table<B: backend::Backend>( fn draw_disk_table<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool,
) { ) {
let disk_data: &[Vec<String>] = &app_state.canvas_data.disk_data; let disk_data: &[Vec<String>] = &app_state.canvas_data.disk_data;
let num_rows = max(0, i64::from(draw_loc.height) - 5) as u64; let num_rows = max(0, i64::from(draw_loc.height) - 5) as u64;
@ -1089,12 +1149,13 @@ impl Painter {
Row::StyledData( Row::StyledData(
disk.iter(), disk.iter(),
match app_state.current_widget_selected { match app_state.current_widget_selected {
app::WidgetPosition::Disk => { WidgetPosition::Disk => {
if disk_counter as u64 if disk_counter as u64
== app_state == app_state
.app_scroll_positions .app_scroll_positions
.disk_scroll_state .disk_scroll_state
.current_scroll_position - start_position .current_scroll_position
- start_position
{ {
disk_counter = -1; disk_counter = -1;
self.colours.currently_selected_text_style self.colours.currently_selected_text_style
@ -1128,28 +1189,51 @@ impl Painter {
" Disk ─{}─ Esc to go back ", " Disk ─{}─ Esc to go back ",
"".repeat(repeat_num as usize) "".repeat(repeat_num as usize)
); );
result_title result_title
} else if app_state.app_config_fields.use_basic_mode {
String::new()
} else { } else {
" Disk ".to_string() " Disk ".to_string()
}; };
// Draw! let disk_block = if draw_border {
Table::new(DISK_HEADERS.iter(), disk_rows)
.block(
Block::default() Block::default()
.title(&title) .title(&title)
.title_style(if app_state.is_expanded { .title_style(if app_state.is_expanded {
self.colours.highlighted_border_style match app_state.current_widget_selected {
WidgetPosition::Disk => self.colours.highlighted_border_style,
_ => self.colours.border_style,
}
} else { } else {
self.colours.widget_title_style self.colours.widget_title_style
}) })
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(match app_state.current_widget_selected { .border_style(match app_state.current_widget_selected {
app::WidgetPosition::Disk => self.colours.highlighted_border_style, WidgetPosition::Disk => self.colours.highlighted_border_style,
_ => self.colours.border_style, _ => self.colours.border_style,
}), })
) } else {
match app_state.current_widget_selected {
WidgetPosition::Disk => Block::default()
.borders(*SIDE_BORDERS)
.border_style(self.colours.highlighted_border_style),
_ => Block::default().borders(Borders::NONE),
}
};
let margined_draw_loc = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref())
.horizontal_margin(match app_state.current_widget_selected {
WidgetPosition::Disk => 0,
_ if !draw_border => 1,
_ => 0,
})
.direction(Direction::Horizontal)
.split(draw_loc);
// Draw!
Table::new(DISK_HEADERS.iter(), disk_rows)
.block(disk_block)
.header_style(self.colours.table_header_style) .header_style(self.colours.table_header_style)
.widths( .widths(
&(intrinsic_widths &(intrinsic_widths
@ -1157,11 +1241,11 @@ impl Painter {
.map(|calculated_width| Constraint::Length(*calculated_width as u16)) .map(|calculated_width| Constraint::Length(*calculated_width as u16))
.collect::<Vec<_>>()), .collect::<Vec<_>>()),
) )
.render(f, draw_loc); .render(f, margined_draw_loc[0]);
} }
fn draw_search_field<B: backend::Backend>( fn draw_search_field<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool,
) { ) {
let width = max(0, draw_loc.width as i64 - 34) as u64; // TODO: [REFACTOR] Hard coding this is terrible. let width = max(0, draw_loc.width as i64 - 34) as u64; // TODO: [REFACTOR] Hard coding this is terrible.
let cursor_position = app_state.get_cursor_position(); let cursor_position = app_state.get_cursor_position();
@ -1179,7 +1263,7 @@ impl Painter {
let grapheme_indices = UnicodeSegmentation::grapheme_indices(query, true); let grapheme_indices = UnicodeSegmentation::grapheme_indices(query, true);
let mut current_grapheme_posn = 0; let mut current_grapheme_posn = 0;
let query_with_cursor: Vec<Text<'_>> = let query_with_cursor: Vec<Text<'_>> =
if let app::WidgetPosition::ProcessSearch = app_state.current_widget_selected { if let WidgetPosition::ProcessSearch = app_state.current_widget_selected {
let mut res = grapheme_indices let mut res = grapheme_indices
.filter_map(|grapheme| { .filter_map(|grapheme| {
current_grapheme_posn += UnicodeWidthStr::width(grapheme.1); current_grapheme_posn += UnicodeWidthStr::width(grapheme.1);
@ -1237,10 +1321,6 @@ impl Painter {
// Text options shamelessly stolen from VS Code. // Text options shamelessly stolen from VS Code.
let mut option_text = vec![]; let mut option_text = vec![];
for _ in 0..(draw_loc.height - 3) {
option_text.push(Text::raw("\n"));
}
let case_style = if !app_state.process_search_state.is_ignoring_case { let case_style = if !app_state.process_search_state.is_ignoring_case {
self.colours.currently_selected_text_style self.colours.currently_selected_text_style
} else { } else {
@ -1290,6 +1370,7 @@ impl Painter {
); );
let option_row = vec![ let option_row = vec![
Text::raw("\n\n"),
Text::styled(&case_text, case_style), Text::styled(&case_text, case_style),
Text::raw(" "), Text::raw(" "),
Text::styled(&whole_text, whole_word_style), Text::styled(&whole_text, whole_word_style),
@ -1301,13 +1382,6 @@ impl Painter {
search_text.extend(query_with_cursor); search_text.extend(query_with_cursor);
search_text.extend(option_text); search_text.extend(option_text);
const TITLE_BASE: &str = " Esc to close ";
let repeat_num = max(
0,
draw_loc.width as i32 - TITLE_BASE.chars().count() as i32 - 2,
);
let title = format!("{} Esc to close ", "".repeat(repeat_num as usize));
let current_border_style: Style = if app_state let current_border_style: Style = if app_state
.process_search_state .process_search_state
.search_state .search_state
@ -1316,27 +1390,58 @@ impl Painter {
Style::default().fg(Color::Rgb(255, 0, 0)) Style::default().fg(Color::Rgb(255, 0, 0))
} else { } else {
match app_state.current_widget_selected { match app_state.current_widget_selected {
app::WidgetPosition::ProcessSearch => self.colours.highlighted_border_style, WidgetPosition::ProcessSearch => self.colours.highlighted_border_style,
_ => self.colours.border_style, _ => self.colours.border_style,
} }
}; };
Paragraph::new(search_text.iter()) let title = if draw_border {
.block( const TITLE_BASE: &str = " Esc to close ";
let repeat_num = max(
0,
draw_loc.width as i32 - TITLE_BASE.chars().count() as i32 - 2,
);
format!("{} Esc to close ", "".repeat(repeat_num as usize))
} else {
String::new()
};
let process_search_block = if draw_border {
Block::default() Block::default()
.borders(Borders::ALL)
.title(&title) .title(&title)
.title_style(current_border_style) .title_style(current_border_style)
.borders(Borders::ALL)
.border_style(current_border_style)
} else {
match app_state.current_widget_selected {
WidgetPosition::ProcessSearch => Block::default()
.borders(*SIDE_BORDERS)
.border_style(current_border_style), .border_style(current_border_style),
) _ => Block::default().borders(Borders::NONE),
}
};
let margined_draw_loc = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref())
.horizontal_margin(match app_state.current_widget_selected {
WidgetPosition::ProcessSearch => 0,
_ if !draw_border => 1,
_ => 0,
})
.direction(Direction::Horizontal)
.split(draw_loc);
Paragraph::new(search_text.iter())
.block(process_search_block)
.style(self.colours.text_style) .style(self.colours.text_style)
.alignment(Alignment::Left) .alignment(Alignment::Left)
.wrap(false) .wrap(false)
.render(f, draw_loc); .render(f, margined_draw_loc[0]);
} }
fn draw_processes_table<B: backend::Backend>( fn draw_processes_table<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, &self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect, draw_border: bool,
) { ) {
let process_data: &[ConvertedProcessData] = &app_state.canvas_data.finalized_process_data; let process_data: &[ConvertedProcessData] = &app_state.canvas_data.finalized_process_data;
@ -1389,12 +1494,13 @@ impl Painter {
Row::StyledData( Row::StyledData(
stringified_process_vec.into_iter(), stringified_process_vec.into_iter(),
match app_state.current_widget_selected { match app_state.current_widget_selected {
app::WidgetPosition::Process => { WidgetPosition::Process => {
if process_counter as u64 if process_counter as u64
== app_state == app_state
.app_scroll_positions .app_scroll_positions
.process_scroll_state .process_scroll_state
.current_scroll_position - start_position .current_scroll_position
- start_position
{ {
process_counter = -1; process_counter = -1;
self.colours.currently_selected_text_style self.colours.currently_selected_text_style
@ -1447,7 +1553,7 @@ impl Painter {
get_variable_intrinsic_widths(width as u16, &width_ratios, &process_headers_lens); get_variable_intrinsic_widths(width as u16, &width_ratios, &process_headers_lens);
let intrinsic_widths = &(variable_intrinsic_results.0)[0..variable_intrinsic_results.1]; let intrinsic_widths = &(variable_intrinsic_results.0)[0..variable_intrinsic_results.1];
let title = let title = if draw_border {
if app_state.is_expanded && !app_state.process_search_state.search_state.is_enabled { if app_state.is_expanded && !app_state.process_search_state.search_state.is_enabled {
const TITLE_BASE: &str = " Processes ── Esc to go back "; const TITLE_BASE: &str = " Processes ── Esc to go back ";
let repeat_num = max( let repeat_num = max(
@ -1462,15 +1568,17 @@ impl Painter {
result_title result_title
} else { } else {
" Processes ".to_string() " Processes ".to_string()
}
} else {
String::default()
}; };
Table::new(process_headers.iter(), process_rows) let process_block = if draw_border {
.block(
Block::default() Block::default()
.title(&title) .title(&title)
.title_style(if app_state.is_expanded { .title_style(if app_state.is_expanded {
match app_state.current_widget_selected { match app_state.current_widget_selected {
app::WidgetPosition::Process => self.colours.highlighted_border_style, WidgetPosition::Process => self.colours.highlighted_border_style,
_ => self.colours.border_style, _ => self.colours.border_style,
} }
} else { } else {
@ -1478,10 +1586,30 @@ impl Painter {
}) })
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(match app_state.current_widget_selected { .border_style(match app_state.current_widget_selected {
app::WidgetPosition::Process => self.colours.highlighted_border_style, WidgetPosition::Process => self.colours.highlighted_border_style,
_ => self.colours.border_style, _ => self.colours.border_style,
}), })
) } else {
match app_state.current_widget_selected {
WidgetPosition::Process => Block::default()
.borders(*SIDE_BORDERS)
.border_style(self.colours.highlighted_border_style),
_ => Block::default().borders(Borders::NONE),
}
};
let margined_draw_loc = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref())
.horizontal_margin(match app_state.current_widget_selected {
WidgetPosition::Process => 0,
_ if !draw_border => 1,
_ => 0,
})
.direction(Direction::Horizontal)
.split(draw_loc);
Table::new(process_headers.iter(), process_rows)
.block(process_block)
.header_style(self.colours.table_header_style) .header_style(self.colours.table_header_style)
.widths( .widths(
&(intrinsic_widths &(intrinsic_widths
@ -1489,6 +1617,273 @@ impl Painter {
.map(|calculated_width| Constraint::Length(*calculated_width as u16)) .map(|calculated_width| Constraint::Length(*calculated_width as u16))
.collect::<Vec<_>>()), .collect::<Vec<_>>()),
) )
.render(f, margined_draw_loc[0]);
}
fn draw_basic_cpu<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect,
) {
let cpu_data: &[ConvertedCpuData] = &app_state.canvas_data.cpu_data;
// This is a bit complicated, but basically, we want to draw SOME number
// of columns to draw all CPUs. Ideally, as well, we want to not have
// to ever scroll.
// **General logic** - count number of elements in cpu_data. Then see how
// many rows and columns we have in draw_loc (-2 on both sides for border?).
// I think what we can do is try to fit in as many in one column as possible.
// If not, then add a new column.
// Then, from this, split the row space across ALL columns. From there, generate
// the desired lengths.
if let WidgetPosition::BasicCpu = app_state.current_widget_selected {
Block::default()
.borders(*SIDE_BORDERS)
.border_style(self.colours.highlighted_border_style)
.render(f, draw_loc); .render(f, draw_loc);
} }
let num_cpus = cpu_data.len();
if draw_loc.height > 0 {
let remaining_height = draw_loc.height as usize;
const REQUIRED_COLUMNS: usize = 4;
let chunk_vec =
vec![Constraint::Percentage((100 / REQUIRED_COLUMNS) as u16); REQUIRED_COLUMNS];
let chunks = Layout::default()
.constraints(chunk_vec.as_ref())
.direction(Direction::Horizontal)
.split(draw_loc);
// +9 due to 3 + 4 + 2 columns for the name & space + percentage + bar bounds
let margin_space = 2;
let remaining_width = max(
0,
draw_loc.width as i64
- ((9 + margin_space) * REQUIRED_COLUMNS - margin_space) as i64,
) as usize;
let bar_length = remaining_width / REQUIRED_COLUMNS;
// CPU (and RAM) percent bars are, uh, "heavily" inspired from htop.
let cpu_bars = (0..num_cpus)
.map(|cpu_index| {
let use_percentage =
if let Some(cpu_usage) = cpu_data[cpu_index].cpu_data.last() {
cpu_usage.1
} else {
0.0
};
let num_bars = calculate_basic_use_bars(use_percentage, bar_length);
format!(
"{:3}[{}{}{:3.0}%]\n",
if app_state.app_config_fields.show_average_cpu {
if cpu_index == 0 {
"AVG".to_string()
} else {
(cpu_index - 1).to_string()
}
} else {
cpu_index.to_string()
},
"|".repeat(num_bars),
" ".repeat(bar_length - num_bars),
use_percentage.round(),
)
})
.collect::<Vec<_>>();
let mut row_counter = num_cpus;
let mut start_index = 0;
for (itx, chunk) in chunks.iter().enumerate() {
let to_divide = REQUIRED_COLUMNS - itx;
let how_many_cpus = min(
remaining_height,
(row_counter / to_divide) + (if row_counter % to_divide == 0 { 0 } else { 1 }),
);
row_counter -= how_many_cpus;
let end_index = min(start_index + how_many_cpus, num_cpus);
let cpu_column: Vec<Text<'_>> = (start_index..end_index)
.map(|cpu_index| {
Text::Styled(
(&cpu_bars[cpu_index]).into(),
self.colours.cpu_colour_styles
[cpu_index as usize % self.colours.cpu_colour_styles.len()],
)
})
.collect::<Vec<_>>();
start_index += how_many_cpus;
let margined_loc = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(100)].as_ref())
.horizontal_margin(1)
.split(*chunk);
Paragraph::new(cpu_column.iter())
.block(Block::default())
.render(f, margined_loc[0]);
}
}
}
fn draw_basic_memory<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect,
) {
let mem_data: &[(f64, f64)] = &app_state.canvas_data.mem_data;
let swap_data: &[(f64, f64)] = &app_state.canvas_data.swap_data;
let margined_loc = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref())
.horizontal_margin(1)
.split(draw_loc);
if let WidgetPosition::BasicMem = app_state.current_widget_selected {
Block::default()
.borders(*SIDE_BORDERS)
.border_style(self.colours.highlighted_border_style)
.render(f, draw_loc);
}
// +9 due to 3 + 4 + 2 + 2 columns for the name & space + percentage + bar bounds + margin spacing
let bar_length = max(0, draw_loc.width as i64 - 11) as usize;
let ram_use_percentage = if let Some(mem) = mem_data.last() {
mem.1
} else {
0.0
};
let swap_use_percentage = if let Some(swap) = swap_data.last() {
swap.1
} else {
0.0
};
let num_bars_ram = calculate_basic_use_bars(ram_use_percentage, bar_length);
let num_bars_swap = calculate_basic_use_bars(swap_use_percentage, bar_length);
let mem_label = format!(
"RAM[{}{}{:3.0}%]\n",
"|".repeat(num_bars_ram),
" ".repeat(bar_length - num_bars_ram),
ram_use_percentage.round(),
);
let swap_label = format!(
"SWP[{}{}{:3.0}%]",
"|".repeat(num_bars_swap),
" ".repeat(bar_length - num_bars_swap),
swap_use_percentage.round(),
);
let mem_text: Vec<Text<'_>> = vec![
Text::Styled(mem_label.into(), self.colours.ram_style),
Text::Styled(swap_label.into(), self.colours.swap_style),
];
Paragraph::new(mem_text.iter())
.block(Block::default())
.render(f, margined_loc[0]);
}
fn draw_basic_network<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect,
) {
let divided_loc = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(draw_loc);
let net_loc = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(100)].as_ref())
.horizontal_margin(1)
.split(divided_loc[0]);
let total_loc = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(100)].as_ref())
.horizontal_margin(1)
.split(divided_loc[1]);
if let WidgetPosition::BasicNet = app_state.current_widget_selected {
Block::default()
.borders(*SIDE_BORDERS)
.border_style(self.colours.highlighted_border_style)
.render(f, draw_loc);
}
let rx_label = format!("RX: {}\n", &app_state.canvas_data.rx_display);
let tx_label = format!("TX: {}", &app_state.canvas_data.tx_display);
let total_rx_label = format!("Total RX: {}\n", &app_state.canvas_data.total_rx_display);
let total_tx_label = format!("Total TX: {}", &app_state.canvas_data.total_tx_display);
let net_text = vec![
Text::Styled(rx_label.into(), self.colours.rx_style),
Text::Styled(tx_label.into(), self.colours.tx_style),
];
let total_net_text = vec![
Text::Styled(total_rx_label.into(), self.colours.total_rx_style),
Text::Styled(total_tx_label.into(), self.colours.total_tx_style),
];
Paragraph::new(net_text.iter())
.block(Block::default())
.render(f, net_loc[0]);
Paragraph::new(total_net_text.iter())
.block(Block::default())
.render(f, total_loc[0]);
}
fn draw_basic_table_arrows<B: Backend>(
&self, f: &mut Frame<'_, B>, app_state: &mut app::App, draw_loc: Rect,
) {
// Effectively a paragraph with a ton of spacing
// TODO: [MODULARITY] This is hard coded. Gross.
let (left_table, right_table) = if app_state.current_widget_selected.is_widget_table() {
match app_state.current_widget_selected {
WidgetPosition::Process | WidgetPosition::ProcessSearch => {
(WidgetPosition::Temp, WidgetPosition::Disk)
}
WidgetPosition::Disk => (WidgetPosition::Process, WidgetPosition::Temp),
WidgetPosition::Temp => (WidgetPosition::Disk, WidgetPosition::Process),
_ => (WidgetPosition::Disk, WidgetPosition::Temp),
}
} else {
match app_state.previous_basic_table_selected {
WidgetPosition::Process | WidgetPosition::ProcessSearch => {
(WidgetPosition::Temp, WidgetPosition::Disk)
}
WidgetPosition::Disk => (WidgetPosition::Process, WidgetPosition::Temp),
WidgetPosition::Temp => (WidgetPosition::Disk, WidgetPosition::Process),
_ => (WidgetPosition::Disk, WidgetPosition::Temp),
}
};
let left_name = left_table.get_pretty_name();
let right_name = right_table.get_pretty_name();
let num_spaces = max(
0,
draw_loc.width as i64 - 2 - 4 - (left_name.len() + right_name.len()) as i64,
) as usize;
let arrow_text = vec![
Text::Styled(
format!("\n{}", right_name).into(),
self.colours.text_style,
),
Text::Raw(" ".repeat(num_spaces).into()),
Text::Styled(format!("{}", left_name).into(), self.colours.text_style),
];
let margined_draw_loc = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref())
.horizontal_margin(1)
.split(draw_loc);
Paragraph::new(arrow_text.iter())
.block(Block::default())
.render(f, margined_draw_loc[0]);
}
} }

View File

@ -15,8 +15,8 @@ pub struct CanvasColours {
pub swap_style: Style, pub swap_style: Style,
pub rx_style: Style, pub rx_style: Style,
pub tx_style: Style, pub tx_style: Style,
pub rx_total_style: Style, pub total_rx_style: Style,
pub tx_total_style: Style, pub total_tx_style: Style,
pub avg_colour_style: Style, pub avg_colour_style: Style,
pub cpu_colour_styles: Vec<Style>, pub cpu_colour_styles: Vec<Style>,
pub border_style: Style, pub border_style: Style,
@ -39,8 +39,8 @@ impl Default for CanvasColours {
swap_style: Style::default().fg(STANDARD_SECOND_COLOUR), swap_style: Style::default().fg(STANDARD_SECOND_COLOUR),
rx_style: Style::default().fg(STANDARD_FIRST_COLOUR), rx_style: Style::default().fg(STANDARD_FIRST_COLOUR),
tx_style: Style::default().fg(STANDARD_SECOND_COLOUR), tx_style: Style::default().fg(STANDARD_SECOND_COLOUR),
rx_total_style: Style::default().fg(STANDARD_THIRD_COLOUR), total_rx_style: Style::default().fg(STANDARD_THIRD_COLOUR),
tx_total_style: Style::default().fg(STANDARD_FOURTH_COLOUR), total_tx_style: Style::default().fg(STANDARD_FOURTH_COLOUR),
avg_colour_style: Style::default().fg(AVG_COLOUR), avg_colour_style: Style::default().fg(AVG_COLOUR),
cpu_colour_styles: Vec::new(), cpu_colour_styles: Vec::new(),
border_style: Style::default().fg(text_colour), border_style: Style::default().fg(text_colour),
@ -94,12 +94,12 @@ impl CanvasColours {
} }
pub fn set_rx_total_colour(&mut self, colour: &str) -> error::Result<()> { pub fn set_rx_total_colour(&mut self, colour: &str) -> error::Result<()> {
self.rx_total_style = get_style_from_config(colour)?; self.total_rx_style = get_style_from_config(colour)?;
Ok(()) Ok(())
} }
pub fn set_tx_total_colour(&mut self, colour: &str) -> error::Result<()> { pub fn set_tx_total_colour(&mut self, colour: &str) -> error::Result<()> {
self.tx_total_style = get_style_from_config(colour)?; self.total_tx_style = get_style_from_config(colour)?;
Ok(()) Ok(())
} }

View File

@ -4,7 +4,8 @@ use tui::style::{Color, Style};
use crate::utils::{error, gen_util::*}; use crate::utils::{error, gen_util::*};
const GOLDEN_RATIO: f32 = 0.618_034; // Approx, good enough for use (also Clippy gets mad if it's too long) const GOLDEN_RATIO: f32 = 0.618_034;
// Approx, good enough for use (also Clippy gets mad if it's too long)
pub const STANDARD_FIRST_COLOUR: Color = Color::LightMagenta; pub const STANDARD_FIRST_COLOUR: Color = Color::LightMagenta;
pub const STANDARD_SECOND_COLOUR: Color = Color::LightYellow; pub const STANDARD_SECOND_COLOUR: Color = Color::LightYellow;
pub const STANDARD_THIRD_COLOUR: Color = Color::LightCyan; pub const STANDARD_THIRD_COLOUR: Color = Color::LightCyan;

View File

@ -54,7 +54,7 @@ pub fn get_variable_intrinsic_widths(
} }
// Simple redistribution tactic - if there's any space left, split it evenly amongst all members // Simple redistribution tactic - if there's any space left, split it evenly amongst all members
if last_index < num_widths { if last_index < num_widths && last_index != 0 {
let for_all_widths = (remaining_width / last_index as i32) as u16; let for_all_widths = (remaining_width / last_index as i32) as u16;
let mut remainder = remaining_width % last_index as i32; let mut remainder = remaining_width % last_index as i32;
@ -150,3 +150,12 @@ pub fn get_start_position(
} }
} }
} }
/// Calculate how many bars are to be
/// drawn within basic mode's components.
pub fn calculate_basic_use_bars(use_percentage: f64, num_bars_available: usize) -> usize {
std::cmp::min(
(num_bars_available as f64 * use_percentage / 100.0).round() as usize,
num_bars_available,
)
}

View File

@ -1,6 +1,8 @@
pub const STALE_MAX_MILLISECONDS: u128 = 60 * 1000; // How long to store data. pub const STALE_MAX_MILLISECONDS: u128 = 60 * 1000;
// How long to store data.
pub const TIME_STARTS_FROM: u64 = 60 * 1000; pub const TIME_STARTS_FROM: u64 = 60 * 1000;
pub const TICK_RATE_IN_MILLISECONDS: u64 = 200; // How fast the screen refreshes pub const TICK_RATE_IN_MILLISECONDS: u64 = 200;
// How fast the screen refreshes
pub const DEFAULT_REFRESH_RATE_IN_MILLISECONDS: u128 = 1000; pub const DEFAULT_REFRESH_RATE_IN_MILLISECONDS: u128 = 1000;
pub const MAX_KEY_TIMEOUT_IN_MILLISECONDS: u128 = 1000; pub const MAX_KEY_TIMEOUT_IN_MILLISECONDS: u128 = 1000;
pub const NUM_COLOURS: i32 = 256; pub const NUM_COLOURS: i32 = 256;

View File

@ -37,6 +37,7 @@ pub struct ConvertedProcessData {
#[derive(Clone, Default, Debug)] #[derive(Clone, Default, Debug)]
pub struct ConvertedCpuData { pub struct ConvertedCpuData {
pub cpu_name: String, pub cpu_name: String,
/// Tuple is time, value
pub cpu_data: Vec<(f64, f64)>, pub cpu_data: Vec<(f64, f64)>,
} }
@ -195,7 +196,8 @@ pub fn convert_mem_labels(current_data: &data_farmer::DataCollection) -> (String
(current_data.memory_harvest.mem_used_in_mb as f64 * 100.0 (current_data.memory_harvest.mem_used_in_mb as f64 * 100.0
/ current_data.memory_harvest.mem_total_in_mb as f64) / current_data.memory_harvest.mem_total_in_mb as f64)
.round() .round()
) + &format!( )
+ &format!(
" {:.1}GB/{:.1}GB", " {:.1}GB/{:.1}GB",
current_data.memory_harvest.mem_used_in_mb as f64 / 1024.0, current_data.memory_harvest.mem_used_in_mb as f64 / 1024.0,
(current_data.memory_harvest.mem_total_in_mb as f64 / 1024.0).round() (current_data.memory_harvest.mem_total_in_mb as f64 / 1024.0).round()
@ -211,7 +213,8 @@ pub fn convert_mem_labels(current_data: &data_farmer::DataCollection) -> (String
(current_data.swap_harvest.mem_used_in_mb as f64 * 100.0 (current_data.swap_harvest.mem_used_in_mb as f64 * 100.0
/ current_data.swap_harvest.mem_total_in_mb as f64) / current_data.swap_harvest.mem_total_in_mb as f64)
.round() .round()
) + &format!( )
+ &format!(
" {:.1}GB/{:.1}GB", " {:.1}GB/{:.1}GB",
current_data.swap_harvest.mem_used_in_mb as f64 / 1024.0, current_data.swap_harvest.mem_used_in_mb as f64 / 1024.0,
(current_data.swap_harvest.mem_total_in_mb as f64 / 1024.0).round() (current_data.swap_harvest.mem_total_in_mb as f64 / 1024.0).round()

View File

@ -20,14 +20,13 @@ use std::{
use crossterm::{ use crossterm::{
event::{ event::{
poll, read, DisableMouseCapture, EnableMouseCapture, Event as CEvent, KeyCode, KeyEvent, poll, read, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent,
KeyModifiers, MouseEvent, KeyModifiers, MouseEvent,
}, },
execute, execute,
style::Print, style::Print,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
}; };
use serde::Deserialize;
use tui::{backend::CrosstermBackend, Terminal}; use tui::{backend::CrosstermBackend, Terminal};
use app::{ use app::{
@ -36,19 +35,24 @@ use app::{
}; };
use constants::*; use constants::*;
use data_conversion::*; use data_conversion::*;
use options::*;
use utils::error::{self, BottomError}; use utils::error::{self, BottomError};
pub mod app; pub mod app;
mod utils { mod utils {
pub mod error; pub mod error;
pub mod gen_util; pub mod gen_util;
pub mod logging; pub mod logging;
} }
mod canvas; mod canvas;
mod constants; mod constants;
mod data_conversion; mod data_conversion;
enum Event<I, J> { pub mod options;
enum BottomEvent<I, J> {
KeyInput(I), KeyInput(I),
MouseInput(J), MouseInput(J),
Update(Box<data_harvester::Data>), Update(Box<data_harvester::Data>),
@ -59,49 +63,6 @@ enum ResetEvent {
Reset, Reset,
} }
#[derive(Default, Deserialize)]
struct Config {
flags: Option<ConfigFlags>,
colors: Option<ConfigColours>,
}
#[derive(Default, Deserialize)]
struct ConfigFlags {
avg_cpu: Option<bool>,
dot_marker: Option<bool>,
temperature_type: Option<String>,
rate: Option<u64>,
left_legend: Option<bool>,
current_usage: Option<bool>,
group_processes: Option<bool>,
case_sensitive: Option<bool>,
whole_word: Option<bool>,
regex: Option<bool>,
default_widget: Option<String>,
show_disabled_data: Option<bool>,
//disabled_cpu_cores: Option<Vec<u64>>, // TODO: [FEATURE] Enable disabling cores in config/flags
}
#[derive(Default, Deserialize)]
struct ConfigColours {
table_header_color: Option<String>,
avg_cpu_color: Option<String>,
cpu_core_colors: Option<Vec<String>>,
ram_color: Option<String>,
swap_color: Option<String>,
rx_color: Option<String>,
tx_color: Option<String>,
rx_total_color: Option<String>,
tx_total_color: Option<String>,
border_color: Option<String>,
highlighted_border_color: Option<String>,
text_color: Option<String>,
selected_text_color: Option<String>,
selected_bg_color: Option<String>,
widget_title_color: Option<String>,
graph_color: Option<String>,
}
fn get_matches() -> clap::ArgMatches<'static> { fn get_matches() -> clap::ArgMatches<'static> {
clap_app!(app => clap_app!(app =>
(name: crate_name!()) (name: crate_name!())
@ -118,8 +79,8 @@ fn get_matches() -> clap::ArgMatches<'static> {
(@arg RATE_MILLIS: -r --rate +takes_value "Sets a refresh rate in milliseconds; the minimum is 250ms, defaults to 1000ms. Smaller values may take more resources.") (@arg RATE_MILLIS: -r --rate +takes_value "Sets a refresh rate in milliseconds; the minimum is 250ms, defaults to 1000ms. Smaller values may take more resources.")
(@arg LEFT_LEGEND: -l --left_legend "Puts external chart legends on the left side rather than the default right side.") (@arg LEFT_LEGEND: -l --left_legend "Puts external chart legends on the left side rather than the default right side.")
(@arg USE_CURR_USAGE: -u --current_usage "Within Linux, sets a process' CPU usage to be based on the total current CPU usage, rather than assuming 100% usage.") (@arg USE_CURR_USAGE: -u --current_usage "Within Linux, sets a process' CPU usage to be based on the total current CPU usage, rather than assuming 100% usage.")
(@arg CONFIG_LOCATION: -C --config +takes_value "Sets the location of the config file. Expects a config file in the TOML format. If doesn't exist, creates one.") (@arg CONFIG_LOCATION: -C --config +takes_value "Sets the location of the config file. Expects a config file in the TOML format. If it doesn't exist, one is created.")
//(@arg BASIC_MODE: -b --basic "Sets bottom to basic mode, not showing graphs and only showing basic tables.") // TODO: [FEATURE] Min mode (@arg BASIC_MODE: -b --basic "Hides graphs and uses a more basic look")
(@arg GROUP_PROCESSES: -g --group "Groups processes with the same name together on launch.") (@arg GROUP_PROCESSES: -g --group "Groups processes with the same name together on launch.")
(@arg CASE_SENSITIVE: -S --case_sensitive "Match case when searching by default.") (@arg CASE_SENSITIVE: -S --case_sensitive "Match case when searching by default.")
(@arg WHOLE_WORD: -W --whole_word "Match whole word when searching by default.") (@arg WHOLE_WORD: -W --whole_word "Match whole word when searching by default.")
@ -156,6 +117,7 @@ fn main() -> error::Result<()> {
let use_current_cpu_total = get_use_current_cpu_total_option(&matches, &config); let use_current_cpu_total = get_use_current_cpu_total_option(&matches, &config);
let current_widget_selected = get_default_widget(&matches, &config); let current_widget_selected = get_default_widget(&matches, &config);
let show_disabled_data = get_show_disabled_data_option(&matches, &config); let show_disabled_data = get_show_disabled_data_option(&matches, &config);
let use_basic_mode = get_use_basic_mode_option(&matches, &config);
// Create "app" struct, which will control most of the program and store settings/state // Create "app" struct, which will control most of the program and store settings/state
let mut app = App::new( let mut app = App::new(
@ -167,6 +129,7 @@ fn main() -> error::Result<()> {
use_current_cpu_total, use_current_cpu_total,
current_widget_selected, current_widget_selected,
show_disabled_data, show_disabled_data,
use_basic_mode,
); );
enable_app_grouping(&matches, &config, &mut app); enable_app_grouping(&matches, &config, &mut app);
@ -196,7 +159,7 @@ fn main() -> error::Result<()> {
thread::sleep(Duration::from_millis( thread::sleep(Duration::from_millis(
constants::STALE_MAX_MILLISECONDS as u64 + 5000, constants::STALE_MAX_MILLISECONDS as u64 + 5000,
)); ));
tx.send(Event::Clean).unwrap(); tx.send(BottomEvent::Clean).unwrap();
}); });
} }
// Event loop // Event loop
@ -221,7 +184,7 @@ fn main() -> error::Result<()> {
loop { loop {
if let Ok(recv) = rx.recv_timeout(Duration::from_millis(TICK_RATE_IN_MILLISECONDS)) { if let Ok(recv) = rx.recv_timeout(Duration::from_millis(TICK_RATE_IN_MILLISECONDS)) {
match recv { match recv {
Event::KeyInput(event) => { BottomEvent::KeyInput(event) => {
if handle_key_event_or_break(event, &mut app, &rtx) { if handle_key_event_or_break(event, &mut app, &rtx) {
break; break;
} }
@ -231,8 +194,8 @@ fn main() -> error::Result<()> {
app.update_process_gui = false; app.update_process_gui = false;
} }
} }
Event::MouseInput(event) => handle_mouse_event(event, &mut app), BottomEvent::MouseInput(event) => handle_mouse_event(event, &mut app),
Event::Update(data) => { BottomEvent::Update(data) => {
app.data_collection.eat_data(&data); app.data_collection.eat_data(&data);
if !app.is_frozen { if !app.is_frozen {
@ -282,7 +245,7 @@ fn main() -> error::Result<()> {
update_final_process_list(&mut app); update_final_process_list(&mut app);
} }
} }
Event::Clean => { BottomEvent::Clean => {
app.data_collection app.data_collection
.clean_data(constants::STALE_MAX_MILLISECONDS); .clean_data(constants::STALE_MAX_MILLISECONDS);
} }
@ -397,6 +360,10 @@ fn handle_key_event_or_break(
KeyCode::Char('u') => app.clear_search(), KeyCode::Char('u') => app.clear_search(),
KeyCode::Char('a') => app.skip_cursor_beginning(), KeyCode::Char('a') => app.skip_cursor_beginning(),
KeyCode::Char('e') => app.skip_cursor_end(), KeyCode::Char('e') => app.skip_cursor_end(),
// KeyCode::Char('j') => {}, // Move down
// KeyCode::Char('k') => {}, // Move up
// KeyCode::Char('h') => {}, // Move right
// KeyCode::Char('l') => {}, // Move left
// Can't do now, CTRL+BACKSPACE doesn't work and graphemes // Can't do now, CTRL+BACKSPACE doesn't work and graphemes
// are hard to iter while truncating last (eloquently). // are hard to iter while truncating last (eloquently).
// KeyCode::Backspace => app.skip_word_backspace(), // KeyCode::Backspace => app.skip_word_backspace(),
@ -498,23 +465,15 @@ fn get_temperature_option(
if let Some(temp_type) = &flags.temperature_type { if let Some(temp_type) = &flags.temperature_type {
// Give lowest priority to config. // Give lowest priority to config.
return match temp_type.as_str() { return match temp_type.as_str() {
"fahrenheit" | "f" => { "fahrenheit" | "f" => Ok(data_harvester::temperature::TemperatureType::Fahrenheit),
Ok(data_harvester::temperature::TemperatureType::Fahrenheit) "kelvin" | "k" => Ok(data_harvester::temperature::TemperatureType::Kelvin),
} "celsius" | "c" => Ok(data_harvester::temperature::TemperatureType::Celsius),
"kelvin" | "k" => { _ => Err(BottomError::ConfigError(
Ok(data_harvester::temperature::TemperatureType::Kelvin)
}
"celsius" | "c" => {
Ok(data_harvester::temperature::TemperatureType::Celsius)
}
_ => {
Err(BottomError::ConfigError(
"Invalid temperature type. Please have the value be of the form \ "Invalid temperature type. Please have the value be of the form \
<kelvin|k|celsius|c|fahrenheit|f>" <kelvin|k|celsius|c|fahrenheit|f>"
.to_string(), .to_string(),
)) )),
} };
}
} }
} }
Ok(data_harvester::temperature::TemperatureType::Celsius) Ok(data_harvester::temperature::TemperatureType::Celsius)
@ -900,7 +859,9 @@ fn sort_process_data(to_sort_vec: &mut Vec<ConvertedProcessData>, app: &App) {
} }
fn create_input_thread( fn create_input_thread(
tx: std::sync::mpsc::Sender<Event<crossterm::event::KeyEvent, crossterm::event::MouseEvent>>, tx: std::sync::mpsc::Sender<
BottomEvent<crossterm::event::KeyEvent, crossterm::event::MouseEvent>,
>,
) { ) {
thread::spawn(move || loop { thread::spawn(move || loop {
if poll(Duration::from_millis(20)).is_ok() { if poll(Duration::from_millis(20)).is_ok() {
@ -910,16 +871,16 @@ fn create_input_thread(
loop { loop {
if poll(Duration::from_millis(20)).is_ok() { if poll(Duration::from_millis(20)).is_ok() {
if let Ok(event) = read() { if let Ok(event) = read() {
if let CEvent::Key(key) = event { if let Event::Key(key) = event {
if Instant::now().duration_since(keyboard_timer).as_millis() >= 20 { if Instant::now().duration_since(keyboard_timer).as_millis() >= 20 {
if tx.send(Event::KeyInput(key)).is_err() { if tx.send(BottomEvent::KeyInput(key)).is_err() {
return; return;
} }
keyboard_timer = Instant::now(); keyboard_timer = Instant::now();
} }
} else if let CEvent::Mouse(mouse) = event { } else if let Event::Mouse(mouse) = event {
if Instant::now().duration_since(mouse_timer).as_millis() >= 20 { if Instant::now().duration_since(mouse_timer).as_millis() >= 20 {
if tx.send(Event::MouseInput(mouse)).is_err() { if tx.send(BottomEvent::MouseInput(mouse)).is_err() {
return; return;
} }
mouse_timer = Instant::now(); mouse_timer = Instant::now();
@ -933,7 +894,9 @@ fn create_input_thread(
} }
fn create_event_thread( fn create_event_thread(
tx: std::sync::mpsc::Sender<Event<crossterm::event::KeyEvent, crossterm::event::MouseEvent>>, tx: std::sync::mpsc::Sender<
BottomEvent<crossterm::event::KeyEvent, crossterm::event::MouseEvent>,
>,
rrx: std::sync::mpsc::Receiver<ResetEvent>, use_current_cpu_total: bool, rrx: std::sync::mpsc::Receiver<ResetEvent>, use_current_cpu_total: bool,
update_rate_in_milliseconds: u64, temp_type: data_harvester::temperature::TemperatureType, update_rate_in_milliseconds: u64, temp_type: data_harvester::temperature::TemperatureType,
) { ) {
@ -952,7 +915,7 @@ fn create_event_thread(
} }
} }
futures::executor::block_on(data_state.update_data()); futures::executor::block_on(data_state.update_data());
let event = Event::Update(Box::from(data_state.data)); let event = BottomEvent::Update(Box::from(data_state.data));
data_state.data = data_harvester::Data::default(); data_state.data = data_harvester::Data::default();
tx.send(event).unwrap(); tx.send(event).unwrap();
thread::sleep(Duration::from_millis(update_rate_in_milliseconds)); thread::sleep(Duration::from_millis(update_rate_in_milliseconds));

267
src/options.rs Normal file
View File

@ -0,0 +1,267 @@
use serde::Deserialize;
use crate::{
app::{data_harvester, App, WidgetPosition},
constants::*,
utils::error::{self, BottomError},
};
#[derive(Default, Deserialize)]
pub struct Config {
pub flags: Option<ConfigFlags>,
pub colors: Option<ConfigColours>,
}
#[derive(Default, Deserialize)]
pub struct ConfigFlags {
pub avg_cpu: Option<bool>,
pub dot_marker: Option<bool>,
pub temperature_type: Option<String>,
pub rate: Option<u64>,
pub left_legend: Option<bool>,
pub current_usage: Option<bool>,
pub group_processes: Option<bool>,
pub case_sensitive: Option<bool>,
pub whole_word: Option<bool>,
pub regex: Option<bool>,
pub default_widget: Option<String>,
pub show_disabled_data: Option<bool>,
pub basic: Option<bool>,
//disabled_cpu_cores: Option<Vec<u64>>, // TODO: [FEATURE] Enable disabling cores in config/flags
}
#[derive(Default, Deserialize)]
pub struct ConfigColours {
pub table_header_color: Option<String>,
pub avg_cpu_color: Option<String>,
pub cpu_core_colors: Option<Vec<String>>,
pub ram_color: Option<String>,
pub swap_color: Option<String>,
pub rx_color: Option<String>,
pub tx_color: Option<String>,
pub rx_total_color: Option<String>,
pub tx_total_color: Option<String>,
pub border_color: Option<String>,
pub highlighted_border_color: Option<String>,
pub text_color: Option<String>,
pub selected_text_color: Option<String>,
pub selected_bg_color: Option<String>,
pub widget_title_color: Option<String>,
pub graph_color: Option<String>,
}
pub fn get_update_rate_in_milliseconds(
update_rate: &Option<&str>, config: &Config,
) -> error::Result<u128> {
let update_rate_in_milliseconds = if let Some(update_rate) = update_rate {
update_rate.parse::<u128>()?
} else if let Some(flags) = &config.flags {
if let Some(rate) = flags.rate {
rate as u128
} else {
DEFAULT_REFRESH_RATE_IN_MILLISECONDS
}
} else {
DEFAULT_REFRESH_RATE_IN_MILLISECONDS
};
if update_rate_in_milliseconds < 250 {
return Err(BottomError::InvalidArg(
"Please set your update rate to be greater than 250 milliseconds.".to_string(),
));
} else if update_rate_in_milliseconds > u128::from(std::u64::MAX) {
return Err(BottomError::InvalidArg(
"Please set your update rate to be less than unsigned INT_MAX.".to_string(),
));
}
Ok(update_rate_in_milliseconds)
}
pub fn get_temperature_option(
matches: &clap::ArgMatches<'static>, config: &Config,
) -> error::Result<data_harvester::temperature::TemperatureType> {
if matches.is_present("FAHRENHEIT") {
return Ok(data_harvester::temperature::TemperatureType::Fahrenheit);
} else if matches.is_present("KELVIN") {
return Ok(data_harvester::temperature::TemperatureType::Kelvin);
} else if matches.is_present("CELSIUS") {
return Ok(data_harvester::temperature::TemperatureType::Celsius);
} else if let Some(flags) = &config.flags {
if let Some(temp_type) = &flags.temperature_type {
// Give lowest priority to config.
return match temp_type.as_str() {
"fahrenheit" | "f" => {
Ok(data_harvester::temperature::TemperatureType::Fahrenheit)
}
"kelvin" | "k" => {
Ok(data_harvester::temperature::TemperatureType::Kelvin)
}
"celsius" | "c" => {
Ok(data_harvester::temperature::TemperatureType::Celsius)
}
_ => {
Err(BottomError::ConfigError(
"Invalid temperature type. Please have the value be of the form <kelvin|k|celsius|c|fahrenheit|f>".to_string()
))
}
};
}
}
Ok(data_harvester::temperature::TemperatureType::Celsius)
}
pub fn get_avg_cpu_option(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
if matches.is_present("AVG_CPU") {
return true;
} else if let Some(flags) = &config.flags {
if let Some(avg_cpu) = flags.avg_cpu {
return avg_cpu;
}
}
false
}
pub fn get_use_dot_option(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
if matches.is_present("DOT_MARKER") {
return true;
} else if let Some(flags) = &config.flags {
if let Some(dot_marker) = flags.dot_marker {
return dot_marker;
}
}
false
}
pub fn get_use_left_legend_option(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
if matches.is_present("LEFT_LEGEND") {
return true;
} else if let Some(flags) = &config.flags {
if let Some(left_legend) = flags.left_legend {
return left_legend;
}
}
false
}
pub fn get_use_current_cpu_total_option(
matches: &clap::ArgMatches<'static>, config: &Config,
) -> bool {
if matches.is_present("USE_CURR_USAGE") {
return true;
} else if let Some(flags) = &config.flags {
if let Some(current_usage) = flags.current_usage {
return current_usage;
}
}
false
}
pub fn get_show_disabled_data_option(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
if matches.is_present("SHOW_DISABLED_DATA") {
return true;
} else if let Some(flags) = &config.flags {
if let Some(show_disabled_data) = flags.show_disabled_data {
return show_disabled_data;
}
}
false
}
pub fn get_use_basic_mode_option(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
if matches.is_present("BASIC_MODE") {
return true;
} else if let Some(flags) = &config.flags {
if let Some(basic) = flags.basic {
return basic;
}
}
false
}
pub fn enable_app_grouping(matches: &clap::ArgMatches<'static>, config: &Config, app: &mut App) {
if matches.is_present("GROUP_PROCESSES") {
app.toggle_grouping();
} else if let Some(flags) = &config.flags {
if let Some(grouping) = flags.group_processes {
if grouping {
app.toggle_grouping();
}
}
}
}
pub fn enable_app_case_sensitive(
matches: &clap::ArgMatches<'static>, config: &Config, app: &mut App,
) {
if matches.is_present("CASE_SENSITIVE") {
app.process_search_state.search_toggle_ignore_case();
} else if let Some(flags) = &config.flags {
if let Some(case_sensitive) = flags.case_sensitive {
if case_sensitive {
app.process_search_state.search_toggle_ignore_case();
}
}
}
}
pub fn enable_app_match_whole_word(
matches: &clap::ArgMatches<'static>, config: &Config, app: &mut App,
) {
if matches.is_present("WHOLE_WORD") {
app.process_search_state.search_toggle_whole_word();
} else if let Some(flags) = &config.flags {
if let Some(whole_word) = flags.whole_word {
if whole_word {
app.process_search_state.search_toggle_whole_word();
}
}
}
}
pub fn enable_app_use_regex(matches: &clap::ArgMatches<'static>, config: &Config, app: &mut App) {
if matches.is_present("REGEX_DEFAULT") {
app.process_search_state.search_toggle_regex();
} else if let Some(flags) = &config.flags {
if let Some(regex) = flags.regex {
if regex {
app.process_search_state.search_toggle_regex();
}
}
}
}
pub fn get_default_widget(matches: &clap::ArgMatches<'static>, config: &Config) -> WidgetPosition {
if matches.is_present("CPU_WIDGET") {
return WidgetPosition::Cpu;
} else if matches.is_present("MEM_WIDGET") {
return WidgetPosition::Mem;
} else if matches.is_present("DISK_WIDGET") {
return WidgetPosition::Disk;
} else if matches.is_present("TEMP_WIDGET") {
return WidgetPosition::Temp;
} else if matches.is_present("NET_WIDGET") {
return WidgetPosition::Network;
} else if matches.is_present("PROC_WIDGET") {
return WidgetPosition::Process;
} else if let Some(flags) = &config.flags {
if let Some(default_widget) = &flags.default_widget {
return match default_widget.as_str() {
"cpu_default" => WidgetPosition::Cpu,
"memory_default" => WidgetPosition::Mem,
"processes_default" => WidgetPosition::Process,
"network_default" => WidgetPosition::Network,
"temperature_default" => WidgetPosition::Temp,
"disk_default" => WidgetPosition::Disk,
_ => WidgetPosition::Process,
};
}
}
WidgetPosition::Process
}

View File

@ -5,7 +5,7 @@ use predicates::prelude::*;
// These tests are mostly here just to ensure that invalid results will be caught when passing arguments... // These tests are mostly here just to ensure that invalid results will be caught when passing arguments...
// TODO: [TEST] Allow for release testing. // TODO: [TEST] Allow for release testing. Do this with paths.
//======================RATES======================// //======================RATES======================//
@ -71,3 +71,31 @@ fn test_invalid_rate() -> Result<(), Box<dyn std::error::Error>> {
Ok(()) Ok(())
} }
#[test]
fn test_conflicting_temps() -> Result<(), Box<dyn std::error::Error>> {
Command::new(get_os_binary_loc())
.arg("-c")
.arg("-f")
.assert()
.failure()
.stderr(predicate::str::contains(
"cannot be used with one or more of the other specified arguments",
));
Ok(())
}
#[test]
fn test_conflicting_default_widget() -> Result<(), Box<dyn std::error::Error>> {
Command::new(get_os_binary_loc())
.arg("--cpu_default")
.arg("--disk_default")
.assert()
.failure()
.stderr(predicate::str::contains(
"cannot be used with one or more of the other specified arguments",
));
Ok(())
}