feature: Add ability to filter out disks and temp (#220)
You can now filter out disks and temp sensors by name via config.
3
.vscode/settings.json
vendored
|
@ -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",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
57
README.md
|
@ -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
After Width: | Height: | Size: 17 KiB |
BIN
assets/disk_filter_post2.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
assets/disk_filter_pre.png
Normal file
After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 891 KiB |
BIN
assets/temp_filter_post.png
Normal file
After Width: | Height: | Size: 8 KiB |
BIN
assets/temp_filter_post2.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
assets/temp_filter_pre.png
Normal file
After Width: | Height: | Size: 45 KiB |
26
src/app.rs
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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>>();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ¤t_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(¤t_data.io_labels)
|
||||
.for_each(|(disk, (io_read, io_write))| {
|
||||
let converted_free_space = get_simple_byte_values(disk.free_space, false);
|
||||
|
|
11
src/lib.rs
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|