mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-07-25 22:55:06 +02:00
refactor: more glue code to build layout
Even more glue code to help glue together our layout options to our new layout system! Note that this PR will most likely likely break the two options: - default_widget_type - default_widget_count and as such, they'll probably be deleted in a later commit. As for why, it's since they're kinda a pain to support and don't work well. Users can still enable default widget selection through the layout system (which will also see a revamp in the future).
This commit is contained in:
parent
88ebcab8f2
commit
b5e6dea324
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -231,7 +231,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bottom"
|
||||
version = "0.6.4"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bottom"
|
||||
version = "0.6.4"
|
||||
version = "0.7.0"
|
||||
authors = ["Clement Tsang <cjhtsang@uwaterloo.ca>"]
|
||||
edition = "2018"
|
||||
repository = "https://github.com/ClementTsang/bottom"
|
||||
|
140
src/app.rs
140
src/app.rs
@ -13,6 +13,9 @@ use std::{
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use crossterm::event::{KeyEvent, KeyModifiers};
|
||||
use fxhash::FxHashMap;
|
||||
use indextree::{Arena, NodeId};
|
||||
use unicode_segmentation::GraphemeCursor;
|
||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||
|
||||
@ -29,9 +32,11 @@ use crate::{
|
||||
constants::{self, MAX_SIGNAL},
|
||||
units::data_units::DataUnit,
|
||||
utils::error::{BottomError, Result},
|
||||
Pid,
|
||||
BottomEvent, Pid,
|
||||
};
|
||||
|
||||
use self::event::{does_point_intersect_rect, EventResult, ReturnSignal};
|
||||
|
||||
const MAX_SEARCH_LENGTH: usize = 200;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -40,6 +45,17 @@ pub enum AxisScaling {
|
||||
Linear,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct UsedWidgets {
|
||||
pub use_cpu: bool,
|
||||
pub use_mem: bool,
|
||||
pub use_net: bool,
|
||||
pub use_proc: bool,
|
||||
pub use_disk: bool,
|
||||
pub use_temp: bool,
|
||||
pub use_battery: bool,
|
||||
}
|
||||
|
||||
/// AppConfigFields is meant to cover basic fields that would normally be set
|
||||
/// by config files or launch options.
|
||||
#[derive(Debug)]
|
||||
@ -67,14 +83,9 @@ pub struct AppConfigFields {
|
||||
pub network_use_binary_prefix: bool,
|
||||
}
|
||||
|
||||
// FIXME: Get rid of TypedBuilder here!
|
||||
#[derive(TypedBuilder)]
|
||||
pub struct AppState {
|
||||
#[builder(default = false, setter(skip))]
|
||||
awaiting_second_char: bool,
|
||||
|
||||
#[builder(default, setter(skip))]
|
||||
second_char: Option<char>,
|
||||
|
||||
#[builder(default, setter(skip))]
|
||||
pub dd_err: Option<String>,
|
||||
|
||||
@ -93,12 +104,6 @@ pub struct AppState {
|
||||
#[builder(default, setter(skip))]
|
||||
pub data_collection: DataCollection,
|
||||
|
||||
#[builder(default, setter(skip))]
|
||||
pub delete_dialog_state: AppDeleteDialogState,
|
||||
|
||||
#[builder(default, setter(skip))]
|
||||
pub help_dialog_state: AppHelpDialogState,
|
||||
|
||||
#[builder(default = false, setter(skip))]
|
||||
pub is_expanded: bool,
|
||||
|
||||
@ -115,6 +120,18 @@ pub struct AppState {
|
||||
#[builder(default, setter(skip))]
|
||||
pub user_table: processes::UserTable,
|
||||
|
||||
pub used_widgets: UsedWidgets,
|
||||
pub filters: DataFilters,
|
||||
pub app_config_fields: AppConfigFields,
|
||||
|
||||
// --- Possibly delete? ---
|
||||
#[builder(default, setter(skip))]
|
||||
pub delete_dialog_state: AppDeleteDialogState,
|
||||
|
||||
#[builder(default, setter(skip))]
|
||||
pub help_dialog_state: AppHelpDialogState,
|
||||
|
||||
// --- TO DELETE---
|
||||
pub cpu_state: CpuState,
|
||||
pub mem_state: MemState,
|
||||
pub net_state: NetState,
|
||||
@ -123,14 +140,31 @@ pub struct AppState {
|
||||
pub disk_state: DiskState,
|
||||
pub battery_state: BatteryState,
|
||||
pub basic_table_widget_state: Option<BasicTableWidgetState>,
|
||||
pub app_config_fields: AppConfigFields,
|
||||
pub widget_map: HashMap<u64, BottomWidget>,
|
||||
pub current_widget: BottomWidget,
|
||||
pub used_widgets: UsedWidgets,
|
||||
pub filters: DataFilters,
|
||||
|
||||
#[builder(default = false, setter(skip))]
|
||||
awaiting_second_char: bool,
|
||||
|
||||
#[builder(default, setter(skip))]
|
||||
second_char: Option<char>,
|
||||
|
||||
// --- NEW STUFF ---
|
||||
pub selected_widget: NodeId,
|
||||
pub widget_lookup_map: FxHashMap<NodeId, TmpBottomWidget>,
|
||||
pub layout_tree: Arena<LayoutNode>,
|
||||
pub layout_tree_root: NodeId,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
/// Creates a new [`AppState`].
|
||||
pub fn new(
|
||||
_app_config_fields: AppConfigFields, _filters: DataFilters,
|
||||
_layout_tree_output: LayoutCreationOutput,
|
||||
) -> Self {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
// Reset multi
|
||||
self.reset_multi_tap_keys();
|
||||
@ -176,6 +210,79 @@ impl AppState {
|
||||
self.dd_err = None;
|
||||
}
|
||||
|
||||
/// Handles a global [`KeyEvent`], and returns [`Some(EventResult)`] if the global shortcut is consumed by some global
|
||||
/// shortcut. If not, it returns [`None`].
|
||||
fn handle_global_shortcut(&mut self, event: KeyEvent) -> Option<EventResult> {
|
||||
// TODO: Write this.
|
||||
|
||||
if event.modifiers.is_empty() {
|
||||
todo!()
|
||||
} else if let KeyModifiers::ALT = event.modifiers {
|
||||
todo!()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles a [`BottomEvent`] and updates the [`AppState`] if needed. Returns an [`EventResult`] indicating
|
||||
/// whether the app now requires a redraw.
|
||||
pub fn handle_event(&mut self, event: BottomEvent) -> EventResult {
|
||||
match event {
|
||||
BottomEvent::KeyInput(event) => {
|
||||
if let Some(event_result) = self.handle_global_shortcut(event) {
|
||||
// See if it's caught by a global shortcut first...
|
||||
event_result
|
||||
} else if let Some(widget) = self.widget_lookup_map.get_mut(&self.selected_widget) {
|
||||
// If it isn't, send it to the current widget!
|
||||
widget.handle_key_event(event)
|
||||
} else {
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
BottomEvent::MouseInput(event) => {
|
||||
// Not great, but basically a blind lookup through the table for anything that clips the click location.
|
||||
// TODO: Would be cool to use a kd-tree or something like that in the future.
|
||||
|
||||
let x = event.column;
|
||||
let y = event.row;
|
||||
|
||||
for (id, widget) in self.widget_lookup_map.iter_mut() {
|
||||
if does_point_intersect_rect(x, y, widget.bounds()) {
|
||||
if self.selected_widget == *id {
|
||||
self.selected_widget = *id;
|
||||
return widget.handle_mouse_event(event);
|
||||
} else {
|
||||
// If the aren't equal, *force* a redraw.
|
||||
self.selected_widget = *id;
|
||||
widget.handle_mouse_event(event);
|
||||
return EventResult::Redraw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
BottomEvent::Update(new_data) => {
|
||||
if !self.is_frozen {
|
||||
// TODO: Update all data, and redraw.
|
||||
EventResult::Redraw
|
||||
} else {
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
BottomEvent::Clean => {
|
||||
self.data_collection
|
||||
.clean_data(constants::STALE_MAX_MILLISECONDS);
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles a [`ReturnSignal`], and returns
|
||||
pub fn handle_return_signal(&mut self, return_signal: ReturnSignal) -> EventResult {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn on_esc(&mut self) {
|
||||
self.reset_multi_tap_keys();
|
||||
if self.is_in_dialog() {
|
||||
@ -200,6 +307,7 @@ impl AppState {
|
||||
.process_search_state
|
||||
.search_state
|
||||
.is_enabled = false;
|
||||
|
||||
current_proc_state.is_sort_open = false;
|
||||
self.is_force_redraw = true;
|
||||
return;
|
||||
|
@ -1,10 +0,0 @@
|
||||
//! Data collection for batteries.
|
||||
//!
|
||||
//! For Linux, macOS, Windows, FreeBSD, Dragonfly, and iOS, this is handled by the battery crate.
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "ios"))] {
|
||||
pub mod battery;
|
||||
pub use self::battery::*;
|
||||
}
|
||||
}
|
@ -1,170 +0,0 @@
|
||||
//! CPU stats through heim.
|
||||
//! Supports macOS, Linux, and Windows.
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_os = "linux")] {
|
||||
pub mod linux;
|
||||
pub use linux::*;
|
||||
} else if #[cfg(any(target_os = "macos", target_os = "windows"))] {
|
||||
pub mod windows_macos;
|
||||
pub use windows_macos::*;
|
||||
}
|
||||
}
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_family = "unix")] {
|
||||
pub mod unix;
|
||||
pub use unix::*;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct CpuData {
|
||||
pub cpu_prefix: String,
|
||||
pub cpu_count: Option<usize>,
|
||||
pub cpu_usage: f64,
|
||||
}
|
||||
|
||||
pub type CpuHarvest = Vec<CpuData>;
|
||||
|
||||
pub type PastCpuWork = f64;
|
||||
pub type PastCpuTotal = f64;
|
||||
|
||||
use futures::StreamExt;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
pub async fn get_cpu_data_list(
|
||||
show_average_cpu: bool, previous_cpu_times: &mut Vec<(PastCpuWork, PastCpuTotal)>,
|
||||
previous_average_cpu_time: &mut Option<(PastCpuWork, PastCpuTotal)>,
|
||||
) -> crate::error::Result<CpuHarvest> {
|
||||
fn calculate_cpu_usage_percentage(
|
||||
(previous_working_time, previous_total_time): (f64, f64),
|
||||
(current_working_time, current_total_time): (f64, f64),
|
||||
) -> f64 {
|
||||
((if current_working_time > previous_working_time {
|
||||
current_working_time - previous_working_time
|
||||
} else {
|
||||
0.0
|
||||
}) * 100.0)
|
||||
/ (if current_total_time > previous_total_time {
|
||||
current_total_time - previous_total_time
|
||||
} else {
|
||||
1.0
|
||||
})
|
||||
}
|
||||
|
||||
// Get all CPU times...
|
||||
let cpu_times = heim::cpu::times().await?;
|
||||
futures::pin_mut!(cpu_times);
|
||||
|
||||
let mut cpu_deque: VecDeque<CpuData> = if previous_cpu_times.is_empty() {
|
||||
// Must initialize ourselves. Use a very quick timeout to calculate an initial.
|
||||
futures_timer::Delay::new(std::time::Duration::from_millis(100)).await;
|
||||
|
||||
let second_cpu_times = heim::cpu::times().await?;
|
||||
futures::pin_mut!(second_cpu_times);
|
||||
|
||||
let mut new_cpu_times: Vec<(PastCpuWork, PastCpuTotal)> = Vec::new();
|
||||
let mut cpu_deque: VecDeque<CpuData> = VecDeque::new();
|
||||
let mut collected_zip = cpu_times.zip(second_cpu_times).enumerate(); // Gotta move it here, can't on while line.
|
||||
|
||||
while let Some((itx, (past, present))) = collected_zip.next().await {
|
||||
if let (Ok(past), Ok(present)) = (past, present) {
|
||||
let present_times = convert_cpu_times(&present);
|
||||
new_cpu_times.push(present_times);
|
||||
cpu_deque.push_back(CpuData {
|
||||
cpu_prefix: "CPU".to_string(),
|
||||
cpu_count: Some(itx),
|
||||
cpu_usage: calculate_cpu_usage_percentage(
|
||||
convert_cpu_times(&past),
|
||||
present_times,
|
||||
),
|
||||
});
|
||||
} else {
|
||||
new_cpu_times.push((0.0, 0.0));
|
||||
cpu_deque.push_back(CpuData {
|
||||
cpu_prefix: "CPU".to_string(),
|
||||
cpu_count: Some(itx),
|
||||
cpu_usage: 0.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
*previous_cpu_times = new_cpu_times;
|
||||
cpu_deque
|
||||
} else {
|
||||
let (new_cpu_times, cpu_deque): (Vec<(PastCpuWork, PastCpuTotal)>, VecDeque<CpuData>) =
|
||||
cpu_times
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.iter()
|
||||
.zip(&*previous_cpu_times)
|
||||
.enumerate()
|
||||
.map(|(itx, (current_cpu, (past_cpu_work, past_cpu_total)))| {
|
||||
if let Ok(cpu_time) = current_cpu {
|
||||
let present_times = convert_cpu_times(cpu_time);
|
||||
|
||||
(
|
||||
present_times,
|
||||
CpuData {
|
||||
cpu_prefix: "CPU".to_string(),
|
||||
cpu_count: Some(itx),
|
||||
cpu_usage: calculate_cpu_usage_percentage(
|
||||
(*past_cpu_work, *past_cpu_total),
|
||||
present_times,
|
||||
),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(
|
||||
(*past_cpu_work, *past_cpu_total),
|
||||
CpuData {
|
||||
cpu_prefix: "CPU".to_string(),
|
||||
cpu_count: Some(itx),
|
||||
cpu_usage: 0.0,
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
.unzip();
|
||||
|
||||
*previous_cpu_times = new_cpu_times;
|
||||
cpu_deque
|
||||
};
|
||||
|
||||
// Get average CPU if needed... and slap it at the top
|
||||
if show_average_cpu {
|
||||
let cpu_time = heim::cpu::time().await?;
|
||||
|
||||
let (cpu_usage, new_average_cpu_time) = if let Some((past_cpu_work, past_cpu_total)) =
|
||||
previous_average_cpu_time
|
||||
{
|
||||
let present_times = convert_cpu_times(&cpu_time);
|
||||
(
|
||||
calculate_cpu_usage_percentage((*past_cpu_work, *past_cpu_total), present_times),
|
||||
present_times,
|
||||
)
|
||||
} else {
|
||||
// Again, we need to do a quick timeout...
|
||||
futures_timer::Delay::new(std::time::Duration::from_millis(100)).await;
|
||||
let second_cpu_time = heim::cpu::time().await?;
|
||||
|
||||
let present_times = convert_cpu_times(&second_cpu_time);
|
||||
(
|
||||
calculate_cpu_usage_percentage(convert_cpu_times(&cpu_time), present_times),
|
||||
present_times,
|
||||
)
|
||||
};
|
||||
|
||||
*previous_average_cpu_time = Some(new_average_cpu_time);
|
||||
cpu_deque.push_front(CpuData {
|
||||
cpu_prefix: "AVG".to_string(),
|
||||
cpu_count: None,
|
||||
cpu_usage,
|
||||
})
|
||||
}
|
||||
|
||||
// Ok(Vec::from(cpu_deque.drain(0..3).collect::<Vec<_>>())) // For artificially limiting the CPU results
|
||||
|
||||
Ok(Vec::from(cpu_deque))
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
//! Data collection for CPU usage and load average.
|
||||
//!
|
||||
//! For CPU usage, Linux, macOS, and Windows are handled by Heim.
|
||||
//!
|
||||
//! For load average, macOS and Linux are supported through Heim.
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] {
|
||||
pub mod heim;
|
||||
pub use self::heim::*;
|
||||
}
|
||||
}
|
||||
|
||||
pub type LoadAvgHarvest = [f32; 3];
|
@ -1,154 +0,0 @@
|
||||
use crate::app::Filter;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_os = "linux")] {
|
||||
pub mod linux;
|
||||
pub use linux::*;
|
||||
} else if #[cfg(any(target_os = "macos", target_os = "windows"))] {
|
||||
pub mod windows_macos;
|
||||
pub use windows_macos::*;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DiskHarvest {
|
||||
pub name: String,
|
||||
pub mount_point: String,
|
||||
pub free_space: Option<u64>,
|
||||
pub used_space: Option<u64>,
|
||||
pub total_space: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct IoData {
|
||||
pub read_bytes: u64,
|
||||
pub write_bytes: u64,
|
||||
}
|
||||
|
||||
pub type IoHarvest = std::collections::HashMap<String, Option<IoData>>;
|
||||
|
||||
pub async fn get_io_usage(actually_get: bool) -> crate::utils::error::Result<Option<IoHarvest>> {
|
||||
if !actually_get {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
use futures::StreamExt;
|
||||
|
||||
let mut io_hash: std::collections::HashMap<String, Option<IoData>> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
let counter_stream = heim::disk::io_counters().await?;
|
||||
futures::pin_mut!(counter_stream);
|
||||
|
||||
while let Some(io) = counter_stream.next().await {
|
||||
if let Ok(io) = io {
|
||||
let mount_point = io.device_name().to_str().unwrap_or("Name Unavailable");
|
||||
|
||||
io_hash.insert(
|
||||
mount_point.to_string(),
|
||||
Some(IoData {
|
||||
read_bytes: io.read_bytes().get::<heim::units::information::byte>(),
|
||||
write_bytes: io.write_bytes().get::<heim::units::information::byte>(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(io_hash))
|
||||
}
|
||||
|
||||
pub async fn get_disk_usage(
|
||||
actually_get: bool, disk_filter: &Option<Filter>, mount_filter: &Option<Filter>,
|
||||
) -> crate::utils::error::Result<Option<Vec<DiskHarvest>>> {
|
||||
if !actually_get {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
use futures::StreamExt;
|
||||
|
||||
let mut vec_disks: Vec<DiskHarvest> = Vec::new();
|
||||
let partitions_stream = heim::disk::partitions_physical().await?;
|
||||
futures::pin_mut!(partitions_stream);
|
||||
|
||||
while let Some(part) = partitions_stream.next().await {
|
||||
if let Ok(partition) = part {
|
||||
let name = get_device_name(&partition);
|
||||
|
||||
let mount_point = (partition
|
||||
.mount_point()
|
||||
.to_str()
|
||||
.unwrap_or("Name Unavailable"))
|
||||
.to_string();
|
||||
|
||||
// Precedence ordering in the case where name and mount filters disagree, "allow" takes precedence over "deny".
|
||||
//
|
||||
// For implementation, we do this as follows:
|
||||
// 1. Is the entry allowed through any filter? That is, does it match an entry in a filter where `is_list_ignored` is `false`? If so, we always keep this entry.
|
||||
// 2. Is the entry denied through any filter? That is, does it match an entry in a filter where `is_list_ignored` is `true`? If so, we always deny this entry.
|
||||
// 3. Anything else is allowed.
|
||||
|
||||
let filter_check_map = [(disk_filter, &name), (mount_filter, &mount_point)];
|
||||
|
||||
// This represents case 1. That is, if there is a match in an allowing list - if there is, then
|
||||
// immediately allow it!
|
||||
let matches_allow_list = filter_check_map.iter().any(|(filter, text)| {
|
||||
if let Some(filter) = filter {
|
||||
if !filter.is_list_ignored {
|
||||
for r in &filter.list {
|
||||
if r.is_match(text) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
});
|
||||
|
||||
let to_keep = if matches_allow_list {
|
||||
true
|
||||
} else {
|
||||
// If it doesn't match an allow list, then check if it is denied.
|
||||
// That is, if it matches in a reject filter, then reject. Otherwise, we always keep it.
|
||||
!filter_check_map.iter().any(|(filter, text)| {
|
||||
if let Some(filter) = filter {
|
||||
if filter.is_list_ignored {
|
||||
for r in &filter.list {
|
||||
if r.is_match(text) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
};
|
||||
|
||||
if to_keep {
|
||||
// The usage line can fail in some cases (for example, if you use Void Linux + LUKS,
|
||||
// see https://github.com/ClementTsang/bottom/issues/419 for details). As such, check
|
||||
// it like this instead.
|
||||
if let Ok(usage) = heim::disk::usage(partition.mount_point().to_path_buf()).await {
|
||||
vec_disks.push(DiskHarvest {
|
||||
free_space: Some(usage.free().get::<heim::units::information::byte>()),
|
||||
used_space: Some(usage.used().get::<heim::units::information::byte>()),
|
||||
total_space: Some(usage.total().get::<heim::units::information::byte>()),
|
||||
mount_point,
|
||||
name,
|
||||
});
|
||||
} else {
|
||||
vec_disks.push(DiskHarvest {
|
||||
free_space: None,
|
||||
used_space: None,
|
||||
total_space: None,
|
||||
mount_point,
|
||||
name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vec_disks.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
Ok(Some(vec_disks))
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
//! Data collection for disks (IO, usage, space, etc.).
|
||||
//!
|
||||
//! For Linux, macOS, and Windows, this is handled by heim.
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] {
|
||||
pub mod heim;
|
||||
pub use self::heim::*;
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
//! Data collection for memory.
|
||||
//!
|
||||
//! For Linux, macOS, and Windows, this is handled by Heim.
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] {
|
||||
pub mod general;
|
||||
pub use self::general::*;
|
||||
}
|
||||
}
|
@ -1,366 +0,0 @@
|
||||
//! This is the main file to house data collection functions.
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use fxhash::FxHashMap;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
use sysinfo::{System, SystemExt};
|
||||
|
||||
use battery::{Battery, Manager};
|
||||
|
||||
use crate::app::layout_manager::UsedWidgets;
|
||||
|
||||
use futures::join;
|
||||
|
||||
use super::DataFilters;
|
||||
|
||||
pub mod batteries;
|
||||
pub mod cpu;
|
||||
pub mod disks;
|
||||
pub mod memory;
|
||||
pub mod network;
|
||||
pub mod processes;
|
||||
pub mod temperature;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Data {
|
||||
pub last_collection_time: Instant,
|
||||
pub cpu: Option<cpu::CpuHarvest>,
|
||||
pub load_avg: Option<cpu::LoadAvgHarvest>,
|
||||
pub memory: Option<memory::MemHarvest>,
|
||||
pub swap: Option<memory::MemHarvest>,
|
||||
pub temperature_sensors: Option<Vec<temperature::TempHarvest>>,
|
||||
pub network: Option<network::NetworkHarvest>,
|
||||
pub list_of_processes: Option<Vec<processes::ProcessHarvest>>,
|
||||
pub disks: Option<Vec<disks::DiskHarvest>>,
|
||||
pub io: Option<disks::IoHarvest>,
|
||||
pub list_of_batteries: Option<Vec<batteries::BatteryHarvest>>,
|
||||
}
|
||||
|
||||
impl Default for Data {
|
||||
fn default() -> Self {
|
||||
Data {
|
||||
last_collection_time: Instant::now(),
|
||||
cpu: None,
|
||||
load_avg: None,
|
||||
memory: None,
|
||||
swap: None,
|
||||
temperature_sensors: None,
|
||||
list_of_processes: None,
|
||||
disks: None,
|
||||
io: None,
|
||||
network: None,
|
||||
list_of_batteries: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Data {
|
||||
pub fn cleanup(&mut self) {
|
||||
self.io = None;
|
||||
self.temperature_sensors = None;
|
||||
self.list_of_processes = None;
|
||||
self.disks = None;
|
||||
self.memory = None;
|
||||
self.swap = None;
|
||||
self.cpu = None;
|
||||
self.load_avg = None;
|
||||
|
||||
if let Some(network) = &mut self.network {
|
||||
network.first_run_cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DataCollector {
|
||||
pub data: Data,
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
sys: System,
|
||||
previous_cpu_times: Vec<(cpu::PastCpuWork, cpu::PastCpuTotal)>,
|
||||
previous_average_cpu_time: Option<(cpu::PastCpuWork, cpu::PastCpuTotal)>,
|
||||
#[cfg(target_os = "linux")]
|
||||
pid_mapping: FxHashMap<crate::Pid, processes::PrevProcDetails>,
|
||||
#[cfg(target_os = "linux")]
|
||||
prev_idle: f64,
|
||||
#[cfg(target_os = "linux")]
|
||||
prev_non_idle: f64,
|
||||
mem_total_kb: u64,
|
||||
temperature_type: temperature::TemperatureType,
|
||||
use_current_cpu_total: bool,
|
||||
last_collection_time: Instant,
|
||||
total_rx: u64,
|
||||
total_tx: u64,
|
||||
show_average_cpu: bool,
|
||||
widgets_to_harvest: UsedWidgets,
|
||||
battery_manager: Option<Manager>,
|
||||
battery_list: Option<Vec<Battery>>,
|
||||
filters: DataFilters,
|
||||
}
|
||||
|
||||
impl DataCollector {
|
||||
pub fn new(filters: DataFilters) -> Self {
|
||||
DataCollector {
|
||||
data: Data::default(),
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
sys: System::new_with_specifics(sysinfo::RefreshKind::new()),
|
||||
previous_cpu_times: vec![],
|
||||
previous_average_cpu_time: None,
|
||||
#[cfg(target_os = "linux")]
|
||||
pid_mapping: FxHashMap::default(),
|
||||
#[cfg(target_os = "linux")]
|
||||
prev_idle: 0_f64,
|
||||
#[cfg(target_os = "linux")]
|
||||
prev_non_idle: 0_f64,
|
||||
mem_total_kb: 0,
|
||||
temperature_type: temperature::TemperatureType::Celsius,
|
||||
use_current_cpu_total: false,
|
||||
last_collection_time: Instant::now(),
|
||||
total_rx: 0,
|
||||
total_tx: 0,
|
||||
show_average_cpu: false,
|
||||
widgets_to_harvest: UsedWidgets::default(),
|
||||
battery_manager: None,
|
||||
battery_list: None,
|
||||
filters,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(&mut self) {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
futures::executor::block_on(self.initialize_memory_size());
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
self.sys.refresh_memory();
|
||||
self.mem_total_kb = self.sys.get_total_memory();
|
||||
|
||||
// TODO: Would be good to get this and network list running on a timer instead...?
|
||||
// Refresh components list once...
|
||||
if self.widgets_to_harvest.use_temp {
|
||||
self.sys.refresh_components_list();
|
||||
}
|
||||
|
||||
// Refresh network list once...
|
||||
if cfg!(target_os = "windows") && self.widgets_to_harvest.use_net {
|
||||
self.sys.refresh_networks_list();
|
||||
}
|
||||
}
|
||||
|
||||
if self.widgets_to_harvest.use_battery {
|
||||
if let Ok(battery_manager) = Manager::new() {
|
||||
if let Ok(batteries) = battery_manager.batteries() {
|
||||
let battery_list: Vec<Battery> = batteries.filter_map(Result::ok).collect();
|
||||
if !battery_list.is_empty() {
|
||||
self.battery_list = Some(battery_list);
|
||||
self.battery_manager = Some(battery_manager);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
futures::executor::block_on(self.update_data());
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
||||
|
||||
self.data.cleanup();
|
||||
|
||||
// trace!("Enabled widgets to harvest: {:#?}", self.widgets_to_harvest);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn initialize_memory_size(&mut self) {
|
||||
self.mem_total_kb = if let Ok(mem) = heim::memory::memory().await {
|
||||
mem.total().get::<heim::units::information::kilobyte>()
|
||||
} else {
|
||||
1
|
||||
};
|
||||
}
|
||||
|
||||
pub fn set_collected_data(&mut self, used_widgets: UsedWidgets) {
|
||||
self.widgets_to_harvest = used_widgets;
|
||||
}
|
||||
|
||||
pub fn set_temperature_type(&mut self, temperature_type: temperature::TemperatureType) {
|
||||
self.temperature_type = temperature_type;
|
||||
}
|
||||
|
||||
pub fn set_use_current_cpu_total(&mut self, use_current_cpu_total: bool) {
|
||||
self.use_current_cpu_total = use_current_cpu_total;
|
||||
}
|
||||
|
||||
pub fn set_show_average_cpu(&mut self, show_average_cpu: bool) {
|
||||
self.show_average_cpu = show_average_cpu;
|
||||
}
|
||||
|
||||
pub async fn update_data(&mut self) {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
if self.widgets_to_harvest.use_proc {
|
||||
self.sys.refresh_processes();
|
||||
}
|
||||
if self.widgets_to_harvest.use_temp {
|
||||
self.sys.refresh_components();
|
||||
}
|
||||
|
||||
if cfg!(target_os = "windows") && self.widgets_to_harvest.use_net {
|
||||
self.sys.refresh_networks();
|
||||
}
|
||||
}
|
||||
|
||||
let current_instant = std::time::Instant::now();
|
||||
|
||||
// CPU
|
||||
if self.widgets_to_harvest.use_cpu {
|
||||
if let Ok(cpu_data) = cpu::get_cpu_data_list(
|
||||
self.show_average_cpu,
|
||||
&mut self.previous_cpu_times,
|
||||
&mut self.previous_average_cpu_time,
|
||||
)
|
||||
.await
|
||||
{
|
||||
self.data.cpu = Some(cpu_data);
|
||||
}
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
{
|
||||
// Load Average
|
||||
if let Ok(load_avg_data) = cpu::get_load_avg().await {
|
||||
self.data.load_avg = Some(load_avg_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Batteries
|
||||
if let Some(battery_manager) = &self.battery_manager {
|
||||
if let Some(battery_list) = &mut self.battery_list {
|
||||
self.data.list_of_batteries =
|
||||
Some(batteries::refresh_batteries(battery_manager, battery_list));
|
||||
}
|
||||
}
|
||||
|
||||
if self.widgets_to_harvest.use_proc {
|
||||
if let Ok(process_list) = {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
processes::get_process_data(
|
||||
&mut self.prev_idle,
|
||||
&mut self.prev_non_idle,
|
||||
&mut self.pid_mapping,
|
||||
self.use_current_cpu_total,
|
||||
current_instant
|
||||
.duration_since(self.last_collection_time)
|
||||
.as_secs(),
|
||||
self.mem_total_kb,
|
||||
)
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
processes::get_process_data(
|
||||
&self.sys,
|
||||
self.use_current_cpu_total,
|
||||
self.mem_total_kb,
|
||||
)
|
||||
}
|
||||
} {
|
||||
self.data.list_of_processes = Some(process_list);
|
||||
}
|
||||
}
|
||||
|
||||
let network_data_fut = {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
network::get_network_data(
|
||||
&self.sys,
|
||||
self.last_collection_time,
|
||||
&mut self.total_rx,
|
||||
&mut self.total_tx,
|
||||
current_instant,
|
||||
self.widgets_to_harvest.use_net,
|
||||
&self.filters.net_filter,
|
||||
)
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
network::get_network_data(
|
||||
self.last_collection_time,
|
||||
&mut self.total_rx,
|
||||
&mut self.total_tx,
|
||||
current_instant,
|
||||
self.widgets_to_harvest.use_net,
|
||||
&self.filters.net_filter,
|
||||
)
|
||||
}
|
||||
};
|
||||
let mem_data_fut = memory::get_mem_data(self.widgets_to_harvest.use_mem);
|
||||
let disk_data_fut = disks::get_disk_usage(
|
||||
self.widgets_to_harvest.use_disk,
|
||||
&self.filters.disk_filter,
|
||||
&self.filters.mount_filter,
|
||||
);
|
||||
let disk_io_usage_fut = disks::get_io_usage(self.widgets_to_harvest.use_disk);
|
||||
let temp_data_fut = {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
temperature::get_temperature_data(
|
||||
&self.sys,
|
||||
&self.temperature_type,
|
||||
self.widgets_to_harvest.use_temp,
|
||||
&self.filters.temp_filter,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
temperature::get_temperature_data(
|
||||
&self.temperature_type,
|
||||
self.widgets_to_harvest.use_temp,
|
||||
&self.filters.temp_filter,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let (net_data, mem_res, disk_res, io_res, temp_res) = join!(
|
||||
network_data_fut,
|
||||
mem_data_fut,
|
||||
disk_data_fut,
|
||||
disk_io_usage_fut,
|
||||
temp_data_fut
|
||||
);
|
||||
|
||||
if let Ok(net_data) = net_data {
|
||||
if let Some(net_data) = &net_data {
|
||||
self.total_rx = net_data.total_rx;
|
||||
self.total_tx = net_data.total_tx;
|
||||
}
|
||||
self.data.network = net_data;
|
||||
}
|
||||
|
||||
if let Ok(memory) = mem_res.0 {
|
||||
self.data.memory = memory;
|
||||
}
|
||||
|
||||
if let Ok(swap) = mem_res.1 {
|
||||
self.data.swap = swap;
|
||||
}
|
||||
|
||||
if let Ok(disks) = disk_res {
|
||||
self.data.disks = disks;
|
||||
}
|
||||
|
||||
if let Ok(io) = io_res {
|
||||
self.data.io = io;
|
||||
}
|
||||
|
||||
if let Ok(temp) = temp_res {
|
||||
self.data.temperature_sensors = temp;
|
||||
}
|
||||
|
||||
// Update time
|
||||
self.data.last_collection_time = current_instant;
|
||||
self.last_collection_time = current_instant;
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
//! Data collection for network usage/IO.
|
||||
//!
|
||||
//! For Linux and macOS, this is handled by Heim.
|
||||
//! For Windows, this is handled by sysinfo.
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(any(target_os = "linux", target_os = "macos"))] {
|
||||
pub mod heim;
|
||||
pub use self::heim::*;
|
||||
} else if #[cfg(target_os = "windows")] {
|
||||
pub mod sysinfo;
|
||||
pub use self::sysinfo::*;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
/// All units in bits.
|
||||
pub struct NetworkHarvest {
|
||||
pub rx: u64,
|
||||
pub tx: u64,
|
||||
pub total_rx: u64,
|
||||
pub total_tx: u64,
|
||||
}
|
||||
|
||||
impl NetworkHarvest {
|
||||
pub fn first_run_cleanup(&mut self) {
|
||||
self.rx = 0;
|
||||
self.tx = 0;
|
||||
}
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
//! Data collection for processes.
|
||||
//!
|
||||
//! For Linux, this is handled by a custom set of functions.
|
||||
//! For Windows and macOS, this is handled by sysinfo.
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_os = "linux")] {
|
||||
pub mod linux;
|
||||
pub use self::linux::*;
|
||||
} else if #[cfg(target_os = "macos")] {
|
||||
pub mod macos;
|
||||
pub use self::macos::*;
|
||||
} else if #[cfg(target_os = "windows")] {
|
||||
pub mod windows;
|
||||
pub use self::windows::*;
|
||||
}
|
||||
}
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_family = "unix")] {
|
||||
pub mod unix;
|
||||
pub use self::unix::*;
|
||||
}
|
||||
}
|
||||
|
||||
use crate::Pid;
|
||||
|
||||
// TODO: Add value so we know if it's sorted ascending or descending by default?
|
||||
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub enum ProcessSorting {
|
||||
CpuPercent,
|
||||
Mem,
|
||||
MemPercent,
|
||||
Pid,
|
||||
ProcessName,
|
||||
Command,
|
||||
ReadPerSecond,
|
||||
WritePerSecond,
|
||||
TotalRead,
|
||||
TotalWrite,
|
||||
State,
|
||||
User,
|
||||
Count,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ProcessSorting {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match &self {
|
||||
ProcessSorting::CpuPercent => "CPU%",
|
||||
ProcessSorting::MemPercent => "Mem%",
|
||||
ProcessSorting::Mem => "Mem",
|
||||
ProcessSorting::ReadPerSecond => "R/s",
|
||||
ProcessSorting::WritePerSecond => "W/s",
|
||||
ProcessSorting::TotalRead => "T.Read",
|
||||
ProcessSorting::TotalWrite => "T.Write",
|
||||
ProcessSorting::State => "State",
|
||||
ProcessSorting::ProcessName => "Name",
|
||||
ProcessSorting::Command => "Command",
|
||||
ProcessSorting::Pid => "PID",
|
||||
ProcessSorting::Count => "Count",
|
||||
ProcessSorting::User => "User",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProcessSorting {
|
||||
fn default() -> Self {
|
||||
ProcessSorting::CpuPercent
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ProcessHarvest {
|
||||
pub pid: Pid,
|
||||
pub parent_pid: Option<Pid>, // Remember, parent_pid 0 is root...
|
||||
pub cpu_usage_percent: f64,
|
||||
pub mem_usage_percent: f64,
|
||||
pub mem_usage_bytes: u64,
|
||||
// pub rss_kb: u64,
|
||||
// pub virt_kb: u64,
|
||||
pub name: String,
|
||||
pub command: String,
|
||||
pub read_bytes_per_sec: u64,
|
||||
pub write_bytes_per_sec: u64,
|
||||
pub total_read_bytes: u64,
|
||||
pub total_write_bytes: u64,
|
||||
pub process_state: String,
|
||||
pub process_state_char: char,
|
||||
|
||||
/// This is the *effective* user ID.
|
||||
#[cfg(target_family = "unix")]
|
||||
pub uid: Option<libc::uid_t>,
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
//! Data collection for temperature metrics.
|
||||
//!
|
||||
//! For Linux and macOS, this is handled by Heim.
|
||||
//! For Windows, this is handled by sysinfo.
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_os = "linux")] {
|
||||
pub mod heim;
|
||||
pub use self::heim::*;
|
||||
} else if #[cfg(any(target_os = "macos", target_os = "windows"))] {
|
||||
pub mod sysinfo;
|
||||
pub use self::sysinfo::*;
|
||||
}
|
||||
}
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::app::Filter;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct TempHarvest {
|
||||
pub name: String,
|
||||
pub temperature: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum TemperatureType {
|
||||
Celsius,
|
||||
Kelvin,
|
||||
Fahrenheit,
|
||||
}
|
||||
|
||||
impl Default for TemperatureType {
|
||||
fn default() -> Self {
|
||||
TemperatureType::Celsius
|
||||
}
|
||||
}
|
||||
|
||||
fn is_temp_filtered(filter: &Option<Filter>, text: &str) -> bool {
|
||||
if let Some(filter) = filter {
|
||||
if filter.is_list_ignored {
|
||||
let mut ret = true;
|
||||
for r in &filter.list {
|
||||
if r.is_match(text) {
|
||||
ret = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ret
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn temp_vec_sort(temperature_vec: &mut Vec<TempHarvest>) {
|
||||
// By default, sort temperature, then by alphabetically!
|
||||
// TODO: [TEMPS] Allow users to control this.
|
||||
|
||||
// Note we sort in reverse here; we want greater temps to be higher priority.
|
||||
temperature_vec.sort_by(|a, b| match a.temperature.partial_cmp(&b.temperature) {
|
||||
Some(x) => match x {
|
||||
Ordering::Less => Ordering::Greater,
|
||||
Ordering::Greater => Ordering::Less,
|
||||
Ordering::Equal => Ordering::Equal,
|
||||
},
|
||||
None => Ordering::Equal,
|
||||
});
|
||||
|
||||
temperature_vec.sort_by(|a, b| a.name.partial_cmp(&b.name).unwrap_or(Ordering::Equal));
|
||||
}
|
@ -1,25 +1,45 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use tui::layout::Rect;
|
||||
|
||||
const MAX_TIMEOUT: Duration = Duration::from_millis(400);
|
||||
|
||||
/// These are "signals" that are sent along with an [`EventResult`] to signify a potential additional action
|
||||
/// that the caller must do, along with the "core" result of either drawing or redrawing.
|
||||
pub enum ReturnSignal {
|
||||
/// Do nothing.
|
||||
Nothing,
|
||||
/// A signal returned when some process widget was told to try to kill a process (or group of processes).
|
||||
KillProcess,
|
||||
}
|
||||
|
||||
/// The results of handling a [`ReturnSignal`].
|
||||
pub enum ReturnSignalResult {
|
||||
/// Kill the program.
|
||||
Quit,
|
||||
/// Trigger a redraw.
|
||||
Redraw,
|
||||
/// Don't trigger a redraw.
|
||||
NoRedraw,
|
||||
}
|
||||
|
||||
/// The results of handling some user input event, like a mouse or key event, signifying what
|
||||
/// the program should then do next.
|
||||
pub enum EventResult {
|
||||
/// Kill the program.
|
||||
Quit,
|
||||
|
||||
/// Trigger a redraw.
|
||||
Redraw,
|
||||
|
||||
/// Don't trigger a redraw.
|
||||
NoRedraw,
|
||||
/// Return a signal.
|
||||
Signal(ReturnSignal),
|
||||
}
|
||||
|
||||
/// How a widget should handle a widget selection request.
|
||||
pub enum SelectionAction {
|
||||
/// This event occurs if the widget internally handled the selection action.
|
||||
Handled,
|
||||
|
||||
/// This event occurs if the widget did not handle the selection action; the caller must handle it.
|
||||
NotHandled,
|
||||
}
|
||||
@ -28,7 +48,6 @@ pub enum SelectionAction {
|
||||
enum MultiKeyState {
|
||||
/// Currently not waiting on any next input.
|
||||
Idle,
|
||||
|
||||
/// Waiting for the next input, with a given trigger [`Instant`].
|
||||
Waiting {
|
||||
/// When it was triggered.
|
||||
@ -43,10 +62,8 @@ enum MultiKeyState {
|
||||
pub enum MultiKeyResult {
|
||||
/// Returned when a character was *accepted*, but has not completed the sequence required.
|
||||
Accepted,
|
||||
|
||||
/// Returned when a character is accepted and completes the sequence.
|
||||
Completed,
|
||||
|
||||
/// Returned if a character breaks the sequence or if it has already timed out.
|
||||
Rejected,
|
||||
}
|
||||
@ -124,3 +141,8 @@ impl MultiKey {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks whether points `(x, y)` intersect a given [`Rect`].
|
||||
pub fn does_point_intersect_rect(x: u16, y: u16, rect: Rect) -> bool {
|
||||
x >= rect.left() && x <= rect.right() && y >= rect.top() && y <= rect.bottom()
|
||||
}
|
||||
|
@ -1,14 +1,24 @@
|
||||
use crate::error::{BottomError, Result};
|
||||
use crate::{
|
||||
app::{DiskTable, MemGraph, NetGraph, OldNetGraph, ProcessManager, TempTable},
|
||||
error::{BottomError, Result},
|
||||
options::layout_options::{Row, RowChildren},
|
||||
};
|
||||
use fxhash::FxHashMap;
|
||||
use indextree::{Arena, NodeId};
|
||||
use std::collections::BTreeMap;
|
||||
use tui::layout::Constraint;
|
||||
use typed_builder::*;
|
||||
|
||||
use crate::app::widgets::Widget;
|
||||
use crate::constants::DEFAULT_WIDGET_ID;
|
||||
|
||||
use super::{event::SelectionAction, CpuGraph, TextTable, TimeGraph, TmpBottomWidget};
|
||||
|
||||
/// Represents a more usable representation of the layout, derived from the
|
||||
/// config.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BottomLayout {
|
||||
pub rows: Vec<BottomRow>,
|
||||
pub rows: Vec<OldBottomRow>,
|
||||
pub total_row_height_ratio: u32,
|
||||
}
|
||||
|
||||
@ -535,7 +545,7 @@ impl BottomLayout {
|
||||
pub fn init_basic_default(use_battery: bool) -> Self {
|
||||
let table_widgets = if use_battery {
|
||||
vec![
|
||||
BottomCol::builder()
|
||||
OldBottomCol::builder()
|
||||
.canvas_handle_width(true)
|
||||
.children(vec![BottomColRow::builder()
|
||||
.canvas_handle_height(true)
|
||||
@ -549,7 +559,7 @@ impl BottomLayout {
|
||||
.build()])
|
||||
.build()])
|
||||
.build(),
|
||||
BottomCol::builder()
|
||||
OldBottomCol::builder()
|
||||
.canvas_handle_width(true)
|
||||
.children(vec![
|
||||
BottomColRow::builder()
|
||||
@ -593,7 +603,7 @@ impl BottomLayout {
|
||||
.build(),
|
||||
])
|
||||
.build(),
|
||||
BottomCol::builder()
|
||||
OldBottomCol::builder()
|
||||
.canvas_handle_width(true)
|
||||
.children(vec![BottomColRow::builder()
|
||||
.canvas_handle_height(true)
|
||||
@ -607,7 +617,7 @@ impl BottomLayout {
|
||||
.build()])
|
||||
.build()])
|
||||
.build(),
|
||||
BottomCol::builder()
|
||||
OldBottomCol::builder()
|
||||
.canvas_handle_width(true)
|
||||
.children(vec![BottomColRow::builder()
|
||||
.canvas_handle_height(true)
|
||||
@ -624,7 +634,7 @@ impl BottomLayout {
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
BottomCol::builder()
|
||||
OldBottomCol::builder()
|
||||
.canvas_handle_width(true)
|
||||
.children(vec![BottomColRow::builder()
|
||||
.canvas_handle_height(true)
|
||||
@ -638,7 +648,7 @@ impl BottomLayout {
|
||||
.build()])
|
||||
.build()])
|
||||
.build(),
|
||||
BottomCol::builder()
|
||||
OldBottomCol::builder()
|
||||
.canvas_handle_width(true)
|
||||
.children(vec![
|
||||
BottomColRow::builder()
|
||||
@ -679,7 +689,7 @@ impl BottomLayout {
|
||||
.build(),
|
||||
])
|
||||
.build(),
|
||||
BottomCol::builder()
|
||||
OldBottomCol::builder()
|
||||
.canvas_handle_width(true)
|
||||
.children(vec![BottomColRow::builder()
|
||||
.canvas_handle_height(true)
|
||||
@ -699,9 +709,9 @@ impl BottomLayout {
|
||||
BottomLayout {
|
||||
total_row_height_ratio: 3,
|
||||
rows: vec![
|
||||
BottomRow::builder()
|
||||
OldBottomRow::builder()
|
||||
.canvas_handle_height(true)
|
||||
.children(vec![BottomCol::builder()
|
||||
.children(vec![OldBottomCol::builder()
|
||||
.canvas_handle_width(true)
|
||||
.children(vec![BottomColRow::builder()
|
||||
.canvas_handle_height(true)
|
||||
@ -714,9 +724,9 @@ impl BottomLayout {
|
||||
.build()])
|
||||
.build()])
|
||||
.build(),
|
||||
BottomRow::builder()
|
||||
OldBottomRow::builder()
|
||||
.canvas_handle_height(true)
|
||||
.children(vec![BottomCol::builder()
|
||||
.children(vec![OldBottomCol::builder()
|
||||
.canvas_handle_width(true)
|
||||
.children(vec![BottomColRow::builder()
|
||||
.canvas_handle_height(true)
|
||||
@ -741,9 +751,9 @@ impl BottomLayout {
|
||||
.build()])
|
||||
.build()])
|
||||
.build(),
|
||||
BottomRow::builder()
|
||||
OldBottomRow::builder()
|
||||
.canvas_handle_height(true)
|
||||
.children(vec![BottomCol::builder()
|
||||
.children(vec![OldBottomCol::builder()
|
||||
.canvas_handle_width(true)
|
||||
.children(vec![BottomColRow::builder()
|
||||
.canvas_handle_height(true)
|
||||
@ -756,7 +766,7 @@ impl BottomLayout {
|
||||
.build()])
|
||||
.build()])
|
||||
.build(),
|
||||
BottomRow::builder()
|
||||
OldBottomRow::builder()
|
||||
.canvas_handle_height(true)
|
||||
.children(table_widgets)
|
||||
.build(),
|
||||
@ -767,8 +777,8 @@ impl BottomLayout {
|
||||
|
||||
/// Represents a single row in the layout.
|
||||
#[derive(Clone, Debug, TypedBuilder)]
|
||||
pub struct BottomRow {
|
||||
pub children: Vec<BottomCol>,
|
||||
pub struct OldBottomRow {
|
||||
pub children: Vec<OldBottomCol>,
|
||||
|
||||
#[builder(default = 1)]
|
||||
pub total_col_ratio: u32,
|
||||
@ -787,7 +797,7 @@ pub struct BottomRow {
|
||||
/// contains only ONE element, it is still a column (rather than either a col or
|
||||
/// a widget, as per the config, for simplicity's sake).
|
||||
#[derive(Clone, Debug, TypedBuilder)]
|
||||
pub struct BottomCol {
|
||||
pub struct OldBottomCol {
|
||||
pub children: Vec<BottomColRow>,
|
||||
|
||||
#[builder(default = 1)]
|
||||
@ -972,13 +982,479 @@ Supported widget names:
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct UsedWidgets {
|
||||
pub use_cpu: bool,
|
||||
pub use_mem: bool,
|
||||
pub use_net: bool,
|
||||
pub use_proc: bool,
|
||||
pub use_disk: bool,
|
||||
pub use_temp: bool,
|
||||
pub use_battery: bool,
|
||||
// --- New stuff ---
|
||||
|
||||
/// Represents a row in the layout tree.
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub struct RowLayout {
|
||||
last_selected_index: usize,
|
||||
pub constraint: Constraint,
|
||||
}
|
||||
|
||||
impl Default for RowLayout {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
last_selected_index: 0,
|
||||
constraint: Constraint::Min(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a column in the layout tree.
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub struct ColLayout {
|
||||
last_selected_index: usize,
|
||||
pub constraint: Constraint,
|
||||
}
|
||||
|
||||
impl Default for ColLayout {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
last_selected_index: 0,
|
||||
constraint: Constraint::Min(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a widget in the layout tree.
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub struct WidgetLayout {
|
||||
pub constraint: Constraint,
|
||||
}
|
||||
|
||||
impl Default for WidgetLayout {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
constraint: Constraint::Min(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`LayoutNode`] represents a single node in the overall widget hierarchy. Each node is one of:
|
||||
/// - [`LayoutNode::Row`] (a a non-leaf that distributes its children horizontally)
|
||||
/// - [`LayoutNode::Col`] (a non-leaf node that distributes its children vertically)
|
||||
/// - [`LayoutNode::Widget`] (a leaf node that contains the ID of the widget it is associated with)
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum LayoutNode {
|
||||
Row(RowLayout),
|
||||
Col(ColLayout),
|
||||
Widget(WidgetLayout),
|
||||
}
|
||||
|
||||
/// Relative movement direction from the currently selected widget.
|
||||
pub enum MovementDirection {
|
||||
Left,
|
||||
Right,
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
/// A wrapper struct to simplify the output of [`create_layout_tree`].
|
||||
pub struct LayoutCreationOutput {
|
||||
pub layout_tree: Arena<LayoutNode>,
|
||||
pub root: NodeId,
|
||||
pub widget_lookup_map: FxHashMap<NodeId, TmpBottomWidget>,
|
||||
pub selected: Option<NodeId>,
|
||||
}
|
||||
|
||||
/// Creates a new [`Arena<LayoutNode>`] from the given config and returns it, along with the [`NodeId`] representing
|
||||
/// the root of the newly created [`Arena`], a mapping from [`NodeId`]s to [`BottomWidget`]s, and optionally, a default
|
||||
/// selected [`NodeId`].
|
||||
// FIXME: This is currently jury-rigged "glue" just to work with the existing config system! We are NOT keeping it like this, it's too awful to keep like this!
|
||||
pub fn create_layout_tree(
|
||||
rows: &[Row], process_defaults: crate::options::ProcessDefaults,
|
||||
app_config_fields: &super::AppConfigFields,
|
||||
) -> Result<LayoutCreationOutput> {
|
||||
fn add_widget_to_map(
|
||||
widget_lookup_map: &mut FxHashMap<NodeId, TmpBottomWidget>, widget_type: &str,
|
||||
widget_id: NodeId, process_defaults: &crate::options::ProcessDefaults,
|
||||
app_config_fields: &super::AppConfigFields,
|
||||
) -> Result<()> {
|
||||
match widget_type.parse::<BottomWidgetType>()? {
|
||||
BottomWidgetType::Cpu => {
|
||||
let graph = TimeGraph::from_config(app_config_fields);
|
||||
let legend = TextTable::new(vec![("CPU", None, false), ("Use%", None, false)]);
|
||||
let legend_position = super::CpuGraphLegendPosition::Right;
|
||||
|
||||
widget_lookup_map.insert(
|
||||
widget_id,
|
||||
CpuGraph::new(graph, legend, legend_position).into(),
|
||||
);
|
||||
}
|
||||
BottomWidgetType::Mem => {
|
||||
let graph = TimeGraph::from_config(app_config_fields);
|
||||
widget_lookup_map.insert(widget_id, MemGraph::new(graph).into());
|
||||
}
|
||||
BottomWidgetType::Net => {
|
||||
let graph = TimeGraph::from_config(app_config_fields);
|
||||
if app_config_fields.use_old_network_legend {
|
||||
widget_lookup_map.insert(widget_id, OldNetGraph::new(graph).into());
|
||||
} else {
|
||||
widget_lookup_map.insert(widget_id, NetGraph::new(graph).into());
|
||||
}
|
||||
}
|
||||
BottomWidgetType::Proc => {
|
||||
widget_lookup_map.insert(
|
||||
widget_id,
|
||||
ProcessManager::new(process_defaults.is_tree).into(),
|
||||
);
|
||||
}
|
||||
BottomWidgetType::Temp => {
|
||||
let table = TextTable::new(vec![("Sensor", None, false), ("Temp", None, false)]);
|
||||
widget_lookup_map.insert(widget_id, TempTable::new(table).into());
|
||||
}
|
||||
BottomWidgetType::Disk => {
|
||||
let table = TextTable::new(vec![
|
||||
("Disk", None, false),
|
||||
("Mount", None, false),
|
||||
("Used", None, false),
|
||||
("Free", None, false),
|
||||
("Total", None, false),
|
||||
("R/s", None, false),
|
||||
("W/s", None, false),
|
||||
]);
|
||||
widget_lookup_map.insert(widget_id, DiskTable::new(table).into());
|
||||
}
|
||||
BottomWidgetType::Battery => {}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let mut layout_tree = Arena::new();
|
||||
let root_id = layout_tree.new_node(LayoutNode::Col(ColLayout::default()));
|
||||
let mut widget_lookup_map = FxHashMap::default();
|
||||
let mut selected = None;
|
||||
|
||||
let row_sum: u32 = rows.iter().map(|row| row.ratio.unwrap_or(1)).sum();
|
||||
for row in rows {
|
||||
let ratio = row.ratio.unwrap_or(1);
|
||||
let layout_node = LayoutNode::Row(RowLayout {
|
||||
constraint: Constraint::Ratio(ratio, row_sum),
|
||||
..Default::default()
|
||||
});
|
||||
let row_id = layout_tree.new_node(layout_node);
|
||||
root_id.append(row_id, &mut layout_tree);
|
||||
|
||||
if let Some(cols) = &row.child {
|
||||
let col_sum: u32 = cols
|
||||
.iter()
|
||||
.map(|col| match col {
|
||||
RowChildren::Widget(widget) => widget.ratio.unwrap_or(1),
|
||||
RowChildren::Col { ratio, child: _ } => ratio.unwrap_or(1),
|
||||
})
|
||||
.sum();
|
||||
|
||||
for col in cols {
|
||||
match col {
|
||||
RowChildren::Widget(widget) => {
|
||||
let widget_node = LayoutNode::Widget(WidgetLayout {
|
||||
constraint: Constraint::Ratio(widget.ratio.unwrap_or(1), col_sum),
|
||||
});
|
||||
let widget_id = layout_tree.new_node(widget_node);
|
||||
row_id.append(widget_id, &mut layout_tree);
|
||||
|
||||
if let Some(true) = widget.default {
|
||||
selected = Some(widget_id);
|
||||
}
|
||||
add_widget_to_map(
|
||||
&mut widget_lookup_map,
|
||||
&widget.widget_type,
|
||||
widget_id,
|
||||
&process_defaults,
|
||||
app_config_fields,
|
||||
)?;
|
||||
}
|
||||
RowChildren::Col {
|
||||
ratio,
|
||||
child: children,
|
||||
} => {
|
||||
let col_node = LayoutNode::Col(ColLayout {
|
||||
constraint: Constraint::Ratio(ratio.unwrap_or(1), col_sum),
|
||||
..Default::default()
|
||||
});
|
||||
let col_id = layout_tree.new_node(col_node);
|
||||
row_id.append(col_id, &mut layout_tree);
|
||||
|
||||
let child_sum: u32 =
|
||||
children.iter().map(|child| child.ratio.unwrap_or(1)).sum();
|
||||
|
||||
for child in children {
|
||||
let widget_node = LayoutNode::Widget(WidgetLayout {
|
||||
constraint: Constraint::Ratio(child.ratio.unwrap_or(1), child_sum),
|
||||
});
|
||||
let widget_id = layout_tree.new_node(widget_node);
|
||||
col_id.append(widget_id, &mut layout_tree);
|
||||
|
||||
if let Some(true) = child.default {
|
||||
selected = Some(widget_id);
|
||||
}
|
||||
add_widget_to_map(
|
||||
&mut widget_lookup_map,
|
||||
&child.widget_type,
|
||||
widget_id,
|
||||
&process_defaults,
|
||||
app_config_fields,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(LayoutCreationOutput {
|
||||
layout_tree,
|
||||
root: root_id,
|
||||
widget_lookup_map,
|
||||
selected,
|
||||
})
|
||||
}
|
||||
|
||||
/// Attempts to find and return the selected [`BottomWidgetId`] after moving in a direction.
|
||||
///
|
||||
/// Note this function assumes a properly built tree - if not, bad things may happen! We generally assume that:
|
||||
/// - Only [`LayoutNode::Widget`]s are leaves.
|
||||
/// - Only [`LayoutNode::Row`]s or [`LayoutNode::Col`]s are non-leaves.
|
||||
pub fn move_widget_selection(
|
||||
layout_tree: &mut Arena<LayoutNode>, current_widget: &mut TmpBottomWidget,
|
||||
current_widget_id: NodeId, direction: MovementDirection,
|
||||
) -> NodeId {
|
||||
// We first give our currently-selected widget a chance to react to the movement - it may handle it internally!
|
||||
let handled = match direction {
|
||||
MovementDirection::Left => current_widget.handle_widget_selection_left(),
|
||||
MovementDirection::Right => current_widget.handle_widget_selection_right(),
|
||||
MovementDirection::Up => current_widget.handle_widget_selection_up(),
|
||||
MovementDirection::Down => current_widget.handle_widget_selection_down(),
|
||||
};
|
||||
|
||||
match handled {
|
||||
SelectionAction::Handled => {
|
||||
// If it was handled by the widget, then we don't have to do anything - return the current one.
|
||||
current_widget_id
|
||||
}
|
||||
SelectionAction::NotHandled => {
|
||||
/// Keeps traversing up the `layout_tree` until it hits a parent where `current_id` is a child and parent
|
||||
/// is a [`LayoutNode::Row`], returning its parent's [`NodeId`] and the child's [`NodeId`] (in that order).
|
||||
/// If this crawl fails (i.e. hits a root, it is an invalid tree for some reason), it returns [`None`].
|
||||
fn find_first_row(
|
||||
layout_tree: &Arena<LayoutNode>, current_id: NodeId,
|
||||
) -> Option<(NodeId, NodeId)> {
|
||||
layout_tree
|
||||
.get(current_id)
|
||||
.and_then(|current_node| current_node.parent())
|
||||
.and_then(|parent_id| {
|
||||
layout_tree
|
||||
.get(parent_id)
|
||||
.map(|parent_node| (parent_id, parent_node))
|
||||
})
|
||||
.and_then(|(parent_id, parent_node)| match parent_node.get() {
|
||||
LayoutNode::Row(_) => Some((parent_id, current_id)),
|
||||
LayoutNode::Col(_) => find_first_row(layout_tree, parent_id),
|
||||
LayoutNode::Widget(_) => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Keeps traversing up the `layout_tree` until it hits a parent where `current_id` is a child and parent
|
||||
/// is a [`LayoutNode::Col`], returning its parent's [`NodeId`] and the child's [`NodeId`] (in that order).
|
||||
/// If this crawl fails (i.e. hits a root, it is an invalid tree for some reason), it returns [`None`].
|
||||
fn find_first_col(
|
||||
layout_tree: &Arena<LayoutNode>, current_id: NodeId,
|
||||
) -> Option<(NodeId, NodeId)> {
|
||||
layout_tree
|
||||
.get(current_id)
|
||||
.and_then(|current_node| current_node.parent())
|
||||
.and_then(|parent_id| {
|
||||
layout_tree
|
||||
.get(parent_id)
|
||||
.map(|parent_node| (parent_id, parent_node))
|
||||
})
|
||||
.and_then(|(parent_id, parent_node)| match parent_node.get() {
|
||||
LayoutNode::Row(_) => find_first_col(layout_tree, parent_id),
|
||||
LayoutNode::Col(_) => Some((parent_id, current_id)),
|
||||
LayoutNode::Widget(_) => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Descends to a leaf.
|
||||
fn descend_to_leaf(layout_tree: &Arena<LayoutNode>, current_id: NodeId) -> NodeId {
|
||||
if let Some(current_node) = layout_tree.get(current_id) {
|
||||
match current_node.get() {
|
||||
LayoutNode::Row(RowLayout {
|
||||
last_selected_index,
|
||||
constraint: _,
|
||||
})
|
||||
| LayoutNode::Col(ColLayout {
|
||||
last_selected_index,
|
||||
constraint: _,
|
||||
}) => {
|
||||
if let Some(next_child) =
|
||||
current_id.children(layout_tree).nth(*last_selected_index)
|
||||
{
|
||||
descend_to_leaf(layout_tree, next_child)
|
||||
} else {
|
||||
current_id
|
||||
}
|
||||
}
|
||||
LayoutNode::Widget(_) => {
|
||||
// Halt!
|
||||
current_id
|
||||
}
|
||||
}
|
||||
} else {
|
||||
current_id
|
||||
}
|
||||
}
|
||||
|
||||
// If it was NOT handled by the current widget, then move in the correct direction; we can rely
|
||||
// on the tree layout to help us decide where to go.
|
||||
// Movement logic is inspired by i3. When we enter a new column/row, we go to the *last* selected
|
||||
// element; if we can't, go to the nearest one.
|
||||
match direction {
|
||||
MovementDirection::Left => {
|
||||
// When we move "left":
|
||||
// 1. Look for the parent of the current widget.
|
||||
// 2. Depending on whether it is a Row or Col:
|
||||
// a) If we are in a Row, try to move to the child (it can be a Row, Col, or Widget) before it,
|
||||
// and update the last-selected index. If we can't (i.e. we are the first element), then
|
||||
// instead move to the parent, and try again to select the element before it. If there is
|
||||
// no parent (i.e. we hit the root), then just return the original index.
|
||||
// b) If we are in a Col, then just try to move to the parent. If there is no
|
||||
// parent (i.e. we hit the root), then just return the original index.
|
||||
// c) A Widget should be impossible to select.
|
||||
// 3. Assuming we have now selected a new child, then depending on what the child is:
|
||||
// a) If we are in a Row or Col, then take the last selected index, and repeat step 3 until you hit
|
||||
// a Widget.
|
||||
// b) If we are in a Widget, return the corresponding NodeId.
|
||||
|
||||
fn find_left(
|
||||
layout_tree: &mut Arena<LayoutNode>, current_id: NodeId,
|
||||
) -> NodeId {
|
||||
if let Some((parent_id, child_id)) = find_first_row(layout_tree, current_id)
|
||||
{
|
||||
if let Some(prev_sibling) =
|
||||
child_id.preceding_siblings(layout_tree).nth(1)
|
||||
{
|
||||
// Subtract one from the currently selected index...
|
||||
if let Some(parent) = layout_tree.get_mut(parent_id) {
|
||||
if let LayoutNode::Row(row) = parent.get_mut() {
|
||||
row.last_selected_index =
|
||||
row.last_selected_index.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Now descend downwards!
|
||||
descend_to_leaf(layout_tree, prev_sibling)
|
||||
} else {
|
||||
// Darn, we can't go further back! Recurse on this ID.
|
||||
find_left(layout_tree, child_id)
|
||||
}
|
||||
} else {
|
||||
// Failed, just return the current ID.
|
||||
current_id
|
||||
}
|
||||
}
|
||||
find_left(layout_tree, current_widget_id)
|
||||
}
|
||||
MovementDirection::Right => {
|
||||
// When we move "right", repeat the steps for "left", but instead try to move to the child *after*
|
||||
// it in all cases.
|
||||
|
||||
fn find_right(
|
||||
layout_tree: &mut Arena<LayoutNode>, current_id: NodeId,
|
||||
) -> NodeId {
|
||||
if let Some((parent_id, child_id)) = find_first_row(layout_tree, current_id)
|
||||
{
|
||||
if let Some(prev_sibling) =
|
||||
child_id.following_siblings(layout_tree).nth(1)
|
||||
{
|
||||
// Add one to the currently selected index...
|
||||
if let Some(parent) = layout_tree.get_mut(parent_id) {
|
||||
if let LayoutNode::Row(row) = parent.get_mut() {
|
||||
row.last_selected_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Now descend downwards!
|
||||
descend_to_leaf(layout_tree, prev_sibling)
|
||||
} else {
|
||||
// Darn, we can't go further back! Recurse on this ID.
|
||||
find_right(layout_tree, child_id)
|
||||
}
|
||||
} else {
|
||||
// Failed, just return the current ID.
|
||||
current_id
|
||||
}
|
||||
}
|
||||
find_right(layout_tree, current_widget_id)
|
||||
}
|
||||
MovementDirection::Up => {
|
||||
// When we move "up", copy the steps for "left", but switch "Row" and "Col". We instead want to move
|
||||
// vertically, so we want to now avoid Rows and look for Cols!
|
||||
|
||||
fn find_above(
|
||||
layout_tree: &mut Arena<LayoutNode>, current_id: NodeId,
|
||||
) -> NodeId {
|
||||
if let Some((parent_id, child_id)) = find_first_col(layout_tree, current_id)
|
||||
{
|
||||
if let Some(prev_sibling) =
|
||||
child_id.preceding_siblings(layout_tree).nth(1)
|
||||
{
|
||||
// Subtract one from the currently selected index...
|
||||
if let Some(parent) = layout_tree.get_mut(parent_id) {
|
||||
if let LayoutNode::Col(row) = parent.get_mut() {
|
||||
row.last_selected_index =
|
||||
row.last_selected_index.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Now descend downwards!
|
||||
descend_to_leaf(layout_tree, prev_sibling)
|
||||
} else {
|
||||
// Darn, we can't go further back! Recurse on this ID.
|
||||
find_above(layout_tree, child_id)
|
||||
}
|
||||
} else {
|
||||
// Failed, just return the current ID.
|
||||
current_id
|
||||
}
|
||||
}
|
||||
find_above(layout_tree, current_widget_id)
|
||||
}
|
||||
MovementDirection::Down => {
|
||||
// See "up"'s steps, but now we're going for the child *after* the currently selected one in all
|
||||
// cases.
|
||||
|
||||
fn find_below(
|
||||
layout_tree: &mut Arena<LayoutNode>, current_id: NodeId,
|
||||
) -> NodeId {
|
||||
if let Some((parent_id, child_id)) = find_first_col(layout_tree, current_id)
|
||||
{
|
||||
if let Some(prev_sibling) =
|
||||
child_id.following_siblings(layout_tree).nth(1)
|
||||
{
|
||||
// Add one to the currently selected index...
|
||||
if let Some(parent) = layout_tree.get_mut(parent_id) {
|
||||
if let LayoutNode::Col(row) = parent.get_mut() {
|
||||
row.last_selected_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Now descend downwards!
|
||||
descend_to_leaf(layout_tree, prev_sibling)
|
||||
} else {
|
||||
// Darn, we can't go further back! Recurse on this ID.
|
||||
find_below(layout_tree, child_id)
|
||||
}
|
||||
} else {
|
||||
// Failed, just return the current ID.
|
||||
current_id
|
||||
}
|
||||
}
|
||||
find_below(layout_tree, current_widget_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,42 @@
|
||||
use indextree::NodeId;
|
||||
use tui::layout::Rect;
|
||||
|
||||
use crate::app::Component;
|
||||
|
||||
/// A container that "holds"" multiple [`BottomWidget`]s through their [`NodeId`]s.
|
||||
pub struct Carousel {
|
||||
index: usize,
|
||||
children: Vec<NodeId>,
|
||||
bounds: Rect,
|
||||
}
|
||||
|
||||
impl Carousel {
|
||||
/// Creates a new [`Carousel`] with the specified children.
|
||||
pub fn new(children: Vec<NodeId>) -> Self {
|
||||
Self {
|
||||
index: 0,
|
||||
children,
|
||||
bounds: Rect::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a new child to a [`Carousel`].
|
||||
pub fn add_child(&mut self, child: NodeId) {
|
||||
self.children.push(child);
|
||||
}
|
||||
|
||||
/// Returns the currently selected [`NodeId`] if possible.
|
||||
pub fn get_currently_selected(&self) -> Option<&NodeId> {
|
||||
self.children.get(self.index)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Carousel {
|
||||
fn bounds(&self) -> tui::layout::Rect {
|
||||
self.bounds
|
||||
}
|
||||
|
||||
fn set_bounds(&mut self, new_bounds: tui::layout::Rect) {
|
||||
self.bounds = new_bounds;
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
//! A collection of basic components.
|
||||
|
||||
pub mod text_table;
|
||||
pub use text_table::TextTable;
|
||||
|
||||
pub mod time_graph;
|
||||
pub use time_graph::TimeGraph;
|
||||
|
||||
pub mod scrollable;
|
||||
pub use scrollable::Scrollable;
|
||||
|
||||
pub mod text_input;
|
||||
pub use text_input::TextInput;
|
@ -51,9 +51,9 @@ pub struct TextTable {
|
||||
}
|
||||
|
||||
impl TextTable {
|
||||
pub fn new(num_items: usize, columns: Vec<(&'static str, Option<KeyEvent>, bool)>) -> Self {
|
||||
pub fn new(columns: Vec<(&'static str, Option<KeyEvent>, bool)>) -> Self {
|
||||
Self {
|
||||
scrollable: Scrollable::new(num_items),
|
||||
scrollable: Scrollable::new(0),
|
||||
columns: columns
|
||||
.into_iter()
|
||||
.map(|(name, shortcut, default_descending)| Column {
|
||||
|
@ -3,7 +3,10 @@ use std::time::{Duration, Instant};
|
||||
use crossterm::event::{KeyEvent, KeyModifiers, MouseEvent};
|
||||
use tui::layout::Rect;
|
||||
|
||||
use crate::app::{event::EventResult, Component};
|
||||
use crate::{
|
||||
app::{event::EventResult, AppConfigFields, Component},
|
||||
constants::{AUTOHIDE_TIMEOUT_MILLISECONDS, STALE_MAX_MILLISECONDS, STALE_MIN_MILLISECONDS},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum AutohideTimerState {
|
||||
@ -13,7 +16,8 @@ pub enum AutohideTimerState {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum AutohideTimer {
|
||||
Disabled,
|
||||
AlwaysShow,
|
||||
AlwaysHide,
|
||||
Enabled {
|
||||
state: AutohideTimerState,
|
||||
show_duration: Duration,
|
||||
@ -21,10 +25,10 @@ pub enum AutohideTimer {
|
||||
}
|
||||
|
||||
impl AutohideTimer {
|
||||
fn trigger_display_timer(&mut self) {
|
||||
fn start_display_timer(&mut self) {
|
||||
match self {
|
||||
AutohideTimer::Disabled => {
|
||||
// Does nothing.
|
||||
AutohideTimer::AlwaysShow | AutohideTimer::AlwaysHide => {
|
||||
// Do nothing.
|
||||
}
|
||||
AutohideTimer::Enabled {
|
||||
state,
|
||||
@ -37,8 +41,8 @@ impl AutohideTimer {
|
||||
|
||||
pub fn update_display_timer(&mut self) {
|
||||
match self {
|
||||
AutohideTimer::Disabled => {
|
||||
// Does nothing.
|
||||
AutohideTimer::AlwaysShow | AutohideTimer::AlwaysHide => {
|
||||
// Do nothing.
|
||||
}
|
||||
AutohideTimer::Enabled {
|
||||
state,
|
||||
@ -70,6 +74,7 @@ pub struct TimeGraph {
|
||||
}
|
||||
|
||||
impl TimeGraph {
|
||||
/// Creates a new [`TimeGraph`]. All time values are in milliseconds.
|
||||
pub fn new(
|
||||
start_value: u64, autohide_timer: AutohideTimer, min_duration: u64, max_duration: u64,
|
||||
time_interval: u64,
|
||||
@ -85,6 +90,26 @@ impl TimeGraph {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`TimeGraph`] given an [`AppConfigFields`].
|
||||
pub fn from_config(app_config_fields: &AppConfigFields) -> Self {
|
||||
Self::new(
|
||||
app_config_fields.default_time_value,
|
||||
if app_config_fields.hide_time {
|
||||
AutohideTimer::AlwaysHide
|
||||
} else if app_config_fields.autohide_time {
|
||||
AutohideTimer::Enabled {
|
||||
state: AutohideTimerState::Running(Instant::now()),
|
||||
show_duration: Duration::from_millis(AUTOHIDE_TIMEOUT_MILLISECONDS),
|
||||
}
|
||||
} else {
|
||||
AutohideTimer::AlwaysShow
|
||||
},
|
||||
STALE_MIN_MILLISECONDS,
|
||||
STALE_MAX_MILLISECONDS,
|
||||
app_config_fields.time_interval,
|
||||
)
|
||||
}
|
||||
|
||||
/// Handles a char `c`.
|
||||
fn handle_char(&mut self, c: char) -> EventResult {
|
||||
match c {
|
||||
@ -100,12 +125,12 @@ impl TimeGraph {
|
||||
|
||||
if new_time >= self.min_duration {
|
||||
self.current_display_time = new_time;
|
||||
self.autohide_timer.trigger_display_timer();
|
||||
self.autohide_timer.start_display_timer();
|
||||
|
||||
EventResult::Redraw
|
||||
} else if new_time != self.min_duration {
|
||||
self.current_display_time = self.min_duration;
|
||||
self.autohide_timer.trigger_display_timer();
|
||||
self.autohide_timer.start_display_timer();
|
||||
|
||||
EventResult::Redraw
|
||||
} else {
|
||||
@ -118,12 +143,12 @@ impl TimeGraph {
|
||||
|
||||
if new_time <= self.max_duration {
|
||||
self.current_display_time = new_time;
|
||||
self.autohide_timer.trigger_display_timer();
|
||||
self.autohide_timer.start_display_timer();
|
||||
|
||||
EventResult::Redraw
|
||||
} else if new_time != self.max_duration {
|
||||
self.current_display_time = self.max_duration;
|
||||
self.autohide_timer.trigger_display_timer();
|
||||
self.autohide_timer.start_display_timer();
|
||||
|
||||
EventResult::Redraw
|
||||
} else {
|
||||
@ -136,7 +161,7 @@ impl TimeGraph {
|
||||
EventResult::NoRedraw
|
||||
} else {
|
||||
self.current_display_time = self.default_time_value;
|
||||
self.autohide_timer.trigger_display_timer();
|
||||
self.autohide_timer.start_display_timer();
|
||||
EventResult::Redraw
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,9 @@ use std::{collections::HashMap, time::Instant};
|
||||
use crossterm::event::{KeyEvent, MouseEvent};
|
||||
use tui::layout::Rect;
|
||||
|
||||
use crate::app::event::EventResult;
|
||||
use crate::app::event::{does_point_intersect_rect, EventResult};
|
||||
|
||||
use super::{
|
||||
does_point_intersect_rect, AppScrollWidgetState, CanvasTableWidthState, Component, TextTable,
|
||||
TimeGraph, Widget,
|
||||
};
|
||||
use super::{AppScrollWidgetState, CanvasTableWidthState, Component, TextTable, TimeGraph, Widget};
|
||||
|
||||
pub struct CpuWidgetState {
|
||||
pub current_display_time: u64,
|
||||
|
@ -1,492 +0,0 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use crossterm::event::{KeyEvent, MouseEvent};
|
||||
use enum_dispatch::enum_dispatch;
|
||||
use indextree::{Arena, NodeId};
|
||||
use tui::{layout::Rect, widgets::TableState};
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
event::{EventResult, SelectionAction},
|
||||
layout_manager::BottomWidgetType,
|
||||
},
|
||||
constants,
|
||||
};
|
||||
|
||||
pub mod base;
|
||||
pub use base::*;
|
||||
|
||||
pub mod process;
|
||||
pub use process::*;
|
||||
|
||||
pub mod net;
|
||||
pub use net::*;
|
||||
|
||||
pub mod mem;
|
||||
pub use mem::*;
|
||||
|
||||
pub mod cpu;
|
||||
pub use cpu::*;
|
||||
|
||||
pub mod disk;
|
||||
pub use disk::*;
|
||||
|
||||
pub mod battery;
|
||||
pub use self::battery::*;
|
||||
|
||||
pub mod temp;
|
||||
pub use temp::*;
|
||||
|
||||
/// A trait for things that are drawn with state.
|
||||
#[enum_dispatch]
|
||||
#[allow(unused_variables)]
|
||||
pub trait Component {
|
||||
/// Handles a [`KeyEvent`].
|
||||
///
|
||||
/// Defaults to returning [`EventResult::NoRedraw`], indicating nothing should be done.
|
||||
fn handle_key_event(&mut self, event: KeyEvent) -> EventResult {
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
|
||||
/// Handles a [`MouseEvent`].
|
||||
///
|
||||
/// Defaults to returning [`EventResult::Continue`], indicating nothing should be done.
|
||||
fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult {
|
||||
EventResult::NoRedraw
|
||||
}
|
||||
|
||||
/// Returns a [`Component`]'s bounding box. Note that these are defined in *global*, *absolute*
|
||||
/// coordinates.
|
||||
fn bounds(&self) -> Rect;
|
||||
|
||||
/// Updates a [`Component`]s bounding box to `new_bounds`.
|
||||
fn set_bounds(&mut self, new_bounds: Rect);
|
||||
}
|
||||
|
||||
/// A trait for actual fully-fledged widgets to be displayed in bottom.
|
||||
#[enum_dispatch]
|
||||
pub trait Widget {
|
||||
/// Updates a [`Widget`] given some data. Defaults to doing nothing.
|
||||
fn update(&mut self) {}
|
||||
|
||||
/// Handles what to do when trying to respond to a widget selection movement to the left.
|
||||
/// Defaults to just moving to the next-possible widget in that direction.
|
||||
fn handle_widget_selection_left(&mut self) -> SelectionAction {
|
||||
SelectionAction::NotHandled
|
||||
}
|
||||
|
||||
/// Handles what to do when trying to respond to a widget selection movement to the right.
|
||||
/// Defaults to just moving to the next-possible widget in that direction.
|
||||
fn handle_widget_selection_right(&mut self) -> SelectionAction {
|
||||
SelectionAction::NotHandled
|
||||
}
|
||||
|
||||
/// Handles what to do when trying to respond to a widget selection movement upward.
|
||||
/// Defaults to just moving to the next-possible widget in that direction.
|
||||
fn handle_widget_selection_up(&mut self) -> SelectionAction {
|
||||
SelectionAction::NotHandled
|
||||
}
|
||||
|
||||
/// Handles what to do when trying to respond to a widget selection movement downward.
|
||||
/// Defaults to just moving to the next-possible widget in that direction.
|
||||
fn handle_widget_selection_down(&mut self) -> SelectionAction {
|
||||
SelectionAction::NotHandled
|
||||
}
|
||||
|
||||
fn get_pretty_name(&self) -> &'static str;
|
||||
}
|
||||
|
||||
/// The "main" widgets that are used by bottom to display information!
|
||||
#[enum_dispatch(Component, Widget)]
|
||||
enum BottomWidget {
|
||||
MemGraph,
|
||||
TempTable,
|
||||
DiskTable,
|
||||
CpuGraph,
|
||||
NetGraph,
|
||||
OldNetGraph,
|
||||
ProcessManager,
|
||||
BatteryTable,
|
||||
}
|
||||
|
||||
/// Checks whether points `(x, y)` intersect a given [`Rect`].
|
||||
pub fn does_point_intersect_rect(x: u16, y: u16, rect: Rect) -> bool {
|
||||
x >= rect.left() && x <= rect.right() && y >= rect.top() && y <= rect.bottom()
|
||||
}
|
||||
|
||||
/// A [`LayoutNode`] represents a single node in the overall widget hierarchy. Each node is one of:
|
||||
/// - [`LayoutNode::Row`] (a a non-leaf that distributes its children horizontally)
|
||||
/// - [`LayoutNode::Col`] (a non-leaf node that distributes its children vertically)
|
||||
/// - [`LayoutNode::Widget`] (a leaf node that contains the ID of the widget it is associated with)
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum LayoutNode {
|
||||
Row(BottomRow),
|
||||
Col(BottomCol),
|
||||
Widget,
|
||||
}
|
||||
|
||||
/// A simple struct representing a row and its state.
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub struct BottomRow {
|
||||
last_selected_index: usize,
|
||||
}
|
||||
|
||||
/// A simple struct representing a column and its state.
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub struct BottomCol {
|
||||
last_selected_index: usize,
|
||||
}
|
||||
|
||||
/// Relative movement direction from the currently selected widget.
|
||||
pub enum MovementDirection {
|
||||
Left,
|
||||
Right,
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
/// Attempts to find and return the selected [`BottomWidgetId`] after moving in a direction.
|
||||
///
|
||||
/// Note this function assumes a properly built tree - if not, bad things may happen! We generally assume that:
|
||||
/// - Only [`LayoutNode::Widget`]s are leaves.
|
||||
/// - Only [`LayoutNode::Row`]s or [`LayoutNode::Col`]s are non-leaves.
|
||||
fn move_widget_selection(
|
||||
layout_tree: &mut Arena<LayoutNode>, current_widget: &mut BottomWidget,
|
||||
current_widget_id: NodeId, direction: MovementDirection,
|
||||
) -> NodeId {
|
||||
// We first give our currently-selected widget a chance to react to the movement - it may handle it internally!
|
||||
let handled = match direction {
|
||||
MovementDirection::Left => current_widget.handle_widget_selection_left(),
|
||||
MovementDirection::Right => current_widget.handle_widget_selection_right(),
|
||||
MovementDirection::Up => current_widget.handle_widget_selection_up(),
|
||||
MovementDirection::Down => current_widget.handle_widget_selection_down(),
|
||||
};
|
||||
|
||||
match handled {
|
||||
SelectionAction::Handled => {
|
||||
// If it was handled by the widget, then we don't have to do anything - return the current one.
|
||||
current_widget_id
|
||||
}
|
||||
SelectionAction::NotHandled => {
|
||||
/// Keeps traversing up the `layout_tree` until it hits a parent where `current_id` is a child and parent
|
||||
/// is a [`LayoutNode::Row`], returning its parent's [`NodeId`] and the child's [`NodeId`] (in that order).
|
||||
/// If this crawl fails (i.e. hits a root, it is an invalid tree for some reason), it returns [`None`].
|
||||
fn find_first_row(
|
||||
layout_tree: &Arena<LayoutNode>, current_id: NodeId,
|
||||
) -> Option<(NodeId, NodeId)> {
|
||||
layout_tree
|
||||
.get(current_id)
|
||||
.and_then(|current_node| current_node.parent())
|
||||
.and_then(|parent_id| {
|
||||
layout_tree
|
||||
.get(parent_id)
|
||||
.map(|parent_node| (parent_id, parent_node))
|
||||
})
|
||||
.and_then(|(parent_id, parent_node)| match parent_node.get() {
|
||||
LayoutNode::Row(_) => Some((parent_id, current_id)),
|
||||
LayoutNode::Col(_) => find_first_row(layout_tree, parent_id),
|
||||
LayoutNode::Widget => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Keeps traversing up the `layout_tree` until it hits a parent where `current_id` is a child and parent
|
||||
/// is a [`LayoutNode::Col`], returning its parent's [`NodeId`] and the child's [`NodeId`] (in that order).
|
||||
/// If this crawl fails (i.e. hits a root, it is an invalid tree for some reason), it returns [`None`].
|
||||
fn find_first_col(
|
||||
layout_tree: &Arena<LayoutNode>, current_id: NodeId,
|
||||
) -> Option<(NodeId, NodeId)> {
|
||||
layout_tree
|
||||
.get(current_id)
|
||||
.and_then(|current_node| current_node.parent())
|
||||
.and_then(|parent_id| {
|
||||
layout_tree
|
||||
.get(parent_id)
|
||||
.map(|parent_node| (parent_id, parent_node))
|
||||
})
|
||||
.and_then(|(parent_id, parent_node)| match parent_node.get() {
|
||||
LayoutNode::Row(_) => find_first_col(layout_tree, parent_id),
|
||||
LayoutNode::Col(_) => Some((parent_id, current_id)),
|
||||
LayoutNode::Widget => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Descends to a leaf.
|
||||
fn descend_to_leaf(layout_tree: &Arena<LayoutNode>, current_id: NodeId) -> NodeId {
|
||||
if let Some(current_node) = layout_tree.get(current_id) {
|
||||
match current_node.get() {
|
||||
LayoutNode::Row(BottomRow {
|
||||
last_selected_index,
|
||||
})
|
||||
| LayoutNode::Col(BottomCol {
|
||||
last_selected_index,
|
||||
}) => {
|
||||
if let Some(next_child) =
|
||||
current_id.children(layout_tree).nth(*last_selected_index)
|
||||
{
|
||||
descend_to_leaf(layout_tree, next_child)
|
||||
} else {
|
||||
current_id
|
||||
}
|
||||
}
|
||||
LayoutNode::Widget => {
|
||||
// Halt!
|
||||
current_id
|
||||
}
|
||||
}
|
||||
} else {
|
||||
current_id
|
||||
}
|
||||
}
|
||||
|
||||
// If it was NOT handled by the current widget, then move in the correct direction; we can rely
|
||||
// on the tree layout to help us decide where to go.
|
||||
// Movement logic is inspired by i3. When we enter a new column/row, we go to the *last* selected
|
||||
// element; if we can't, go to the nearest one.
|
||||
match direction {
|
||||
MovementDirection::Left => {
|
||||
// When we move "left":
|
||||
// 1. Look for the parent of the current widget.
|
||||
// 2. Depending on whether it is a Row or Col:
|
||||
// a) If we are in a Row, try to move to the child (it can be a Row, Col, or Widget) before it,
|
||||
// and update the last-selected index. If we can't (i.e. we are the first element), then
|
||||
// instead move to the parent, and try again to select the element before it. If there is
|
||||
// no parent (i.e. we hit the root), then just return the original index.
|
||||
// b) If we are in a Col, then just try to move to the parent. If there is no
|
||||
// parent (i.e. we hit the root), then just return the original index.
|
||||
// c) A Widget should be impossible to select.
|
||||
// 3. Assuming we have now selected a new child, then depending on what the child is:
|
||||
// a) If we are in a Row or Col, then take the last selected index, and repeat step 3 until you hit
|
||||
// a Widget.
|
||||
// b) If we are in a Widget, return the corresponding NodeId.
|
||||
|
||||
fn find_left(
|
||||
layout_tree: &mut Arena<LayoutNode>, current_id: NodeId,
|
||||
) -> NodeId {
|
||||
if let Some((parent_id, child_id)) = find_first_row(layout_tree, current_id)
|
||||
{
|
||||
if let Some(prev_sibling) =
|
||||
child_id.preceding_siblings(layout_tree).nth(1)
|
||||
{
|
||||
// Subtract one from the currently selected index...
|
||||
if let Some(parent) = layout_tree.get_mut(parent_id) {
|
||||
if let LayoutNode::Row(row) = parent.get_mut() {
|
||||
row.last_selected_index =
|
||||
row.last_selected_index.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Now descend downwards!
|
||||
descend_to_leaf(layout_tree, prev_sibling)
|
||||
} else {
|
||||
// Darn, we can't go further back! Recurse on this ID.
|
||||
find_left(layout_tree, child_id)
|
||||
}
|
||||
} else {
|
||||
// Failed, just return the current ID.
|
||||
current_id
|
||||
}
|
||||
}
|
||||
find_left(layout_tree, current_widget_id)
|
||||
}
|
||||
MovementDirection::Right => {
|
||||
// When we move "right", repeat the steps for "left", but instead try to move to the child *after*
|
||||
// it in all cases.
|
||||
|
||||
fn find_right(
|
||||
layout_tree: &mut Arena<LayoutNode>, current_id: NodeId,
|
||||
) -> NodeId {
|
||||
if let Some((parent_id, child_id)) = find_first_row(layout_tree, current_id)
|
||||
{
|
||||
if let Some(prev_sibling) =
|
||||
child_id.following_siblings(layout_tree).nth(1)
|
||||
{
|
||||
// Add one to the currently selected index...
|
||||
if let Some(parent) = layout_tree.get_mut(parent_id) {
|
||||
if let LayoutNode::Row(row) = parent.get_mut() {
|
||||
row.last_selected_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Now descend downwards!
|
||||
descend_to_leaf(layout_tree, prev_sibling)
|
||||
} else {
|
||||
// Darn, we can't go further back! Recurse on this ID.
|
||||
find_right(layout_tree, child_id)
|
||||
}
|
||||
} else {
|
||||
// Failed, just return the current ID.
|
||||
current_id
|
||||
}
|
||||
}
|
||||
find_right(layout_tree, current_widget_id)
|
||||
}
|
||||
MovementDirection::Up => {
|
||||
// When we move "up", copy the steps for "left", but switch "Row" and "Col". We instead want to move
|
||||
// vertically, so we want to now avoid Rows and look for Cols!
|
||||
|
||||
fn find_above(
|
||||
layout_tree: &mut Arena<LayoutNode>, current_id: NodeId,
|
||||
) -> NodeId {
|
||||
if let Some((parent_id, child_id)) = find_first_col(layout_tree, current_id)
|
||||
{
|
||||
if let Some(prev_sibling) =
|
||||
child_id.preceding_siblings(layout_tree).nth(1)
|
||||
{
|
||||
// Subtract one from the currently selected index...
|
||||
if let Some(parent) = layout_tree.get_mut(parent_id) {
|
||||
if let LayoutNode::Col(row) = parent.get_mut() {
|
||||
row.last_selected_index =
|
||||
row.last_selected_index.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Now descend downwards!
|
||||
descend_to_leaf(layout_tree, prev_sibling)
|
||||
} else {
|
||||
// Darn, we can't go further back! Recurse on this ID.
|
||||
find_above(layout_tree, child_id)
|
||||
}
|
||||
} else {
|
||||
// Failed, just return the current ID.
|
||||
current_id
|
||||
}
|
||||
}
|
||||
find_above(layout_tree, current_widget_id)
|
||||
}
|
||||
MovementDirection::Down => {
|
||||
// See "up"'s steps, but now we're going for the child *after* the currently selected one in all
|
||||
// cases.
|
||||
|
||||
fn find_below(
|
||||
layout_tree: &mut Arena<LayoutNode>, current_id: NodeId,
|
||||
) -> NodeId {
|
||||
if let Some((parent_id, child_id)) = find_first_col(layout_tree, current_id)
|
||||
{
|
||||
if let Some(prev_sibling) =
|
||||
child_id.following_siblings(layout_tree).nth(1)
|
||||
{
|
||||
// Add one to the currently selected index...
|
||||
if let Some(parent) = layout_tree.get_mut(parent_id) {
|
||||
if let LayoutNode::Col(row) = parent.get_mut() {
|
||||
row.last_selected_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Now descend downwards!
|
||||
descend_to_leaf(layout_tree, prev_sibling)
|
||||
} else {
|
||||
// Darn, we can't go further back! Recurse on this ID.
|
||||
find_below(layout_tree, child_id)
|
||||
}
|
||||
} else {
|
||||
// Failed, just return the current ID.
|
||||
current_id
|
||||
}
|
||||
}
|
||||
find_below(layout_tree, current_widget_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Old stuff below -----
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ScrollDirection {
|
||||
// UP means scrolling up --- this usually DECREMENTS
|
||||
Up,
|
||||
// DOWN means scrolling down --- this usually INCREMENTS
|
||||
Down,
|
||||
}
|
||||
|
||||
impl Default for ScrollDirection {
|
||||
fn default() -> Self {
|
||||
ScrollDirection::Down
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CursorDirection {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
/// AppScrollWidgetState deals with fields for a scrollable app's current state.
|
||||
#[derive(Default)]
|
||||
pub struct AppScrollWidgetState {
|
||||
pub current_scroll_position: usize,
|
||||
pub previous_scroll_position: usize,
|
||||
pub scroll_direction: ScrollDirection,
|
||||
pub table_state: TableState,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum KillSignal {
|
||||
Cancel,
|
||||
Kill(usize),
|
||||
}
|
||||
|
||||
impl Default for KillSignal {
|
||||
#[cfg(target_family = "unix")]
|
||||
fn default() -> Self {
|
||||
KillSignal::Kill(15)
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
fn default() -> Self {
|
||||
KillSignal::Kill(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AppDeleteDialogState {
|
||||
pub is_showing_dd: bool,
|
||||
pub selected_signal: KillSignal,
|
||||
/// tl x, tl y, br x, br y, index/signal
|
||||
pub button_positions: Vec<(u16, u16, u16, u16, usize)>,
|
||||
pub keyboard_signal_select: usize,
|
||||
pub last_number_press: Option<Instant>,
|
||||
pub scroll_pos: usize,
|
||||
}
|
||||
|
||||
pub struct AppHelpDialogState {
|
||||
pub is_showing_help: bool,
|
||||
pub scroll_state: ParagraphScrollState,
|
||||
pub index_shortcuts: Vec<u16>,
|
||||
}
|
||||
|
||||
impl Default for AppHelpDialogState {
|
||||
fn default() -> Self {
|
||||
AppHelpDialogState {
|
||||
is_showing_help: false,
|
||||
scroll_state: ParagraphScrollState::default(),
|
||||
index_shortcuts: vec![0; constants::HELP_TEXT.len()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Meant for canvas operations involving table column widths.
|
||||
#[derive(Default)]
|
||||
pub struct CanvasTableWidthState {
|
||||
pub desired_column_widths: Vec<u16>,
|
||||
pub calculated_column_widths: Vec<u16>,
|
||||
}
|
||||
|
||||
pub struct BasicTableWidgetState {
|
||||
// Since this is intended (currently) to only be used for ONE widget, that's
|
||||
// how it's going to be written. If we want to allow for multiple of these,
|
||||
// then we can expand outwards with a normal BasicTableState and a hashmap
|
||||
pub currently_displayed_widget_type: BottomWidgetType,
|
||||
pub currently_displayed_widget_id: u64,
|
||||
pub widget_id: i64,
|
||||
pub left_tlc: Option<(u16, u16)>,
|
||||
pub left_brc: Option<(u16, u16)>,
|
||||
pub right_tlc: Option<(u16, u16)>,
|
||||
pub right_brc: Option<(u16, u16)>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ParagraphScrollState {
|
||||
pub current_scroll_index: u16,
|
||||
pub max_scroll_index: u16,
|
||||
}
|
@ -154,15 +154,15 @@ impl Widget for NetGraph {
|
||||
/// A widget denoting network usage via a graph and a separate, single row table. This is built on [`NetGraph`],
|
||||
/// and the main difference is that it also contains a bounding box for the graph + text.
|
||||
pub struct OldNetGraph {
|
||||
graph: NetGraph,
|
||||
net_graph: NetGraph,
|
||||
bounds: Rect,
|
||||
}
|
||||
|
||||
impl OldNetGraph {
|
||||
/// Creates a new [`OldNetGraph`].
|
||||
pub fn new(graph: NetGraph) -> Self {
|
||||
pub fn new(graph: TimeGraph) -> Self {
|
||||
Self {
|
||||
graph,
|
||||
net_graph: NetGraph::new(graph),
|
||||
bounds: Rect::default(),
|
||||
}
|
||||
}
|
||||
@ -180,13 +180,13 @@ impl Component for OldNetGraph {
|
||||
fn handle_key_event(
|
||||
&mut self, event: crossterm::event::KeyEvent,
|
||||
) -> crate::app::event::EventResult {
|
||||
self.graph.handle_key_event(event)
|
||||
self.net_graph.handle_key_event(event)
|
||||
}
|
||||
|
||||
fn handle_mouse_event(
|
||||
&mut self, event: crossterm::event::MouseEvent,
|
||||
) -> crate::app::event::EventResult {
|
||||
self.graph.handle_mouse_event(event)
|
||||
self.net_graph.handle_mouse_event(event)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ use tui::{layout::Rect, widgets::TableState};
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
event::{EventResult, MultiKey, MultiKeyResult},
|
||||
event::{does_point_intersect_rect, EventResult, MultiKey, MultiKeyResult},
|
||||
query::*,
|
||||
},
|
||||
data_harvester::processes::{self, ProcessSorting},
|
||||
@ -15,8 +15,8 @@ use crate::{
|
||||
use ProcessSorting::*;
|
||||
|
||||
use super::{
|
||||
does_point_intersect_rect, AppScrollWidgetState, CanvasTableWidthState, Component,
|
||||
CursorDirection, ScrollDirection, TextInput, TextTable, Widget,
|
||||
AppScrollWidgetState, CanvasTableWidthState, Component, CursorDirection, ScrollDirection,
|
||||
TextInput, TextTable, Widget,
|
||||
};
|
||||
|
||||
/// AppSearchState deals with generic searching (I might do this in the future).
|
||||
@ -652,8 +652,8 @@ impl ProcessManager {
|
||||
pub fn new(default_in_tree_mode: bool) -> Self {
|
||||
Self {
|
||||
bounds: Rect::default(),
|
||||
process_table: TextTable::new(0, vec![]), // TODO: Do this
|
||||
sort_table: TextTable::new(0, vec![]), // TODO: Do this too
|
||||
process_table: TextTable::new(vec![]), // TODO: Do this
|
||||
sort_table: TextTable::new(vec![]), // TODO: Do this too
|
||||
search_input: TextInput::new(),
|
||||
dd_multi: MultiKey::register(vec!['d', 'd']), // TODO: Use a static arrayvec
|
||||
selected: ProcessManagerSelection::Processes,
|
||||
|
@ -39,18 +39,12 @@ fn main() -> Result<()> {
|
||||
.context("Unable to properly parse or create the config file.")?;
|
||||
|
||||
// Get widget layout separately
|
||||
let (widget_layout, default_widget_id, default_widget_type_option) =
|
||||
let (widget_layout, _default_widget_id, _default_widget_type_option) =
|
||||
get_widget_layout(&matches, &config)
|
||||
.context("Found an issue while trying to build the widget layout.")?;
|
||||
|
||||
// Create "app" struct, which will control most of the program and store settings/state
|
||||
let mut app = build_app(
|
||||
&matches,
|
||||
&mut config,
|
||||
&widget_layout,
|
||||
default_widget_id,
|
||||
&default_widget_type_option,
|
||||
)?;
|
||||
let mut app = build_app(&matches, &mut config)?;
|
||||
|
||||
// Create painter and set colours.
|
||||
let mut painter = canvas::Painter::init(
|
||||
@ -67,10 +61,11 @@ fn main() -> Result<()> {
|
||||
let thread_termination_cvar = Arc::new(Condvar::new());
|
||||
|
||||
// Set up input handling
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let (sender, receiver) = mpsc::channel(); // FIXME: Make this bounded, prevents overloading.
|
||||
let _input_thread = create_input_thread(sender.clone(), thread_termination_lock.clone());
|
||||
|
||||
// Cleaning loop
|
||||
// TODO: Probably worth spinning this off into an async thread or something...
|
||||
let _cleaning_thread = {
|
||||
let lock = thread_termination_lock.clone();
|
||||
let cvar = thread_termination_cvar.clone();
|
||||
@ -140,7 +135,7 @@ fn main() -> Result<()> {
|
||||
force_redraw(&mut app);
|
||||
try_drawing(&mut terminal, &mut app, &mut painter)?;
|
||||
}
|
||||
EventResult::NoRedraw => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
BottomEvent::MouseInput(event) => match handle_mouse_event(event, &mut app) {
|
||||
@ -152,7 +147,7 @@ fn main() -> Result<()> {
|
||||
force_redraw(&mut app);
|
||||
try_drawing(&mut terminal, &mut app, &mut painter)?;
|
||||
}
|
||||
EventResult::NoRedraw => {}
|
||||
_ => {}
|
||||
},
|
||||
BottomEvent::Update(data) => {
|
||||
app.data_collection.eat_data(data);
|
||||
|
19
src/lib.rs
19
src/lib.rs
@ -32,8 +32,8 @@ use crossterm::{
|
||||
use app::{
|
||||
data_harvester::{self, processes::ProcessSorting},
|
||||
event::EventResult,
|
||||
layout_manager::{UsedWidgets, WidgetDirection},
|
||||
AppState,
|
||||
layout_manager::WidgetDirection,
|
||||
AppState, UsedWidgets,
|
||||
};
|
||||
use constants::*;
|
||||
use data_conversion::*;
|
||||
@ -60,9 +60,9 @@ pub type Pid = usize;
|
||||
pub type Pid = libc::pid_t;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BottomEvent<I, J> {
|
||||
KeyInput(I),
|
||||
MouseInput(J),
|
||||
pub enum BottomEvent {
|
||||
KeyInput(KeyEvent),
|
||||
MouseInput(MouseEvent),
|
||||
Update(Box<data_harvester::Data>),
|
||||
Clean,
|
||||
}
|
||||
@ -598,10 +598,7 @@ fn sort_process_data(
|
||||
}
|
||||
|
||||
pub fn create_input_thread(
|
||||
sender: std::sync::mpsc::Sender<
|
||||
BottomEvent<crossterm::event::KeyEvent, crossterm::event::MouseEvent>,
|
||||
>,
|
||||
termination_ctrl_lock: Arc<Mutex<bool>>,
|
||||
sender: std::sync::mpsc::Sender<BottomEvent>, termination_ctrl_lock: Arc<Mutex<bool>>,
|
||||
) -> std::thread::JoinHandle<()> {
|
||||
thread::spawn(move || {
|
||||
let mut mouse_timer = Instant::now();
|
||||
@ -646,9 +643,7 @@ pub fn create_input_thread(
|
||||
}
|
||||
|
||||
pub fn create_collection_thread(
|
||||
sender: std::sync::mpsc::Sender<
|
||||
BottomEvent<crossterm::event::KeyEvent, crossterm::event::MouseEvent>,
|
||||
>,
|
||||
sender: std::sync::mpsc::Sender<BottomEvent>,
|
||||
control_receiver: std::sync::mpsc::Receiver<ThreadControlEvent>,
|
||||
termination_ctrl_lock: Arc<Mutex<bool>>, termination_ctrl_cvar: Arc<Condvar>,
|
||||
app_config_fields: &app::AppConfigFields, filters: app::DataFilters,
|
||||
|
300
src/options.rs
300
src/options.rs
@ -1,10 +1,6 @@
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
str::FromStr,
|
||||
time::Instant,
|
||||
};
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use crate::{
|
||||
app::{layout_manager::*, *},
|
||||
@ -178,169 +174,39 @@ pub struct IgnoreList {
|
||||
pub whole_word: bool,
|
||||
}
|
||||
|
||||
pub fn build_app(
|
||||
matches: &clap::ArgMatches<'static>, config: &mut Config, widget_layout: &BottomLayout,
|
||||
default_widget_id: u64, default_widget_type_option: &Option<BottomWidgetType>,
|
||||
) -> Result<AppState> {
|
||||
use BottomWidgetType::*;
|
||||
/// Represents the default states of all process widgets.
|
||||
pub struct ProcessDefaults {
|
||||
pub is_grouped: bool,
|
||||
pub is_case_sensitive: bool,
|
||||
pub is_match_whole_word: bool,
|
||||
pub is_use_regex: bool,
|
||||
pub is_show_mem_as_values: bool,
|
||||
pub is_tree: bool,
|
||||
pub is_command: bool,
|
||||
}
|
||||
|
||||
pub fn build_app(matches: &clap::ArgMatches<'static>, config: &mut Config) -> Result<AppState> {
|
||||
// Process defaults
|
||||
let process_defaults = ProcessDefaults {
|
||||
is_grouped: get_process_grouping(matches, config),
|
||||
is_case_sensitive: get_case_sensitive(matches, config),
|
||||
is_match_whole_word: get_match_whole_word(matches, config),
|
||||
is_use_regex: get_use_regex(matches, config),
|
||||
is_show_mem_as_values: get_mem_as_value(matches, config),
|
||||
is_tree: get_is_default_tree(matches, config),
|
||||
is_command: get_is_default_process_command(matches, config),
|
||||
};
|
||||
|
||||
// App config fields
|
||||
let autohide_time = get_autohide_time(matches, config);
|
||||
let default_time_value = get_default_time_value(matches, config)
|
||||
.context("Update 'default_time_value' in your config file.")?;
|
||||
let use_basic_mode = get_use_basic_mode(matches, config);
|
||||
|
||||
// For processes
|
||||
let is_grouped = get_app_grouping(matches, config);
|
||||
let is_case_sensitive = get_app_case_sensitive(matches, config);
|
||||
let is_match_whole_word = get_app_match_whole_word(matches, config);
|
||||
let is_use_regex = get_app_use_regex(matches, config);
|
||||
|
||||
let mut widget_map = HashMap::new();
|
||||
let mut cpu_state_map: HashMap<u64, CpuWidgetState> = HashMap::new();
|
||||
let mut mem_state_map: HashMap<u64, MemWidgetState> = HashMap::new();
|
||||
let mut net_state_map: HashMap<u64, NetWidgetState> = HashMap::new();
|
||||
let mut proc_state_map: HashMap<u64, ProcWidgetState> = HashMap::new();
|
||||
let mut temp_state_map: HashMap<u64, TempWidgetState> = HashMap::new();
|
||||
let mut disk_state_map: HashMap<u64, DiskWidgetState> = HashMap::new();
|
||||
let mut battery_state_map: HashMap<u64, BatteryWidgetState> = HashMap::new();
|
||||
|
||||
let autohide_timer = if autohide_time {
|
||||
Some(Instant::now())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut initial_widget_id: u64 = default_widget_id;
|
||||
let mut initial_widget_type = Proc;
|
||||
let is_custom_layout = config.row.is_some();
|
||||
let mut used_widget_set = HashSet::new();
|
||||
|
||||
let show_memory_as_values = get_mem_as_value(matches, config);
|
||||
let is_default_tree = get_is_default_tree(matches, config);
|
||||
let is_default_command = get_is_default_process_command(matches, config);
|
||||
let is_advanced_kill = !get_is_advanced_kill_disabled(matches, config);
|
||||
|
||||
let network_unit_type = get_network_unit_type(matches, config);
|
||||
let network_scale_type = get_network_scale_type(matches, config);
|
||||
let network_use_binary_prefix = get_network_use_binary_prefix(matches, config);
|
||||
|
||||
for row in &widget_layout.rows {
|
||||
for col in &row.children {
|
||||
for col_row in &col.children {
|
||||
for widget in &col_row.children {
|
||||
widget_map.insert(widget.widget_id, widget.clone());
|
||||
if let Some(default_widget_type) = &default_widget_type_option {
|
||||
if !is_custom_layout || use_basic_mode {
|
||||
match widget.widget_type {
|
||||
BasicCpu => {
|
||||
if let Cpu = *default_widget_type {
|
||||
initial_widget_id = widget.widget_id;
|
||||
initial_widget_type = Cpu;
|
||||
}
|
||||
}
|
||||
BasicMem => {
|
||||
if let Mem = *default_widget_type {
|
||||
initial_widget_id = widget.widget_id;
|
||||
initial_widget_type = Cpu;
|
||||
}
|
||||
}
|
||||
BasicNet => {
|
||||
if let Net = *default_widget_type {
|
||||
initial_widget_id = widget.widget_id;
|
||||
initial_widget_type = Cpu;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if *default_widget_type == widget.widget_type {
|
||||
initial_widget_id = widget.widget_id;
|
||||
initial_widget_type = widget.widget_type.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
used_widget_set.insert(widget.widget_type.clone());
|
||||
|
||||
match widget.widget_type {
|
||||
Cpu => {
|
||||
cpu_state_map.insert(
|
||||
widget.widget_id,
|
||||
CpuWidgetState::init(default_time_value, autohide_timer),
|
||||
);
|
||||
}
|
||||
Mem => {
|
||||
mem_state_map.insert(
|
||||
widget.widget_id,
|
||||
MemWidgetState::init(default_time_value, autohide_timer),
|
||||
);
|
||||
}
|
||||
Net => {
|
||||
net_state_map.insert(
|
||||
widget.widget_id,
|
||||
NetWidgetState::init(
|
||||
default_time_value,
|
||||
autohide_timer,
|
||||
// network_unit_type.clone(),
|
||||
// network_scale_type.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
Proc => {
|
||||
proc_state_map.insert(
|
||||
widget.widget_id,
|
||||
ProcWidgetState::init(
|
||||
is_case_sensitive,
|
||||
is_match_whole_word,
|
||||
is_use_regex,
|
||||
is_grouped,
|
||||
show_memory_as_values,
|
||||
is_default_tree,
|
||||
is_default_command,
|
||||
),
|
||||
);
|
||||
}
|
||||
Disk => {
|
||||
disk_state_map.insert(widget.widget_id, DiskWidgetState::init());
|
||||
}
|
||||
Temp => {
|
||||
temp_state_map.insert(widget.widget_id, TempWidgetState::init());
|
||||
}
|
||||
Battery => {
|
||||
battery_state_map
|
||||
.insert(widget.widget_id, BatteryWidgetState::default());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let basic_table_widget_state = if use_basic_mode {
|
||||
Some(match initial_widget_type {
|
||||
Proc | Disk | Temp => BasicTableWidgetState {
|
||||
currently_displayed_widget_type: initial_widget_type,
|
||||
currently_displayed_widget_id: initial_widget_id,
|
||||
widget_id: 100,
|
||||
left_tlc: None,
|
||||
left_brc: None,
|
||||
right_tlc: None,
|
||||
right_brc: None,
|
||||
},
|
||||
_ => BasicTableWidgetState {
|
||||
currently_displayed_widget_type: Proc,
|
||||
currently_displayed_widget_id: DEFAULT_WIDGET_ID,
|
||||
widget_id: 100,
|
||||
left_tlc: None,
|
||||
left_brc: None,
|
||||
right_tlc: None,
|
||||
right_brc: None,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let app_config_fields = AppConfigFields {
|
||||
update_rate_in_milliseconds: get_update_rate_in_milliseconds(matches, config)
|
||||
.context("Update 'rate' in your config file.")?,
|
||||
@ -372,14 +238,20 @@ pub fn build_app(
|
||||
network_use_binary_prefix,
|
||||
};
|
||||
|
||||
let used_widgets = UsedWidgets {
|
||||
use_cpu: used_widget_set.get(&Cpu).is_some() || used_widget_set.get(&BasicCpu).is_some(),
|
||||
use_mem: used_widget_set.get(&Mem).is_some() || used_widget_set.get(&BasicMem).is_some(),
|
||||
use_net: used_widget_set.get(&Net).is_some() || used_widget_set.get(&BasicNet).is_some(),
|
||||
use_proc: used_widget_set.get(&Proc).is_some(),
|
||||
use_disk: used_widget_set.get(&Disk).is_some(),
|
||||
use_temp: used_widget_set.get(&Temp).is_some(),
|
||||
use_battery: used_widget_set.get(&Battery).is_some(),
|
||||
let layout_tree_output = if get_use_basic_mode(matches, config) {
|
||||
todo!()
|
||||
} else if let Some(row) = &config.row {
|
||||
create_layout_tree(row, process_defaults, &app_config_fields)?
|
||||
} else {
|
||||
if get_use_battery(matches, config) {
|
||||
let rows = toml::from_str::<Config>(DEFAULT_BATTERY_LAYOUT)?
|
||||
.row
|
||||
.unwrap();
|
||||
create_layout_tree(&rows, process_defaults, &app_config_fields)?
|
||||
} else {
|
||||
let rows = toml::from_str::<Config>(DEFAULT_LAYOUT)?.row.unwrap();
|
||||
create_layout_tree(&rows, process_defaults, &app_config_fields)?
|
||||
}
|
||||
};
|
||||
|
||||
let disk_filter =
|
||||
@ -390,63 +262,39 @@ pub fn build_app(
|
||||
get_ignore_list(&config.temp_filter).context("Update 'temp_filter' in your config file")?;
|
||||
let net_filter =
|
||||
get_ignore_list(&config.net_filter).context("Update 'net_filter' in your config file")?;
|
||||
let data_filter = DataFilters {
|
||||
disk_filter,
|
||||
mount_filter,
|
||||
temp_filter,
|
||||
net_filter,
|
||||
};
|
||||
|
||||
// One more thing - we have to update the search settings of our proc_state_map, and create the hashmaps if needed!
|
||||
// Note that if you change your layout, this might not actually match properly... not sure if/where we should deal with that...
|
||||
if let Some(flags) = &mut config.flags {
|
||||
if flags.case_sensitive.is_none() && !matches.is_present("case_sensitive") {
|
||||
if let Some(search_case_enabled_widgets) = &flags.search_case_enabled_widgets {
|
||||
for widget in search_case_enabled_widgets {
|
||||
if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) {
|
||||
proc_widget.process_search_state.is_ignoring_case = !widget.enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ok(AppState::builder()
|
||||
// .app_config_fields(app_config_fields)
|
||||
// .cpu_state(CpuState::init(cpu_state_map))
|
||||
// .mem_state(MemState::init(mem_state_map))
|
||||
// .net_state(NetState::init(net_state_map))
|
||||
// .proc_state(ProcState::init(proc_state_map))
|
||||
// .disk_state(DiskState::init(disk_state_map))
|
||||
// .temp_state(TempState::init(temp_state_map))
|
||||
// .battery_state(BatteryState::init(battery_state_map))
|
||||
// .basic_table_widget_state(basic_table_widget_state)
|
||||
// .current_widget(widget_map.get(&initial_widget_id).unwrap().clone()) // TODO: [UNWRAP] - many of the unwraps are fine (like this one) but do a once-over and/or switch to expect?
|
||||
// .widget_map(widget_map)
|
||||
// .used_widgets(used_widgets)
|
||||
// .filters(DataFilters {
|
||||
// disk_filter,
|
||||
// mount_filter,
|
||||
// temp_filter,
|
||||
// net_filter,
|
||||
// })
|
||||
// .build())
|
||||
|
||||
if flags.whole_word.is_none() && !matches.is_present("whole_word") {
|
||||
if let Some(search_whole_word_enabled_widgets) =
|
||||
&flags.search_whole_word_enabled_widgets
|
||||
{
|
||||
for widget in search_whole_word_enabled_widgets {
|
||||
if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) {
|
||||
proc_widget.process_search_state.is_searching_whole_word = widget.enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if flags.regex.is_none() && !matches.is_present("regex") {
|
||||
if let Some(search_regex_enabled_widgets) = &flags.search_regex_enabled_widgets {
|
||||
for widget in search_regex_enabled_widgets {
|
||||
if let Some(proc_widget) = proc_state_map.get_mut(&widget.id) {
|
||||
proc_widget.process_search_state.is_searching_with_regex = widget.enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AppState::builder()
|
||||
.app_config_fields(app_config_fields)
|
||||
.cpu_state(CpuState::init(cpu_state_map))
|
||||
.mem_state(MemState::init(mem_state_map))
|
||||
.net_state(NetState::init(net_state_map))
|
||||
.proc_state(ProcState::init(proc_state_map))
|
||||
.disk_state(DiskState::init(disk_state_map))
|
||||
.temp_state(TempState::init(temp_state_map))
|
||||
.battery_state(BatteryState::init(battery_state_map))
|
||||
.basic_table_widget_state(basic_table_widget_state)
|
||||
.current_widget(widget_map.get(&initial_widget_id).unwrap().clone()) // TODO: [UNWRAP] - many of the unwraps are fine (like this one) but do a once-over and/or switch to expect?
|
||||
.widget_map(widget_map)
|
||||
.used_widgets(used_widgets)
|
||||
.filters(DataFilters {
|
||||
disk_filter,
|
||||
mount_filter,
|
||||
temp_filter,
|
||||
net_filter,
|
||||
})
|
||||
.build())
|
||||
Ok(AppState::new(
|
||||
app_config_fields,
|
||||
data_filter,
|
||||
layout_tree_output,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn get_widget_layout(
|
||||
@ -684,7 +532,7 @@ fn get_time_interval(matches: &clap::ArgMatches<'static>, config: &Config) -> er
|
||||
Ok(time_interval as u64)
|
||||
}
|
||||
|
||||
pub fn get_app_grouping(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
|
||||
pub fn get_process_grouping(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
|
||||
if matches.is_present("group") {
|
||||
return true;
|
||||
} else if let Some(flags) = &config.flags {
|
||||
@ -695,7 +543,7 @@ pub fn get_app_grouping(matches: &clap::ArgMatches<'static>, config: &Config) ->
|
||||
false
|
||||
}
|
||||
|
||||
pub fn get_app_case_sensitive(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
|
||||
pub fn get_case_sensitive(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
|
||||
if matches.is_present("case_sensitive") {
|
||||
return true;
|
||||
} else if let Some(flags) = &config.flags {
|
||||
@ -706,7 +554,7 @@ pub fn get_app_case_sensitive(matches: &clap::ArgMatches<'static>, config: &Conf
|
||||
false
|
||||
}
|
||||
|
||||
pub fn get_app_match_whole_word(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
|
||||
pub fn get_match_whole_word(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
|
||||
if matches.is_present("whole_word") {
|
||||
return true;
|
||||
} else if let Some(flags) = &config.flags {
|
||||
@ -717,7 +565,7 @@ pub fn get_app_match_whole_word(matches: &clap::ArgMatches<'static>, config: &Co
|
||||
false
|
||||
}
|
||||
|
||||
pub fn get_app_use_regex(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
|
||||
pub fn get_use_regex(matches: &clap::ArgMatches<'static>, config: &Config) -> bool {
|
||||
if matches.is_present("regex") {
|
||||
return true;
|
||||
} else if let Some(flags) = &config.flags {
|
||||
|
@ -16,7 +16,7 @@ impl Row {
|
||||
&self, iter_id: &mut u64, total_height_ratio: &mut u32, default_widget_id: &mut u64,
|
||||
default_widget_type: &Option<BottomWidgetType>, default_widget_count: &mut u64,
|
||||
left_legend: bool,
|
||||
) -> Result<BottomRow> {
|
||||
) -> Result<OldBottomRow> {
|
||||
// TODO: In the future we want to also add percentages.
|
||||
// But for MVP, we aren't going to bother.
|
||||
let row_ratio = self.ratio.unwrap_or(1);
|
||||
@ -55,7 +55,7 @@ impl Row {
|
||||
BottomWidgetType::Cpu => {
|
||||
let cpu_id = *iter_id;
|
||||
*iter_id += 1;
|
||||
BottomCol::builder()
|
||||
OldBottomCol::builder()
|
||||
.col_width_ratio(width_ratio)
|
||||
.children(if left_legend {
|
||||
vec![BottomColRow::builder()
|
||||
@ -108,7 +108,7 @@ impl Row {
|
||||
let proc_id = *iter_id;
|
||||
let proc_search_id = *iter_id + 1;
|
||||
*iter_id += 2;
|
||||
BottomCol::builder()
|
||||
OldBottomCol::builder()
|
||||
.total_col_row_ratio(2)
|
||||
.col_width_ratio(width_ratio)
|
||||
.children(vec![
|
||||
@ -144,7 +144,7 @@ impl Row {
|
||||
])
|
||||
.build()
|
||||
}
|
||||
_ => BottomCol::builder()
|
||||
_ => OldBottomCol::builder()
|
||||
.col_width_ratio(width_ratio)
|
||||
.children(vec![BottomColRow::builder()
|
||||
.children(vec![BottomWidget::builder()
|
||||
@ -310,7 +310,7 @@ impl Row {
|
||||
}
|
||||
|
||||
children.push(
|
||||
BottomCol::builder()
|
||||
OldBottomCol::builder()
|
||||
.total_col_row_ratio(total_col_row_ratio)
|
||||
.col_width_ratio(col_width_ratio)
|
||||
.children(col_row_children)
|
||||
@ -321,7 +321,7 @@ impl Row {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(BottomRow::builder()
|
||||
Ok(OldBottomRow::builder()
|
||||
.total_col_ratio(total_col_ratio)
|
||||
.row_height_ratio(row_ratio)
|
||||
.children(children)
|
||||
|
Loading…
x
Reference in New Issue
Block a user