mirror of
https://github.com/ClementTsang/bottom
synced 2024-11-10 14:44:18 +00:00
Added maximizing mode to allow users to zoom into a particular widget. Not 100% done.
This commit is contained in:
parent
908960f783
commit
f0dad8f5bf
7 changed files with 447 additions and 195 deletions
|
@ -22,6 +22,10 @@ Features of bottom include:
|
|||
|
||||
- Temperature widget to monitor detected sensors in your system.
|
||||
|
||||
- Config file support for custom colours and default options.
|
||||
|
||||
- Maximizing of widgets of interest.
|
||||
|
||||
The compatibility of each widget and operating systems are, as of version 0.1.0, as follows:
|
||||
|
||||
| OS | CPU | Memory | Disks | Temperature | Processes | Networks |
|
||||
|
@ -137,10 +141,12 @@ Run using `btm`.
|
|||
|
||||
- `Ctrl/Shift-Arrow` or `H/J/K/L` to navigate between widgets. **Note that on macOS, `Ctrl`-arrow keys conflicts with an existing macOS binding, use `Shift`-arrow key instead.**
|
||||
|
||||
- `Esc` to close a dialog window.
|
||||
- `Esc` to close a dialog window or exit maximized mode.
|
||||
|
||||
- `?` to get a help screen explaining the controls. Note all controls except `Esc` to close the dialog will be disabled while this is open.
|
||||
|
||||
- `Enter` on a widget to maximize the widget.
|
||||
|
||||
#### Scrollable Tables
|
||||
|
||||
- `Up` or `k` and `Down` or `j` scrolls through the list if the widget is a table (Temperature, Disks, Processes).
|
||||
|
|
409
src/app.rs
409
src/app.rs
|
@ -33,17 +33,31 @@ lazy_static! {
|
|||
regex::Regex::new(".*");
|
||||
}
|
||||
|
||||
/// AppConfigFields is meant to cover basic fields that would normally be set
|
||||
/// by config files or launch options. Don't need to be mutable (set and forget).
|
||||
pub struct AppConfigFields {
|
||||
pub update_rate_in_milliseconds: u64,
|
||||
pub temperature_type: temperature::TemperatureType,
|
||||
pub use_dot: bool,
|
||||
/// AppScrollWidgetState deals with fields for a scrollable app's current state.
|
||||
#[derive(Default)]
|
||||
pub struct AppScrollWidgetState {
|
||||
pub current_scroll_position: u64,
|
||||
pub previous_scroll_position: u64,
|
||||
}
|
||||
|
||||
/// AppScrollWidgetState deals with fields for a scrollable app's current state.
|
||||
pub struct AppScrollWidgetState {
|
||||
pub widget_scroll_position: i64,
|
||||
pub struct AppScrollState {
|
||||
pub scroll_direction: ScrollDirection,
|
||||
pub process_scroll_state: AppScrollWidgetState,
|
||||
pub disk_scroll_state: AppScrollWidgetState,
|
||||
pub temp_scroll_state: AppScrollWidgetState,
|
||||
pub cpu_scroll_state: AppScrollWidgetState,
|
||||
}
|
||||
|
||||
impl Default for AppScrollState {
|
||||
fn default() -> Self {
|
||||
AppScrollState {
|
||||
scroll_direction: ScrollDirection::DOWN,
|
||||
process_scroll_state: AppScrollWidgetState::default(),
|
||||
disk_scroll_state: AppScrollWidgetState::default(),
|
||||
temp_scroll_state: AppScrollWidgetState::default(),
|
||||
cpu_scroll_state: AppScrollWidgetState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// AppSearchState only deals with the search's current settings and state.
|
||||
|
@ -131,35 +145,29 @@ impl Default for AppHelpDialogState {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: [OPT] Group like fields together... this is kinda gross to step through
|
||||
/// AppConfigFields is meant to cover basic fields that would normally be set
|
||||
/// by config files or launch options. Don't need to be mutable (set and forget).
|
||||
pub struct AppConfigFields {
|
||||
pub update_rate_in_milliseconds: u64,
|
||||
pub temperature_type: temperature::TemperatureType,
|
||||
pub use_dot: bool,
|
||||
pub left_legend: bool,
|
||||
pub show_average_cpu: bool,
|
||||
pub use_current_cpu_total: bool,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
// Sorting
|
||||
pub process_sorting_type: processes::ProcessSorting,
|
||||
pub process_sorting_reverse: bool,
|
||||
pub update_process_gui: bool,
|
||||
// Positioning
|
||||
pub scroll_direction: ScrollDirection,
|
||||
pub currently_selected_process_position: u64,
|
||||
pub currently_selected_disk_position: u64,
|
||||
pub currently_selected_temperature_position: u64,
|
||||
pub currently_selected_cpu_table_position: u64,
|
||||
pub previous_disk_position: u64,
|
||||
pub previous_temp_position: u64,
|
||||
pub previous_process_position: u64,
|
||||
pub previous_cpu_table_position: u64,
|
||||
pub temperature_type: temperature::TemperatureType,
|
||||
pub update_rate_in_milliseconds: u64,
|
||||
pub show_average_cpu: bool,
|
||||
pub app_scroll_positions: AppScrollState,
|
||||
pub current_widget_selected: WidgetPosition,
|
||||
pub data: data_harvester::Data,
|
||||
awaiting_second_char: bool,
|
||||
second_char: char,
|
||||
pub use_dot: bool,
|
||||
second_char: Option<char>,
|
||||
pub dd_err: Option<String>,
|
||||
to_delete_process_list: Option<(String, Vec<u32>)>,
|
||||
pub is_frozen: bool,
|
||||
pub left_legend: bool,
|
||||
pub use_current_cpu_total: bool,
|
||||
last_key_press: Instant,
|
||||
pub canvas_data: canvas::DisplayableData,
|
||||
enable_grouping: bool,
|
||||
|
@ -168,6 +176,8 @@ pub struct App {
|
|||
pub search_state: AppSearchState,
|
||||
pub delete_dialog_state: AppDeleteDialogState,
|
||||
pub help_dialog_state: AppHelpDialogState,
|
||||
pub app_config_fields: AppConfigFields,
|
||||
pub is_expanded: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
|
@ -180,28 +190,14 @@ impl App {
|
|||
process_sorting_type: processes::ProcessSorting::CPU,
|
||||
process_sorting_reverse: true,
|
||||
update_process_gui: false,
|
||||
temperature_type,
|
||||
update_rate_in_milliseconds,
|
||||
show_average_cpu,
|
||||
current_widget_selected: WidgetPosition::Process,
|
||||
scroll_direction: ScrollDirection::DOWN,
|
||||
currently_selected_process_position: 0,
|
||||
currently_selected_disk_position: 0,
|
||||
currently_selected_temperature_position: 0,
|
||||
currently_selected_cpu_table_position: 0,
|
||||
previous_process_position: 0,
|
||||
previous_disk_position: 0,
|
||||
previous_temp_position: 0,
|
||||
previous_cpu_table_position: 0,
|
||||
app_scroll_positions: AppScrollState::default(),
|
||||
data: data_harvester::Data::default(),
|
||||
awaiting_second_char: false,
|
||||
second_char: ' ',
|
||||
use_dot,
|
||||
second_char: None,
|
||||
dd_err: None,
|
||||
to_delete_process_list: None,
|
||||
is_frozen: false,
|
||||
left_legend,
|
||||
use_current_cpu_total,
|
||||
last_key_press: Instant::now(),
|
||||
canvas_data: canvas::DisplayableData::default(),
|
||||
enable_grouping: false,
|
||||
|
@ -210,6 +206,15 @@ impl App {
|
|||
search_state: AppSearchState::default(),
|
||||
delete_dialog_state: AppDeleteDialogState::default(),
|
||||
help_dialog_state: AppHelpDialogState::default(),
|
||||
app_config_fields: AppConfigFields {
|
||||
show_average_cpu,
|
||||
temperature_type,
|
||||
use_dot,
|
||||
update_rate_in_milliseconds,
|
||||
left_legend,
|
||||
use_current_cpu_total,
|
||||
},
|
||||
is_expanded: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -239,12 +244,14 @@ impl App {
|
|||
} else if self.enable_searching {
|
||||
self.current_widget_selected = WidgetPosition::Process;
|
||||
self.enable_searching = false;
|
||||
} else if self.is_expanded {
|
||||
self.is_expanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_multi_tap_keys(&mut self) {
|
||||
self.awaiting_second_char = false;
|
||||
self.second_char = ' ';
|
||||
self.second_char = None;
|
||||
}
|
||||
|
||||
fn is_in_dialog(&self) -> bool {
|
||||
|
@ -354,8 +361,12 @@ impl App {
|
|||
|
||||
regex::Regex::new(&final_regex_string)
|
||||
};
|
||||
self.previous_process_position = 0;
|
||||
self.currently_selected_process_position = 0;
|
||||
self.app_scroll_positions
|
||||
.process_scroll_state
|
||||
.previous_scroll_position = 0;
|
||||
self.app_scroll_positions
|
||||
.process_scroll_state
|
||||
.current_scroll_position = 0;
|
||||
}
|
||||
|
||||
pub fn get_cursor_position(&self) -> usize {
|
||||
|
@ -364,22 +375,27 @@ 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 && self.delete_dialog_state.is_on_yes {
|
||||
// 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.is_on_yes = false;
|
||||
if self.delete_dialog_state.is_showing_dd {
|
||||
if self.delete_dialog_state.is_on_yes {
|
||||
// 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.is_on_yes = false;
|
||||
|
||||
// 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;
|
||||
// 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.is_showing_dd = false;
|
||||
}
|
||||
} else {
|
||||
self.delete_dialog_state.is_showing_dd = false;
|
||||
} else if !self.is_in_dialog() {
|
||||
// Pop-out mode.
|
||||
self.is_expanded = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -490,55 +506,79 @@ impl App {
|
|||
}
|
||||
'd' => {
|
||||
if let WidgetPosition::Process = self.current_widget_selected {
|
||||
if self.awaiting_second_char && self.second_char == 'd' {
|
||||
self.awaiting_second_char = false;
|
||||
self.second_char = ' ';
|
||||
let mut is_first_d = true;
|
||||
if let Some(second_char) = self.second_char {
|
||||
if self.awaiting_second_char && second_char == 'd' {
|
||||
is_first_d = false;
|
||||
self.awaiting_second_char = false;
|
||||
self.second_char = None;
|
||||
|
||||
if self.currently_selected_process_position
|
||||
< self.canvas_data.finalized_process_data.len() as u64
|
||||
{
|
||||
let current_process = if self.is_grouped() {
|
||||
let group_pids = &self.canvas_data.finalized_process_data
|
||||
[self.currently_selected_process_position as usize]
|
||||
.group_pids;
|
||||
if self
|
||||
.app_scroll_positions
|
||||
.process_scroll_state
|
||||
.current_scroll_position < self
|
||||
.canvas_data
|
||||
.finalized_process_data
|
||||
.len() as u64
|
||||
{
|
||||
let current_process = if self.is_grouped() {
|
||||
let group_pids = &self
|
||||
.canvas_data
|
||||
.finalized_process_data[self
|
||||
.app_scroll_positions
|
||||
.process_scroll_state
|
||||
.current_scroll_position as usize]
|
||||
.group_pids;
|
||||
|
||||
let mut ret = ("".to_string(), group_pids.clone());
|
||||
let mut ret = ("".to_string(), group_pids.clone());
|
||||
|
||||
for pid in group_pids {
|
||||
if let Some(process) =
|
||||
self.canvas_data.process_data.get(&pid)
|
||||
{
|
||||
ret.0 = process.name.clone();
|
||||
break;
|
||||
for pid in group_pids {
|
||||
if let Some(process) =
|
||||
self.canvas_data.process_data.get(&pid)
|
||||
{
|
||||
ret.0 = process.name.clone();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
} else {
|
||||
let process = self.canvas_data.finalized_process_data
|
||||
[self.currently_selected_process_position as usize]
|
||||
.clone();
|
||||
(process.name.clone(), vec![process.pid])
|
||||
};
|
||||
ret
|
||||
} else {
|
||||
let process = self.canvas_data.finalized_process_data
|
||||
[self
|
||||
.app_scroll_positions
|
||||
.process_scroll_state
|
||||
.current_scroll_position as usize]
|
||||
.clone();
|
||||
(process.name.clone(), vec![process.pid])
|
||||
};
|
||||
|
||||
self.to_delete_process_list = Some(current_process);
|
||||
self.delete_dialog_state.is_showing_dd = true;
|
||||
self.to_delete_process_list = Some(current_process);
|
||||
self.delete_dialog_state.is_showing_dd = true;
|
||||
}
|
||||
|
||||
self.reset_multi_tap_keys();
|
||||
}
|
||||
}
|
||||
|
||||
self.reset_multi_tap_keys();
|
||||
} else {
|
||||
if is_first_d {
|
||||
self.awaiting_second_char = true;
|
||||
self.second_char = 'd';
|
||||
self.second_char = Some('d');
|
||||
}
|
||||
}
|
||||
}
|
||||
'g' => {
|
||||
if self.awaiting_second_char && self.second_char == 'g' {
|
||||
self.awaiting_second_char = false;
|
||||
self.second_char = ' ';
|
||||
self.skip_to_first();
|
||||
} else {
|
||||
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 = 'g';
|
||||
self.second_char = Some('g');
|
||||
}
|
||||
}
|
||||
'G' => self.skip_to_last(),
|
||||
|
@ -558,7 +598,9 @@ impl App {
|
|||
}
|
||||
}
|
||||
self.update_process_gui = true;
|
||||
self.currently_selected_process_position = 0;
|
||||
self.app_scroll_positions
|
||||
.process_scroll_state
|
||||
.current_scroll_position = 0;
|
||||
}
|
||||
'm' => {
|
||||
match self.process_sorting_type {
|
||||
|
@ -571,7 +613,9 @@ impl App {
|
|||
}
|
||||
}
|
||||
self.update_process_gui = true;
|
||||
self.currently_selected_process_position = 0;
|
||||
self.app_scroll_positions
|
||||
.process_scroll_state
|
||||
.current_scroll_position = 0;
|
||||
}
|
||||
'p' => {
|
||||
// Disable if grouping
|
||||
|
@ -586,7 +630,9 @@ impl App {
|
|||
}
|
||||
}
|
||||
self.update_process_gui = true;
|
||||
self.currently_selected_process_position = 0;
|
||||
self.app_scroll_positions
|
||||
.process_scroll_state
|
||||
.current_scroll_position = 0;
|
||||
}
|
||||
}
|
||||
'n' => {
|
||||
|
@ -600,15 +646,20 @@ impl App {
|
|||
}
|
||||
}
|
||||
self.update_process_gui = true;
|
||||
self.currently_selected_process_position = 0;
|
||||
self.app_scroll_positions
|
||||
.process_scroll_state
|
||||
.current_scroll_position = 0;
|
||||
}
|
||||
'?' => {
|
||||
self.help_dialog_state.is_showing_help = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if self.awaiting_second_char && caught_char != self.second_char {
|
||||
self.awaiting_second_char = false;
|
||||
|
||||
if let Some(second_char) = self.second_char {
|
||||
if self.awaiting_second_char && caught_char != second_char {
|
||||
self.awaiting_second_char = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if self.help_dialog_state.is_showing_help {
|
||||
|
@ -648,8 +699,8 @@ impl App {
|
|||
// Network -(up)> MEM, -(right)> PROC
|
||||
// PROC -(up)> Disk, -(down)> PROC_SEARCH, -(left)> Network
|
||||
// PROC_SEARCH -(up)> PROC, -(left)> Network
|
||||
pub fn move_left(&mut self) {
|
||||
if !self.is_in_dialog() {
|
||||
pub fn move_widget_selection_left(&mut self) {
|
||||
if !self.is_in_dialog() && !self.is_expanded {
|
||||
self.current_widget_selected = match self.current_widget_selected {
|
||||
WidgetPosition::Process => WidgetPosition::Network,
|
||||
WidgetPosition::ProcessSearch => WidgetPosition::Network,
|
||||
|
@ -657,23 +708,25 @@ impl App {
|
|||
WidgetPosition::Temp => WidgetPosition::Mem,
|
||||
_ => self.current_widget_selected,
|
||||
};
|
||||
self.reset_multi_tap_keys();
|
||||
}
|
||||
|
||||
self.reset_multi_tap_keys();
|
||||
}
|
||||
|
||||
pub fn move_right(&mut self) {
|
||||
if !self.is_in_dialog() {
|
||||
pub fn move_widget_selection_right(&mut self) {
|
||||
if !self.is_in_dialog() && !self.is_expanded {
|
||||
self.current_widget_selected = match self.current_widget_selected {
|
||||
WidgetPosition::Mem => WidgetPosition::Temp,
|
||||
WidgetPosition::Network => WidgetPosition::Process,
|
||||
_ => self.current_widget_selected,
|
||||
};
|
||||
self.reset_multi_tap_keys();
|
||||
}
|
||||
|
||||
self.reset_multi_tap_keys();
|
||||
}
|
||||
|
||||
pub fn move_up(&mut self) {
|
||||
if !self.is_in_dialog() {
|
||||
pub fn move_widget_selection_up(&mut self) {
|
||||
if !self.is_in_dialog() && !self.is_expanded {
|
||||
self.current_widget_selected = match self.current_widget_selected {
|
||||
WidgetPosition::Mem => WidgetPosition::Cpu,
|
||||
WidgetPosition::Network => WidgetPosition::Mem,
|
||||
|
@ -683,12 +736,18 @@ impl App {
|
|||
WidgetPosition::Disk => WidgetPosition::Temp,
|
||||
_ => self.current_widget_selected,
|
||||
};
|
||||
self.reset_multi_tap_keys();
|
||||
} else if self.is_expanded {
|
||||
self.current_widget_selected = match self.current_widget_selected {
|
||||
WidgetPosition::ProcessSearch => WidgetPosition::Process,
|
||||
_ => self.current_widget_selected,
|
||||
};
|
||||
}
|
||||
|
||||
self.reset_multi_tap_keys();
|
||||
}
|
||||
|
||||
pub fn move_down(&mut self) {
|
||||
if !self.is_in_dialog() {
|
||||
pub fn move_widget_selection_down(&mut self) {
|
||||
if !self.is_in_dialog() && !self.is_expanded {
|
||||
self.current_widget_selected = match self.current_widget_selected {
|
||||
WidgetPosition::Cpu => WidgetPosition::Mem,
|
||||
WidgetPosition::Mem => WidgetPosition::Network,
|
||||
|
@ -703,21 +762,49 @@ impl App {
|
|||
}
|
||||
_ => self.current_widget_selected,
|
||||
};
|
||||
self.reset_multi_tap_keys();
|
||||
} else if self.is_expanded {
|
||||
self.current_widget_selected = match self.current_widget_selected {
|
||||
WidgetPosition::Process => {
|
||||
if self.is_searching() {
|
||||
WidgetPosition::ProcessSearch
|
||||
} else {
|
||||
WidgetPosition::Process
|
||||
}
|
||||
}
|
||||
_ => self.current_widget_selected,
|
||||
};
|
||||
}
|
||||
|
||||
self.reset_multi_tap_keys();
|
||||
}
|
||||
|
||||
pub fn skip_to_first(&mut self) {
|
||||
if !self.is_in_dialog() {
|
||||
match self.current_widget_selected {
|
||||
WidgetPosition::Process => self.currently_selected_process_position = 0,
|
||||
WidgetPosition::Temp => self.currently_selected_temperature_position = 0,
|
||||
WidgetPosition::Disk => self.currently_selected_disk_position = 0,
|
||||
WidgetPosition::Cpu => self.currently_selected_cpu_table_position = 0,
|
||||
WidgetPosition::Process => {
|
||||
self.app_scroll_positions
|
||||
.process_scroll_state
|
||||
.current_scroll_position = 0
|
||||
}
|
||||
WidgetPosition::Temp => {
|
||||
self.app_scroll_positions
|
||||
.temp_scroll_state
|
||||
.current_scroll_position = 0
|
||||
}
|
||||
WidgetPosition::Disk => {
|
||||
self.app_scroll_positions
|
||||
.disk_scroll_state
|
||||
.current_scroll_position = 0
|
||||
}
|
||||
WidgetPosition::Cpu => {
|
||||
self.app_scroll_positions
|
||||
.cpu_scroll_state
|
||||
.current_scroll_position = 0
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
self.scroll_direction = ScrollDirection::UP;
|
||||
self.app_scroll_positions.scroll_direction = ScrollDirection::UP;
|
||||
self.reset_multi_tap_keys();
|
||||
}
|
||||
}
|
||||
|
@ -726,24 +813,28 @@ impl App {
|
|||
if !self.is_in_dialog() {
|
||||
match self.current_widget_selected {
|
||||
WidgetPosition::Process => {
|
||||
self.currently_selected_process_position =
|
||||
self.canvas_data.finalized_process_data.len() as u64 - 1
|
||||
self.app_scroll_positions
|
||||
.process_scroll_state
|
||||
.current_scroll_position = self.canvas_data.finalized_process_data.len() as u64 - 1
|
||||
}
|
||||
WidgetPosition::Temp => {
|
||||
self.currently_selected_temperature_position =
|
||||
self.canvas_data.temp_sensor_data.len() as u64 - 1
|
||||
self.app_scroll_positions
|
||||
.temp_scroll_state
|
||||
.current_scroll_position = self.canvas_data.temp_sensor_data.len() as u64 - 1
|
||||
}
|
||||
WidgetPosition::Disk => {
|
||||
self.currently_selected_disk_position =
|
||||
self.canvas_data.disk_data.len() as u64 - 1
|
||||
self.app_scroll_positions
|
||||
.disk_scroll_state
|
||||
.current_scroll_position = self.canvas_data.disk_data.len() as u64 - 1
|
||||
}
|
||||
WidgetPosition::Cpu => {
|
||||
self.currently_selected_cpu_table_position =
|
||||
self.canvas_data.cpu_data.len() as u64 - 1;
|
||||
self.app_scroll_positions
|
||||
.cpu_scroll_state
|
||||
.current_scroll_position = self.canvas_data.cpu_data.len() as u64 - 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self.scroll_direction = ScrollDirection::DOWN;
|
||||
self.app_scroll_positions.scroll_direction = ScrollDirection::DOWN;
|
||||
self.reset_multi_tap_keys();
|
||||
}
|
||||
}
|
||||
|
@ -757,7 +848,7 @@ impl App {
|
|||
WidgetPosition::Cpu => self.change_cpu_table_position(-1), // TODO: [PO?] Temporary, may change if we add scaling
|
||||
_ => {}
|
||||
}
|
||||
self.scroll_direction = ScrollDirection::UP;
|
||||
self.app_scroll_positions.scroll_direction = ScrollDirection::UP;
|
||||
self.reset_multi_tap_keys();
|
||||
}
|
||||
}
|
||||
|
@ -771,48 +862,70 @@ impl App {
|
|||
WidgetPosition::Cpu => self.change_cpu_table_position(1), // TODO: [PO?] Temporary, may change if we add scaling
|
||||
_ => {}
|
||||
}
|
||||
self.scroll_direction = ScrollDirection::DOWN;
|
||||
self.app_scroll_positions.scroll_direction = ScrollDirection::DOWN;
|
||||
self.reset_multi_tap_keys();
|
||||
}
|
||||
}
|
||||
|
||||
fn change_cpu_table_position(&mut self, num_to_change_by: i64) {
|
||||
if self.currently_selected_cpu_table_position as i64 + num_to_change_by >= 0
|
||||
&& self.currently_selected_cpu_table_position as i64 + num_to_change_by
|
||||
< self.canvas_data.cpu_data.len() as i64
|
||||
let current_posn = self
|
||||
.app_scroll_positions
|
||||
.cpu_scroll_state
|
||||
.current_scroll_position;
|
||||
|
||||
if current_posn as i64 + num_to_change_by >= 0
|
||||
&& current_posn as i64 + num_to_change_by < self.canvas_data.cpu_data.len() as i64
|
||||
{
|
||||
self.currently_selected_cpu_table_position =
|
||||
(self.currently_selected_cpu_table_position as i64 + num_to_change_by) as u64;
|
||||
self.app_scroll_positions
|
||||
.cpu_scroll_state
|
||||
.current_scroll_position = (current_posn as i64 + num_to_change_by) as u64;
|
||||
}
|
||||
}
|
||||
|
||||
fn change_process_position(&mut self, num_to_change_by: i64) {
|
||||
if self.currently_selected_process_position as i64 + num_to_change_by >= 0
|
||||
&& self.currently_selected_process_position as i64 + num_to_change_by
|
||||
let current_posn = self
|
||||
.app_scroll_positions
|
||||
.process_scroll_state
|
||||
.current_scroll_position;
|
||||
|
||||
if current_posn as i64 + num_to_change_by >= 0
|
||||
&& current_posn as i64 + num_to_change_by
|
||||
< self.canvas_data.finalized_process_data.len() as i64
|
||||
{
|
||||
self.currently_selected_process_position =
|
||||
(self.currently_selected_process_position as i64 + num_to_change_by) as u64;
|
||||
self.app_scroll_positions
|
||||
.process_scroll_state
|
||||
.current_scroll_position = (current_posn as i64 + num_to_change_by) as u64;
|
||||
}
|
||||
}
|
||||
|
||||
fn change_temp_position(&mut self, num_to_change_by: i64) {
|
||||
if self.currently_selected_temperature_position as i64 + num_to_change_by >= 0
|
||||
&& self.currently_selected_temperature_position as i64 + num_to_change_by
|
||||
let current_posn = self
|
||||
.app_scroll_positions
|
||||
.temp_scroll_state
|
||||
.current_scroll_position;
|
||||
|
||||
if current_posn as i64 + num_to_change_by >= 0
|
||||
&& current_posn as i64 + num_to_change_by
|
||||
< self.canvas_data.temp_sensor_data.len() as i64
|
||||
{
|
||||
self.currently_selected_temperature_position =
|
||||
(self.currently_selected_temperature_position as i64 + num_to_change_by) as u64;
|
||||
self.app_scroll_positions
|
||||
.temp_scroll_state
|
||||
.current_scroll_position = (current_posn as i64 + num_to_change_by) as u64;
|
||||
}
|
||||
}
|
||||
|
||||
fn change_disk_position(&mut self, num_to_change_by: i64) {
|
||||
if self.currently_selected_disk_position as i64 + num_to_change_by >= 0
|
||||
&& self.currently_selected_disk_position as i64 + num_to_change_by
|
||||
< self.canvas_data.disk_data.len() as i64
|
||||
let current_posn = self
|
||||
.app_scroll_positions
|
||||
.disk_scroll_state
|
||||
.current_scroll_position;
|
||||
|
||||
if current_posn as i64 + num_to_change_by >= 0
|
||||
&& current_posn as i64 + num_to_change_by < self.canvas_data.disk_data.len() as i64
|
||||
{
|
||||
self.currently_selected_disk_position =
|
||||
(self.currently_selected_disk_position as i64 + num_to_change_by) as u64;
|
||||
self.app_scroll_positions
|
||||
.disk_scroll_state
|
||||
.current_scroll_position = (current_posn as i64 + num_to_change_by) as u64;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -115,6 +115,8 @@ impl DataState {
|
|||
|
||||
let current_instant = std::time::Instant::now();
|
||||
|
||||
// TODO: [OPT] MT/Async the harvesting step.
|
||||
|
||||
// Network
|
||||
self.data.network = network::get_network_data(
|
||||
&self.sys,
|
||||
|
|
176
src/canvas.rs
176
src/canvas.rs
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
app::{self, data_harvester::processes::ProcessHarvest},
|
||||
app::{self, data_harvester::processes::ProcessHarvest, WidgetPosition},
|
||||
constants::*,
|
||||
data_conversion::{ConvertedCpuData, ConvertedProcessData},
|
||||
utils::error,
|
||||
|
@ -146,9 +146,9 @@ impl Painter {
|
|||
.margin(1)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(32),
|
||||
Constraint::Percentage(36),
|
||||
Constraint::Percentage(32),
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(40),
|
||||
Constraint::Percentage(30),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
|
@ -319,6 +319,72 @@ impl Painter {
|
|||
// This is a bit nasty, but it works well... I guess.
|
||||
app_state.delete_dialog_state.is_showing_dd = false;
|
||||
}
|
||||
} else if app_state.is_expanded {
|
||||
// TODO: [REF] we should combine this with normal drawing tbh
|
||||
|
||||
let rect = Layout::default()
|
||||
.margin(1)
|
||||
.constraints([Constraint::Percentage(100)].as_ref())
|
||||
.split(f.size());
|
||||
match &app_state.current_widget_selected {
|
||||
WidgetPosition::Cpu => {
|
||||
let cpu_chunk = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.margin(0)
|
||||
.constraints(
|
||||
if app_state.app_config_fields.left_legend {
|
||||
[Constraint::Percentage(15), Constraint::Percentage(85)]
|
||||
} else {
|
||||
[Constraint::Percentage(85), Constraint::Percentage(15)]
|
||||
}
|
||||
.as_ref(),
|
||||
)
|
||||
.split(rect[0]);
|
||||
|
||||
let legend_index = if app_state.app_config_fields.left_legend {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let graph_index = if app_state.app_config_fields.left_legend {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
self.draw_cpu_graph(&mut f, &app_state, cpu_chunk[graph_index]);
|
||||
self.draw_cpu_legend(&mut f, app_state, cpu_chunk[legend_index]);
|
||||
}
|
||||
WidgetPosition::Mem => {
|
||||
self.draw_memory_graph(&mut f, &app_state, rect[0]);
|
||||
}
|
||||
WidgetPosition::Disk => {
|
||||
self.draw_disk_table(&mut f, app_state, rect[0]);
|
||||
}
|
||||
WidgetPosition::Temp => {
|
||||
self.draw_temp_table(&mut f, app_state, rect[0]);
|
||||
}
|
||||
WidgetPosition::Network => {
|
||||
self.draw_network_graph(&mut f, &app_state, rect[0]);
|
||||
}
|
||||
WidgetPosition::Process | WidgetPosition::ProcessSearch => {
|
||||
if app_state.is_searching() {
|
||||
let processes_chunk = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(0)
|
||||
.constraints(
|
||||
[Constraint::Percentage(85), Constraint::Percentage(15)]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(rect[0]);
|
||||
|
||||
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, rect[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO: [TUI] Change this back to a more even 33/33/34 when TUI releases
|
||||
let vertical_chunks = Layout::default()
|
||||
|
@ -357,7 +423,7 @@ impl Painter {
|
|||
.direction(Direction::Horizontal)
|
||||
.margin(0)
|
||||
.constraints(
|
||||
if app_state.left_legend {
|
||||
if app_state.app_config_fields.left_legend {
|
||||
[Constraint::Percentage(15), Constraint::Percentage(85)]
|
||||
} else {
|
||||
[Constraint::Percentage(85), Constraint::Percentage(15)]
|
||||
|
@ -386,8 +452,16 @@ impl Painter {
|
|||
.split(bottom_chunks[0]);
|
||||
|
||||
// Default chunk index based on left or right legend setting
|
||||
let legend_index = if app_state.left_legend { 0 } else { 1 };
|
||||
let graph_index = if app_state.left_legend { 1 } else { 0 };
|
||||
let legend_index = if app_state.app_config_fields.left_legend {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let graph_index = if app_state.app_config_fields.left_legend {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// Set up blocks and their components
|
||||
// CPU graph
|
||||
|
@ -468,7 +542,7 @@ impl Painter {
|
|||
));
|
||||
}
|
||||
|
||||
if app_state.show_average_cpu {
|
||||
if app_state.app_config_fields.show_average_cpu {
|
||||
if let Some(avg_cpu_entry) = cpu_data.first() {
|
||||
cpu_entries_vec.push((
|
||||
self.colours.cpu_colour_styles[0],
|
||||
|
@ -484,7 +558,7 @@ impl Painter {
|
|||
for cpu_entry in &cpu_entries_vec {
|
||||
dataset_vector.push(
|
||||
Dataset::default()
|
||||
.marker(if app_state.use_dot {
|
||||
.marker(if app_state.app_config_fields.use_dot {
|
||||
Marker::Dot
|
||||
} else {
|
||||
Marker::Braille
|
||||
|
@ -519,9 +593,15 @@ impl Painter {
|
|||
let num_rows = max(0, i64::from(draw_loc.height) - 5) as u64;
|
||||
let start_position = get_start_position(
|
||||
num_rows,
|
||||
&(app_state.scroll_direction),
|
||||
&mut app_state.previous_cpu_table_position,
|
||||
app_state.currently_selected_cpu_table_position,
|
||||
&(app_state.app_scroll_positions.scroll_direction),
|
||||
&mut app_state
|
||||
.app_scroll_positions
|
||||
.cpu_scroll_state
|
||||
.previous_scroll_position,
|
||||
app_state
|
||||
.app_scroll_positions
|
||||
.cpu_scroll_state
|
||||
.current_scroll_position,
|
||||
);
|
||||
|
||||
let sliced_cpu_data = &cpu_data[start_position as usize..];
|
||||
|
@ -547,7 +627,10 @@ impl Painter {
|
|||
match app_state.current_widget_selected {
|
||||
app::WidgetPosition::Cpu => {
|
||||
if cpu_row_counter as u64
|
||||
== app_state.currently_selected_cpu_table_position - start_position
|
||||
== app_state
|
||||
.app_scroll_positions
|
||||
.cpu_scroll_state
|
||||
.current_scroll_position - start_position
|
||||
{
|
||||
cpu_row_counter = -1;
|
||||
self.colours.currently_selected_text_style
|
||||
|
@ -612,7 +695,7 @@ impl Painter {
|
|||
|
||||
let mut mem_canvas_vec: Vec<Dataset> = vec![Dataset::default()
|
||||
.name(&app_state.canvas_data.mem_label)
|
||||
.marker(if app_state.use_dot {
|
||||
.marker(if app_state.app_config_fields.use_dot {
|
||||
Marker::Dot
|
||||
} else {
|
||||
Marker::Braille
|
||||
|
@ -624,7 +707,7 @@ impl Painter {
|
|||
mem_canvas_vec.push(
|
||||
Dataset::default()
|
||||
.name(&app_state.canvas_data.swap_label)
|
||||
.marker(if app_state.use_dot {
|
||||
.marker(if app_state.app_config_fields.use_dot {
|
||||
Marker::Dot
|
||||
} else {
|
||||
Marker::Braille
|
||||
|
@ -682,7 +765,7 @@ impl Painter {
|
|||
"RX: {:7}",
|
||||
app_state.canvas_data.rx_display.clone()
|
||||
))
|
||||
.marker(if app_state.use_dot {
|
||||
.marker(if app_state.app_config_fields.use_dot {
|
||||
Marker::Dot
|
||||
} else {
|
||||
Marker::Braille
|
||||
|
@ -694,13 +777,21 @@ impl Painter {
|
|||
"TX: {:7}",
|
||||
app_state.canvas_data.tx_display.clone()
|
||||
))
|
||||
.marker(if app_state.use_dot {
|
||||
.marker(if app_state.app_config_fields.use_dot {
|
||||
Marker::Dot
|
||||
} else {
|
||||
Marker::Braille
|
||||
})
|
||||
.style(self.colours.tx_style)
|
||||
.data(&network_data_tx),
|
||||
Dataset::default().name(&format!(
|
||||
"Total RX: {:7}",
|
||||
app_state.canvas_data.total_rx_display.clone()
|
||||
)),
|
||||
Dataset::default().name(&format!(
|
||||
"Total TX: {:7}",
|
||||
app_state.canvas_data.total_tx_display.clone()
|
||||
)),
|
||||
])
|
||||
.render(f, draw_loc);
|
||||
}
|
||||
|
@ -763,9 +854,15 @@ impl Painter {
|
|||
let num_rows = max(0, i64::from(draw_loc.height) - 5) as u64;
|
||||
let start_position = get_start_position(
|
||||
num_rows,
|
||||
&(app_state.scroll_direction),
|
||||
&mut app_state.previous_temp_position,
|
||||
app_state.currently_selected_temperature_position,
|
||||
&(app_state.app_scroll_positions.scroll_direction),
|
||||
&mut app_state
|
||||
.app_scroll_positions
|
||||
.temp_scroll_state
|
||||
.previous_scroll_position,
|
||||
app_state
|
||||
.app_scroll_positions
|
||||
.temp_scroll_state
|
||||
.current_scroll_position,
|
||||
);
|
||||
|
||||
let sliced_vec = &(temp_sensor_data[start_position as usize..]);
|
||||
|
@ -777,7 +874,10 @@ impl Painter {
|
|||
match app_state.current_widget_selected {
|
||||
app::WidgetPosition::Temp => {
|
||||
if temp_row_counter as u64
|
||||
== app_state.currently_selected_temperature_position - start_position
|
||||
== app_state
|
||||
.app_scroll_positions
|
||||
.temp_scroll_state
|
||||
.current_scroll_position - start_position
|
||||
{
|
||||
temp_row_counter = -1;
|
||||
self.colours.currently_selected_text_style
|
||||
|
@ -829,9 +929,15 @@ impl Painter {
|
|||
let num_rows = max(0, i64::from(draw_loc.height) - 5) as u64;
|
||||
let start_position = get_start_position(
|
||||
num_rows,
|
||||
&(app_state.scroll_direction),
|
||||
&mut app_state.previous_disk_position,
|
||||
app_state.currently_selected_disk_position,
|
||||
&(app_state.app_scroll_positions.scroll_direction),
|
||||
&mut app_state
|
||||
.app_scroll_positions
|
||||
.disk_scroll_state
|
||||
.previous_scroll_position,
|
||||
app_state
|
||||
.app_scroll_positions
|
||||
.disk_scroll_state
|
||||
.current_scroll_position,
|
||||
);
|
||||
|
||||
let sliced_vec = &disk_data[start_position as usize..];
|
||||
|
@ -843,7 +949,10 @@ impl Painter {
|
|||
match app_state.current_widget_selected {
|
||||
app::WidgetPosition::Disk => {
|
||||
if disk_counter as u64
|
||||
== app_state.currently_selected_disk_position - start_position
|
||||
== app_state
|
||||
.app_scroll_positions
|
||||
.disk_scroll_state
|
||||
.current_scroll_position - start_position
|
||||
{
|
||||
disk_counter = -1;
|
||||
self.colours.currently_selected_text_style
|
||||
|
@ -1029,9 +1138,15 @@ impl Painter {
|
|||
|
||||
let position = get_start_position(
|
||||
num_rows,
|
||||
&(app_state.scroll_direction),
|
||||
&mut app_state.previous_process_position,
|
||||
app_state.currently_selected_process_position,
|
||||
&(app_state.app_scroll_positions.scroll_direction),
|
||||
&mut app_state
|
||||
.app_scroll_positions
|
||||
.process_scroll_state
|
||||
.previous_scroll_position,
|
||||
app_state
|
||||
.app_scroll_positions
|
||||
.process_scroll_state
|
||||
.current_scroll_position,
|
||||
);
|
||||
|
||||
// Sanity check
|
||||
|
@ -1061,7 +1176,10 @@ impl Painter {
|
|||
match app_state.current_widget_selected {
|
||||
app::WidgetPosition::Process => {
|
||||
if process_counter as u64
|
||||
== app_state.currently_selected_process_position - start_position
|
||||
== app_state
|
||||
.app_scroll_positions
|
||||
.process_scroll_state
|
||||
.current_scroll_position - start_position
|
||||
{
|
||||
process_counter = -1;
|
||||
self.colours.currently_selected_text_style
|
||||
|
|
|
@ -12,7 +12,7 @@ pub const DEFAULT_UNIX_CONFIG_FILE_PATH: &str = "~/.config/btm/btm.toml";
|
|||
pub const DEFAULT_WINDOWS_CONFIG_FILE_PATH: &str = "";
|
||||
|
||||
// Help text
|
||||
pub const GENERAL_HELP_TEXT: [&str; 15] = [
|
||||
pub const GENERAL_HELP_TEXT: [&str; 16] = [
|
||||
"General Keybindings\n\n",
|
||||
"Esc Close dialog box\n",
|
||||
"q, Ctrl-c Quit bottom\n",
|
||||
|
@ -28,6 +28,7 @@ pub const GENERAL_HELP_TEXT: [&str; 15] = [
|
|||
"? Open the help screen\n",
|
||||
"gg Skip to the first entry of a list\n",
|
||||
"G Skip to the last entry of a list\n",
|
||||
"Enter Maximize the currently selected widget\n",
|
||||
];
|
||||
|
||||
pub const PROCESS_HELP_TEXT: [&str; 8] = [
|
||||
|
|
|
@ -62,7 +62,7 @@ pub fn convert_temp_row(app: &App) -> Vec<Vec<String>> {
|
|||
let mut sensor_vector: Vec<Vec<String>> = Vec::new();
|
||||
|
||||
let current_data = &app.data_collection;
|
||||
let temp_type = &app.temperature_type;
|
||||
let temp_type = &app.app_config_fields.temperature_type;
|
||||
|
||||
if current_data.temp_harvest.is_empty() {
|
||||
sensor_vector.push(vec!["No Sensors Found".to_string(), "".to_string()])
|
||||
|
|
42
src/main.rs
42
src/main.rs
|
@ -182,7 +182,7 @@ fn main() -> error::Result<()> {
|
|||
rrx,
|
||||
use_current_cpu_total,
|
||||
update_rate_in_milliseconds as u64,
|
||||
app.temperature_type.clone(),
|
||||
app.app_config_fields.temperature_type.clone(),
|
||||
);
|
||||
|
||||
let mut painter = canvas::Painter::default();
|
||||
|
@ -214,6 +214,8 @@ fn main() -> error::Result<()> {
|
|||
|
||||
// Convert all data into tui-compliant components
|
||||
|
||||
// TODO: [OPT] MT the conversion step.
|
||||
|
||||
// Network
|
||||
let network_data = convert_network_data_points(&app.data_collection);
|
||||
app.canvas_data.network_data_rx = network_data.rx;
|
||||
|
@ -236,8 +238,10 @@ fn main() -> error::Result<()> {
|
|||
app.canvas_data.swap_label = memory_and_swap_labels.1;
|
||||
|
||||
// CPU
|
||||
app.canvas_data.cpu_data =
|
||||
convert_cpu_data_points(app.show_average_cpu, &app.data_collection);
|
||||
app.canvas_data.cpu_data = convert_cpu_data_points(
|
||||
app.app_config_fields.show_average_cpu,
|
||||
&app.data_collection,
|
||||
);
|
||||
|
||||
// Processes
|
||||
let (single, grouped) = convert_process_data(&app.data_collection);
|
||||
|
@ -294,10 +298,10 @@ fn handle_key_event_or_break(
|
|||
KeyCode::Down => app.on_down_key(),
|
||||
KeyCode::Left => app.on_left_key(),
|
||||
KeyCode::Right => app.on_right_key(),
|
||||
KeyCode::Char('H') => app.move_left(),
|
||||
KeyCode::Char('L') => app.move_right(),
|
||||
KeyCode::Char('K') => app.move_up(),
|
||||
KeyCode::Char('J') => app.move_down(),
|
||||
KeyCode::Char('H') => app.move_widget_selection_left(),
|
||||
KeyCode::Char('L') => app.move_widget_selection_right(),
|
||||
KeyCode::Char('K') => app.move_widget_selection_up(),
|
||||
KeyCode::Char('J') => app.move_widget_selection_down(),
|
||||
KeyCode::Char(character) => app.on_char_key(character),
|
||||
KeyCode::Esc => app.on_esc(),
|
||||
KeyCode::Enter => app.on_enter(),
|
||||
|
@ -314,10 +318,10 @@ fn handle_key_event_or_break(
|
|||
|
||||
match event.code {
|
||||
KeyCode::Char('f') => app.enable_searching(),
|
||||
KeyCode::Left => app.move_left(),
|
||||
KeyCode::Right => app.move_right(),
|
||||
KeyCode::Up => app.move_up(),
|
||||
KeyCode::Down => app.move_down(),
|
||||
KeyCode::Left => app.move_widget_selection_left(),
|
||||
KeyCode::Right => app.move_widget_selection_right(),
|
||||
KeyCode::Up => app.move_widget_selection_up(),
|
||||
KeyCode::Down => app.move_widget_selection_down(),
|
||||
KeyCode::Char('r') => {
|
||||
if rtx.send(ResetEvent::Reset).is_ok() {
|
||||
app.reset();
|
||||
|
@ -329,10 +333,18 @@ fn handle_key_event_or_break(
|
|||
}
|
||||
} else if let KeyModifiers::SHIFT = event.modifiers {
|
||||
match event.code {
|
||||
KeyCode::Left | KeyCode::Char('h') | KeyCode::Char('H') => app.move_left(),
|
||||
KeyCode::Right | KeyCode::Char('l') | KeyCode::Char('L') => app.move_right(),
|
||||
KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => app.move_up(),
|
||||
KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => app.move_down(),
|
||||
KeyCode::Left | KeyCode::Char('h') | KeyCode::Char('H') => {
|
||||
app.move_widget_selection_left()
|
||||
}
|
||||
KeyCode::Right | KeyCode::Char('l') | KeyCode::Char('L') => {
|
||||
app.move_widget_selection_right()
|
||||
}
|
||||
KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => {
|
||||
app.move_widget_selection_up()
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => {
|
||||
app.move_widget_selection_down()
|
||||
}
|
||||
KeyCode::Char('/') | KeyCode::Char('?') => app.on_char_key('?'),
|
||||
_ => {}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue