feature: Add ability to filter out disks and temp (#220)

You can now filter out disks and temp sensors by name via config.
This commit is contained in:
Clement Tsang 2020-09-02 22:02:49 -04:00 committed by GitHub
parent a949073728
commit cef3166cf8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 217 additions and 52 deletions

View file

@ -40,6 +40,7 @@
"crossterm",
"curr",
"czvf",
"denylist",
"fpath",
"fract",
"gnueabihf",
@ -57,6 +58,7 @@
"noheader",
"ntdef",
"nuget",
"nvme",
"paren",
"pmem",
"prepush",
@ -82,6 +84,7 @@
"virt",
"vsize",
"whitespaces",
"wifi",
"winapi",
"winget",
"winnt",

View file

@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#208](https://github.com/ClementTsang/bottom/pull/208): Mouse support for tables and moving to widgets.
- [#217](https://github.com/ClementTsang/bottom/pull/217): Unofficial ARM support.
- [#220](https://github.com/ClementTsang/bottom/pull/220): Add ability to hide specific temperature and disk entries via config.
### Changes
- [#213](https://github.com/ClementTsang/bottom/pull/213), [#214](https://github.com/ClementTsang/bottom/pull/214): Updated help descriptions, added auto-complete generation.

View file

@ -46,10 +46,9 @@ If you want to help contribute by submitting a PR, by all means, I'm open! In re
- You can check clippy using `cargo clippy`.
- I use [cargo-husky](https://github.com/rhysd/cargo-husky) to automatically run a clippy check on push. You can disable this in the `Cargo.toml` file if you find this annoying.
- I use [cargo-husky](https://github.com/rhysd/cargo-husky) to automatically run a `cargo clippy` and `cargo test` check.
- You may notice that I have fern and log as dependencies; this is mostly for easy debugging via the `debug!()` macro. It writes to the
`debug.log` file that will automatically be created if you run in debug mode (so `cargo run`).
- You may notice that I have fern and log as dependencies; this is mostly for easy debugging via the `debug!()` macro. It writes to the `debug.log` file that will automatically be created if you run in debug mode (so `cargo run`).
And in regards to the pull request process:

View file

@ -48,6 +48,7 @@ A cross-platform graphical process/system monitor with a customizable interface
- [Config flags](#config-flags)
- [Theming](#theming)
- [Layout](#layout)
- [Disk and temperature filtering](#disk-and-temperature-filtering)
- [Battery](#battery)
- [Compatibility](#compatibility)
- [Contribution](#contribution)
@ -352,16 +353,16 @@ Note that the `and` operator takes precedence over the `or` operator.
#### General
| | |
| ------------ | --------------------------------------------------------------------------------------------------------------------- |
| Mouse scroll | Table: Scroll<br>Chart: Zooms in or out by scrolling up or down respectively |
| Mouse click | Selects the clicked widget. For tables, clicking can also select a specific entry. Can be disabled via options/flags. |
| | |
| ------ | ---------------------------------------------------------------------------------------------------------------- |
| Scroll | Table: Scroll<br>Chart: Zooms in or out by scrolling up or down respectively |
| Click | Selects the clicked widget. For tables, clicking can also select an entry.<br>Can be disabled via options/flags. |
#### CPU bindings
| | |
| ------------ | --------------------------------------------------------------------- |
| Mouse scroll | Scrolling over an CPU core/average shows only that entry on the chart |
| | |
| ------ | --------------------------------------------------------------------- |
| Scroll | Scrolling over an CPU core/average shows only that entry on the chart |
## Features
@ -596,6 +597,48 @@ Furthermore, you can have duplicate widgets. This means you could do something l
and get the following CPU donut:
![CPU donut](./assets/cpu_layout.png)
#### Disk and temperature filtering
You can hide specific disks and temperature sensors by name in the config file via `disk_filter` and `temp_filter` respectively. Regex (`regex = true`) and case-sensitivity (`case_sensitive = true`) are supported, but are off by default.
For example, let's say , given this disk list:
![Disk filter not ignoring list](./assets/disk_filter_pre.png)
I wish to _only_ show disks that follow the form `/dev/sda\d+`, or `/dev/nvme0n1p2`:
```toml
[disk_filter]
is_list_ignored = false
list = ["/dev/sda\\d+", "/dev/nvme0n1p2"]
regex = true
```
![Disk filter not ignoring list](./assets/disk_filter_post.png)
This would ignore anything that does not match either of these two conditions. If I instead wish to ignore anything that matches this list, then I can set `is_list_ignored = true` instead:
![Disk filter ignoring list](./assets/disk_filter_post2.png)
Likewise, I can do something similar for `temp_filter`:
![Temp filter before](./assets/temp_filter_pre.png)
If I, say, only wanted to see any entry with the words "cpu" or "wifi" in it, case sensitive:
```toml
[temp_filter]
is_list_ignored = false
list = ["cpu", "wifi"]
case_sensitive = true
```
![Temp filter after](./assets/temp_filter_post.png)
Now, flipping to `case_sensitive = false` would instead show:
![Temp filter after with case sensitivity off](./assets/temp_filter_post2.png)
### Battery
You can get battery statistics (charge, time to fill/discharge, consumption in watts, and battery health) via the battery widget.

BIN
assets/disk_filter_post.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/disk_filter_pre.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 891 KiB

BIN
assets/temp_filter_post.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
assets/temp_filter_pre.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View file

@ -43,6 +43,18 @@ pub struct AppConfigFields {
pub disable_click: bool,
}
/// For filtering out information
pub struct DataFilters {
pub disk_filter: Option<Filter>,
pub temp_filter: Option<Filter>,
}
#[derive(Debug)]
pub struct Filter {
pub is_list_ignored: bool,
pub list: Vec<regex::Regex>,
}
#[derive(TypedBuilder)]
pub struct App {
#[builder(default = false, setter(skip))]
@ -99,6 +111,7 @@ pub struct App {
pub widget_map: HashMap<u64, BottomWidget>,
pub current_widget: BottomWidget,
pub used_widgets: UsedWidgets,
pub filters: DataFilters,
}
impl App {
@ -312,10 +325,9 @@ impl App {
pub fn toggle_sort(&mut self) {
match &self.current_widget.widget_type {
// FIXME: [REFACTOR] Remove these @'s if unneeded, they were an idea but they're ultimately useless for me here...?
widget_type @ BottomWidgetType::Proc | widget_type @ BottomWidgetType::ProcSort => {
BottomWidgetType::Proc | BottomWidgetType::ProcSort => {
let widget_id = self.current_widget.widget_id
- match &widget_type {
- match &self.current_widget.widget_type {
BottomWidgetType::Proc => 0,
BottomWidgetType::ProcSort => 2,
_ => 0,
@ -348,9 +360,9 @@ impl App {
pub fn invert_sort(&mut self) {
match &self.current_widget.widget_type {
widget_type @ BottomWidgetType::Proc | widget_type @ BottomWidgetType::ProcSort => {
BottomWidgetType::Proc | BottomWidgetType::ProcSort => {
let widget_id = self.current_widget.widget_id
- match &widget_type {
- match &self.current_widget.widget_type {
BottomWidgetType::Proc => 0,
BottomWidgetType::ProcSort => 2,
_ => 0,
@ -1571,9 +1583,9 @@ impl App {
}
}
WidgetDirection::Down => match &self.current_widget.widget_type {
proc_type @ BottomWidgetType::Proc | proc_type @ BottomWidgetType::ProcSort => {
BottomWidgetType::Proc | BottomWidgetType::ProcSort => {
let widget_id = self.current_widget.widget_id
- match proc_type {
- match &self.current_widget.widget_type {
BottomWidgetType::ProcSort => 2,
_ => 0,
};

View file

@ -46,7 +46,9 @@ pub async fn get_sysinfo_disk_usage_list(
name: disk.get_name().to_string_lossy().into(),
mount_point: disk.get_mount_point().to_string_lossy().into(),
free_space: disk.get_available_space(),
used_space: disk.get_total_space() - disk.get_available_space(),
used_space: disk
.get_total_space()
.saturating_sub(disk.get_available_space()),
total_space: disk.get_total_space(),
})
.collect::<Vec<DiskHarvest>>();

View file

@ -83,10 +83,7 @@ fn main() -> Result<()> {
create_event_thread(
sender,
reset_receiver,
app.app_config_fields.use_current_cpu_total,
app.app_config_fields.update_rate_in_milliseconds,
app.app_config_fields.temperature_type.clone(),
app.app_config_fields.show_average_cpu,
&app.app_config_fields,
app.used_widgets.clone(),
);
@ -151,7 +148,8 @@ fn main() -> Result<()> {
// Disk
if app.used_widgets.use_disk {
app.canvas_data.disk_data = convert_disk_row(&app.data_collection);
app.canvas_data.disk_data =
convert_disk_row(&app.data_collection, &app.filters.disk_filter);
}
// Temperatures

View file

@ -379,9 +379,9 @@ impl Painter {
app_state.current_widget.widget_id,
false,
),
proc_type @ Proc | proc_type @ ProcSearch | proc_type @ ProcSort => {
Proc | ProcSearch | ProcSort => {
let widget_id = app_state.current_widget.widget_id
- match proc_type {
- match &app_state.current_widget.widget_type {
ProcSearch => 1,
ProcSort => 2,
_ => 0,

View file

@ -67,7 +67,6 @@ impl NetworkGraphWidget for Painter {
// Update draw loc in widget map
// Note that in both cases, we always go to the same widget id so it's fine to do it like
// this lol.
debug!("!@#!@");
if let Some(network_widget) = app_state.widget_map.get_mut(&widget_id) {
network_widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
network_widget.bottom_right_corner =

View file

@ -248,6 +248,7 @@ pub const DEFAULT_BATTERY_LAYOUT: &str = r##"
pub const DEFAULT_CONFIG_FILE_PATH: &str = "bottom/bottom.toml";
// Default config file
// FIXME: Update the default config
pub const DEFAULT_CONFIG_CONTENT: &str = r##"
# This is a default config file for bottom. All of the settings are commented
# out by default; if you wish to change them uncomment and modify as you see

View file

@ -4,7 +4,7 @@
use std::collections::HashMap;
use crate::{
app::{data_farmer, data_harvester, App},
app::{data_farmer, data_harvester, App, Filter},
utils::gen_util::*,
};
@ -83,40 +83,77 @@ pub struct ConvertedCpuData {
}
pub fn convert_temp_row(app: &App) -> Vec<Vec<String>> {
let mut sensor_vector: Vec<Vec<String>> = Vec::new();
let current_data = &app.data_collection;
let temp_type = &app.app_config_fields.temperature_type;
let temp_filter = &app.filters.temp_filter;
if current_data.temp_harvest.is_empty() {
sensor_vector.push(vec!["No Sensors Found".to_string(), "".to_string()])
} else {
for sensor in &current_data.temp_harvest {
sensor_vector.push(vec![
match (&sensor.component_name, &sensor.component_label) {
(Some(name), Some(label)) => format!("{}: {}", name, label),
(None, Some(label)) => label.to_string(),
(Some(name), None) => name.to_string(),
(None, None) => String::default(),
},
(sensor.temperature.ceil() as u64).to_string()
+ match temp_type {
data_harvester::temperature::TemperatureType::Celsius => "C",
data_harvester::temperature::TemperatureType::Kelvin => "K",
data_harvester::temperature::TemperatureType::Fahrenheit => "F",
},
]);
}
let mut sensor_vector: Vec<Vec<String>> = current_data
.temp_harvest
.iter()
.filter_map(|temp_harvest| {
let name = match (&temp_harvest.component_name, &temp_harvest.component_label) {
(Some(name), Some(label)) => format!("{}: {}", name, label),
(None, Some(label)) => label.to_string(),
(Some(name), None) => name.to_string(),
(None, None) => String::default(),
};
let to_keep = if let Some(temp_filter) = temp_filter {
let mut ret = temp_filter.is_list_ignored;
for r in &temp_filter.list {
if r.is_match(&name) {
ret = !temp_filter.is_list_ignored;
break;
}
}
ret
} else {
true
};
if to_keep {
Some(vec![
name,
(temp_harvest.temperature.ceil() as u64).to_string()
+ match temp_type {
data_harvester::temperature::TemperatureType::Celsius => "C",
data_harvester::temperature::TemperatureType::Kelvin => "K",
data_harvester::temperature::TemperatureType::Fahrenheit => "F",
},
])
} else {
None
}
})
.collect();
if sensor_vector.is_empty() {
sensor_vector.push(vec!["No Sensors Found".to_string(), "".to_string()]);
}
sensor_vector
}
pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> Vec<Vec<String>> {
pub fn convert_disk_row(
current_data: &data_farmer::DataCollection, disk_filter: &Option<Filter>,
) -> Vec<Vec<String>> {
let mut disk_vector: Vec<Vec<String>> = Vec::new();
current_data
.disk_harvest
.iter()
.filter(|disk_harvest| {
if let Some(disk_filter) = disk_filter {
for r in &disk_filter.list {
if r.is_match(&disk_harvest.name) {
return !disk_filter.is_list_ignored;
}
}
disk_filter.is_list_ignored
} else {
true
}
})
.zip(&current_data.io_labels)
.for_each(|(disk, (io_read, io_write))| {
let converted_free_space = get_simple_byte_values(disk.free_space, false);

View file

@ -687,16 +687,21 @@ pub fn create_event_thread(
sender: std::sync::mpsc::Sender<
BottomEvent<crossterm::event::KeyEvent, crossterm::event::MouseEvent>,
>,
reset_receiver: std::sync::mpsc::Receiver<ResetEvent>, use_current_cpu_total: bool,
update_rate_in_milliseconds: u64, temp_type: data_harvester::temperature::TemperatureType,
show_average_cpu: bool, used_widget_set: UsedWidgets,
reset_receiver: std::sync::mpsc::Receiver<ResetEvent>,
app_config_fields: &app::AppConfigFields, used_widget_set: UsedWidgets,
) {
let temp_type = app_config_fields.temperature_type.clone();
let use_current_cpu_total = app_config_fields.use_current_cpu_total;
let show_average_cpu = app_config_fields.show_average_cpu;
let update_rate_in_milliseconds = app_config_fields.update_rate_in_milliseconds;
thread::spawn(move || {
let mut data_state = data_harvester::DataCollector::default();
data_state.set_collected_data(used_widget_set);
data_state.set_temperature_type(temp_type);
data_state.set_use_current_cpu_total(use_current_cpu_total);
data_state.set_show_average_cpu(show_average_cpu);
data_state.init();
loop {
if let Ok(message) = reset_receiver.try_recv() {

View file

@ -1,3 +1,4 @@
use regex::Regex;
use serde::Deserialize;
use std::collections::{HashMap, HashSet};
use std::time::Instant;
@ -19,6 +20,8 @@ pub struct Config {
pub flags: Option<ConfigFlags>,
pub colors: Option<ConfigColours>,
pub row: Option<Vec<Row>>,
pub disk_filter: Option<IgnoreList>,
pub temp_filter: Option<IgnoreList>,
}
#[derive(Default, Deserialize)]
@ -69,6 +72,14 @@ pub struct ConfigColours {
pub battery_colors: Option<Vec<String>>,
}
#[derive(Default, Deserialize)]
pub struct IgnoreList {
pub is_list_ignored: bool,
pub list: Vec<String>,
pub regex: Option<bool>,
pub case_sensitive: Option<bool>,
}
pub fn build_app(
matches: &clap::ArgMatches<'static>, config: &Config, widget_layout: &BottomLayout,
default_widget_id: u64, default_widget_type_option: &Option<BottomWidgetType>,
@ -249,6 +260,11 @@ pub fn build_app(
use_battery: used_widget_set.get(&Battery).is_some(),
};
let disk_filter =
get_ignore_list(&config.disk_filter).context("Update 'disk_filter' in your config file")?;
let temp_filter =
get_ignore_list(&config.temp_filter).context("Update 'temp_filter' in your config file")?;
Ok(App::builder()
.app_config_fields(app_config_fields)
.cpu_state(CpuState::init(cpu_state_map))
@ -262,6 +278,10 @@ pub fn build_app(
.current_widget(widget_map.get(&initial_widget_id).unwrap().clone()) // I think the unwrap is fine here
.widget_map(widget_map)
.used_widgets(used_widgets)
.filters(DataFilters {
disk_filter,
temp_filter,
})
.build())
}
@ -665,3 +685,45 @@ pub fn get_use_battery(matches: &clap::ArgMatches<'static>, config: &Config) ->
}
false
}
pub fn get_ignore_list(ignore_list: &Option<IgnoreList>) -> error::Result<Option<Filter>> {
if let Some(ignore_list) = ignore_list {
let list: Result<Vec<_>, _> = ignore_list
.list
.iter()
.map(|name| {
let use_regex = if let Some(use_regex) = ignore_list.regex {
use_regex
} else {
false
};
let use_cs = if let Some(use_cs) = ignore_list.case_sensitive {
use_cs
} else {
false
};
let escaped_string: String;
let res = format!(
"{}{}",
if use_cs { "" } else { "(?i)" },
if use_regex {
name
} else {
escaped_string = regex::escape(name);
&escaped_string
}
);
Regex::new(&res)
})
.collect();
Ok(Some(Filter {
list: list?,
is_list_ignored: ignore_list.is_list_ignored,
}))
} else {
Ok(None)
}
}