mirror of
https://github.com/ClementTsang/bottom
synced 2024-11-10 06:34:16 +00:00
change: change how disk, temp, and net filters in config are set (#1481)
* change: change how disk, temp, and net filters in config are set * run rustfmt * update default config
This commit is contained in:
parent
46520d8b4e
commit
982b7181a6
85 changed files with 954 additions and 652 deletions
|
@ -28,6 +28,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- `mem_as_value` is now `process_memory_as_value`.
|
- `mem_as_value` is now `process_memory_as_value`.
|
||||||
- [#1472](https://github.com/ClementTsang/bottom/pull/1472): The following config fields have changed names:
|
- [#1472](https://github.com/ClementTsang/bottom/pull/1472): The following config fields have changed names:
|
||||||
- `mem_as_value` is now `process_memory_as_value`.
|
- `mem_as_value` is now `process_memory_as_value`.
|
||||||
|
- [#1481](https://github.com/ClementTsang/bottom/pull/1481): The following config fields have changed names:
|
||||||
|
- `disk_filter` is now `disk.name_filter`.
|
||||||
|
- `mount_filter` is now `disk.mount_filter`.
|
||||||
|
- `temp_filter` is now `temperature.sensor_filter`
|
||||||
|
- `net_filter` is now `network.interface_filter`
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
|
|
@ -143,6 +143,7 @@ clap_complete_nushell = "4.5.1"
|
||||||
clap_complete_fig = "4.5.0"
|
clap_complete_fig = "4.5.0"
|
||||||
clap_mangen = "0.2.20"
|
clap_mangen = "0.2.20"
|
||||||
indoc = "2.0.5"
|
indoc = "2.0.5"
|
||||||
|
# schemars = "0.8.21"
|
||||||
|
|
||||||
[package.metadata.deb]
|
[package.metadata.deb]
|
||||||
section = "utility"
|
section = "utility"
|
||||||
|
|
|
@ -80,18 +80,53 @@
|
||||||
# How much data is stored at once in terms of time.
|
# How much data is stored at once in terms of time.
|
||||||
#retention = "10m"
|
#retention = "10m"
|
||||||
|
|
||||||
# These are flags around the process widget.
|
# Processes widget configuration
|
||||||
|
|
||||||
#[processes]
|
#[processes]
|
||||||
|
# The columns shown by the process widget. The following columns are supported:
|
||||||
|
# PID, Name, CPU%, Mem%, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU%
|
||||||
#columns = ["PID", "Name", "CPU%", "Mem%", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMEM%", "GPU%"]
|
#columns = ["PID", "Name", "CPU%", "Mem%", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMEM%", "GPU%"]
|
||||||
|
|
||||||
# [cpu]
|
# CPU widget configuration
|
||||||
|
#[cpu]
|
||||||
# One of "all" (default), "average"/"avg"
|
# One of "all" (default), "average"/"avg"
|
||||||
# default = "average"
|
# default = "average"
|
||||||
|
|
||||||
|
# Disk widget configuration
|
||||||
|
#[disk]
|
||||||
|
#[disk.name_filter]
|
||||||
|
#is_list_ignored = true
|
||||||
|
#list = ["/dev/sda\\d+", "/dev/nvme0n1p2"]
|
||||||
|
#regex = true
|
||||||
|
#case_sensitive = false
|
||||||
|
#whole_word = false
|
||||||
|
|
||||||
|
#[disk.mount_filter]
|
||||||
|
#is_list_ignored = true
|
||||||
|
#list = ["/mnt/.*", "/boot"]
|
||||||
|
#regex = true
|
||||||
|
#case_sensitive = false
|
||||||
|
#whole_word = false
|
||||||
|
|
||||||
|
# Temperature widget configuration
|
||||||
|
#[temperature]
|
||||||
|
#[temperature.sensor_filter]
|
||||||
|
#is_list_ignored = true
|
||||||
|
#list = ["cpu", "wifi"]
|
||||||
|
#regex = false
|
||||||
|
#case_sensitive = false
|
||||||
|
#whole_word = false
|
||||||
|
|
||||||
|
# Network widget configuration
|
||||||
|
#[network]
|
||||||
|
#[network.interface_filter]
|
||||||
|
#is_list_ignored = true
|
||||||
|
#list = ["virbr0.*"]
|
||||||
|
#regex = true
|
||||||
|
#case_sensitive = false
|
||||||
|
#whole_word = false
|
||||||
|
|
||||||
# These are all the components that support custom theming. Note that colour support
|
# These are all the components that support custom theming. Note that colour support
|
||||||
# will depend on terminal support.
|
# will depend on terminal support.
|
||||||
|
|
||||||
#[colors] # Uncomment if you want to use custom colors
|
#[colors] # Uncomment if you want to use custom colors
|
||||||
# Represents the colour of table headers (processes, CPU, disks, temperature).
|
# Represents the colour of table headers (processes, CPU, disks, temperature).
|
||||||
#table_header_color="LightBlue"
|
#table_header_color="LightBlue"
|
||||||
|
@ -160,33 +195,3 @@
|
||||||
# [[row.child]]
|
# [[row.child]]
|
||||||
# type="proc"
|
# type="proc"
|
||||||
# default=true
|
# default=true
|
||||||
|
|
||||||
# Filters - you can hide specific temperature sensors, network interfaces, and disks using filters. This is admittedly
|
|
||||||
# a bit hard to use as of now, and there is a planned in-app interface for managing this in the future:
|
|
||||||
#[disk_filter]
|
|
||||||
#is_list_ignored = true
|
|
||||||
#list = ["/dev/sda\\d+", "/dev/nvme0n1p2"]
|
|
||||||
#regex = true
|
|
||||||
#case_sensitive = false
|
|
||||||
#whole_word = false
|
|
||||||
|
|
||||||
#[mount_filter]
|
|
||||||
#is_list_ignored = true
|
|
||||||
#list = ["/mnt/.*", "/boot"]
|
|
||||||
#regex = true
|
|
||||||
#case_sensitive = false
|
|
||||||
#whole_word = false
|
|
||||||
|
|
||||||
#[temp_filter]
|
|
||||||
#is_list_ignored = true
|
|
||||||
#list = ["cpu", "wifi"]
|
|
||||||
#regex = false
|
|
||||||
#case_sensitive = false
|
|
||||||
#whole_word = false
|
|
||||||
|
|
||||||
#[net_filter]
|
|
||||||
#is_list_ignored = true
|
|
||||||
#list = ["virbr0.*"]
|
|
||||||
#regex = true
|
|
||||||
#case_sensitive = false
|
|
||||||
#whole_word = false
|
|
||||||
|
|
57
src/app.rs
57
src/app.rs
|
@ -421,7 +421,8 @@ impl App {
|
||||||
pws.is_sort_open = !pws.is_sort_open;
|
pws.is_sort_open = !pws.is_sort_open;
|
||||||
pws.force_rerender = true;
|
pws.force_rerender = true;
|
||||||
|
|
||||||
// If the sort is now open, move left. Otherwise, if the proc sort was selected, force move right.
|
// If the sort is now open, move left. Otherwise, if the proc sort was selected,
|
||||||
|
// force move right.
|
||||||
if pws.is_sort_open {
|
if pws.is_sort_open {
|
||||||
pws.sort_table.set_position(pws.table.sort_index());
|
pws.sort_table.set_position(pws.table.sort_index());
|
||||||
self.move_widget_selection(&WidgetDirection::Left);
|
self.move_widget_selection(&WidgetDirection::Left);
|
||||||
|
@ -1054,13 +1055,15 @@ impl App {
|
||||||
.widget_states
|
.widget_states
|
||||||
.get_mut(&(self.current_widget.widget_id - 1))
|
.get_mut(&(self.current_widget.widget_id - 1))
|
||||||
{
|
{
|
||||||
// Traverse backwards from the current cursor location until you hit non-whitespace characters,
|
// Traverse backwards from the current cursor location until you hit
|
||||||
// then continue to traverse (and delete) backwards until you hit a whitespace character. Halt.
|
// non-whitespace characters, then continue to traverse (and
|
||||||
|
// delete) backwards until you hit a whitespace character. Halt.
|
||||||
|
|
||||||
// So... first, let's get our current cursor position in terms of char indices.
|
// So... first, let's get our current cursor position in terms of char indices.
|
||||||
let end_index = proc_widget_state.cursor_char_index();
|
let end_index = proc_widget_state.cursor_char_index();
|
||||||
|
|
||||||
// Then, let's crawl backwards until we hit our location, and store the "head"...
|
// Then, let's crawl backwards until we hit our location, and store the
|
||||||
|
// "head"...
|
||||||
let query = proc_widget_state.current_search_query();
|
let query = proc_widget_state.current_search_query();
|
||||||
let mut start_index = 0;
|
let mut start_index = 0;
|
||||||
let mut saw_non_whitespace = false;
|
let mut saw_non_whitespace = false;
|
||||||
|
@ -1617,7 +1620,8 @@ impl App {
|
||||||
if let Some(basic_table_widget_state) =
|
if let Some(basic_table_widget_state) =
|
||||||
&mut self.states.basic_table_widget_state
|
&mut self.states.basic_table_widget_state
|
||||||
{
|
{
|
||||||
// We also want to move towards Proc if we had set it to ProcSort.
|
// We also want to move towards Proc if we had set it to
|
||||||
|
// ProcSort.
|
||||||
if let BottomWidgetType::ProcSort =
|
if let BottomWidgetType::ProcSort =
|
||||||
basic_table_widget_state.currently_displayed_widget_type
|
basic_table_widget_state.currently_displayed_widget_type
|
||||||
{
|
{
|
||||||
|
@ -2505,20 +2509,22 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Moves the mouse to the widget that was clicked on, then propagates the click down to be
|
/// Moves the mouse to the widget that was clicked on, then propagates the
|
||||||
/// handled by the widget specifically.
|
/// click down to be handled by the widget specifically.
|
||||||
pub fn on_left_mouse_up(&mut self, x: u16, y: u16) {
|
pub fn on_left_mouse_up(&mut self, x: u16, y: u16) {
|
||||||
// Pretty dead simple - iterate through the widget map and go to the widget where the click
|
// Pretty dead simple - iterate through the widget map and go to the widget
|
||||||
// is within.
|
// where the click is within.
|
||||||
|
|
||||||
// TODO: [REFACTOR] might want to refactor this, it's really ugly.
|
// TODO: [REFACTOR] might want to refactor this, it's really ugly.
|
||||||
// TODO: [REFACTOR] Might wanna refactor ALL state things in general, currently everything
|
// TODO: [REFACTOR] Might wanna refactor ALL state things in general, currently
|
||||||
// is grouped up as an app state. We should separate stuff like event state and gui state and etc.
|
// everything is grouped up as an app state. We should separate stuff
|
||||||
|
// like event state and gui state and etc.
|
||||||
|
|
||||||
// TODO: [MOUSE] double click functionality...? We would do this above all other actions and SC if needed.
|
// TODO: [MOUSE] double click functionality...? We would do this above all
|
||||||
|
// other actions and SC if needed.
|
||||||
|
|
||||||
// Short circuit if we're in basic table... we might have to handle the basic table arrow
|
// Short circuit if we're in basic table... we might have to handle the basic
|
||||||
// case here...
|
// table arrow case here...
|
||||||
|
|
||||||
if let Some(bt) = &mut self.states.basic_table_widget_state {
|
if let Some(bt) = &mut self.states.basic_table_widget_state {
|
||||||
if let (
|
if let (
|
||||||
|
@ -2582,8 +2588,8 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second short circuit --- are we in the dd dialog state? If so, only check yes/no/signals
|
// Second short circuit --- are we in the dd dialog state? If so, only check
|
||||||
// and bail after.
|
// yes/no/signals and bail after.
|
||||||
if self.is_in_dialog() {
|
if self.is_in_dialog() {
|
||||||
match self.delete_dialog_state.button_positions.iter().find(
|
match self.delete_dialog_state.button_positions.iter().find(
|
||||||
|(tl_x, tl_y, br_x, br_y, _idx)| {
|
|(tl_x, tl_y, br_x, br_y, _idx)| {
|
||||||
|
@ -2649,7 +2655,8 @@ impl App {
|
||||||
) {
|
) {
|
||||||
let border_offset = u16::from(self.is_drawing_border());
|
let border_offset = u16::from(self.is_drawing_border());
|
||||||
|
|
||||||
// This check ensures the click isn't actually just clicking on the bottom border.
|
// This check ensures the click isn't actually just clicking on the bottom
|
||||||
|
// border.
|
||||||
if y < (brc_y - border_offset) {
|
if y < (brc_y - border_offset) {
|
||||||
match &self.current_widget.widget_type {
|
match &self.current_widget.widget_type {
|
||||||
BottomWidgetType::Proc
|
BottomWidgetType::Proc
|
||||||
|
@ -2682,8 +2689,10 @@ impl App {
|
||||||
|
|
||||||
self.change_process_position(change);
|
self.change_process_position(change);
|
||||||
|
|
||||||
// If in tree mode, also check to see if this click is on
|
// If in tree mode, also check to see if this click is
|
||||||
// the same entry as the already selected one - if it is,
|
// on
|
||||||
|
// the same entry as the already selected one - if it
|
||||||
|
// is,
|
||||||
// then we minimize.
|
// then we minimize.
|
||||||
if is_tree_mode && change == 0 {
|
if is_tree_mode && change == 0 {
|
||||||
self.toggle_collapsing_process_branch();
|
self.toggle_collapsing_process_branch();
|
||||||
|
@ -2755,8 +2764,9 @@ impl App {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// We might have clicked on a header! Check if we only exceeded the table + border offset, and
|
// We might have clicked on a header! Check if we only exceeded the
|
||||||
// it's implied we exceeded the gap offset.
|
// table + border offset, and it's implied
|
||||||
|
// we exceeded the gap offset.
|
||||||
if clicked_entry == border_offset {
|
if clicked_entry == border_offset {
|
||||||
match &self.current_widget.widget_type {
|
match &self.current_widget.widget_type {
|
||||||
BottomWidgetType::Proc => {
|
BottomWidgetType::Proc => {
|
||||||
|
@ -2851,8 +2861,9 @@ impl App {
|
||||||
|
|
||||||
/// A quick and dirty way to handle paste events.
|
/// A quick and dirty way to handle paste events.
|
||||||
pub fn handle_paste(&mut self, paste: String) {
|
pub fn handle_paste(&mut self, paste: String) {
|
||||||
// Partially copy-pasted from the single-char variant; should probably clean up this process in the future.
|
// Partially copy-pasted from the single-char variant; should probably clean up
|
||||||
// In particular, encapsulate this entire logic and add some tests to make it less potentially error-prone.
|
// this process in the future. In particular, encapsulate this entire
|
||||||
|
// logic and add some tests to make it less potentially error-prone.
|
||||||
let is_in_search_widget = self.is_in_search_widget();
|
let is_in_search_widget = self.is_in_search_widget();
|
||||||
if let Some(proc_widget_state) = self
|
if let Some(proc_widget_state) = self
|
||||||
.states
|
.states
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
//! a better name for the file. Since I called data collection "harvesting",
|
//! a better name for the file. Since I called data collection "harvesting",
|
||||||
//! then this is the farmer I guess.
|
//! then this is the farmer I guess.
|
||||||
//!
|
//!
|
||||||
//! Essentially the main goal is to shift the initial calculation and distribution
|
//! Essentially the main goal is to shift the initial calculation and
|
||||||
//! of joiner points and data to one central location that will only do it
|
//! distribution of joiner points and data to one central location that will
|
||||||
//! *once* upon receiving the data --- as opposed to doing it on canvas draw,
|
//! only do it *once* upon receiving the data --- as opposed to doing it on
|
||||||
//! which will be a costly process.
|
//! canvas draw, which will be a costly process.
|
||||||
//!
|
//!
|
||||||
//! This will also handle the *cleaning* of stale data. That should be done
|
//! This will also handle the *cleaning* of stale data. That should be done
|
||||||
//! in some manner (timer on another thread, some loop) that will occasionally
|
//! in some manner (timer on another thread, some loop) that will occasionally
|
||||||
|
@ -102,8 +102,8 @@ impl ProcessData {
|
||||||
/// collected, and what is needed to convert into a displayable form.
|
/// collected, and what is needed to convert into a displayable form.
|
||||||
///
|
///
|
||||||
/// If the app is *frozen* - that is, we do not want to *display* any changing
|
/// If the app is *frozen* - that is, we do not want to *display* any changing
|
||||||
/// data, keep updating this. As of 2021-09-08, we just clone the current collection
|
/// data, keep updating this. As of 2021-09-08, we just clone the current
|
||||||
/// when it freezes to have a snapshot floating around.
|
/// collection when it freezes to have a snapshot floating around.
|
||||||
///
|
///
|
||||||
/// Note that with this method, the *app* thread is responsible for cleaning -
|
/// Note that with this method, the *app* thread is responsible for cleaning -
|
||||||
/// not the data collector.
|
/// not the data collector.
|
||||||
|
@ -355,7 +355,8 @@ impl DataCollection {
|
||||||
#[cfg(feature = "zfs")]
|
#[cfg(feature = "zfs")]
|
||||||
{
|
{
|
||||||
if !device.name.starts_with('/') {
|
if !device.name.starts_with('/') {
|
||||||
Some(device.name.as_str()) // use the whole zfs dataset name
|
Some(device.name.as_str()) // use the whole zfs
|
||||||
|
// dataset name
|
||||||
} else {
|
} else {
|
||||||
device.name.split('/').last()
|
device.name.split('/').last()
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,9 @@ impl Filter {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn keep_entry(&self, value: &str) -> bool {
|
pub(crate) fn keep_entry(&self, value: &str) -> bool {
|
||||||
if self.has_match(value) {
|
if self.has_match(value) {
|
||||||
// If a match is found, then if we wanted to ignore if we match, return false. If we want
|
// If a match is found, then if we wanted to ignore if we match, return false.
|
||||||
// to keep if we match, return true. Thus, return the inverse of `is_list_ignored`.
|
// If we want to keep if we match, return true. Thus, return the
|
||||||
|
// inverse of `is_list_ignored`.
|
||||||
!self.is_list_ignored
|
!self.is_list_ignored
|
||||||
} else {
|
} else {
|
||||||
self.is_list_ignored
|
self.is_list_ignored
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use super::DataCollection;
|
use super::DataCollection;
|
||||||
|
|
||||||
/// The [`FrozenState`] indicates whether the application state should be frozen. It is either not frozen or
|
/// The [`FrozenState`] indicates whether the application state should be
|
||||||
/// frozen and containing a copy of the state at the time.
|
/// frozen. It is either not frozen or frozen and containing a copy of the state
|
||||||
|
/// at the time.
|
||||||
pub enum FrozenState {
|
pub enum FrozenState {
|
||||||
NotFrozen,
|
NotFrozen,
|
||||||
Frozen(Box<DataCollection>),
|
Frozen(Box<DataCollection>),
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
//! This file is meant to house (OS specific) implementations on how to kill processes.
|
//! This file is meant to house (OS specific) implementations on how to kill
|
||||||
|
//! processes.
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
use windows::Win32::{
|
use windows::Win32::{
|
||||||
|
@ -61,7 +62,8 @@ pub fn kill_process_given_pid(pid: Pid) -> crate::utils::error::Result<()> {
|
||||||
/// Kills a process, given a PID, for UNIX.
|
/// Kills a process, given a PID, for UNIX.
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
pub fn kill_process_given_pid(pid: Pid, signal: usize) -> crate::utils::error::Result<()> {
|
pub fn kill_process_given_pid(pid: Pid, signal: usize) -> crate::utils::error::Result<()> {
|
||||||
// SAFETY: the signal should be valid, and we act properly on an error (exit code not 0).
|
// SAFETY: the signal should be valid, and we act properly on an error (exit
|
||||||
|
// code not 0).
|
||||||
|
|
||||||
let output = unsafe { libc::kill(pid, signal as i32) };
|
let output = unsafe { libc::kill(pid, signal as i32) };
|
||||||
if output != 0 {
|
if output != 0 {
|
||||||
|
|
|
@ -26,13 +26,15 @@ const OR_LIST: [&str; 2] = ["or", "||"];
|
||||||
const AND_LIST: [&str; 2] = ["and", "&&"];
|
const AND_LIST: [&str; 2] = ["and", "&&"];
|
||||||
|
|
||||||
/// In charge of parsing the given query.
|
/// In charge of parsing the given query.
|
||||||
/// We are defining the following language for a query (case-insensitive prefixes):
|
/// We are defining the following language for a query (case-insensitive
|
||||||
|
/// prefixes):
|
||||||
///
|
///
|
||||||
/// - Process names: No prefix required, can use regex, match word, or case.
|
/// - Process names: No prefix required, can use regex, match word, or case.
|
||||||
/// Enclosing anything, including prefixes, in quotes, means we treat it as an entire process
|
/// Enclosing anything, including prefixes, in quotes, means we treat it as an
|
||||||
/// rather than a prefix.
|
/// entire process rather than a prefix.
|
||||||
/// - PIDs: Use prefix `pid`, can use regex or match word (case is irrelevant).
|
/// - PIDs: Use prefix `pid`, can use regex or match word (case is irrelevant).
|
||||||
/// - CPU: Use prefix `cpu`, cannot use r/m/c (regex, match word, case). Can compare.
|
/// - CPU: Use prefix `cpu`, cannot use r/m/c (regex, match word, case). Can
|
||||||
|
/// compare.
|
||||||
/// - MEM: Use prefix `mem`, cannot use r/m/c. Can compare.
|
/// - MEM: Use prefix `mem`, cannot use r/m/c. Can compare.
|
||||||
/// - STATE: Use prefix `state`, can use regex, match word, or case.
|
/// - STATE: Use prefix `state`, can use regex, match word, or case.
|
||||||
/// - USER: Use prefix `user`, can use regex, match word, or case.
|
/// - USER: Use prefix `user`, can use regex, match word, or case.
|
||||||
|
@ -41,9 +43,10 @@ const AND_LIST: [&str; 2] = ["and", "&&"];
|
||||||
/// - Total read: Use prefix `read`. Can compare.
|
/// - Total read: Use prefix `read`. Can compare.
|
||||||
/// - Total write: Use prefix `write`. Can compare.
|
/// - Total write: Use prefix `write`. Can compare.
|
||||||
///
|
///
|
||||||
/// For queries, whitespaces are our delimiters. We will merge together any adjacent non-prefixed
|
/// For queries, whitespaces are our delimiters. We will merge together any
|
||||||
/// or quoted elements after splitting to treat as process names.
|
/// adjacent non-prefixed or quoted elements after splitting to treat as process
|
||||||
/// Furthermore, we want to support boolean joiners like AND and OR, and brackets.
|
/// names. Furthermore, we want to support boolean joiners like AND and OR, and
|
||||||
|
/// brackets.
|
||||||
pub fn parse_query(
|
pub fn parse_query(
|
||||||
search_query: &str, is_searching_whole_word: bool, is_ignoring_case: bool,
|
search_query: &str, is_searching_whole_word: bool, is_ignoring_case: bool,
|
||||||
is_searching_with_regex: bool,
|
is_searching_with_regex: bool,
|
||||||
|
@ -176,8 +179,9 @@ pub fn parse_query(
|
||||||
if let Some(queue_top) = query.pop_front() {
|
if let Some(queue_top) = query.pop_front() {
|
||||||
if inside_quotation {
|
if inside_quotation {
|
||||||
if queue_top == "\"" {
|
if queue_top == "\"" {
|
||||||
// This means we hit something like "". Return an empty prefix, and to deal with
|
// This means we hit something like "". Return an empty prefix, and to deal
|
||||||
// the close quote checker, add one to the top of the stack. Ugly fix but whatever.
|
// with the close quote checker, add one to the top of the
|
||||||
|
// stack. Ugly fix but whatever.
|
||||||
query.push_front("\"".to_string());
|
query.push_front("\"".to_string());
|
||||||
return Ok(Prefix {
|
return Ok(Prefix {
|
||||||
or: None,
|
or: None,
|
||||||
|
@ -268,8 +272,9 @@ pub fn parse_query(
|
||||||
} else if queue_top == ")" {
|
} else if queue_top == ")" {
|
||||||
return Err(QueryError("Missing opening parentheses".into()));
|
return Err(QueryError("Missing opening parentheses".into()));
|
||||||
} else if queue_top == "\"" {
|
} else if queue_top == "\"" {
|
||||||
// Similar to parentheses, trap and check for missing closing quotes. Note, however, that we
|
// Similar to parentheses, trap and check for missing closing quotes. Note,
|
||||||
// will DIRECTLY call another process_prefix call...
|
// however, that we will DIRECTLY call another process_prefix
|
||||||
|
// call...
|
||||||
|
|
||||||
let prefix = process_prefix(query, true)?;
|
let prefix = process_prefix(query, true)?;
|
||||||
if let Some(close_paren) = query.pop_front() {
|
if let Some(close_paren) = query.pop_front() {
|
||||||
|
@ -308,10 +313,12 @@ pub fn parse_query(
|
||||||
// - (test)
|
// - (test)
|
||||||
// - (test
|
// - (test
|
||||||
// - test)
|
// - test)
|
||||||
// These are split into 2 to 3 different strings due to parentheses being
|
// These are split into 2 to 3 different strings due to
|
||||||
|
// parentheses being
|
||||||
// delimiters in our query system.
|
// delimiters in our query system.
|
||||||
//
|
//
|
||||||
// Do we want these to be valid? They should, as a string, right?
|
// Do we want these to be valid? They should, as a string,
|
||||||
|
// right?
|
||||||
|
|
||||||
return Ok(Prefix {
|
return Ok(Prefix {
|
||||||
or: None,
|
or: None,
|
||||||
|
@ -385,8 +392,8 @@ pub fn parse_query(
|
||||||
let mut condition: Option<QueryComparison> = None;
|
let mut condition: Option<QueryComparison> = None;
|
||||||
let mut value: Option<f64> = None;
|
let mut value: Option<f64> = None;
|
||||||
|
|
||||||
// TODO: Jeez, what the heck did I write here... add some tests and clean this up in the
|
// TODO: Jeez, what the heck did I write here... add some tests and
|
||||||
// future.
|
// clean this up in the future.
|
||||||
if content == "=" {
|
if content == "=" {
|
||||||
condition = Some(QueryComparison::Equal);
|
condition = Some(QueryComparison::Equal);
|
||||||
if let Some(queue_next) = query.pop_front() {
|
if let Some(queue_next) = query.pop_front() {
|
||||||
|
@ -423,8 +430,9 @@ pub fn parse_query(
|
||||||
|
|
||||||
if let Some(condition) = condition {
|
if let Some(condition) = condition {
|
||||||
if let Some(read_value) = value {
|
if let Some(read_value) = value {
|
||||||
// Note that the values *might* have a unit or need to be parsed differently
|
// Note that the values *might* have a unit or need to be parsed
|
||||||
// based on the prefix type!
|
// differently based on the
|
||||||
|
// prefix type!
|
||||||
|
|
||||||
let mut value = read_value;
|
let mut value = read_value;
|
||||||
|
|
||||||
|
@ -691,7 +699,8 @@ impl std::str::FromStr for PrefixType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This is also jank and could be better represented. Add tests, then clean up!
|
// TODO: This is also jank and could be better represented. Add tests, then
|
||||||
|
// clean up!
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Prefix {
|
pub struct Prefix {
|
||||||
pub or: Option<Box<Or>>,
|
pub or: Option<Box<Or>>,
|
||||||
|
|
|
@ -112,7 +112,8 @@ impl Default for AppSearchState {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppSearchState {
|
impl AppSearchState {
|
||||||
/// Resets the [`AppSearchState`] to its default state, albeit still enabled.
|
/// Resets the [`AppSearchState`] to its default state, albeit still
|
||||||
|
/// enabled.
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
*self = AppSearchState {
|
*self = AppSearchState {
|
||||||
is_enabled: self.is_enabled,
|
is_enabled: self.is_enabled,
|
||||||
|
@ -161,7 +162,8 @@ impl AppSearchState {
|
||||||
// Use the current index.
|
// Use the current index.
|
||||||
start_index
|
start_index
|
||||||
} else if cursor_range.end >= available_width {
|
} else if cursor_range.end >= available_width {
|
||||||
// If the current position is past the last visible element, skip until we see it.
|
// If the current position is past the last visible element, skip until we
|
||||||
|
// see it.
|
||||||
|
|
||||||
let mut index = 0;
|
let mut index = 0;
|
||||||
for i in 0..(cursor_index + 1) {
|
for i in 0..(cursor_index + 1) {
|
||||||
|
@ -211,7 +213,8 @@ impl AppSearchState {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(err) => match err {
|
Err(err) => match err {
|
||||||
GraphemeIncomplete::PreContext(ctx) => {
|
GraphemeIncomplete::PreContext(ctx) => {
|
||||||
// Provide the entire string as context. Not efficient but should resolve failures.
|
// Provide the entire string as context. Not efficient but should resolve
|
||||||
|
// failures.
|
||||||
self.grapheme_cursor
|
self.grapheme_cursor
|
||||||
.provide_context(&self.current_search_query[0..ctx], 0);
|
.provide_context(&self.current_search_query[0..ctx], 0);
|
||||||
|
|
||||||
|
@ -233,7 +236,8 @@ impl AppSearchState {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(err) => match err {
|
Err(err) => match err {
|
||||||
GraphemeIncomplete::PreContext(ctx) => {
|
GraphemeIncomplete::PreContext(ctx) => {
|
||||||
// Provide the entire string as context. Not efficient but should resolve failures.
|
// Provide the entire string as context. Not efficient but should resolve
|
||||||
|
// failures.
|
||||||
self.grapheme_cursor
|
self.grapheme_cursor
|
||||||
.provide_context(&self.current_search_query[0..ctx], 0);
|
.provide_context(&self.current_search_query[0..ctx], 0);
|
||||||
|
|
||||||
|
|
|
@ -260,7 +260,8 @@ impl Painter {
|
||||||
let middle_dialog_chunk = Layout::default()
|
let middle_dialog_chunk = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints(if terminal_width < 100 {
|
.constraints(if terminal_width < 100 {
|
||||||
// TODO: [REFACTOR] The point we start changing size at currently hard-coded in.
|
// TODO: [REFACTOR] The point we start changing size at currently hard-coded
|
||||||
|
// in.
|
||||||
[
|
[
|
||||||
Constraint::Percentage(0),
|
Constraint::Percentage(0),
|
||||||
Constraint::Percentage(100),
|
Constraint::Percentage(100),
|
||||||
|
@ -386,7 +387,8 @@ impl Painter {
|
||||||
|
|
||||||
let actual_cpu_data_len = app_state.converted_data.cpu_data.len().saturating_sub(1);
|
let actual_cpu_data_len = app_state.converted_data.cpu_data.len().saturating_sub(1);
|
||||||
|
|
||||||
// This fixes #397, apparently if the height is 1, it can't render the CPU bars...
|
// This fixes #397, apparently if the height is 1, it can't render the CPU
|
||||||
|
// bars...
|
||||||
let cpu_height = {
|
let cpu_height = {
|
||||||
let c =
|
let c =
|
||||||
(actual_cpu_data_len / 4) as u16 + u16::from(actual_cpu_data_len % 4 != 0);
|
(actual_cpu_data_len / 4) as u16 + u16::from(actual_cpu_data_len % 4 != 0);
|
||||||
|
@ -499,15 +501,15 @@ impl Painter {
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.derived_widget_draw_locs.is_empty() || app_state.is_force_redraw {
|
if self.derived_widget_draw_locs.is_empty() || app_state.is_force_redraw {
|
||||||
// TODO: Can I remove this? Does ratatui's layout constraints work properly for fixing
|
// TODO: Can I remove this? Does ratatui's layout constraints work properly for
|
||||||
// https://github.com/ClementTsang/bottom/issues/896 now?
|
// fixing https://github.com/ClementTsang/bottom/issues/896 now?
|
||||||
fn get_constraints(
|
fn get_constraints(
|
||||||
direction: Direction, constraints: &[LayoutConstraint], area: Rect,
|
direction: Direction, constraints: &[LayoutConstraint], area: Rect,
|
||||||
) -> Vec<Rect> {
|
) -> Vec<Rect> {
|
||||||
// Order of operations:
|
// Order of operations:
|
||||||
// - Ratios first + canvas-handled (which is just zero)
|
// - Ratios first + canvas-handled (which is just zero)
|
||||||
// - Then any flex-grows to take up remaining space; divide amongst remaining
|
// - Then any flex-grows to take up remaining space; divide amongst
|
||||||
// hand out any remaining space
|
// remaining hand out any remaining space
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy)]
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
struct Size {
|
struct Size {
|
||||||
|
@ -688,7 +690,8 @@ impl Painter {
|
||||||
&col_rows.children
|
&col_rows.children
|
||||||
)
|
)
|
||||||
.map(|(draw_loc, col_row_constraint_vec, widgets)| {
|
.map(|(draw_loc, col_row_constraint_vec, widgets)| {
|
||||||
// Note that col_row_constraint_vec CONTAINS the widget constraints
|
// Note that col_row_constraint_vec CONTAINS the widget
|
||||||
|
// constraints
|
||||||
let widget_draw_locs = get_constraints(
|
let widget_draw_locs = get_constraints(
|
||||||
Direction::Horizontal,
|
Direction::Horizontal,
|
||||||
col_row_constraint_vec.as_slice(),
|
col_row_constraint_vec.as_slice(),
|
||||||
|
|
|
@ -20,13 +20,15 @@ use crate::utils::general::ClampExt;
|
||||||
|
|
||||||
/// A [`DataTable`] is a component that displays data in a tabular form.
|
/// A [`DataTable`] is a component that displays data in a tabular form.
|
||||||
///
|
///
|
||||||
/// Note that [`DataTable`] takes a generic type `S`, bounded by [`SortType`]. This controls whether this table
|
/// Note that [`DataTable`] takes a generic type `S`, bounded by [`SortType`].
|
||||||
/// expects sorted data or not, with two expected types:
|
/// This controls whether this table expects sorted data or not, with two
|
||||||
|
/// expected types:
|
||||||
///
|
///
|
||||||
/// - [`Unsortable`]: The default if otherwise not specified. This table does not expect sorted data.
|
/// - [`Unsortable`]: The default if otherwise not specified. This table does
|
||||||
/// - [`Sortable`]: This table expects sorted data, and there are helper functions to
|
/// not expect sorted data.
|
||||||
/// facilitate things like sorting based on a selected column, shortcut column selection support, mouse column
|
/// - [`Sortable`]: This table expects sorted data, and there are helper
|
||||||
/// selection support, etc.
|
/// functions to facilitate things like sorting based on a selected column,
|
||||||
|
/// shortcut column selection support, mouse column selection support, etc.
|
||||||
pub struct DataTable<DataType, Header, S = Unsortable, C = Column<Header>> {
|
pub struct DataTable<DataType, Header, S = Unsortable, C = Column<Header>> {
|
||||||
pub columns: Vec<C>,
|
pub columns: Vec<C>,
|
||||||
pub state: DataTableState,
|
pub state: DataTableState,
|
||||||
|
@ -89,8 +91,9 @@ impl<DataType: DataToCell<H>, H: ColumnHeader, S: SortType, C: DataTableColumn<H
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Increments the scroll position if possible by a positive/negative offset. If there is a
|
/// Increments the scroll position if possible by a positive/negative
|
||||||
/// valid change, this function will also return the new position wrapped in an [`Option`].
|
/// offset. If there is a valid change, this function will also return
|
||||||
|
/// the new position wrapped in an [`Option`].
|
||||||
pub fn increment_position(&mut self, change: i64) -> Option<usize> {
|
pub fn increment_position(&mut self, change: i64) -> Option<usize> {
|
||||||
let max_index = self.data.len();
|
let max_index = self.data.len();
|
||||||
let current_index = self.state.current_index;
|
let current_index = self.state.current_index;
|
||||||
|
|
|
@ -7,17 +7,20 @@ use std::{
|
||||||
/// A bound on the width of a column.
|
/// A bound on the width of a column.
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub enum ColumnWidthBounds {
|
pub enum ColumnWidthBounds {
|
||||||
/// A width of this type is as long as `desired`, but can otherwise shrink and grow up to a point.
|
/// A width of this type is as long as `desired`, but can otherwise shrink
|
||||||
|
/// and grow up to a point.
|
||||||
Soft {
|
Soft {
|
||||||
/// The desired, calculated width. Take this if possible as the base starting width.
|
/// The desired, calculated width. Take this if possible as the base
|
||||||
|
/// starting width.
|
||||||
desired: u16,
|
desired: u16,
|
||||||
|
|
||||||
/// The max width, as a percentage of the total width available. If [`None`],
|
/// The max width, as a percentage of the total width available. If
|
||||||
/// then it can grow as desired.
|
/// [`None`], then it can grow as desired.
|
||||||
max_percentage: Option<f32>,
|
max_percentage: Option<f32>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// A width of this type is either as long as specified, or does not appear at all.
|
/// A width of this type is either as long as specified, or does not appear
|
||||||
|
/// at all.
|
||||||
Hard(u16),
|
Hard(u16),
|
||||||
|
|
||||||
/// A width of this type always resizes to the column header's text width.
|
/// A width of this type always resizes to the column header's text width.
|
||||||
|
@ -28,7 +31,8 @@ pub trait ColumnHeader {
|
||||||
/// The "text" version of the column header.
|
/// The "text" version of the column header.
|
||||||
fn text(&self) -> Cow<'static, str>;
|
fn text(&self) -> Cow<'static, str>;
|
||||||
|
|
||||||
/// The version displayed when drawing the table. Defaults to [`ColumnHeader::text`].
|
/// The version displayed when drawing the table. Defaults to
|
||||||
|
/// [`ColumnHeader::text`].
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn header(&self) -> Cow<'static, str> {
|
fn header(&self) -> Cow<'static, str> {
|
||||||
self.text()
|
self.text()
|
||||||
|
@ -63,8 +67,9 @@ pub trait DataTableColumn<H: ColumnHeader> {
|
||||||
/// The actually displayed "header".
|
/// The actually displayed "header".
|
||||||
fn header(&self) -> Cow<'static, str>;
|
fn header(&self) -> Cow<'static, str>;
|
||||||
|
|
||||||
/// The header length, along with any required additional lengths for things like arrows.
|
/// The header length, along with any required additional lengths for things
|
||||||
/// Defaults to getting the length of [`DataTableColumn::header`].
|
/// like arrows. Defaults to getting the length of
|
||||||
|
/// [`DataTableColumn::header`].
|
||||||
fn header_len(&self) -> usize {
|
fn header_len(&self) -> usize {
|
||||||
self.header().len()
|
self.header().len()
|
||||||
}
|
}
|
||||||
|
@ -78,7 +83,8 @@ pub struct Column<H> {
|
||||||
/// A restriction on this column's width.
|
/// A restriction on this column's width.
|
||||||
bounds: ColumnWidthBounds,
|
bounds: ColumnWidthBounds,
|
||||||
|
|
||||||
/// Marks that this column is currently "hidden", and should *always* be skipped.
|
/// Marks that this column is currently "hidden", and should *always* be
|
||||||
|
/// skipped.
|
||||||
is_hidden: bool,
|
is_hidden: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,10 +154,13 @@ impl<H: ColumnHeader> Column<H> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait CalculateColumnWidths<H> {
|
pub trait CalculateColumnWidths<H> {
|
||||||
/// Calculates widths for the columns of this table, given the current width when called.
|
/// Calculates widths for the columns of this table, given the current width
|
||||||
|
/// when called.
|
||||||
///
|
///
|
||||||
/// * `total_width` is the total width on the canvas that the columns can try and work with.
|
/// * `total_width` is the total width on the canvas that the columns can
|
||||||
/// * `left_to_right` is whether to size from left-to-right (`true`) or right-to-left (`false`).
|
/// try and work with.
|
||||||
|
/// * `left_to_right` is whether to size from left-to-right (`true`) or
|
||||||
|
/// right-to-left (`false`).
|
||||||
fn calculate_column_widths(&self, total_width: u16, left_to_right: bool) -> Vec<NonZeroU16>;
|
fn calculate_column_widths(&self, total_width: u16, left_to_right: bool) -> Vec<NonZeroU16>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,9 @@ pub trait DataToCell<H>
|
||||||
where
|
where
|
||||||
H: ColumnHeader,
|
H: ColumnHeader,
|
||||||
{
|
{
|
||||||
/// Given data, a column, and its corresponding width, return the string in the cell that will
|
/// Given data, a column, and its corresponding width, return the string in
|
||||||
/// be displayed in the [`DataTable`](super::DataTable).
|
/// the cell that will be displayed in the
|
||||||
|
/// [`DataTable`](super::DataTable).
|
||||||
fn to_cell(&self, column: &H, calculated_width: NonZeroU16) -> Option<Cow<'static, str>>;
|
fn to_cell(&self, column: &H, calculated_width: NonZeroU16) -> Option<Cow<'static, str>>;
|
||||||
|
|
||||||
/// Apply styling to the generated [`Row`] of cells.
|
/// Apply styling to the generated [`Row`] of cells.
|
||||||
|
|
|
@ -202,7 +202,8 @@ where
|
||||||
|
|
||||||
if !self.data.is_empty() || !self.first_draw {
|
if !self.data.is_empty() || !self.first_draw {
|
||||||
if self.first_draw {
|
if self.first_draw {
|
||||||
self.first_draw = false; // TODO: Doing it this way is fine, but it could be done better (e.g. showing custom no results/entries message)
|
self.first_draw = false; // TODO: Doing it this way is fine, but it could be done better (e.g. showing
|
||||||
|
// custom no results/entries message)
|
||||||
if let Some(first_index) = self.first_index {
|
if let Some(first_index) = self.first_index {
|
||||||
self.set_position(first_index);
|
self.set_position(first_index);
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,8 @@ pub struct Sortable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The [`SortType`] trait is meant to be used in the typing of a [`DataTable`]
|
/// The [`SortType`] trait is meant to be used in the typing of a [`DataTable`]
|
||||||
/// to denote whether the table is meant to display/store sorted or unsorted data.
|
/// to denote whether the table is meant to display/store sorted or unsorted
|
||||||
|
/// data.
|
||||||
///
|
///
|
||||||
/// Note that the trait is [sealed](https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed),
|
/// Note that the trait is [sealed](https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed),
|
||||||
/// and therefore only [`Unsortable`] and [`Sortable`] can implement it.
|
/// and therefore only [`Unsortable`] and [`Sortable`] can implement it.
|
||||||
|
@ -97,9 +98,10 @@ impl SortType for Sortable {
|
||||||
SortOrder::Ascending => UP_ARROW,
|
SortOrder::Ascending => UP_ARROW,
|
||||||
SortOrder::Descending => DOWN_ARROW,
|
SortOrder::Descending => DOWN_ARROW,
|
||||||
};
|
};
|
||||||
// TODO: I think I can get away with removing the truncate_to_text call since
|
// TODO: I think I can get away with removing the truncate_to_text call
|
||||||
// I almost always bind to at least the header size...
|
// since I almost always bind to at least the header
|
||||||
// TODO: Or should we instead truncate but ALWAYS leave the arrow at the end?
|
// size... TODO: Or should we instead truncate but
|
||||||
|
// ALWAYS leave the arrow at the end?
|
||||||
truncate_to_text(&concat_string!(c.header(), arrow), width.get())
|
truncate_to_text(&concat_string!(c.header(), arrow), width.get())
|
||||||
} else {
|
} else {
|
||||||
truncate_to_text(&c.header(), width.get())
|
truncate_to_text(&c.header(), width.get())
|
||||||
|
@ -127,7 +129,8 @@ pub struct SortColumn<T> {
|
||||||
/// A restriction on this column's width.
|
/// A restriction on this column's width.
|
||||||
pub bounds: ColumnWidthBounds,
|
pub bounds: ColumnWidthBounds,
|
||||||
|
|
||||||
/// Marks that this column is currently "hidden", and should *always* be skipped.
|
/// Marks that this column is currently "hidden", and should *always* be
|
||||||
|
/// skipped.
|
||||||
pub is_hidden: bool,
|
pub is_hidden: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,8 +181,9 @@ impl<D, T> SortColumn<T>
|
||||||
where
|
where
|
||||||
T: ColumnHeader + SortsRow<DataType = D>,
|
T: ColumnHeader + SortsRow<DataType = D>,
|
||||||
{
|
{
|
||||||
/// Creates a new [`SortColumn`] with a width that follows the header width, which has no shortcut and sorts by
|
/// Creates a new [`SortColumn`] with a width that follows the header width,
|
||||||
/// default in ascending order ([`SortOrder::Ascending`]).
|
/// which has no shortcut and sorts by default in ascending order
|
||||||
|
/// ([`SortOrder::Ascending`]).
|
||||||
pub fn new(inner: T) -> Self {
|
pub fn new(inner: T) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner,
|
inner,
|
||||||
|
@ -189,8 +193,8 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new [`SortColumn`] with a hard width, which has no shortcut and sorts by default in
|
/// Creates a new [`SortColumn`] with a hard width, which has no shortcut
|
||||||
/// ascending order ([`SortOrder::Ascending`]).
|
/// and sorts by default in ascending order ([`SortOrder::Ascending`]).
|
||||||
pub fn hard(inner: T, width: u16) -> Self {
|
pub fn hard(inner: T, width: u16) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner,
|
inner,
|
||||||
|
@ -200,8 +204,8 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new [`SortColumn`] with a soft width, which has no shortcut and sorts by default in
|
/// Creates a new [`SortColumn`] with a soft width, which has no shortcut
|
||||||
/// ascending order ([`SortOrder::Ascending`]).
|
/// and sorts by default in ascending order ([`SortOrder::Ascending`]).
|
||||||
pub fn soft(inner: T, max_percentage: Option<f32>) -> Self {
|
pub fn soft(inner: T, max_percentage: Option<f32>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner,
|
inner,
|
||||||
|
@ -226,7 +230,8 @@ where
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Given a [`SortColumn`] and the sort order, sort a mutable slice of associated data.
|
/// Given a [`SortColumn`] and the sort order, sort a mutable slice of
|
||||||
|
/// associated data.
|
||||||
pub fn sort_by(&self, data: &mut [D], order: SortOrder) {
|
pub fn sort_by(&self, data: &mut [D], order: SortOrder) {
|
||||||
let descending = matches!(order, SortOrder::Descending);
|
let descending = matches!(order, SortOrder::Descending);
|
||||||
self.inner.sort_data(data, descending);
|
self.inner.sort_data(data, descending);
|
||||||
|
@ -284,11 +289,11 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Given some `x` and `y`, if possible, select the corresponding column or toggle the column if already selected,
|
/// Given some `x` and `y`, if possible, select the corresponding column or
|
||||||
/// and otherwise do nothing.
|
/// toggle the column if already selected, and otherwise do nothing.
|
||||||
///
|
///
|
||||||
/// If there was some update, the corresponding column type will be returned. If nothing happens, [`None`] is
|
/// If there was some update, the corresponding column type will be
|
||||||
/// returned.
|
/// returned. If nothing happens, [`None`] is returned.
|
||||||
pub fn try_select_location(&mut self, x: u16, y: u16) -> Option<usize> {
|
pub fn try_select_location(&mut self, x: u16, y: u16) -> Option<usize> {
|
||||||
if self.state.inner_rect.height > 1 && self.state.inner_rect.y == y {
|
if self.state.inner_rect.height > 1 && self.state.inner_rect.y == y {
|
||||||
if let Some(index) = self.get_range(x) {
|
if let Some(index) = self.get_range(x) {
|
||||||
|
@ -304,10 +309,11 @@ where
|
||||||
|
|
||||||
/// Updates the sort index, and sets the sort order as appropriate.
|
/// Updates the sort index, and sets the sort order as appropriate.
|
||||||
///
|
///
|
||||||
/// If the index is different from the previous one, it will move to the new index and set the sort order
|
/// If the index is different from the previous one, it will move to the new
|
||||||
/// to the prescribed default sort order.
|
/// index and set the sort order to the prescribed default sort order.
|
||||||
///
|
///
|
||||||
/// If the index is the same as the previous one, it will simply toggle the current sort order.
|
/// If the index is the same as the previous one, it will simply toggle the
|
||||||
|
/// current sort order.
|
||||||
pub fn set_sort_index(&mut self, index: usize) {
|
pub fn set_sort_index(&mut self, index: usize) {
|
||||||
if self.sort_type.sort_index == index {
|
if self.sort_type.sort_index == index {
|
||||||
self.toggle_order();
|
self.toggle_order();
|
||||||
|
|
|
@ -23,7 +23,8 @@ pub struct GraphData<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TimeGraph<'a> {
|
pub struct TimeGraph<'a> {
|
||||||
/// The min and max x boundaries. Expects a f64 representing the time range in milliseconds.
|
/// The min and max x boundaries. Expects a f64 representing the time range
|
||||||
|
/// in milliseconds.
|
||||||
pub x_bounds: [u64; 2],
|
pub x_bounds: [u64; 2],
|
||||||
|
|
||||||
/// Whether to hide the time/x-labels.
|
/// Whether to hide the time/x-labels.
|
||||||
|
@ -99,7 +100,8 @@ impl<'a> TimeGraph<'a> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates a title for the [`TimeGraph`] widget, given the available space.
|
/// Generates a title for the [`TimeGraph`] widget, given the available
|
||||||
|
/// space.
|
||||||
fn generate_title(&self, draw_loc: Rect) -> Line<'_> {
|
fn generate_title(&self, draw_loc: Rect) -> Line<'_> {
|
||||||
if self.is_expanded {
|
if self.is_expanded {
|
||||||
let title_base = concat_string!(self.title, "── Esc to go back ");
|
let title_base = concat_string!(self.title, "── Esc to go back ");
|
||||||
|
@ -121,13 +123,15 @@ impl<'a> TimeGraph<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws a time graph at [`Rect`] location provided by `draw_loc`. A time graph is used to display data points
|
/// Draws a time graph at [`Rect`] location provided by `draw_loc`. A time
|
||||||
/// throughout time in the x-axis.
|
/// graph is used to display data points throughout time in the x-axis.
|
||||||
///
|
///
|
||||||
/// This time graph:
|
/// This time graph:
|
||||||
/// - Draws with the higher time value on the left, and lower on the right.
|
/// - Draws with the higher time value on the left, and lower on the right.
|
||||||
/// - Expects a [`TimeGraph`] to be passed in, which details how to draw the graph.
|
/// - Expects a [`TimeGraph`] to be passed in, which details how to draw the
|
||||||
/// - Expects `graph_data`, which represents *what* data to draw, and various details like style and optional legends.
|
/// graph.
|
||||||
|
/// - Expects `graph_data`, which represents *what* data to draw, and
|
||||||
|
/// various details like style and optional legends.
|
||||||
pub fn draw_time_graph(&self, f: &mut Frame<'_>, draw_loc: Rect, graph_data: &[GraphData<'_>]) {
|
pub fn draw_time_graph(&self, f: &mut Frame<'_>, draw_loc: Rect, graph_data: &[GraphData<'_>]) {
|
||||||
let x_axis = self.generate_x_axis();
|
let x_axis = self.generate_x_axis();
|
||||||
let y_axis = self.generate_y_axis();
|
let y_axis = self.generate_y_axis();
|
||||||
|
|
|
@ -47,8 +47,8 @@ impl<'a> Default for PipeGauge<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> PipeGauge<'a> {
|
impl<'a> PipeGauge<'a> {
|
||||||
/// The ratio, a value from 0.0 to 1.0 (any other greater or less will be clamped)
|
/// The ratio, a value from 0.0 to 1.0 (any other greater or less will be
|
||||||
/// represents the portion of the pipe gauge to fill.
|
/// clamped) represents the portion of the pipe gauge to fill.
|
||||||
///
|
///
|
||||||
/// Note: passing in NaN will potentially cause problems.
|
/// Note: passing in NaN will potentially cause problems.
|
||||||
pub fn ratio(mut self, ratio: f64) -> Self {
|
pub fn ratio(mut self, ratio: f64) -> Self {
|
||||||
|
@ -87,7 +87,8 @@ impl<'a> PipeGauge<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether to hide parts of the gauge/label if the inner label wouldn't fit.
|
/// Whether to hide parts of the gauge/label if the inner label wouldn't
|
||||||
|
/// fit.
|
||||||
pub fn hide_parts(mut self, hide_parts: LabelLimit) -> Self {
|
pub fn hide_parts(mut self, hide_parts: LabelLimit) -> Self {
|
||||||
self.hide_parts = hide_parts;
|
self.hide_parts = hide_parts;
|
||||||
self
|
self
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//! A [`tui::widgets::Chart`] but slightly more specialized to show right-aligned timeseries
|
//! A [`tui::widgets::Chart`] but slightly more specialized to show
|
||||||
//! data.
|
//! right-aligned timeseries data.
|
||||||
//!
|
//!
|
||||||
//! Generally should be updated to be in sync with [`chart.rs`](https://github.com/ratatui-org/ratatui/blob/main/src/widgets/chart.rs);
|
//! Generally should be updated to be in sync with [`chart.rs`](https://github.com/ratatui-org/ratatui/blob/main/src/widgets/chart.rs);
|
||||||
//! the specializations are factored out to `time_chart/points.rs`.
|
//! the specializations are factored out to `time_chart/points.rs`.
|
||||||
|
@ -31,7 +31,8 @@ pub type Point = (f64, f64);
|
||||||
pub struct Axis<'a> {
|
pub struct Axis<'a> {
|
||||||
/// Title displayed next to axis end
|
/// Title displayed next to axis end
|
||||||
pub(crate) title: Option<Line<'a>>,
|
pub(crate) title: Option<Line<'a>>,
|
||||||
/// Bounds for the axis (all data points outside these limits will not be represented)
|
/// Bounds for the axis (all data points outside these limits will not be
|
||||||
|
/// represented)
|
||||||
pub(crate) bounds: [f64; 2],
|
pub(crate) bounds: [f64; 2],
|
||||||
/// A list of labels to put to the left or below the axis
|
/// A list of labels to put to the left or below the axis
|
||||||
pub(crate) labels: Option<Vec<Span<'a>>>,
|
pub(crate) labels: Option<Vec<Span<'a>>>,
|
||||||
|
@ -44,10 +45,11 @@ pub struct Axis<'a> {
|
||||||
impl<'a> Axis<'a> {
|
impl<'a> Axis<'a> {
|
||||||
/// Sets the axis title
|
/// Sets the axis title
|
||||||
///
|
///
|
||||||
/// It will be displayed at the end of the axis. For an X axis this is the right, for a Y axis,
|
/// It will be displayed at the end of the axis. For an X axis this is the
|
||||||
/// this is the top.
|
/// right, for a Y axis, this is the top.
|
||||||
///
|
///
|
||||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
/// This is a fluent setter method which must be chained or used as it
|
||||||
|
/// consumes self
|
||||||
#[must_use = "method moves the value of self and returns the modified value"]
|
#[must_use = "method moves the value of self and returns the modified value"]
|
||||||
pub fn title<T>(mut self, title: T) -> Axis<'a>
|
pub fn title<T>(mut self, title: T) -> Axis<'a>
|
||||||
where
|
where
|
||||||
|
@ -61,7 +63,8 @@ impl<'a> Axis<'a> {
|
||||||
///
|
///
|
||||||
/// In other words, sets the min and max value on this axis.
|
/// In other words, sets the min and max value on this axis.
|
||||||
///
|
///
|
||||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
/// This is a fluent setter method which must be chained or used as it
|
||||||
|
/// consumes self
|
||||||
#[must_use = "method moves the value of self and returns the modified value"]
|
#[must_use = "method moves the value of self and returns the modified value"]
|
||||||
pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a> {
|
pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a> {
|
||||||
self.bounds = bounds;
|
self.bounds = bounds;
|
||||||
|
@ -238,11 +241,13 @@ impl FromStr for LegendPosition {
|
||||||
///
|
///
|
||||||
/// This is the main element composing a [`TimeChart`].
|
/// This is the main element composing a [`TimeChart`].
|
||||||
///
|
///
|
||||||
/// A dataset can be [named](Dataset::name). Only named datasets will be rendered in the legend.
|
/// A dataset can be [named](Dataset::name). Only named datasets will be
|
||||||
|
/// rendered in the legend.
|
||||||
///
|
///
|
||||||
/// After that, you can pass it data with [`Dataset::data`]. Data is an array of `f64` tuples
|
/// After that, you can pass it data with [`Dataset::data`]. Data is an array of
|
||||||
/// (`(f64, f64)`), the first element being X and the second Y. It's also worth noting that, unlike
|
/// `f64` tuples (`(f64, f64)`), the first element being X and the second Y.
|
||||||
/// the [`Rect`], here the Y axis is bottom to top, as in math.
|
/// It's also worth noting that, unlike the [`Rect`], here the Y axis is bottom
|
||||||
|
/// to top, as in math.
|
||||||
#[derive(Debug, Default, Clone, PartialEq)]
|
#[derive(Debug, Default, Clone, PartialEq)]
|
||||||
pub struct Dataset<'a> {
|
pub struct Dataset<'a> {
|
||||||
/// Name of the dataset (used in the legend if shown)
|
/// Name of the dataset (used in the legend if shown)
|
||||||
|
@ -270,12 +275,12 @@ impl<'a> Dataset<'a> {
|
||||||
|
|
||||||
/// Sets the data points of this dataset
|
/// Sets the data points of this dataset
|
||||||
///
|
///
|
||||||
/// Points will then either be rendered as scrattered points or with lines between them
|
/// Points will then either be rendered as scrattered points or with lines
|
||||||
/// depending on [`Dataset::graph_type`].
|
/// between them depending on [`Dataset::graph_type`].
|
||||||
///
|
///
|
||||||
/// Data consist in an array of `f64` tuples (`(f64, f64)`), the first element being X and the
|
/// Data consist in an array of `f64` tuples (`(f64, f64)`), the first
|
||||||
/// second Y. It's also worth noting that, unlike the [`Rect`], here the Y axis is bottom to
|
/// element being X and the second Y. It's also worth noting that,
|
||||||
/// top, as in math.
|
/// unlike the [`Rect`], here the Y axis is bottom to top, as in math.
|
||||||
#[must_use = "method moves the value of self and returns the modified value"]
|
#[must_use = "method moves the value of self and returns the modified value"]
|
||||||
pub fn data(mut self, data: &'a [(f64, f64)]) -> Dataset<'a> {
|
pub fn data(mut self, data: &'a [(f64, f64)]) -> Dataset<'a> {
|
||||||
self.data = data;
|
self.data = data;
|
||||||
|
@ -284,12 +289,15 @@ impl<'a> Dataset<'a> {
|
||||||
|
|
||||||
/// Sets the kind of character to use to display this dataset
|
/// Sets the kind of character to use to display this dataset
|
||||||
///
|
///
|
||||||
/// You can use dots (`•`), blocks (`█`), bars (`▄`), braille (`⠓`, `⣇`, `⣿`) or half-blocks
|
/// You can use dots (`•`), blocks (`█`), bars (`▄`), braille (`⠓`, `⣇`,
|
||||||
/// (`█`, `▄`, and `▀`). See [symbols::Marker] for more details.
|
/// `⣿`) or half-blocks (`█`, `▄`, and `▀`). See [symbols::Marker] for
|
||||||
|
/// more details.
|
||||||
///
|
///
|
||||||
/// Note [`Marker::Braille`] requires a font that supports Unicode Braille Patterns.
|
/// Note [`Marker::Braille`] requires a font that supports Unicode Braille
|
||||||
|
/// Patterns.
|
||||||
///
|
///
|
||||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
/// This is a fluent setter method which must be chained or used as it
|
||||||
|
/// consumes self
|
||||||
#[must_use = "method moves the value of self and returns the modified value"]
|
#[must_use = "method moves the value of self and returns the modified value"]
|
||||||
pub fn marker(mut self, marker: symbols::Marker) -> Dataset<'a> {
|
pub fn marker(mut self, marker: symbols::Marker) -> Dataset<'a> {
|
||||||
self.marker = marker;
|
self.marker = marker;
|
||||||
|
@ -298,9 +306,10 @@ impl<'a> Dataset<'a> {
|
||||||
|
|
||||||
/// Sets how the dataset should be drawn
|
/// Sets how the dataset should be drawn
|
||||||
///
|
///
|
||||||
/// [`TimeChart`] can draw either a [scatter](GraphType::Scatter) or [line](GraphType::Line) charts.
|
/// [`TimeChart`] can draw either a [scatter](GraphType::Scatter) or
|
||||||
/// A scatter will draw only the points in the dataset while a line will also draw a line
|
/// [line](GraphType::Line) charts. A scatter will draw only the points
|
||||||
/// between them. See [`GraphType`] for more details
|
/// in the dataset while a line will also draw a line between them. See
|
||||||
|
/// [`GraphType`] for more details
|
||||||
#[must_use = "method moves the value of self and returns the modified value"]
|
#[must_use = "method moves the value of self and returns the modified value"]
|
||||||
pub fn graph_type(mut self, graph_type: GraphType) -> Dataset<'a> {
|
pub fn graph_type(mut self, graph_type: GraphType) -> Dataset<'a> {
|
||||||
self.graph_type = graph_type;
|
self.graph_type = graph_type;
|
||||||
|
@ -309,11 +318,13 @@ impl<'a> Dataset<'a> {
|
||||||
|
|
||||||
/// Sets the style of this dataset
|
/// Sets the style of this dataset
|
||||||
///
|
///
|
||||||
/// The given style will be used to draw the legend and the data points. Currently the legend
|
/// The given style will be used to draw the legend and the data points.
|
||||||
/// will use the entire style whereas the data points will only use the foreground.
|
/// Currently the legend will use the entire style whereas the data
|
||||||
|
/// points will only use the foreground.
|
||||||
///
|
///
|
||||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
/// `style` accepts any type that is convertible to [`Style`] (e.g.
|
||||||
/// your own type that implements [`Into<Style>`]).
|
/// [`Style`], [`Color`], or your own type that implements
|
||||||
|
/// [`Into<Style>`]).
|
||||||
#[must_use = "method moves the value of self and returns the modified value"]
|
#[must_use = "method moves the value of self and returns the modified value"]
|
||||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Dataset<'a> {
|
pub fn style<S: Into<Style>>(mut self, style: S) -> Dataset<'a> {
|
||||||
self.style = style.into();
|
self.style = style.into();
|
||||||
|
@ -321,8 +332,8 @@ impl<'a> Dataset<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A container that holds all the infos about where to display each elements of the chart (axis,
|
/// A container that holds all the infos about where to display each elements of
|
||||||
/// labels, legend, ...).
|
/// the chart (axis, labels, legend, ...).
|
||||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||||
struct ChartLayout {
|
struct ChartLayout {
|
||||||
/// Location of the title of the x axis
|
/// Location of the title of the x axis
|
||||||
|
@ -343,8 +354,9 @@ struct ChartLayout {
|
||||||
graph_area: Rect,
|
graph_area: Rect,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A "custom" chart, just a slightly tweaked [`tui::widgets::Chart`] from ratatui, but with greater control over the
|
/// A "custom" chart, just a slightly tweaked [`tui::widgets::Chart`] from
|
||||||
/// legend, and built with the idea of drawing data points relative to a time-based x-axis.
|
/// ratatui, but with greater control over the legend, and built with the idea
|
||||||
|
/// of drawing data points relative to a time-based x-axis.
|
||||||
///
|
///
|
||||||
/// Main changes:
|
/// Main changes:
|
||||||
/// - Styling option for the legend box
|
/// - Styling option for the legend box
|
||||||
|
@ -368,8 +380,8 @@ pub struct TimeChart<'a> {
|
||||||
legend_style: Style,
|
legend_style: Style,
|
||||||
/// Constraints used to determine whether the legend should be shown or not
|
/// Constraints used to determine whether the legend should be shown or not
|
||||||
hidden_legend_constraints: (Constraint, Constraint),
|
hidden_legend_constraints: (Constraint, Constraint),
|
||||||
/// The position detnermine where the legenth is shown or hide regaurdless of
|
/// The position detnermine where the legenth is shown or hide regaurdless
|
||||||
/// `hidden_legend_constraints`
|
/// of `hidden_legend_constraints`
|
||||||
legend_position: Option<LegendPosition>,
|
legend_position: Option<LegendPosition>,
|
||||||
/// The marker type.
|
/// The marker type.
|
||||||
marker: Marker,
|
marker: Marker,
|
||||||
|
@ -402,8 +414,9 @@ impl<'a> TimeChart<'a> {
|
||||||
|
|
||||||
/// Sets the style of the entire chart
|
/// Sets the style of the entire chart
|
||||||
///
|
///
|
||||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
/// `style` accepts any type that is convertible to [`Style`] (e.g.
|
||||||
/// your own type that implements [`Into<Style>`]).
|
/// [`Style`], [`Color`], or your own type that implements
|
||||||
|
/// [`Into<Style>`]).
|
||||||
///
|
///
|
||||||
/// Styles of [`Axis`] and [`Dataset`] will have priority over this style.
|
/// Styles of [`Axis`] and [`Dataset`] will have priority over this style.
|
||||||
#[must_use = "method moves the value of self and returns the modified value"]
|
#[must_use = "method moves the value of self and returns the modified value"]
|
||||||
|
@ -444,14 +457,16 @@ impl<'a> TimeChart<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the constraints used to determine whether the legend should be shown or not.
|
/// Sets the constraints used to determine whether the legend should be
|
||||||
|
/// shown or not.
|
||||||
///
|
///
|
||||||
/// The tuple's first constraint is used for the width and the second for the height. If the
|
/// The tuple's first constraint is used for the width and the second for
|
||||||
/// legend takes more space than what is allowed by any constraint, the legend is hidden.
|
/// the height. If the legend takes more space than what is allowed by
|
||||||
/// [`Constraint::Min`] is an exception and will always show the legend.
|
/// any constraint, the legend is hidden. [`Constraint::Min`] is an
|
||||||
|
/// exception and will always show the legend.
|
||||||
///
|
///
|
||||||
/// If this is not set, the default behavior is to hide the legend if it is greater than 25% of
|
/// If this is not set, the default behavior is to hide the legend if it is
|
||||||
/// the chart, either horizontally or vertically.
|
/// greater than 25% of the chart, either horizontally or vertically.
|
||||||
#[must_use = "method moves the value of self and returns the modified value"]
|
#[must_use = "method moves the value of self and returns the modified value"]
|
||||||
pub fn hidden_legend_constraints(
|
pub fn hidden_legend_constraints(
|
||||||
mut self, constraints: (Constraint, Constraint),
|
mut self, constraints: (Constraint, Constraint),
|
||||||
|
@ -464,8 +479,9 @@ impl<'a> TimeChart<'a> {
|
||||||
///
|
///
|
||||||
/// The default is [`LegendPosition::TopRight`].
|
/// The default is [`LegendPosition::TopRight`].
|
||||||
///
|
///
|
||||||
/// If [`None`] is given, hide the legend even if [`hidden_legend_constraints`] determines it
|
/// If [`None`] is given, hide the legend even if
|
||||||
/// should be shown. In contrast, if `Some(...)` is given, [`hidden_legend_constraints`] might
|
/// [`hidden_legend_constraints`] determines it should be shown. In
|
||||||
|
/// contrast, if `Some(...)` is given, [`hidden_legend_constraints`] might
|
||||||
/// still decide whether to show the legend or not.
|
/// still decide whether to show the legend or not.
|
||||||
///
|
///
|
||||||
/// See [`LegendPosition`] for all available positions.
|
/// See [`LegendPosition`] for all available positions.
|
||||||
|
@ -477,8 +493,8 @@ impl<'a> TimeChart<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the internal layout of the chart given the area. If the area is too small some
|
/// Compute the internal layout of the chart given the area. If the area is
|
||||||
/// elements may be automatically hidden
|
/// too small some elements may be automatically hidden
|
||||||
fn layout(&self, area: Rect) -> ChartLayout {
|
fn layout(&self, area: Rect) -> ChartLayout {
|
||||||
let mut layout = ChartLayout::default();
|
let mut layout = ChartLayout::default();
|
||||||
if area.height == 0 || area.width == 0 {
|
if area.height == 0 || area.width == 0 {
|
||||||
|
@ -593,7 +609,8 @@ impl<'a> TimeChart<'a> {
|
||||||
};
|
};
|
||||||
max_width = max(max_width, width_left_of_y_axis);
|
max_width = max(max_width, width_left_of_y_axis);
|
||||||
}
|
}
|
||||||
// labels of y axis and first label of x axis can take at most 1/3rd of the total width
|
// labels of y axis and first label of x axis can take at most 1/3rd of the
|
||||||
|
// total width
|
||||||
max_width.min(area.width / 3)
|
max_width.min(area.width / 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -703,9 +720,9 @@ impl Widget for TimeChart<'_> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sample the style of the entire widget. This sample will be used to reset the style of
|
// Sample the style of the entire widget. This sample will be used to reset the
|
||||||
// the cells that are part of the components put on top of the grah area (i.e legend and
|
// style of the cells that are part of the components put on top of the
|
||||||
// axis names).
|
// grah area (i.e legend and axis names).
|
||||||
let original_style = buf.get(area.left(), area.top()).style();
|
let original_style = buf.get(area.left(), area.top()).style();
|
||||||
|
|
||||||
let layout = self.layout(chart_area);
|
let layout = self.layout(chart_area);
|
||||||
|
@ -1020,7 +1037,8 @@ mod tests {
|
||||||
let layout = widget.layout(buffer.area);
|
let layout = widget.layout(buffer.area);
|
||||||
|
|
||||||
assert!(layout.legend_area.is_some());
|
assert!(layout.legend_area.is_some());
|
||||||
assert_eq!(layout.legend_area.unwrap().height, 4); // 2 for borders, 2 for rows
|
assert_eq!(layout.legend_area.unwrap().height, 4); // 2 for borders, 2
|
||||||
|
// for rows
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
//! Vendored from <https://github.com/fdehau/tui-rs/blob/fafad6c96109610825aad89c4bba5253e01101ed/src/widgets/canvas/mod.rs>
|
//! Vendored from <https://github.com/fdehau/tui-rs/blob/fafad6c96109610825aad89c4bba5253e01101ed/src/widgets/canvas/mod.rs>
|
||||||
//! and <https://github.com/ratatui-org/ratatui/blob/c8dd87918d44fff6d4c3c78e1fc821a3275db1ae/src/widgets/canvas.rs>.
|
//! and <https://github.com/ratatui-org/ratatui/blob/c8dd87918d44fff6d4c3c78e1fc821a3275db1ae/src/widgets/canvas.rs>.
|
||||||
//!
|
//!
|
||||||
//! The main thing this is pulled in for is overriding how `BrailleGrid`'s draw logic works, as changing it is
|
//! The main thing this is pulled in for is overriding how `BrailleGrid`'s draw
|
||||||
//! needed in order to draw all datasets in only one layer back in [`super::TimeChart::render`]. More specifically,
|
//! logic works, as changing it is needed in order to draw all datasets in only
|
||||||
//! the current implementation in ratatui `|=`s all the cells together if they overlap, but since we are smashing
|
//! one layer back in [`super::TimeChart::render`]. More specifically,
|
||||||
//! all the layers together which may have different colours, we instead just _replace_ whatever was in that cell
|
//! the current implementation in ratatui `|=`s all the cells together if they
|
||||||
|
//! overlap, but since we are smashing all the layers together which may have
|
||||||
|
//! different colours, we instead just _replace_ whatever was in that cell
|
||||||
//! with the newer colour + character.
|
//! with the newer colour + character.
|
||||||
//!
|
//!
|
||||||
//! See <https://github.com/ClementTsang/bottom/pull/918> and <https://github.com/ClementTsang/bottom/pull/937> for the
|
//! See <https://github.com/ClementTsang/bottom/pull/918> and <https://github.com/ClementTsang/bottom/pull/937> for the
|
||||||
|
@ -285,19 +287,21 @@ pub struct Painter<'a, 'b> {
|
||||||
resolution: (f64, f64),
|
resolution: (f64, f64),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The HalfBlockGrid is a grid made up of cells each containing a half block character.
|
/// The HalfBlockGrid is a grid made up of cells each containing a half block
|
||||||
|
/// character.
|
||||||
///
|
///
|
||||||
/// In terminals, each character is usually twice as tall as it is wide. Unicode has a couple of
|
/// In terminals, each character is usually twice as tall as it is wide. Unicode
|
||||||
/// vertical half block characters, the upper half block '▀' and lower half block '▄' which take up
|
/// has a couple of vertical half block characters, the upper half block '▀' and
|
||||||
/// half the height of a normal character but the full width. Together with an empty space ' ' and a
|
/// lower half block '▄' which take up half the height of a normal character but
|
||||||
/// full block '█', we can effectively double the resolution of a single cell. In addition, because
|
/// the full width. Together with an empty space ' ' and a full block '█', we
|
||||||
/// each character can have a foreground and background color, we can control the color of the upper
|
/// can effectively double the resolution of a single cell. In addition, because
|
||||||
/// and lower half of each cell. This allows us to draw shapes with a resolution of 1x2 "pixels" per
|
/// each character can have a foreground and background color, we can control
|
||||||
/// cell.
|
/// the color of the upper and lower half of each cell. This allows us to draw
|
||||||
|
/// shapes with a resolution of 1x2 "pixels" per cell.
|
||||||
///
|
///
|
||||||
/// This allows for more flexibility than the BrailleGrid which only supports a single
|
/// This allows for more flexibility than the BrailleGrid which only supports a
|
||||||
/// foreground color for each 2x4 dots cell, and the CharGrid which only supports a single
|
/// single foreground color for each 2x4 dots cell, and the CharGrid which only
|
||||||
/// character for each cell.
|
/// supports a single character for each cell.
|
||||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||||
struct HalfBlockGrid {
|
struct HalfBlockGrid {
|
||||||
/// width of the grid in number of terminal columns
|
/// width of the grid in number of terminal columns
|
||||||
|
@ -309,8 +313,8 @@ struct HalfBlockGrid {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HalfBlockGrid {
|
impl HalfBlockGrid {
|
||||||
/// Create a new [`HalfBlockGrid`] with the given width and height measured in terminal columns
|
/// Create a new [`HalfBlockGrid`] with the given width and height measured
|
||||||
/// and rows respectively.
|
/// in terminal columns and rows respectively.
|
||||||
fn new(width: u16, height: u16) -> HalfBlockGrid {
|
fn new(width: u16, height: u16) -> HalfBlockGrid {
|
||||||
HalfBlockGrid {
|
HalfBlockGrid {
|
||||||
width,
|
width,
|
||||||
|
@ -334,10 +338,11 @@ impl Grid for HalfBlockGrid {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save(&self) -> Layer {
|
fn save(&self) -> Layer {
|
||||||
// Given that we store the pixels in a grid, and that we want to use 2 pixels arranged
|
// Given that we store the pixels in a grid, and that we want to use 2 pixels
|
||||||
// vertically to form a single terminal cell, which can be either empty, upper half block,
|
// arranged vertically to form a single terminal cell, which can be
|
||||||
// lower half block or full block, we need examine the pixels in vertical pairs to decide
|
// either empty, upper half block, lower half block or full block, we
|
||||||
// what character to print in each cell. So these are the 4 states we use to represent each
|
// need examine the pixels in vertical pairs to decide what character to
|
||||||
|
// print in each cell. So these are the 4 states we use to represent each
|
||||||
// cell:
|
// cell:
|
||||||
//
|
//
|
||||||
// 1. upper: reset, lower: reset => ' ' fg: reset / bg: reset
|
// 1. upper: reset, lower: reset => ' ' fg: reset / bg: reset
|
||||||
|
@ -345,20 +350,23 @@ impl Grid for HalfBlockGrid {
|
||||||
// 3. upper: color, lower: reset => '▀' fg: upper color / bg: reset
|
// 3. upper: color, lower: reset => '▀' fg: upper color / bg: reset
|
||||||
// 4. upper: color, lower: color => '▀' fg: upper color / bg: lower color
|
// 4. upper: color, lower: color => '▀' fg: upper color / bg: lower color
|
||||||
//
|
//
|
||||||
// Note that because the foreground reset color (i.e. default foreground color) is usually
|
// Note that because the foreground reset color (i.e. default foreground color)
|
||||||
// not the same as the background reset color (i.e. default background color), we need to
|
// is usually not the same as the background reset color (i.e. default
|
||||||
// swap around the colors for that state (2 reset/color).
|
// background color), we need to swap around the colors for that state
|
||||||
|
// (2 reset/color).
|
||||||
//
|
//
|
||||||
// When the upper and lower colors are the same, we could continue to use an upper half
|
// When the upper and lower colors are the same, we could continue to use an
|
||||||
// block, but we choose to use a full block instead. This allows us to write unit tests that
|
// upper half block, but we choose to use a full block instead. This
|
||||||
// treat the cell as a single character instead of two half block characters.
|
// allows us to write unit tests that treat the cell as a single
|
||||||
|
// character instead of two half block characters.
|
||||||
|
|
||||||
// Note we implement this slightly differently to what is done in ratatui's repo,
|
// Note we implement this slightly differently to what is done in ratatui's
|
||||||
// since their version doesn't seem to compile for me...
|
// repo, since their version doesn't seem to compile for me...
|
||||||
// TODO: Whenever I add this as a valid marker, make sure this works fine with the overriden
|
// TODO: Whenever I add this as a valid marker, make sure this works fine with
|
||||||
// time_chart drawing-layer-thing.
|
// the overriden time_chart drawing-layer-thing.
|
||||||
|
|
||||||
// Join the upper and lower rows, and emit a tuple vector of strings to print, and their colours.
|
// Join the upper and lower rows, and emit a tuple vector of strings to print,
|
||||||
|
// and their colours.
|
||||||
let (string, colors) = self
|
let (string, colors) = self
|
||||||
.pixels
|
.pixels
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -503,8 +511,8 @@ impl<'a> Context<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Canvas widget may be used to draw more detailed figures using braille patterns (each
|
/// The Canvas widget may be used to draw more detailed figures using braille
|
||||||
/// cell can have a braille character in 8 different positions).
|
/// patterns (each cell can have a braille character in 8 different positions).
|
||||||
pub struct Canvas<'a, F>
|
pub struct Canvas<'a, F>
|
||||||
where
|
where
|
||||||
F: Fn(&mut Context<'_>),
|
F: Fn(&mut Context<'_>),
|
||||||
|
@ -558,9 +566,10 @@ where
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change the type of points used to draw the shapes. By default the braille patterns are used
|
/// Change the type of points used to draw the shapes. By default the
|
||||||
/// as they provide a more fine grained result but you might want to use the simple dot or
|
/// braille patterns are used as they provide a more fine grained result
|
||||||
/// block instead if the targeted terminal does not support those symbols.
|
/// but you might want to use the simple dot or block instead if the
|
||||||
|
/// targeted terminal does not support those symbols.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
|
|
|
@ -22,9 +22,10 @@ impl TimeChart<'_> {
|
||||||
// See <https://github.com/ClementTsang/bottom/pull/918> and <https://github.com/ClementTsang/bottom/pull/937>
|
// See <https://github.com/ClementTsang/bottom/pull/918> and <https://github.com/ClementTsang/bottom/pull/937>
|
||||||
// for the original motivation.
|
// for the original motivation.
|
||||||
//
|
//
|
||||||
// We also additionally do some interpolation logic because we may get caught missing some points
|
// We also additionally do some interpolation logic because we may get caught
|
||||||
// when drawing, but we generally want to avoid jarring gaps between the edges when there's
|
// missing some points when drawing, but we generally want to avoid
|
||||||
// a point that is off screen and so a line isn't drawn (right edge generally won't have this issue
|
// jarring gaps between the edges when there's a point that is off
|
||||||
|
// screen and so a line isn't drawn (right edge generally won't have this issue
|
||||||
// issue but it can happen in some cases).
|
// issue but it can happen in some cases).
|
||||||
|
|
||||||
for dataset in &self.datasets {
|
for dataset in &self.datasets {
|
||||||
|
@ -112,7 +113,8 @@ impl TimeChart<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the start index and potential interpolation index given the start time and the dataset.
|
/// Returns the start index and potential interpolation index given the start
|
||||||
|
/// time and the dataset.
|
||||||
fn get_start(dataset: &Dataset<'_>, start_bound: f64) -> (usize, Option<usize>) {
|
fn get_start(dataset: &Dataset<'_>, start_bound: f64) -> (usize, Option<usize>) {
|
||||||
match dataset
|
match dataset
|
||||||
.data
|
.data
|
||||||
|
@ -123,18 +125,20 @@ fn get_start(dataset: &Dataset<'_>, start_bound: f64) -> (usize, Option<usize>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the end position and potential interpolation index given the end time and the dataset.
|
/// Returns the end position and potential interpolation index given the end
|
||||||
|
/// time and the dataset.
|
||||||
fn get_end(dataset: &Dataset<'_>, end_bound: f64) -> (usize, Option<usize>) {
|
fn get_end(dataset: &Dataset<'_>, end_bound: f64) -> (usize, Option<usize>) {
|
||||||
match dataset
|
match dataset
|
||||||
.data
|
.data
|
||||||
.binary_search_by(|(x, _y)| partial_ordering(x, &end_bound))
|
.binary_search_by(|(x, _y)| partial_ordering(x, &end_bound))
|
||||||
{
|
{
|
||||||
// In the success case, this means we found an index. Add one since we want to include this index and we
|
// In the success case, this means we found an index. Add one since we want to include this
|
||||||
// expect to use the returned index as part of a (m..n) range.
|
// index and we expect to use the returned index as part of a (m..n) range.
|
||||||
Ok(index) => (index.saturating_add(1), None),
|
Ok(index) => (index.saturating_add(1), None),
|
||||||
// In the fail case, this means we did not find an index, and the returned index is where one would *insert*
|
// In the fail case, this means we did not find an index, and the returned index is where
|
||||||
// the location. This index is where one would insert to fit inside the dataset - and since this is an end
|
// one would *insert* the location. This index is where one would insert to fit
|
||||||
// bound, index is, in a sense, already "+1" for our range later.
|
// inside the dataset - and since this is an end bound, index is, in a sense,
|
||||||
|
// already "+1" for our range later.
|
||||||
Err(index) => (index, {
|
Err(index) => (index, {
|
||||||
let sum = index.checked_add(1);
|
let sum = index.checked_add(1);
|
||||||
match sum {
|
match sum {
|
||||||
|
@ -145,7 +149,8 @@ fn get_end(dataset: &Dataset<'_>, end_bound: f64) -> (usize, Option<usize>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the y-axis value for a given `x`, given two points to draw a line between.
|
/// Returns the y-axis value for a given `x`, given two points to draw a line
|
||||||
|
/// between.
|
||||||
fn interpolate_point(older_point: &Point, newer_point: &Point, x: f64) -> f64 {
|
fn interpolate_point(older_point: &Point, newer_point: &Point, x: f64) -> f64 {
|
||||||
let delta_x = newer_point.0 - older_point.0;
|
let delta_x = newer_point.0 - older_point.0;
|
||||||
let delta_y = newer_point.1 - older_point.1;
|
let delta_y = newer_point.1 - older_point.1;
|
||||||
|
|
|
@ -129,15 +129,18 @@ impl Painter {
|
||||||
|
|
||||||
if app_state.should_get_widget_bounds() {
|
if app_state.should_get_widget_bounds() {
|
||||||
// Some explanations for future readers:
|
// Some explanations for future readers:
|
||||||
// - The "height" as of writing of this entire widget is 2. If it's 1, it occasionally doesn't draw.
|
// - The "height" as of writing of this entire widget is 2. If it's 1, it
|
||||||
|
// occasionally doesn't draw.
|
||||||
// - As such, the buttons are only on the lower part of this 2-high widget.
|
// - As such, the buttons are only on the lower part of this 2-high widget.
|
||||||
// - So, we want to only check at one location, the `draw_loc.y + 1`, and that's it.
|
// - So, we want to only check at one location, the `draw_loc.y + 1`, and that's
|
||||||
|
// it.
|
||||||
// - But why is it "+2" then? Well, it's because I have a REALLY ugly hack
|
// - But why is it "+2" then? Well, it's because I have a REALLY ugly hack
|
||||||
// for mouse button checking, since most button checks are of the form `(draw_loc.y + draw_loc.height)`,
|
// for mouse button checking, since most button checks are of the form `(draw_loc.y + draw_loc.height)`,
|
||||||
// and the same for the x and width. Unfortunately, if you check using >= and <=, the outer bound is
|
// and the same for the x and width. Unfortunately, if you check using >= and <=, the outer bound is
|
||||||
// actually too large - so, we assume all of them are one too big and check via < (see
|
// actually too large - so, we assume all of them are one too big and check via < (see
|
||||||
// https://github.com/ClementTsang/bottom/pull/459 for details).
|
// https://github.com/ClementTsang/bottom/pull/459 for details).
|
||||||
// - So in other words, to make it simple, we keep this to a standard and overshoot by one here.
|
// - So in other words, to make it simple, we keep this to a standard and
|
||||||
|
// overshoot by one here.
|
||||||
if let Some(basic_table) = &mut app_state.states.basic_table_widget_state {
|
if let Some(basic_table) = &mut app_state.states.basic_table_widget_state {
|
||||||
basic_table.left_tlc =
|
basic_table.left_tlc =
|
||||||
Some((margined_draw_loc[0].x, margined_draw_loc[0].y + 1));
|
Some((margined_draw_loc[0].x, margined_draw_loc[0].y + 1));
|
||||||
|
|
|
@ -249,14 +249,15 @@ impl Painter {
|
||||||
const SIGNAL: usize = if cfg!(target_os = "windows") { 1 } else { 15 };
|
const SIGNAL: usize = if cfg!(target_os = "windows") { 1 } else { 15 };
|
||||||
|
|
||||||
// This is kinda weird, but the gist is:
|
// This is kinda weird, but the gist is:
|
||||||
// - We have three sections; we put our mouse bounding box for the "yes" button at the very right edge
|
// - We have three sections; we put our mouse bounding box for the "yes" button
|
||||||
// of the left section and 3 characters back. We then give it a buffer size of 1 on the x-coordinate.
|
// at the very right edge of the left section and 3 characters back. We then
|
||||||
// - Same for the "no" button, except it is the right section and we do it from the start of the right
|
// give it a buffer size of 1 on the x-coordinate.
|
||||||
// section.
|
// - Same for the "no" button, except it is the right section and we do it from
|
||||||
|
// the start of the right section.
|
||||||
//
|
//
|
||||||
// Lastly, note that mouse detection for the dd buttons assume correct widths. As such, we correct
|
// Lastly, note that mouse detection for the dd buttons assume correct widths.
|
||||||
// them here and check with >= and <= mouse bound checks, as opposed to how we do it elsewhere with
|
// As such, we correct them here and check with >= and <= mouse
|
||||||
// >= and <. See https://github.com/ClementTsang/bottom/pull/459 for details.
|
// bound checks, as opposed to how we do it elsewhere with >= and <. See https://github.com/ClementTsang/bottom/pull/459 for details.
|
||||||
app_state.delete_dialog_state.button_positions = vec![
|
app_state.delete_dialog_state.button_positions = vec![
|
||||||
// Yes
|
// Yes
|
||||||
(
|
(
|
||||||
|
|
|
@ -63,8 +63,8 @@ impl Painter {
|
||||||
.border_style(self.colours.border_style);
|
.border_style(self.colours.border_style);
|
||||||
|
|
||||||
if app_state.should_get_widget_bounds() {
|
if app_state.should_get_widget_bounds() {
|
||||||
// We must also recalculate how many lines are wrapping to properly get scrolling to work on
|
// We must also recalculate how many lines are wrapping to properly get
|
||||||
// small terminal sizes... oh joy.
|
// scrolling to work on small terminal sizes... oh joy.
|
||||||
|
|
||||||
app_state.help_dialog_state.height = block.inner(draw_loc).height;
|
app_state.help_dialog_state.height = block.inner(draw_loc).height;
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ use tui::style::{Color, Style};
|
||||||
|
|
||||||
use super::ColourScheme;
|
use super::ColourScheme;
|
||||||
pub use crate::options::ConfigV1;
|
pub use crate::options::ConfigV1;
|
||||||
use crate::{constants::*, options::colours::ConfigColours, utils::error};
|
use crate::{constants::*, options::colours::ColoursConfig, utils::error};
|
||||||
|
|
||||||
pub struct CanvasStyling {
|
pub struct CanvasStyling {
|
||||||
pub currently_selected_text_colour: Color,
|
pub currently_selected_text_colour: Color,
|
||||||
|
@ -154,7 +154,7 @@ impl CanvasStyling {
|
||||||
Ok(canvas_colours)
|
Ok(canvas_colours)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_colours_from_palette(&mut self, colours: &ConfigColours) -> anyhow::Result<()> {
|
pub fn set_colours_from_palette(&mut self, colours: &ColoursConfig) -> anyhow::Result<()> {
|
||||||
// CPU
|
// CPU
|
||||||
try_set_colour!(self.avg_colour_style, colours, avg_cpu_color);
|
try_set_colour!(self.avg_colour_style, colours, avg_cpu_color);
|
||||||
try_set_colour!(self.all_colour_style, colours, all_cpu_color);
|
try_set_colour!(self.all_colour_style, colours, all_cpu_color);
|
||||||
|
|
|
@ -111,8 +111,9 @@ impl Painter {
|
||||||
tab_click_locs
|
tab_click_locs
|
||||||
.push(((current_x, current_y), (current_x + width, current_y)));
|
.push(((current_x, current_y), (current_x + width, current_y)));
|
||||||
|
|
||||||
// +4 because we want to go one space, then one space past to get to the '|', then 2 more
|
// +4 because we want to go one space, then one space past to get to the
|
||||||
// to start at the blank space before the tab label.
|
// '|', then 2 more to start at the blank space
|
||||||
|
// before the tab label.
|
||||||
current_x += width + 4;
|
current_x += width + 4;
|
||||||
}
|
}
|
||||||
battery_widget_state.tab_click_locs = Some(tab_click_locs);
|
battery_widget_state.tab_click_locs = Some(tab_click_locs);
|
||||||
|
|
|
@ -33,8 +33,8 @@ impl Painter {
|
||||||
// many rows and columns we have in draw_loc (-2 on both sides for border?).
|
// many rows and columns we have in draw_loc (-2 on both sides for border?).
|
||||||
// I think what we can do is try to fit in as many in one column as possible.
|
// I think what we can do is try to fit in as many in one column as possible.
|
||||||
// If not, then add a new column.
|
// If not, then add a new column.
|
||||||
// Then, from this, split the row space across ALL columns. From there, generate
|
// Then, from this, split the row space across ALL columns. From there,
|
||||||
// the desired lengths.
|
// generate the desired lengths.
|
||||||
|
|
||||||
if app_state.current_widget.widget_id == widget_id {
|
if app_state.current_widget.widget_id == widget_id {
|
||||||
f.render_widget(
|
f.render_widget(
|
||||||
|
|
|
@ -251,7 +251,8 @@ impl Painter {
|
||||||
.widget_states
|
.widget_states
|
||||||
.get_mut(&(widget_id - 1))
|
.get_mut(&(widget_id - 1))
|
||||||
{
|
{
|
||||||
// TODO: This line (and the one above, see caller) is pretty dumb but I guess needed for now. Refactor if possible!
|
// TODO: This line (and the one above, see caller) is pretty dumb but I guess
|
||||||
|
// needed for now. Refactor if possible!
|
||||||
cpu_widget_state.is_legend_hidden = false;
|
cpu_widget_state.is_legend_hidden = false;
|
||||||
|
|
||||||
let is_on_widget = widget_id == app_state.current_widget.widget_id;
|
let is_on_widget = widget_id == app_state.current_widget.widget_id;
|
||||||
|
|
|
@ -42,8 +42,8 @@ impl Painter {
|
||||||
|
|
||||||
if app_state.should_get_widget_bounds() {
|
if app_state.should_get_widget_bounds() {
|
||||||
// Update draw loc in widget map
|
// 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
|
// Note that in both cases, we always go to the same widget id so it's fine to
|
||||||
// this lol.
|
// do it like this lol.
|
||||||
if let Some(network_widget) = app_state.widget_map.get_mut(&widget_id) {
|
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.top_left_corner = Some((draw_loc.x, draw_loc.y));
|
||||||
network_widget.bottom_right_corner =
|
network_widget.bottom_right_corner =
|
||||||
|
@ -74,7 +74,8 @@ impl Painter {
|
||||||
// TODO: Cache network results: Only update if:
|
// TODO: Cache network results: Only update if:
|
||||||
// - Force update (includes time interval change)
|
// - Force update (includes time interval change)
|
||||||
// - Old max time is off screen
|
// - Old max time is off screen
|
||||||
// - A new time interval is better and does not fit (check from end of vector to last checked; we only want to update if it is TOO big!)
|
// - A new time interval is better and does not fit (check from end of vector to
|
||||||
|
// last checked; we only want to update if it is TOO big!)
|
||||||
|
|
||||||
// Find the maximal rx/tx so we know how to scale, and return it.
|
// Find the maximal rx/tx so we know how to scale, and return it.
|
||||||
let (_best_time, max_entry) = get_max_entry(
|
let (_best_time, max_entry) = get_max_entry(
|
||||||
|
@ -216,7 +217,8 @@ fn get_max_entry(
|
||||||
rx: &[Point], tx: &[Point], time_start: f64, network_scale_type: &AxisScaling,
|
rx: &[Point], tx: &[Point], time_start: f64, network_scale_type: &AxisScaling,
|
||||||
network_use_binary_prefix: bool,
|
network_use_binary_prefix: bool,
|
||||||
) -> Point {
|
) -> Point {
|
||||||
/// Determines a "fake" max value in circumstances where we couldn't find one from the data.
|
/// Determines a "fake" max value in circumstances where we couldn't find
|
||||||
|
/// one from the data.
|
||||||
fn calculate_missing_max(
|
fn calculate_missing_max(
|
||||||
network_scale_type: &AxisScaling, network_use_binary_prefix: bool,
|
network_scale_type: &AxisScaling, network_use_binary_prefix: bool,
|
||||||
) -> f64 {
|
) -> f64 {
|
||||||
|
@ -238,8 +240,9 @@ fn get_max_entry(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, let's shorten our ranges to actually look. We can abuse the fact that our rx and tx arrays
|
// First, let's shorten our ranges to actually look. We can abuse the fact that
|
||||||
// are sorted, so we can short-circuit our search to filter out only the relevant data points...
|
// our rx and tx arrays are sorted, so we can short-circuit our search to
|
||||||
|
// filter out only the relevant data points...
|
||||||
let filtered_rx = if let (Some(rx_start), Some(rx_end)) = (
|
let filtered_rx = if let (Some(rx_start), Some(rx_end)) = (
|
||||||
rx.iter().position(|(time, _data)| *time >= time_start),
|
rx.iter().position(|(time, _data)| *time >= time_start),
|
||||||
rx.iter().rposition(|(time, _data)| *time <= 0.0),
|
rx.iter().rposition(|(time, _data)| *time <= 0.0),
|
||||||
|
@ -337,26 +340,31 @@ fn adjust_network_data_point(
|
||||||
network_use_binary_prefix: bool,
|
network_use_binary_prefix: bool,
|
||||||
) -> (f64, Vec<String>) {
|
) -> (f64, Vec<String>) {
|
||||||
// So, we're going with an approach like this for linear data:
|
// So, we're going with an approach like this for linear data:
|
||||||
// - Main goal is to maximize the amount of information displayed given a specific height.
|
// - Main goal is to maximize the amount of information displayed given a
|
||||||
// We don't want to drown out some data if the ranges are too far though! Nor do we want to filter
|
// specific height. We don't want to drown out some data if the ranges are too
|
||||||
// out too much data...
|
// far though! Nor do we want to filter out too much data...
|
||||||
// - Change the y-axis unit (kilo/kibi, mega/mebi...) dynamically based on max load.
|
// - Change the y-axis unit (kilo/kibi, mega/mebi...) dynamically based on max
|
||||||
|
// load.
|
||||||
//
|
//
|
||||||
// The idea is we take the top value, build our scale such that each "point" is a scaled version of that.
|
// The idea is we take the top value, build our scale such that each "point" is
|
||||||
// So for example, let's say I use 390 Mb/s. If I drew 4 segments, it would be 97.5, 195, 292.5, 390, and
|
// a scaled version of that. So for example, let's say I use 390 Mb/s. If I
|
||||||
|
// drew 4 segments, it would be 97.5, 195, 292.5, 390, and
|
||||||
// probably something like 438.75?
|
// probably something like 438.75?
|
||||||
//
|
//
|
||||||
// So, how do we do this in ratatui? Well, if we are using intervals that tie in perfectly to the max
|
// So, how do we do this in ratatui? Well, if we are using intervals that tie
|
||||||
// value we want... then it's actually not that hard. Since ratatui accepts a vector as labels and will
|
// in perfectly to the max value we want... then it's actually not that
|
||||||
// properly space them all out... we just work with that and space it out properly.
|
// hard. Since ratatui accepts a vector as labels and will properly space
|
||||||
|
// them all out... we just work with that and space it out properly.
|
||||||
//
|
//
|
||||||
// Dynamic chart idea based off of FreeNAS's chart design.
|
// Dynamic chart idea based off of FreeNAS's chart design.
|
||||||
//
|
//
|
||||||
// ===
|
// ===
|
||||||
//
|
//
|
||||||
// For log data, we just use the old method of log intervals (kilo/mega/giga/etc.). Keep it nice and simple.
|
// For log data, we just use the old method of log intervals
|
||||||
|
// (kilo/mega/giga/etc.). Keep it nice and simple.
|
||||||
|
|
||||||
// Now just check the largest unit we correspond to... then proceed to build some entries from there!
|
// Now just check the largest unit we correspond to... then proceed to build
|
||||||
|
// some entries from there!
|
||||||
|
|
||||||
let unit_char = match network_unit_type {
|
let unit_char = match network_unit_type {
|
||||||
DataUnit::Byte => "B",
|
DataUnit::Byte => "B",
|
||||||
|
@ -411,8 +419,9 @@ fn adjust_network_data_point(
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Finally, build an acceptable range starting from there, using the given height!
|
// Finally, build an acceptable range starting from there, using the given
|
||||||
// Note we try to put more of a weight on the bottom section vs. the top, since the top has less data.
|
// height! Note we try to put more of a weight on the bottom section
|
||||||
|
// vs. the top, since the top has less data.
|
||||||
|
|
||||||
let base_unit = max_value_scaled;
|
let base_unit = max_value_scaled;
|
||||||
let labels: Vec<String> = vec![
|
let labels: Vec<String> = vec![
|
||||||
|
@ -422,7 +431,8 @@ fn adjust_network_data_point(
|
||||||
format!("{:.1}", base_unit * 1.5),
|
format!("{:.1}", base_unit * 1.5),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| format!("{s:>5}")) // Pull 5 as the longest legend value is generally going to be 5 digits (if they somehow hit over 5 terabits per second)
|
.map(|s| format!("{s:>5}")) // Pull 5 as the longest legend value is generally going to be 5 digits (if they somehow
|
||||||
|
// hit over 5 terabits per second)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
(bumped_max_entry, labels)
|
(bumped_max_entry, labels)
|
||||||
|
|
|
@ -20,7 +20,8 @@ const SORT_MENU_WIDTH: u16 = 7;
|
||||||
|
|
||||||
impl Painter {
|
impl Painter {
|
||||||
/// Draws and handles all process-related drawing. Use this.
|
/// Draws and handles all process-related drawing. Use this.
|
||||||
/// - `widget_id` here represents the widget ID of the process widget itself!
|
/// - `widget_id` here represents the widget ID of the process widget
|
||||||
|
/// itself!
|
||||||
pub fn draw_process(
|
pub fn draw_process(
|
||||||
&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||||
widget_id: u64,
|
widget_id: u64,
|
||||||
|
@ -106,7 +107,8 @@ impl Painter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws the process search field.
|
/// Draws the process search field.
|
||||||
/// - `widget_id` represents the widget ID of the search box itself --- NOT the process widget
|
/// - `widget_id` represents the widget ID of the search box itself --- NOT
|
||||||
|
/// the process widget
|
||||||
/// state that is stored.
|
/// state that is stored.
|
||||||
fn draw_search_field(
|
fn draw_search_field(
|
||||||
&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
|
||||||
|
@ -310,7 +312,8 @@ impl Painter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws the process sort box.
|
/// Draws the process sort box.
|
||||||
/// - `widget_id` represents the widget ID of the sort box itself --- NOT the process widget
|
/// - `widget_id` represents the widget ID of the sort box itself --- NOT
|
||||||
|
/// the process widget
|
||||||
/// state that is stored.
|
/// state that is stored.
|
||||||
fn draw_sort_table(
|
fn draw_sort_table(
|
||||||
&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
|
||||||
|
|
103
src/constants.rs
103
src/constants.rs
|
@ -1,6 +1,6 @@
|
||||||
use tui::widgets::Borders;
|
use tui::widgets::Borders;
|
||||||
|
|
||||||
use crate::options::ConfigColours;
|
use crate::options::ColoursConfig;
|
||||||
|
|
||||||
// Default widget ID
|
// Default widget ID
|
||||||
pub const DEFAULT_WIDGET_ID: u64 = 56709;
|
pub const DEFAULT_WIDGET_ID: u64 = 56709;
|
||||||
|
@ -16,7 +16,8 @@ pub const TICK_RATE_IN_MILLISECONDS: u64 = 200;
|
||||||
pub const DEFAULT_REFRESH_RATE_IN_MILLISECONDS: u64 = 1000;
|
pub const DEFAULT_REFRESH_RATE_IN_MILLISECONDS: u64 = 1000;
|
||||||
pub const MAX_KEY_TIMEOUT_IN_MILLISECONDS: u64 = 1000;
|
pub const MAX_KEY_TIMEOUT_IN_MILLISECONDS: u64 = 1000;
|
||||||
|
|
||||||
// Limits for when we should stop showing table gaps/labels (anything less means not shown)
|
// Limits for when we should stop showing table gaps/labels (anything less means
|
||||||
|
// not shown)
|
||||||
pub const TABLE_GAP_HEIGHT_LIMIT: u16 = 7;
|
pub const TABLE_GAP_HEIGHT_LIMIT: u16 = 7;
|
||||||
pub const TIME_LABEL_HEIGHT_LIMIT: u16 = 7;
|
pub const TIME_LABEL_HEIGHT_LIMIT: u16 = 7;
|
||||||
|
|
||||||
|
@ -25,8 +26,8 @@ pub const SIDE_BORDERS: Borders = Borders::LEFT.union(Borders::RIGHT);
|
||||||
|
|
||||||
// Colour profiles
|
// Colour profiles
|
||||||
// TODO: Generate these with a macro or something...
|
// TODO: Generate these with a macro or something...
|
||||||
pub fn default_light_mode_colour_palette() -> ConfigColours {
|
pub fn default_light_mode_colour_palette() -> ColoursConfig {
|
||||||
ConfigColours {
|
ColoursConfig {
|
||||||
text_color: Some("black".into()),
|
text_color: Some("black".into()),
|
||||||
border_color: Some("black".into()),
|
border_color: Some("black".into()),
|
||||||
table_header_color: Some("black".into()),
|
table_header_color: Some("black".into()),
|
||||||
|
@ -61,12 +62,12 @@ pub fn default_light_mode_colour_palette() -> ConfigColours {
|
||||||
"Blue".into(),
|
"Blue".into(),
|
||||||
"Red".into(),
|
"Red".into(),
|
||||||
]),
|
]),
|
||||||
..ConfigColours::default()
|
..ColoursConfig::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gruvbox_colour_palette() -> ConfigColours {
|
pub fn gruvbox_colour_palette() -> ColoursConfig {
|
||||||
ConfigColours {
|
ColoursConfig {
|
||||||
table_header_color: Some("#83a598".into()),
|
table_header_color: Some("#83a598".into()),
|
||||||
all_cpu_color: Some("#8ec07c".into()),
|
all_cpu_color: Some("#8ec07c".into()),
|
||||||
avg_cpu_color: Some("#fb4934".into()),
|
avg_cpu_color: Some("#fb4934".into()),
|
||||||
|
@ -124,8 +125,8 @@ pub fn gruvbox_colour_palette() -> ConfigColours {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gruvbox_light_colour_palette() -> ConfigColours {
|
pub fn gruvbox_light_colour_palette() -> ColoursConfig {
|
||||||
ConfigColours {
|
ColoursConfig {
|
||||||
table_header_color: Some("#076678".into()),
|
table_header_color: Some("#076678".into()),
|
||||||
all_cpu_color: Some("#8ec07c".into()),
|
all_cpu_color: Some("#8ec07c".into()),
|
||||||
avg_cpu_color: Some("#fb4934".into()),
|
avg_cpu_color: Some("#fb4934".into()),
|
||||||
|
@ -183,8 +184,8 @@ pub fn gruvbox_light_colour_palette() -> ConfigColours {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nord_colour_palette() -> ConfigColours {
|
pub fn nord_colour_palette() -> ColoursConfig {
|
||||||
ConfigColours {
|
ColoursConfig {
|
||||||
table_header_color: Some("#81a1c1".into()),
|
table_header_color: Some("#81a1c1".into()),
|
||||||
all_cpu_color: Some("#88c0d0".into()),
|
all_cpu_color: Some("#88c0d0".into()),
|
||||||
avg_cpu_color: Some("#8fbcbb".into()),
|
avg_cpu_color: Some("#8fbcbb".into()),
|
||||||
|
@ -230,8 +231,8 @@ pub fn nord_colour_palette() -> ConfigColours {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nord_light_colour_palette() -> ConfigColours {
|
pub fn nord_light_colour_palette() -> ColoursConfig {
|
||||||
ConfigColours {
|
ColoursConfig {
|
||||||
table_header_color: Some("#5e81ac".into()),
|
table_header_color: Some("#5e81ac".into()),
|
||||||
all_cpu_color: Some("#81a1c1".into()),
|
all_cpu_color: Some("#81a1c1".into()),
|
||||||
avg_cpu_color: Some("#8fbcbb".into()),
|
avg_cpu_color: Some("#8fbcbb".into()),
|
||||||
|
@ -601,16 +602,51 @@ pub const CONFIG_TEXT: &str = r#"# This is a default config file for bottom. Al
|
||||||
# Where to place the legend for the network widget. One of "none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right".
|
# Where to place the legend for the network widget. One of "none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right".
|
||||||
#network_legend = "TopRight".
|
#network_legend = "TopRight".
|
||||||
|
|
||||||
# These are flags around the process widget.
|
# Processes widget configuration
|
||||||
#[processes]
|
#[processes]
|
||||||
# The columns shown by the process widget. The following columns are supported:
|
# The columns shown by the process widget. The following columns are supported:
|
||||||
# PID, Name, CPU%, Mem%, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU%
|
# PID, Name, CPU%, Mem%, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU%
|
||||||
#columns = ["PID", "Name", "CPU%", "Mem%", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMEM%", "GPU%"]
|
#columns = ["PID", "Name", "CPU%", "Mem%", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMEM%", "GPU%"]
|
||||||
|
|
||||||
# [cpu]
|
# CPU widget configuration
|
||||||
|
#[cpu]
|
||||||
# One of "all" (default), "average"/"avg"
|
# One of "all" (default), "average"/"avg"
|
||||||
# default = "average"
|
# default = "average"
|
||||||
|
|
||||||
|
# Disk widget configuration
|
||||||
|
#[disk]
|
||||||
|
#[disk.name_filter]
|
||||||
|
#is_list_ignored = true
|
||||||
|
#list = ["/dev/sda\\d+", "/dev/nvme0n1p2"]
|
||||||
|
#regex = true
|
||||||
|
#case_sensitive = false
|
||||||
|
#whole_word = false
|
||||||
|
|
||||||
|
#[disk.mount_filter]
|
||||||
|
#is_list_ignored = true
|
||||||
|
#list = ["/mnt/.*", "/boot"]
|
||||||
|
#regex = true
|
||||||
|
#case_sensitive = false
|
||||||
|
#whole_word = false
|
||||||
|
|
||||||
|
# Temperature widget configuration
|
||||||
|
#[temperature]
|
||||||
|
#[temperature.sensor_filter]
|
||||||
|
#is_list_ignored = true
|
||||||
|
#list = ["cpu", "wifi"]
|
||||||
|
#regex = false
|
||||||
|
#case_sensitive = false
|
||||||
|
#whole_word = false
|
||||||
|
|
||||||
|
# Network widget configuration
|
||||||
|
#[network]
|
||||||
|
#[network.interface_filter]
|
||||||
|
#is_list_ignored = true
|
||||||
|
#list = ["virbr0.*"]
|
||||||
|
#regex = true
|
||||||
|
#case_sensitive = false
|
||||||
|
#whole_word = false
|
||||||
|
|
||||||
# These are all the components that support custom theming. Note that colour support
|
# These are all the components that support custom theming. Note that colour support
|
||||||
# will depend on terminal support.
|
# will depend on terminal support.
|
||||||
#[colors] # Uncomment if you want to use custom colors
|
#[colors] # Uncomment if you want to use custom colors
|
||||||
|
@ -681,37 +717,6 @@ pub const CONFIG_TEXT: &str = r#"# This is a default config file for bottom. Al
|
||||||
# [[row.child]]
|
# [[row.child]]
|
||||||
# type="proc"
|
# type="proc"
|
||||||
# default=true
|
# default=true
|
||||||
|
|
||||||
|
|
||||||
# Filters - you can hide specific temperature sensors, network interfaces, and disks using filters. This is admittedly
|
|
||||||
# a bit hard to use as of now, and there is a planned in-app interface for managing this in the future:
|
|
||||||
#[disk_filter]
|
|
||||||
#is_list_ignored = true
|
|
||||||
#list = ["/dev/sda\\d+", "/dev/nvme0n1p2"]
|
|
||||||
#regex = true
|
|
||||||
#case_sensitive = false
|
|
||||||
#whole_word = false
|
|
||||||
|
|
||||||
#[mount_filter]
|
|
||||||
#is_list_ignored = true
|
|
||||||
#list = ["/mnt/.*", "/boot"]
|
|
||||||
#regex = true
|
|
||||||
#case_sensitive = false
|
|
||||||
#whole_word = false
|
|
||||||
|
|
||||||
#[temp_filter]
|
|
||||||
#is_list_ignored = true
|
|
||||||
#list = ["cpu", "wifi"]
|
|
||||||
#regex = false
|
|
||||||
#case_sensitive = false
|
|
||||||
#whole_word = false
|
|
||||||
|
|
||||||
#[net_filter]
|
|
||||||
#is_list_ignored = true
|
|
||||||
#list = ["virbr0.*"]
|
|
||||||
#regex = true
|
|
||||||
#case_sensitive = false
|
|
||||||
#whole_word = false
|
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
pub const CONFIG_TOP_HEAD: &str = r##"# This is bottom's config file.
|
pub const CONFIG_TOP_HEAD: &str = r##"# This is bottom's config file.
|
||||||
|
@ -773,8 +778,8 @@ mod test {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This test exists because previously, [`SIDE_BORDERS`] was set incorrectly after I moved from
|
/// This test exists because previously, [`SIDE_BORDERS`] was set
|
||||||
/// tui-rs to ratatui.
|
/// incorrectly after I moved from tui-rs to ratatui.
|
||||||
#[test]
|
#[test]
|
||||||
fn assert_side_border_bits_match() {
|
fn assert_side_border_bits_match() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
@ -98,7 +98,8 @@ impl Data {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A wrapper around the sysinfo data source. We use sysinfo for the following data:
|
/// A wrapper around the sysinfo data source. We use sysinfo for the following
|
||||||
|
/// data:
|
||||||
/// - CPU usage
|
/// - CPU usage
|
||||||
/// - Memory usage
|
/// - Memory usage
|
||||||
/// - Network usage
|
/// - Network usage
|
||||||
|
@ -183,7 +184,7 @@ impl DataCollector {
|
||||||
temperature_type: TemperatureType::Celsius,
|
temperature_type: TemperatureType::Celsius,
|
||||||
use_current_cpu_total: false,
|
use_current_cpu_total: false,
|
||||||
unnormalized_cpu: false,
|
unnormalized_cpu: false,
|
||||||
last_collection_time: Instant::now() - Duration::from_secs(600), // Initialize it to the past to force it to load on initialization.
|
last_collection_time: Instant::now() - Duration::from_secs(600), /* Initialize it to the past to force it to load on initialization. */
|
||||||
total_rx: 0,
|
total_rx: 0,
|
||||||
total_tx: 0,
|
total_tx: 0,
|
||||||
show_average_cpu: false,
|
show_average_cpu: false,
|
||||||
|
@ -255,7 +256,8 @@ impl DataCollector {
|
||||||
/// - Disk (Windows)
|
/// - Disk (Windows)
|
||||||
/// - Temperatures (non-Linux)
|
/// - Temperatures (non-Linux)
|
||||||
fn refresh_sysinfo_data(&mut self) {
|
fn refresh_sysinfo_data(&mut self) {
|
||||||
// Refresh the list of objects once every minute. If it's too frequent it can cause segfaults.
|
// Refresh the list of objects once every minute. If it's too frequent it can
|
||||||
|
// cause segfaults.
|
||||||
const LIST_REFRESH_TIME: Duration = Duration::from_secs(60);
|
const LIST_REFRESH_TIME: Duration = Duration::from_secs(60);
|
||||||
let refresh_start = Instant::now();
|
let refresh_start = Instant::now();
|
||||||
|
|
||||||
|
@ -374,9 +376,9 @@ impl DataCollector {
|
||||||
fn update_processes(&mut self) {
|
fn update_processes(&mut self) {
|
||||||
if self.widgets_to_harvest.use_proc {
|
if self.widgets_to_harvest.use_proc {
|
||||||
if let Ok(mut process_list) = self.get_processes() {
|
if let Ok(mut process_list) = self.get_processes() {
|
||||||
// NB: To avoid duplicate sorts on rerenders/events, we sort the processes by PID here.
|
// NB: To avoid duplicate sorts on rerenders/events, we sort the processes by
|
||||||
// We also want to avoid re-sorting *again* later on if we're sorting by PID, since we already
|
// PID here. We also want to avoid re-sorting *again* later on
|
||||||
// did it here!
|
// if we're sorting by PID, since we already did it here!
|
||||||
process_list.sort_unstable_by_key(|p| p.pid);
|
process_list.sort_unstable_by_key(|p| p.pid);
|
||||||
self.data.list_of_processes = Some(process_list);
|
self.data.list_of_processes = Some(process_list);
|
||||||
}
|
}
|
||||||
|
@ -473,12 +475,15 @@ impl DataCollector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// We set a sleep duration between 10ms and 250ms, ideally sysinfo's [`sysinfo::MINIMUM_CPU_UPDATE_INTERVAL`] + 1.
|
/// We set a sleep duration between 10ms and 250ms, ideally sysinfo's
|
||||||
|
/// [`sysinfo::MINIMUM_CPU_UPDATE_INTERVAL`] + 1.
|
||||||
///
|
///
|
||||||
/// We bound the upper end to avoid waiting too long (e.g. FreeBSD is 1s, which I'm fine with losing
|
/// We bound the upper end to avoid waiting too long (e.g. FreeBSD is 1s, which
|
||||||
/// accuracy on for the first refresh), and we bound the lower end just to avoid the off-chance that
|
/// I'm fine with losing accuracy on for the first refresh), and we bound the
|
||||||
/// refreshing too quickly causes problems. This second case should only happen on unsupported
|
/// lower end just to avoid the off-chance that refreshing too quickly causes
|
||||||
/// systems via sysinfo, in which case [`sysinfo::MINIMUM_CPU_UPDATE_INTERVAL`] is defined as 0.
|
/// problems. This second case should only happen on unsupported systems via
|
||||||
|
/// sysinfo, in which case [`sysinfo::MINIMUM_CPU_UPDATE_INTERVAL`] is defined
|
||||||
|
/// as 0.
|
||||||
///
|
///
|
||||||
/// We also do `INTERVAL + 1` for some wiggle room, just in case.
|
/// We also do `INTERVAL + 1` for some wiggle room, just in case.
|
||||||
const fn get_sleep_duration() -> Duration {
|
const fn get_sleep_duration() -> Duration {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
//! Data collection for batteries.
|
//! Data collection for batteries.
|
||||||
//!
|
//!
|
||||||
//! For Linux, macOS, Windows, FreeBSD, Dragonfly, and iOS, this is handled by the battery crate.
|
//! For Linux, macOS, Windows, FreeBSD, Dragonfly, and iOS, this is handled by
|
||||||
|
//! the battery crate.
|
||||||
|
|
||||||
cfg_if::cfg_if! {
|
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"))] {
|
if #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "ios"))] {
|
||||||
|
|
|
@ -32,7 +32,8 @@ pub fn get_cpu_data_list(sys: &System, show_average_cpu: bool) -> crate::error::
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_load_avg() -> crate::error::Result<LoadAvgHarvest> {
|
pub fn get_load_avg() -> crate::error::Result<LoadAvgHarvest> {
|
||||||
// The API for sysinfo apparently wants you to call it like this, rather than using a &System.
|
// The API for sysinfo apparently wants you to call it like this, rather than
|
||||||
|
// using a &System.
|
||||||
let LoadAvg { one, five, fifteen } = sysinfo::System::load_average();
|
let LoadAvg { one, five, fifteen } = sysinfo::System::load_average();
|
||||||
|
|
||||||
Ok([one as f32, five as f32, fifteen as f32])
|
Ok([one as f32, five as f32, fifteen as f32])
|
||||||
|
|
|
@ -86,16 +86,18 @@ cfg_if! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether to keep the current disk entry given the filters, disk name, and disk mount.
|
/// Whether to keep the current disk entry given the filters, disk name, and
|
||||||
/// Precedence ordering in the case where name and mount filters disagree, "allow"
|
/// disk mount. Precedence ordering in the case where name and mount filters
|
||||||
/// takes precedence over "deny".
|
/// disagree, "allow" takes precedence over "deny".
|
||||||
///
|
///
|
||||||
/// For implementation, we do this as follows:
|
/// For implementation, we do this as follows:
|
||||||
///
|
///
|
||||||
/// 1. Is the entry allowed through any filter? That is, does it match an entry in a
|
/// 1. Is the entry allowed through any filter? That is, does it match an entry
|
||||||
/// filter where `is_list_ignored` is `false`? If so, we always keep this entry.
|
/// in a filter where `is_list_ignored` is `false`? If so, we always keep
|
||||||
/// 2. Is the entry denied through any filter? That is, does it match an entry in a
|
/// this entry.
|
||||||
/// filter where `is_list_ignored` is `true`? If so, we always deny 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.
|
/// 3. Anything else is allowed.
|
||||||
pub fn keep_disk_entry(
|
pub fn keep_disk_entry(
|
||||||
disk_name: &str, mount_point: &str, disk_filter: &Option<Filter>, mount_filter: &Option<Filter>,
|
disk_name: &str, mount_point: &str, disk_filter: &Option<Filter>, mount_filter: &Option<Filter>,
|
||||||
|
|
|
@ -28,7 +28,8 @@ struct FileSystem {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_io_usage() -> error::Result<IoHarvest> {
|
pub fn get_io_usage() -> error::Result<IoHarvest> {
|
||||||
// TODO: Should this (and other I/O collectors) fail fast? In general, should collection ever fail fast?
|
// TODO: Should this (and other I/O collectors) fail fast? In general, should
|
||||||
|
// collection ever fail fast?
|
||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
let mut io_harvest: HashMap<String, Option<IoData>> =
|
let mut io_harvest: HashMap<String, Option<IoData>> =
|
||||||
get_disk_info().map(|storage_system_information| {
|
get_disk_info().map(|storage_system_information| {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//! Disk stats for Unix-like systems that aren't supported through other means. Officially,
|
//! Disk stats for Unix-like systems that aren't supported through other means.
|
||||||
//! for now, this means Linux and macOS.
|
//! Officially, for now, this means Linux and macOS.
|
||||||
|
|
||||||
mod file_systems;
|
mod file_systems;
|
||||||
|
|
||||||
|
@ -37,16 +37,21 @@ pub fn get_disk_usage(collector: &DataCollector) -> anyhow::Result<Vec<DiskHarve
|
||||||
let name = partition.get_device_name();
|
let name = partition.get_device_name();
|
||||||
let mount_point = partition.mount_point().to_string_lossy().to_string();
|
let mount_point = partition.mount_point().to_string_lossy().to_string();
|
||||||
|
|
||||||
// Precedence ordering in the case where name and mount filters disagree, "allow" takes precedence over "deny".
|
// Precedence ordering in the case where name and mount filters disagree,
|
||||||
|
// "allow" takes precedence over "deny".
|
||||||
//
|
//
|
||||||
// For implementation, we do this as follows:
|
// 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.
|
// 1. Is the entry allowed through any filter? That is, does it match an 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.
|
// 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.
|
// 3. Anything else is allowed.
|
||||||
|
|
||||||
if keep_disk_entry(&name, &mount_point, disk_filter, mount_filter) {
|
if keep_disk_entry(&name, &mount_point, disk_filter, mount_filter) {
|
||||||
// The usage line can fail in some cases (for example, if you use Void Linux + LUKS,
|
// The usage line can fail in some cases (for example, if you use Void Linux +
|
||||||
// see https://github.com/ClementTsang/bottom/issues/419 for details).
|
// LUKS, see https://github.com/ClementTsang/bottom/issues/419 for details).
|
||||||
if let Ok(usage) = partition.usage() {
|
if let Ok(usage) = partition.usage() {
|
||||||
let total = usage.total();
|
let total = usage.total();
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,8 @@ use crate::multi_eq_ignore_ascii_case;
|
||||||
/// Known filesystems. Original list from
|
/// Known filesystems. Original list from
|
||||||
/// [heim](https://github.com/heim-rs/heim/blob/master/heim-disk/src/filesystem.rs).
|
/// [heim](https://github.com/heim-rs/heim/blob/master/heim-disk/src/filesystem.rs).
|
||||||
///
|
///
|
||||||
/// All physical filesystems should have their own enum element and all virtual filesystems will go into
|
/// All physical filesystems should have their own enum element and all virtual
|
||||||
/// the [`FileSystem::Other`] element.
|
/// filesystems will go into the [`FileSystem::Other`] element.
|
||||||
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
|
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum FileSystem {
|
pub enum FileSystem {
|
||||||
|
@ -81,7 +81,8 @@ impl FileSystem {
|
||||||
!self.is_virtual()
|
!self.is_virtual()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if filesystem is used for a virtual devices (such as `tmpfs` or `smb` mounts).
|
/// Checks if filesystem is used for a virtual devices (such as `tmpfs` or
|
||||||
|
/// `smb` mounts).
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_virtual(&self) -> bool {
|
pub fn is_virtual(&self) -> bool {
|
||||||
matches!(self, FileSystem::Other(..))
|
matches!(self, FileSystem::Other(..))
|
||||||
|
|
|
@ -28,7 +28,8 @@ impl FromStr for IoCounters {
|
||||||
|
|
||||||
/// Converts a `&str` to an [`IoCounters`].
|
/// Converts a `&str` to an [`IoCounters`].
|
||||||
///
|
///
|
||||||
/// Follows the format used in Linux 2.6+. Note that this completely ignores the following stats:
|
/// Follows the format used in Linux 2.6+. Note that this completely ignores
|
||||||
|
/// the following stats:
|
||||||
/// - Discard stats from 4.18+
|
/// - Discard stats from 4.18+
|
||||||
/// - Flush stats from 5.5+
|
/// - Flush stats from 5.5+
|
||||||
///
|
///
|
||||||
|
@ -71,7 +72,8 @@ pub fn io_stats() -> anyhow::Result<Vec<IoCounters>> {
|
||||||
let mut reader = BufReader::new(File::open(PROC_DISKSTATS)?);
|
let mut reader = BufReader::new(File::open(PROC_DISKSTATS)?);
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
|
|
||||||
// This saves us from doing a string allocation on each iteration compared to `lines()`.
|
// This saves us from doing a string allocation on each iteration compared to
|
||||||
|
// `lines()`.
|
||||||
while let Ok(bytes) = reader.read_line(&mut line) {
|
while let Ok(bytes) = reader.read_line(&mut line) {
|
||||||
if bytes > 0 {
|
if bytes > 0 {
|
||||||
if let Ok(counters) = IoCounters::from_str(&line) {
|
if let Ok(counters) = IoCounters::from_str(&line) {
|
||||||
|
|
|
@ -43,8 +43,8 @@ impl Partition {
|
||||||
/// Returns the device name for the partition.
|
/// Returns the device name for the partition.
|
||||||
pub fn get_device_name(&self) -> String {
|
pub fn get_device_name(&self) -> String {
|
||||||
if let Some(device) = self.device() {
|
if let Some(device) = self.device() {
|
||||||
// See if this disk is actually mounted elsewhere on Linux. This is a workaround properly map I/O
|
// See if this disk is actually mounted elsewhere on Linux. This is a workaround
|
||||||
// in some cases (i.e. disk encryption, https://github.com/ClementTsang/bottom/issues/419).
|
// properly map I/O in some cases (i.e. disk encryption, https://github.com/ClementTsang/bottom/issues/419).
|
||||||
if let Ok(path) = std::fs::read_link(device) {
|
if let Ok(path) = std::fs::read_link(device) {
|
||||||
if path.is_absolute() {
|
if path.is_absolute() {
|
||||||
path.into_os_string()
|
path.into_os_string()
|
||||||
|
@ -87,7 +87,8 @@ impl Partition {
|
||||||
|
|
||||||
let mut vfs = mem::MaybeUninit::<libc::statvfs>::uninit();
|
let mut vfs = mem::MaybeUninit::<libc::statvfs>::uninit();
|
||||||
|
|
||||||
// SAFETY: libc call, `path` is a valid C string and buf is a valid pointer to write to.
|
// SAFETY: libc call, `path` is a valid C string and buf is a valid pointer to
|
||||||
|
// write to.
|
||||||
let result = unsafe { libc::statvfs(path.as_ptr(), vfs.as_mut_ptr()) };
|
let result = unsafe { libc::statvfs(path.as_ptr(), vfs.as_mut_ptr()) };
|
||||||
|
|
||||||
if result == 0 {
|
if result == 0 {
|
||||||
|
@ -146,7 +147,8 @@ pub(crate) fn partitions() -> anyhow::Result<Vec<Partition>> {
|
||||||
let mut reader = BufReader::new(File::open(PROC_MOUNTS)?);
|
let mut reader = BufReader::new(File::open(PROC_MOUNTS)?);
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
|
|
||||||
// This saves us from doing a string allocation on each iteration compared to `lines()`.
|
// This saves us from doing a string allocation on each iteration compared to
|
||||||
|
// `lines()`.
|
||||||
while let Ok(bytes) = reader.read_line(&mut line) {
|
while let Ok(bytes) = reader.read_line(&mut line) {
|
||||||
if bytes > 0 {
|
if bytes > 0 {
|
||||||
if let Ok(partition) = Partition::from_str(&line) {
|
if let Ok(partition) = Partition::from_str(&line) {
|
||||||
|
@ -171,7 +173,8 @@ pub(crate) fn physical_partitions() -> anyhow::Result<Vec<Partition>> {
|
||||||
let mut reader = BufReader::new(File::open(PROC_MOUNTS)?);
|
let mut reader = BufReader::new(File::open(PROC_MOUNTS)?);
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
|
|
||||||
// This saves us from doing a string allocation on each iteration compared to `lines()`.
|
// This saves us from doing a string allocation on each iteration compared to
|
||||||
|
// `lines()`.
|
||||||
while let Ok(bytes) = reader.read_line(&mut line) {
|
while let Ok(bytes) = reader.read_line(&mut line) {
|
||||||
if bytes > 0 {
|
if bytes > 0 {
|
||||||
if let Ok(partition) = Partition::from_str(&line) {
|
if let Ok(partition) = Partition::from_str(&line) {
|
||||||
|
|
|
@ -10,23 +10,24 @@ fn get_device_io(device: io_kit::IoObject) -> anyhow::Result<IoCounters> {
|
||||||
//
|
//
|
||||||
// Okay, so this is weird.
|
// Okay, so this is weird.
|
||||||
//
|
//
|
||||||
// The problem is that if I have this check - this is what sources like psutil use, for
|
// The problem is that if I have this check - this is what sources like psutil
|
||||||
// example (see https://github.com/giampaolo/psutil/blob/7eadee31db2f038763a3a6f978db1ea76bbc4674/psutil/_psutil_osx.c#LL1422C20-L1422C20)
|
// use, for example (see https://github.com/giampaolo/psutil/blob/7eadee31db2f038763a3a6f978db1ea76bbc4674/psutil/_psutil_osx.c#LL1422C20-L1422C20)
|
||||||
// then this will only return stuff like disk0.
|
// then this will only return stuff like disk0.
|
||||||
//
|
//
|
||||||
// The problem with this is that there is *never* a disk0 *disk* entry to correspond to this,
|
// The problem with this is that there is *never* a disk0 *disk* entry to
|
||||||
// so there will be entries like disk1 or whatnot. Someone's done some digging on the gopsutil
|
// correspond to this, so there will be entries like disk1 or whatnot.
|
||||||
// repo (https://github.com/shirou/gopsutil/issues/855#issuecomment-610016435), and it seems
|
// Someone's done some digging on the gopsutil repo (https://github.com/shirou/gopsutil/issues/855#issuecomment-610016435), and it seems
|
||||||
// like this is a consequence of how Apple does logical volumes.
|
// like this is a consequence of how Apple does logical volumes.
|
||||||
//
|
//
|
||||||
// So with all that said, what I've found is that I *can* still get a mapping - but I have
|
// So with all that said, what I've found is that I *can* still get a mapping -
|
||||||
// to disable the conform check, which... is weird. I'm not sure if this is valid at all. But
|
// but I have to disable the conform check, which... is weird. I'm not sure
|
||||||
// it *does* seem to match Activity Monitor with regards to disk activity, so... I guess we
|
// if this is valid at all. But it *does* seem to match Activity Monitor
|
||||||
// can leave this for now...?
|
// with regards to disk activity, so... I guess we can leave this for
|
||||||
|
// now...?
|
||||||
|
|
||||||
// if !parent.conforms_to_block_storage_driver() {
|
// if !parent.conforms_to_block_storage_driver() {
|
||||||
// anyhow::bail!("{parent:?}, the parent of {device:?} does not conform to IOBlockStorageDriver")
|
// anyhow::bail!("{parent:?}, the parent of {device:?} does not conform to
|
||||||
// }
|
// IOBlockStorageDriver") }
|
||||||
|
|
||||||
let disk_props = device.properties()?;
|
let disk_props = device.properties()?;
|
||||||
let parent_props = parent.properties()?;
|
let parent_props = parent.properties()?;
|
||||||
|
|
|
@ -49,7 +49,8 @@ extern "C" {
|
||||||
entry: io_registry_entry_t, plane: *const libc::c_char, parent: *mut io_registry_entry_t,
|
entry: io_registry_entry_t, plane: *const libc::c_char, parent: *mut io_registry_entry_t,
|
||||||
) -> kern_return_t;
|
) -> kern_return_t;
|
||||||
|
|
||||||
// pub fn IOObjectConformsTo(object: io_object_t, className: *const libc::c_char) -> mach2::boolean::boolean_t;
|
// pub fn IOObjectConformsTo(object: io_object_t, className: *const
|
||||||
|
// libc::c_char) -> mach2::boolean::boolean_t;
|
||||||
|
|
||||||
pub fn IORegistryEntryCreateCFProperties(
|
pub fn IORegistryEntryCreateCFProperties(
|
||||||
entry: io_registry_entry_t, properties: *mut CFMutableDictionaryRef,
|
entry: io_registry_entry_t, properties: *mut CFMutableDictionaryRef,
|
||||||
|
|
|
@ -37,7 +37,8 @@ impl Iterator for IoIterator {
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
// Basically, we just stop when we hit 0.
|
// Basically, we just stop when we hit 0.
|
||||||
|
|
||||||
// SAFETY: IOKit call, the passed argument (an `io_iterator_t`) is what is expected.
|
// SAFETY: IOKit call, the passed argument (an `io_iterator_t`) is what is
|
||||||
|
// expected.
|
||||||
match unsafe { IOIteratorNext(self.0) } {
|
match unsafe { IOIteratorNext(self.0) } {
|
||||||
0 => None,
|
0 => None,
|
||||||
io_object => Some(IoObject::from(io_object)),
|
io_object => Some(IoObject::from(io_object)),
|
||||||
|
@ -47,7 +48,8 @@ impl Iterator for IoIterator {
|
||||||
|
|
||||||
impl Drop for IoIterator {
|
impl Drop for IoIterator {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
// SAFETY: IOKit call, the passed argument (an `io_iterator_t`) is what is expected.
|
// SAFETY: IOKit call, the passed argument (an `io_iterator_t`) is what is
|
||||||
|
// expected.
|
||||||
let result = unsafe { IOObjectRelease(self.0) };
|
let result = unsafe { IOObjectRelease(self.0) };
|
||||||
assert_eq!(result, kern_return::KERN_SUCCESS);
|
assert_eq!(result, kern_return::KERN_SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,8 +24,9 @@ pub struct IoObject(io_object_t);
|
||||||
impl IoObject {
|
impl IoObject {
|
||||||
/// Returns a typed dictionary with this object's properties.
|
/// Returns a typed dictionary with this object's properties.
|
||||||
pub fn properties(&self) -> anyhow::Result<CFDictionary<CFString, CFType>> {
|
pub fn properties(&self) -> anyhow::Result<CFDictionary<CFString, CFType>> {
|
||||||
// SAFETY: The IOKit call should be fine, the arguments are safe. The `assume_init` should also be fine, as
|
// SAFETY: The IOKit call should be fine, the arguments are safe. The
|
||||||
// we guard against it with a check against `result` to ensure it succeeded.
|
// `assume_init` should also be fine, as we guard against it with a
|
||||||
|
// check against `result` to ensure it succeeded.
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut props = mem::MaybeUninit::<CFMutableDictionaryRef>::uninit();
|
let mut props = mem::MaybeUninit::<CFMutableDictionaryRef>::uninit();
|
||||||
|
|
||||||
|
@ -45,8 +46,8 @@ impl IoObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the [`kIOServicePlane`] parent [`io_object_t`] for this [`io_object_t`], if there
|
/// Gets the [`kIOServicePlane`] parent [`io_object_t`] for this
|
||||||
/// is one.
|
/// [`io_object_t`], if there is one.
|
||||||
pub fn service_parent(&self) -> anyhow::Result<IoObject> {
|
pub fn service_parent(&self) -> anyhow::Result<IoObject> {
|
||||||
let mut parent: io_registry_entry_t = 0;
|
let mut parent: io_registry_entry_t = 0;
|
||||||
|
|
||||||
|
@ -65,7 +66,8 @@ impl IoObject {
|
||||||
// pub fn conforms_to_block_storage_driver(&self) -> bool {
|
// pub fn conforms_to_block_storage_driver(&self) -> bool {
|
||||||
// // SAFETY: IOKit call, the arguments should be safe.
|
// // SAFETY: IOKit call, the arguments should be safe.
|
||||||
// let result =
|
// let result =
|
||||||
// unsafe { IOObjectConformsTo(self.0, "IOBlockStorageDriver\0".as_ptr().cast()) };
|
// unsafe { IOObjectConformsTo(self.0,
|
||||||
|
// "IOBlockStorageDriver\0".as_ptr().cast()) };
|
||||||
|
|
||||||
// result != 0
|
// result != 0
|
||||||
// }
|
// }
|
||||||
|
@ -79,7 +81,8 @@ impl From<io_object_t> for IoObject {
|
||||||
|
|
||||||
impl Drop for IoObject {
|
impl Drop for IoObject {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
// SAFETY: IOKit call, the argument here (an `io_object_t`) should be safe and expected.
|
// SAFETY: IOKit call, the argument here (an `io_object_t`) should be safe and
|
||||||
|
// expected.
|
||||||
let result = unsafe { IOObjectRelease(self.0) };
|
let result = unsafe { IOObjectRelease(self.0) };
|
||||||
assert_eq!(result, kern_return::KERN_SUCCESS);
|
assert_eq!(result, kern_return::KERN_SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,8 +34,9 @@ pub(crate) fn mounts() -> anyhow::Result<Vec<libc::statfs>> {
|
||||||
"Expected {expected_len} statfs entries, but instead got {result} entries",
|
"Expected {expected_len} statfs entries, but instead got {result} entries",
|
||||||
);
|
);
|
||||||
|
|
||||||
// SAFETY: We have a debug assert check, and if `result` is not correct (-1), we check against it.
|
// SAFETY: We have a debug assert check, and if `result` is not correct (-1), we
|
||||||
// Otherwise, getfsstat64 should return the number of statfs structures if it succeeded.
|
// check against it. Otherwise, getfsstat64 should return the number of
|
||||||
|
// statfs structures if it succeeded.
|
||||||
//
|
//
|
||||||
// Source: https://man.freebsd.org/cgi/man.cgi?query=getfsstat&sektion=2&format=html
|
// Source: https://man.freebsd.org/cgi/man.cgi?query=getfsstat&sektion=2&format=html
|
||||||
unsafe {
|
unsafe {
|
||||||
|
|
|
@ -38,7 +38,8 @@ impl Partition {
|
||||||
let result = unsafe { libc::statvfs(path.as_ptr(), vfs.as_mut_ptr()) };
|
let result = unsafe { libc::statvfs(path.as_ptr(), vfs.as_mut_ptr()) };
|
||||||
|
|
||||||
if result == 0 {
|
if result == 0 {
|
||||||
// SAFETY: We check that it succeeded (result is 0), which means vfs should be populated.
|
// SAFETY: We check that it succeeded (result is 0), which means vfs should be
|
||||||
|
// populated.
|
||||||
Ok(Usage::new(unsafe { vfs.assume_init() }))
|
Ok(Usage::new(unsafe { vfs.assume_init() }))
|
||||||
} else {
|
} else {
|
||||||
bail!("statvfs failed to get the disk usage for disk {path:?}")
|
bail!("statvfs failed to get the disk usage for disk {path:?}")
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
pub struct Usage(libc::statvfs);
|
pub struct Usage(libc::statvfs);
|
||||||
|
|
||||||
// Note that x86 returns `u32` values while x86-64 returns `u64`s, so we convert everything
|
// Note that x86 returns `u32` values while x86-64 returns `u64`s, so we convert
|
||||||
// to `u64` for consistency.
|
// everything to `u64` for consistency.
|
||||||
#[allow(clippy::useless_conversion)]
|
#[allow(clippy::useless_conversion)]
|
||||||
impl Usage {
|
impl Usage {
|
||||||
pub(crate) fn new(vfs: libc::statvfs) -> Self {
|
pub(crate) fn new(vfs: libc::statvfs) -> Self {
|
||||||
|
@ -13,19 +13,22 @@ impl Usage {
|
||||||
u64::from(self.0.f_blocks) * u64::from(self.0.f_frsize)
|
u64::from(self.0.f_blocks) * u64::from(self.0.f_frsize)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the available number of bytes used. Note this is not necessarily the same as [`Usage::free`].
|
/// Returns the available number of bytes used. Note this is not necessarily
|
||||||
|
/// the same as [`Usage::free`].
|
||||||
pub fn available(&self) -> u64 {
|
pub fn available(&self) -> u64 {
|
||||||
u64::from(self.0.f_bfree) * u64::from(self.0.f_frsize)
|
u64::from(self.0.f_bfree) * u64::from(self.0.f_frsize)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
/// Returns the total number of bytes used. Equal to `total - available` on Unix.
|
/// Returns the total number of bytes used. Equal to `total - available` on
|
||||||
|
/// Unix.
|
||||||
pub fn used(&self) -> u64 {
|
pub fn used(&self) -> u64 {
|
||||||
let avail_to_root = u64::from(self.0.f_bfree) * u64::from(self.0.f_frsize);
|
let avail_to_root = u64::from(self.0.f_bfree) * u64::from(self.0.f_frsize);
|
||||||
self.total() - avail_to_root
|
self.total() - avail_to_root
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the total number of bytes free. Note this is not necessarily the same as [`Usage::available`].
|
/// Returns the total number of bytes free. Note this is not necessarily the
|
||||||
|
/// same as [`Usage::available`].
|
||||||
pub fn free(&self) -> u64 {
|
pub fn free(&self) -> u64 {
|
||||||
u64::from(self.0.f_bavail) * u64::from(self.0.f_frsize)
|
u64::from(self.0.f_bavail) * u64::from(self.0.f_frsize)
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,8 @@ fn volume_io(volume: &Path) -> anyhow::Result<DISK_PERFORMANCE> {
|
||||||
wide_path
|
wide_path
|
||||||
};
|
};
|
||||||
|
|
||||||
// SAFETY: API call, arguments should be correct. We must also check after the call to ensure it is valid.
|
// SAFETY: API call, arguments should be correct. We must also check after the
|
||||||
|
// call to ensure it is valid.
|
||||||
let h_device = unsafe {
|
let h_device = unsafe {
|
||||||
CreateFileW(
|
CreateFileW(
|
||||||
windows::core::PCWSTR(volume.as_ptr()),
|
windows::core::PCWSTR(volume.as_ptr()),
|
||||||
|
@ -61,7 +62,8 @@ fn volume_io(volume: &Path) -> anyhow::Result<DISK_PERFORMANCE> {
|
||||||
let mut disk_performance = DISK_PERFORMANCE::default();
|
let mut disk_performance = DISK_PERFORMANCE::default();
|
||||||
let mut bytes_returned = 0;
|
let mut bytes_returned = 0;
|
||||||
|
|
||||||
// SAFETY: This should be safe, we'll manually check the results and the arguments should be valid.
|
// SAFETY: This should be safe, we'll manually check the results and the
|
||||||
|
// arguments should be valid.
|
||||||
let ret = unsafe {
|
let ret = unsafe {
|
||||||
DeviceIoControl(
|
DeviceIoControl(
|
||||||
h_device,
|
h_device,
|
||||||
|
@ -112,7 +114,8 @@ pub(crate) fn all_volume_io() -> anyhow::Result<Vec<anyhow::Result<(DISK_PERFORM
|
||||||
let mut buffer = [0_u16; Foundation::MAX_PATH as usize];
|
let mut buffer = [0_u16; Foundation::MAX_PATH as usize];
|
||||||
|
|
||||||
// Get the first volume and add the stats needed.
|
// Get the first volume and add the stats needed.
|
||||||
// SAFETY: We must verify the handle is correct. If no volume is found, it will be set to `INVALID_HANDLE_VALUE`.
|
// SAFETY: We must verify the handle is correct. If no volume is found, it will
|
||||||
|
// be set to `INVALID_HANDLE_VALUE`.
|
||||||
let handle = unsafe { FindFirstVolumeW(&mut buffer) }?;
|
let handle = unsafe { FindFirstVolumeW(&mut buffer) }?;
|
||||||
if handle.is_invalid() {
|
if handle.is_invalid() {
|
||||||
bail!("Invalid handle value: {:?}", io::Error::last_os_error());
|
bail!("Invalid handle value: {:?}", io::Error::last_os_error());
|
||||||
|
@ -148,8 +151,8 @@ pub(crate) fn all_volume_io() -> anyhow::Result<Vec<anyhow::Result<(DISK_PERFORM
|
||||||
|
|
||||||
/// Returns the volume name from a mount name if possible.
|
/// Returns the volume name from a mount name if possible.
|
||||||
pub(crate) fn volume_name_from_mount(mount: &str) -> anyhow::Result<String> {
|
pub(crate) fn volume_name_from_mount(mount: &str) -> anyhow::Result<String> {
|
||||||
// According to winapi docs 50 is a reasonable length to accomodate the volume path
|
// According to winapi docs 50 is a reasonable length to accomodate the volume
|
||||||
// https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumenameforvolumemountpointw
|
// path https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumenameforvolumemountpointw
|
||||||
const VOLUME_MAX_LEN: usize = 50;
|
const VOLUME_MAX_LEN: usize = 50;
|
||||||
|
|
||||||
let mount = {
|
let mount = {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::data_collection::disks::IoCounters;
|
use crate::data_collection::disks::IoCounters;
|
||||||
|
|
||||||
/// Returns zpool I/O stats. Pulls data from `sysctl kstat.zfs.{POOL}.dataset.{objset-*}`
|
/// Returns zpool I/O stats. Pulls data from `sysctl
|
||||||
|
/// kstat.zfs.{POOL}.dataset.{objset-*}`
|
||||||
#[cfg(target_os = "freebsd")]
|
#[cfg(target_os = "freebsd")]
|
||||||
pub fn zfs_io_stats() -> anyhow::Result<Vec<IoCounters>> {
|
pub fn zfs_io_stats() -> anyhow::Result<Vec<IoCounters>> {
|
||||||
use sysctl::Sysctl;
|
use sysctl::Sysctl;
|
||||||
|
|
|
@ -19,5 +19,6 @@ pub mod arc;
|
||||||
pub struct MemHarvest {
|
pub struct MemHarvest {
|
||||||
pub used_bytes: u64,
|
pub used_bytes: u64,
|
||||||
pub total_bytes: u64,
|
pub total_bytes: u64,
|
||||||
pub use_percent: Option<f64>, // TODO: Might be find to just make this an f64, and any consumer checks NaN.
|
pub use_percent: Option<f64>, /* TODO: Might be find to just make this an f64, and any
|
||||||
|
* consumer checks NaN. */
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,11 +36,12 @@ pub(crate) fn get_swap_usage(sys: &System) -> Option<MemHarvest> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns cache usage. sysinfo has no way to do this directly but it should equal the difference
|
/// Returns cache usage. sysinfo has no way to do this directly but it should
|
||||||
/// between the available and free memory. Free memory is defined as memory not containing any data,
|
/// equal the difference between the available and free memory. Free memory is
|
||||||
/// which means cache and buffer memory are not "free". Available memory is defined as memory able
|
/// defined as memory not containing any data, which means cache and buffer
|
||||||
/// to be allocated by processes, which includes cache and buffer memory. On Windows, this will
|
/// memory are not "free". Available memory is defined as memory able
|
||||||
/// always be 0. For more information, see [docs](https://docs.rs/sysinfo/latest/sysinfo/struct.System.html#method.available_memory)
|
/// to be allocated by processes, which includes cache and buffer memory. On
|
||||||
|
/// Windows, this will always be 0. For more information, see [docs](https://docs.rs/sysinfo/latest/sysinfo/struct.System.html#method.available_memory)
|
||||||
/// and [memory explanation](https://askubuntu.com/questions/867068/what-is-available-memory-while-using-free-command)
|
/// and [memory explanation](https://askubuntu.com/questions/867068/what-is-available-memory-while-using-free-command)
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
pub(crate) fn get_cache_usage(sys: &System) -> Option<MemHarvest> {
|
pub(crate) fn get_cache_usage(sys: &System) -> Option<MemHarvest> {
|
||||||
|
|
|
@ -7,7 +7,8 @@ use sysinfo::Networks;
|
||||||
use super::NetworkHarvest;
|
use super::NetworkHarvest;
|
||||||
use crate::app::filter::Filter;
|
use crate::app::filter::Filter;
|
||||||
|
|
||||||
// TODO: Eventually make it so that this thing also takes individual usage into account, so we can show per-interface!
|
// TODO: Eventually make it so that this thing also takes individual usage into
|
||||||
|
// account, so we can show per-interface!
|
||||||
pub fn get_network_data(
|
pub fn get_network_data(
|
||||||
networks: &Networks, prev_net_access_time: Instant, prev_net_rx: &mut u64,
|
networks: &Networks, prev_net_access_time: Instant, prev_net_rx: &mut u64,
|
||||||
prev_net_tx: &mut u64, curr_time: Instant, filter: &Option<Filter>,
|
prev_net_tx: &mut u64, curr_time: Instant, filter: &Option<Filter>,
|
||||||
|
|
|
@ -87,7 +87,8 @@ pub struct ProcessHarvest {
|
||||||
/// Cumulative process uptime.
|
/// Cumulative process uptime.
|
||||||
pub time: Duration,
|
pub time: Duration,
|
||||||
|
|
||||||
/// This is the *effective* user ID of the process. This is only used on Unix platforms.
|
/// This is the *effective* user ID of the process. This is only used on
|
||||||
|
/// Unix platforms.
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
pub uid: Option<libc::uid_t>,
|
pub uid: Option<libc::uid_t>,
|
||||||
|
|
||||||
|
|
|
@ -29,11 +29,11 @@ pub struct PrevProcDetails {
|
||||||
cpu_time: u64,
|
cpu_time: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Given `/proc/stat` file contents, determine the idle and non-idle values of the CPU
|
/// Given `/proc/stat` file contents, determine the idle and non-idle values of
|
||||||
/// used to calculate CPU usage.
|
/// the CPU used to calculate CPU usage.
|
||||||
fn fetch_cpu_usage(line: &str) -> (f64, f64) {
|
fn fetch_cpu_usage(line: &str) -> (f64, f64) {
|
||||||
/// Converts a `Option<&str>` value to an f64. If it fails to parse or is `None`, it
|
/// Converts a `Option<&str>` value to an f64. If it fails to parse or is
|
||||||
/// will return `0_f64`.
|
/// `None`, it will return `0_f64`.
|
||||||
fn str_to_f64(val: Option<&str>) -> f64 {
|
fn str_to_f64(val: Option<&str>) -> f64 {
|
||||||
val.and_then(|v| v.parse::<f64>().ok()).unwrap_or(0_f64)
|
val.and_then(|v| v.parse::<f64>().ok()).unwrap_or(0_f64)
|
||||||
}
|
}
|
||||||
|
@ -48,8 +48,8 @@ fn fetch_cpu_usage(line: &str) -> (f64, f64) {
|
||||||
let softirq: f64 = str_to_f64(val.next());
|
let softirq: f64 = str_to_f64(val.next());
|
||||||
let steal: f64 = str_to_f64(val.next());
|
let steal: f64 = str_to_f64(val.next());
|
||||||
|
|
||||||
// Note we do not get guest/guest_nice, as they are calculated as part of user/nice respectively
|
// Note we do not get guest/guest_nice, as they are calculated as part of
|
||||||
// See https://github.com/htop-dev/htop/blob/main/linux/LinuxProcessList.c
|
// user/nice respectively See https://github.com/htop-dev/htop/blob/main/linux/LinuxProcessList.c
|
||||||
let idle = idle + iowait;
|
let idle = idle + iowait;
|
||||||
let non_idle = user + nice + system + irq + softirq + steal;
|
let non_idle = user + nice + system + irq + softirq + steal;
|
||||||
|
|
||||||
|
@ -331,8 +331,8 @@ pub(crate) fn linux_process_data(
|
||||||
if unnormalized_cpu {
|
if unnormalized_cpu {
|
||||||
let num_processors = collector.sys.system.cpus().len() as f64;
|
let num_processors = collector.sys.system.cpus().len() as f64;
|
||||||
|
|
||||||
// Note we *divide* here because the later calculation divides `cpu_usage` - in effect,
|
// Note we *divide* here because the later calculation divides `cpu_usage` - in
|
||||||
// multiplying over the number of cores.
|
// effect, multiplying over the number of cores.
|
||||||
cpu_usage /= num_processors;
|
cpu_usage /= num_processors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,8 @@ fn next_part<'a>(iter: &mut impl Iterator<Item = &'a str>) -> Result<&'a str, io
|
||||||
/// A wrapper around the data in `/proc/<PID>/stat`. For documentation, see
|
/// A wrapper around the data in `/proc/<PID>/stat`. For documentation, see
|
||||||
/// [here](https://man7.org/linux/man-pages/man5/proc.5.html).
|
/// [here](https://man7.org/linux/man-pages/man5/proc.5.html).
|
||||||
///
|
///
|
||||||
/// Note this does not necessarily get all fields, only the ones we use in bottom.
|
/// Note this does not necessarily get all fields, only the ones we use in
|
||||||
|
/// bottom.
|
||||||
pub(crate) struct Stat {
|
pub(crate) struct Stat {
|
||||||
/// The filename of the executable without parentheses.
|
/// The filename of the executable without parentheses.
|
||||||
pub comm: String,
|
pub comm: String,
|
||||||
|
@ -40,13 +41,16 @@ pub(crate) struct Stat {
|
||||||
/// The parent process PID.
|
/// The parent process PID.
|
||||||
pub ppid: Pid,
|
pub ppid: Pid,
|
||||||
|
|
||||||
/// The amount of time this process has been scheduled in user mode in clock ticks.
|
/// The amount of time this process has been scheduled in user mode in clock
|
||||||
|
/// ticks.
|
||||||
pub utime: u64,
|
pub utime: u64,
|
||||||
|
|
||||||
/// The amount of time this process has been scheduled in kernel mode in clock ticks.
|
/// The amount of time this process has been scheduled in kernel mode in
|
||||||
|
/// clock ticks.
|
||||||
pub stime: u64,
|
pub stime: u64,
|
||||||
|
|
||||||
/// The resident set size, or the number of pages the process has in real memory.
|
/// The resident set size, or the number of pages the process has in real
|
||||||
|
/// memory.
|
||||||
pub rss: u64,
|
pub rss: u64,
|
||||||
|
|
||||||
/// The start time of the process, represented in clock ticks.
|
/// The start time of the process, represented in clock ticks.
|
||||||
|
@ -56,8 +60,8 @@ pub(crate) struct Stat {
|
||||||
impl Stat {
|
impl Stat {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from_file(mut f: File, buffer: &mut String) -> anyhow::Result<Stat> {
|
fn from_file(mut f: File, buffer: &mut String) -> anyhow::Result<Stat> {
|
||||||
// Since this is just one line, we can read it all at once. However, since it might have non-utf8 characters,
|
// Since this is just one line, we can read it all at once. However, since it
|
||||||
// we can't just use read_to_string.
|
// might have non-utf8 characters, we can't just use read_to_string.
|
||||||
f.read_to_end(unsafe { buffer.as_mut_vec() })?;
|
f.read_to_end(unsafe { buffer.as_mut_vec() })?;
|
||||||
|
|
||||||
let line = buffer.to_string_lossy();
|
let line = buffer.to_string_lossy();
|
||||||
|
@ -82,12 +86,14 @@ impl Stat {
|
||||||
.ok_or_else(|| anyhow!("missing state"))?;
|
.ok_or_else(|| anyhow!("missing state"))?;
|
||||||
let ppid: Pid = next_part(&mut rest)?.parse()?;
|
let ppid: Pid = next_part(&mut rest)?.parse()?;
|
||||||
|
|
||||||
// Skip 9 fields until utime (pgrp, session, tty_nr, tpgid, flags, minflt, cminflt, majflt, cmajflt).
|
// Skip 9 fields until utime (pgrp, session, tty_nr, tpgid, flags, minflt,
|
||||||
|
// cminflt, majflt, cmajflt).
|
||||||
let mut rest = rest.skip(9);
|
let mut rest = rest.skip(9);
|
||||||
let utime: u64 = next_part(&mut rest)?.parse()?;
|
let utime: u64 = next_part(&mut rest)?.parse()?;
|
||||||
let stime: u64 = next_part(&mut rest)?.parse()?;
|
let stime: u64 = next_part(&mut rest)?.parse()?;
|
||||||
|
|
||||||
// Skip 6 fields until starttime (cutime, cstime, priority, nice, num_threads, itrealvalue).
|
// Skip 6 fields until starttime (cutime, cstime, priority, nice, num_threads,
|
||||||
|
// itrealvalue).
|
||||||
let mut rest = rest.skip(6);
|
let mut rest = rest.skip(6);
|
||||||
let start_time: u64 = next_part(&mut rest)?.parse()?;
|
let start_time: u64 = next_part(&mut rest)?.parse()?;
|
||||||
|
|
||||||
|
@ -115,7 +121,8 @@ impl Stat {
|
||||||
|
|
||||||
/// A wrapper around the data in `/proc/<PID>/io`.
|
/// A wrapper around the data in `/proc/<PID>/io`.
|
||||||
///
|
///
|
||||||
/// Note this does not necessarily get all fields, only the ones we use in bottom.
|
/// Note this does not necessarily get all fields, only the ones we use in
|
||||||
|
/// bottom.
|
||||||
pub(crate) struct Io {
|
pub(crate) struct Io {
|
||||||
pub read_bytes: u64,
|
pub read_bytes: u64,
|
||||||
pub write_bytes: u64,
|
pub write_bytes: u64,
|
||||||
|
@ -136,7 +143,8 @@ impl Io {
|
||||||
let mut read_bytes = 0;
|
let mut read_bytes = 0;
|
||||||
let mut write_bytes = 0;
|
let mut write_bytes = 0;
|
||||||
|
|
||||||
// This saves us from doing a string allocation on each iteration compared to `lines()`.
|
// This saves us from doing a string allocation on each iteration compared to
|
||||||
|
// `lines()`.
|
||||||
while let Ok(bytes) = reader.read_line(buffer) {
|
while let Ok(bytes) = reader.read_line(buffer) {
|
||||||
if bytes > 0 {
|
if bytes > 0 {
|
||||||
if buffer.is_empty() {
|
if buffer.is_empty() {
|
||||||
|
@ -207,12 +215,13 @@ fn reset(root: &mut PathBuf, buffer: &mut String) {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Process {
|
impl Process {
|
||||||
/// Creates a new [`Process`] given a `/proc/<PID>` path. This may fail if the process
|
/// Creates a new [`Process`] given a `/proc/<PID>` path. This may fail if
|
||||||
/// no longer exists or there are permissions issues.
|
/// the process no longer exists or there are permissions issues.
|
||||||
///
|
///
|
||||||
/// Note that this pre-allocates fields on **creation**! As such, some data might end
|
/// Note that this pre-allocates fields on **creation**! As such, some data
|
||||||
/// up "outdated" depending on when you call some of the methods. Therefore, this struct
|
/// might end up "outdated" depending on when you call some of the
|
||||||
/// is only useful for either fields that are unlikely to change, or are short-lived and
|
/// methods. Therefore, this struct is only useful for either fields
|
||||||
|
/// that are unlikely to change, or are short-lived and
|
||||||
/// will be discarded quickly.
|
/// will be discarded quickly.
|
||||||
pub(crate) fn from_path(pid_path: PathBuf) -> anyhow::Result<Process> {
|
pub(crate) fn from_path(pid_path: PathBuf) -> anyhow::Result<Process> {
|
||||||
// TODO: Pass in a buffer vec/string to share?
|
// TODO: Pass in a buffer vec/string to share?
|
||||||
|
@ -247,7 +256,8 @@ impl Process {
|
||||||
let mut root = pid_path;
|
let mut root = pid_path;
|
||||||
let mut buffer = String::new();
|
let mut buffer = String::new();
|
||||||
|
|
||||||
// NB: Whenever you add a new stat, make sure to pop the root and clear the buffer!
|
// NB: Whenever you add a new stat, make sure to pop the root and clear the
|
||||||
|
// buffer!
|
||||||
let stat =
|
let stat =
|
||||||
open_at(&mut root, "stat", &fd).and_then(|file| Stat::from_file(file, &mut buffer))?;
|
open_at(&mut root, "stat", &fd).and_then(|file| Stat::from_file(file, &mut buffer))?;
|
||||||
reset(&mut root, &mut buffer);
|
reset(&mut root, &mut buffer);
|
||||||
|
@ -286,8 +296,9 @@ fn cmdline(root: &mut PathBuf, fd: &OwnedFd, buffer: &mut String) -> anyhow::Res
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens a path. Note that this function takes in a mutable root - this will mutate it to avoid allocations. You
|
/// Opens a path. Note that this function takes in a mutable root - this will
|
||||||
/// probably will want to pop the most recent child after if you need to use the buffer again.
|
/// mutate it to avoid allocations. You probably will want to pop the most
|
||||||
|
/// recent child after if you need to use the buffer again.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn open_at(root: &mut PathBuf, child: &str, fd: &OwnedFd) -> anyhow::Result<File> {
|
fn open_at(root: &mut PathBuf, child: &str, fd: &OwnedFd) -> anyhow::Result<File> {
|
||||||
root.push(child);
|
root.push(child);
|
||||||
|
|
|
@ -22,7 +22,8 @@ impl UnixProcessExt for MacOSProcessExt {
|
||||||
let output = Command::new("ps")
|
let output = Command::new("ps")
|
||||||
.args(["-o", "pid=,pcpu=", "-p"])
|
.args(["-o", "pid=,pcpu=", "-p"])
|
||||||
.arg(
|
.arg(
|
||||||
// Has to look like this since otherwise, it you hit a `unstable_name_collisions` warning.
|
// Has to look like this since otherwise, it you hit a `unstable_name_collisions`
|
||||||
|
// warning.
|
||||||
Itertools::intersperse(pids.iter().map(i32::to_string), ",".to_string())
|
Itertools::intersperse(pids.iter().map(i32::to_string), ",".to_string())
|
||||||
.collect::<String>(),
|
.collect::<String>(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//! Partial bindings from Apple's open source code for getting process information.
|
//! Partial bindings from Apple's open source code for getting process
|
||||||
//! Some of this is based on [heim's binding implementation](https://github.com/heim-rs/heim/blob/master/heim-process/src/sys/macos/bindings/process.rs).
|
//! information. Some of this is based on [heim's binding implementation](https://github.com/heim-rs/heim/blob/master/heim-process/src/sys/macos/bindings/process.rs).
|
||||||
|
|
||||||
use std::mem;
|
use std::mem;
|
||||||
|
|
||||||
|
@ -152,7 +152,8 @@ pub(crate) struct extern_proc {
|
||||||
/// Pointer to process group. Originally a pointer to a `pgrp`.
|
/// Pointer to process group. Originally a pointer to a `pgrp`.
|
||||||
pub p_pgrp: *mut c_void,
|
pub p_pgrp: *mut c_void,
|
||||||
|
|
||||||
/// Kernel virtual addr of u-area (PROC ONLY). Originally a pointer to a `user`.
|
/// Kernel virtual addr of u-area (PROC ONLY). Originally a pointer to a
|
||||||
|
/// `user`.
|
||||||
pub p_addr: *mut c_void,
|
pub p_addr: *mut c_void,
|
||||||
|
|
||||||
/// Exit status for wait; also stop signal.
|
/// Exit status for wait; also stop signal.
|
||||||
|
@ -207,10 +208,12 @@ pub(crate) struct vmspace {
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub(crate) struct eproc {
|
pub(crate) struct eproc {
|
||||||
/// Address of proc. We just cheat and use a c_void pointer since we aren't using this.
|
/// Address of proc. We just cheat and use a c_void pointer since we aren't
|
||||||
|
/// using this.
|
||||||
pub e_paddr: *mut c_void,
|
pub e_paddr: *mut c_void,
|
||||||
|
|
||||||
/// Session pointer. We just cheat and use a c_void pointer since we aren't using this.
|
/// Session pointer. We just cheat and use a c_void pointer since we aren't
|
||||||
|
/// using this.
|
||||||
pub e_sess: *mut c_void,
|
pub e_sess: *mut c_void,
|
||||||
|
|
||||||
/// Process credentials
|
/// Process credentials
|
||||||
|
@ -237,7 +240,8 @@ pub(crate) struct eproc {
|
||||||
/// tty process group id
|
/// tty process group id
|
||||||
pub e_tpgid: pid_t,
|
pub e_tpgid: pid_t,
|
||||||
|
|
||||||
/// tty session pointer. We just cheat and use a c_void pointer since we aren't using this.
|
/// tty session pointer. We just cheat and use a c_void pointer since we
|
||||||
|
/// aren't using this.
|
||||||
pub e_tsess: *mut c_void,
|
pub e_tsess: *mut c_void,
|
||||||
|
|
||||||
/// wchan message
|
/// wchan message
|
||||||
|
@ -291,8 +295,8 @@ pub(crate) fn kinfo_process(pid: Pid) -> Result<kinfo_proc> {
|
||||||
bail!("failed to get process for pid {pid}");
|
bail!("failed to get process for pid {pid}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// SAFETY: info is initialized if result succeeded and returned a non-negative result. If sysctl failed, it returns
|
// SAFETY: info is initialized if result succeeded and returned a non-negative
|
||||||
// -1 with errno set.
|
// result. If sysctl failed, it returns -1 with errno set.
|
||||||
//
|
//
|
||||||
// Source: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/sysctl.3.html
|
// Source: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/sysctl.3.html
|
||||||
unsafe { Ok(info.assume_init()) }
|
unsafe { Ok(info.assume_init()) }
|
||||||
|
|
|
@ -6,7 +6,10 @@ use hashbrown::HashMap;
|
||||||
use sysinfo::{ProcessStatus, System};
|
use sysinfo::{ProcessStatus, System};
|
||||||
|
|
||||||
use super::ProcessHarvest;
|
use super::ProcessHarvest;
|
||||||
use crate::{data_collection::processes::UserTable, data_collection::Pid, utils::error};
|
use crate::{
|
||||||
|
data_collection::{processes::UserTable, Pid},
|
||||||
|
utils::error,
|
||||||
|
};
|
||||||
|
|
||||||
pub(crate) trait UnixProcessExt {
|
pub(crate) trait UnixProcessExt {
|
||||||
fn sysinfo_process_data(
|
fn sysinfo_process_data(
|
||||||
|
|
|
@ -12,7 +12,8 @@ impl UserTable {
|
||||||
if let Some(user) = self.uid_user_mapping.get(&uid) {
|
if let Some(user) = self.uid_user_mapping.get(&uid) {
|
||||||
Ok(user.clone())
|
Ok(user.clone())
|
||||||
} else {
|
} else {
|
||||||
// SAFETY: getpwuid returns a null pointer if no passwd entry is found for the uid
|
// SAFETY: getpwuid returns a null pointer if no passwd entry is found for the
|
||||||
|
// uid
|
||||||
let passwd = unsafe { libc::getpwuid(uid) };
|
let passwd = unsafe { libc::getpwuid(uid) };
|
||||||
|
|
||||||
if passwd.is_null() {
|
if passwd.is_null() {
|
||||||
|
|
|
@ -103,8 +103,9 @@ pub fn sysinfo_process_data(
|
||||||
.and_then(|uid| users.get_user_by_id(uid))
|
.and_then(|uid| users.get_user_by_id(uid))
|
||||||
.map_or_else(|| "N/A".into(), |user| user.name().to_owned().into()),
|
.map_or_else(|| "N/A".into(), |user| user.name().to_owned().into()),
|
||||||
time: if process_val.start_time() == 0 {
|
time: if process_val.start_time() == 0 {
|
||||||
// Workaround for Windows occasionally returning a start time equal to UNIX epoch, giving a run time
|
// Workaround for Windows occasionally returning a start time equal to UNIX
|
||||||
// in the range of 50+ years. We just return a time of zero in this case for simplicity.
|
// epoch, giving a run time in the range of 50+ years. We just
|
||||||
|
// return a time of zero in this case for simplicity.
|
||||||
Duration::ZERO
|
Duration::ZERO
|
||||||
} else {
|
} else {
|
||||||
Duration::from_secs(process_val.run_time())
|
Duration::from_secs(process_val.run_time())
|
||||||
|
|
|
@ -47,7 +47,8 @@ impl FromStr for TemperatureType {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TemperatureType {
|
impl TemperatureType {
|
||||||
/// Given a temperature in Celsius, covert it if necessary for a different unit.
|
/// Given a temperature in Celsius, covert it if necessary for a different
|
||||||
|
/// unit.
|
||||||
pub fn convert_temp_unit(&self, temp_celsius: f32) -> f32 {
|
pub fn convert_temp_unit(&self, temp_celsius: f32) -> f32 {
|
||||||
fn convert_celsius_to_kelvin(celsius: f32) -> f32 {
|
fn convert_celsius_to_kelvin(celsius: f32) -> f32 {
|
||||||
celsius + 273.15
|
celsius + 273.15
|
||||||
|
|
|
@ -13,13 +13,15 @@ use crate::{app::filter::Filter, utils::error::BottomError};
|
||||||
|
|
||||||
const EMPTY_NAME: &str = "Unknown";
|
const EMPTY_NAME: &str = "Unknown";
|
||||||
|
|
||||||
/// Returned results from grabbing hwmon/coretemp temperature sensor values/names.
|
/// Returned results from grabbing hwmon/coretemp temperature sensor
|
||||||
|
/// values/names.
|
||||||
struct HwmonResults {
|
struct HwmonResults {
|
||||||
temperatures: Vec<TempHarvest>,
|
temperatures: Vec<TempHarvest>,
|
||||||
num_hwmon: usize,
|
num_hwmon: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses and reads temperatures that were in millidegree Celsius, and if successful, returns a temperature in Celsius.
|
/// Parses and reads temperatures that were in millidegree Celsius, and if
|
||||||
|
/// successful, returns a temperature in Celsius.
|
||||||
fn parse_temp(path: &Path) -> Result<f32> {
|
fn parse_temp(path: &Path) -> Result<f32> {
|
||||||
Ok(fs::read_to_string(path)?
|
Ok(fs::read_to_string(path)?
|
||||||
.trim_end()
|
.trim_end()
|
||||||
|
@ -28,7 +30,8 @@ fn parse_temp(path: &Path) -> Result<f32> {
|
||||||
/ 1_000.0)
|
/ 1_000.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all candidates from hwmon and coretemp. It will also return the number of entries from hwmon.
|
/// Get all candidates from hwmon and coretemp. It will also return the number
|
||||||
|
/// of entries from hwmon.
|
||||||
fn get_hwmon_candidates() -> (HashSet<PathBuf>, usize) {
|
fn get_hwmon_candidates() -> (HashSet<PathBuf>, usize) {
|
||||||
let mut dirs = HashSet::default();
|
let mut dirs = HashSet::default();
|
||||||
|
|
||||||
|
@ -36,11 +39,13 @@ fn get_hwmon_candidates() -> (HashSet<PathBuf>, usize) {
|
||||||
for entry in read_dir.flatten() {
|
for entry in read_dir.flatten() {
|
||||||
let mut path = entry.path();
|
let mut path = entry.path();
|
||||||
|
|
||||||
// hwmon includes many sensors, we only want ones with at least one temperature sensor
|
// hwmon includes many sensors, we only want ones with at least one temperature
|
||||||
// Reading this file will wake the device, but we're only checking existence, so it should be fine.
|
// sensor Reading this file will wake the device, but we're only
|
||||||
|
// checking existence, so it should be fine.
|
||||||
if !path.join("temp1_input").exists() {
|
if !path.join("temp1_input").exists() {
|
||||||
// Note we also check for a `device` subdirectory (e.g. `/sys/class/hwmon/hwmon*/device/`).
|
// Note we also check for a `device` subdirectory (e.g.
|
||||||
// This is needed for CentOS, which adds this extra `/device` directory. See:
|
// `/sys/class/hwmon/hwmon*/device/`). This is needed for
|
||||||
|
// CentOS, which adds this extra `/device` directory. See:
|
||||||
// - https://github.com/nicolargo/glances/issues/1060
|
// - https://github.com/nicolargo/glances/issues/1060
|
||||||
// - https://github.com/giampaolo/psutil/issues/971
|
// - https://github.com/giampaolo/psutil/issues/971
|
||||||
// - https://github.com/giampaolo/psutil/blob/642438375e685403b4cd60b0c0e25b80dd5a813d/psutil/_pslinux.py#L1316
|
// - https://github.com/giampaolo/psutil/blob/642438375e685403b4cd60b0c0e25b80dd5a813d/psutil/_pslinux.py#L1316
|
||||||
|
@ -65,8 +70,9 @@ fn get_hwmon_candidates() -> (HashSet<PathBuf>, usize) {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
|
||||||
if path.join("temp1_input").exists() {
|
if path.join("temp1_input").exists() {
|
||||||
// It's possible that there are dupes (represented by symlinks) - the easy
|
// It's possible that there are dupes (represented by symlinks) - the
|
||||||
// way is to just substitute the parent directory and check if the hwmon
|
// easy way is to just substitute the parent
|
||||||
|
// directory and check if the hwmon
|
||||||
// variant exists already in a set.
|
// variant exists already in a set.
|
||||||
//
|
//
|
||||||
// For more info, see https://github.com/giampaolo/psutil/pull/1822/files
|
// For more info, see https://github.com/giampaolo/psutil/pull/1822/files
|
||||||
|
@ -175,7 +181,8 @@ fn finalize_name(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the temperature should *actually* be read during enumeration.
|
/// Whether the temperature should *actually* be read during enumeration.
|
||||||
/// Will return false if the state is not D0/unknown, or if it does not support `device/power_state`.
|
/// Will return false if the state is not D0/unknown, or if it does not support
|
||||||
|
/// `device/power_state`.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn is_device_awake(path: &Path) -> bool {
|
fn is_device_awake(path: &Path) -> bool {
|
||||||
// Whether the temperature should *actually* be read during enumeration.
|
// Whether the temperature should *actually* be read during enumeration.
|
||||||
|
@ -186,10 +193,12 @@ fn is_device_awake(path: &Path) -> bool {
|
||||||
if power_state.exists() {
|
if power_state.exists() {
|
||||||
if let Ok(state) = fs::read_to_string(power_state) {
|
if let Ok(state) = fs::read_to_string(power_state) {
|
||||||
let state = state.trim();
|
let state = state.trim();
|
||||||
// The zenpower3 kernel module (incorrectly?) reports "unknown", causing this check
|
// The zenpower3 kernel module (incorrectly?) reports "unknown", causing this
|
||||||
// to fail and temperatures to appear as zero instead of having the file not exist.
|
// check to fail and temperatures to appear as zero instead of
|
||||||
|
// having the file not exist.
|
||||||
//
|
//
|
||||||
// Their self-hosted git instance has disabled sign up, so this bug cant be reported either.
|
// Their self-hosted git instance has disabled sign up, so this bug cant be
|
||||||
|
// reported either.
|
||||||
state == "D0" || state == "unknown"
|
state == "D0" || state == "unknown"
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
|
@ -199,9 +208,10 @@ fn is_device_awake(path: &Path) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get temperature sensors from the linux sysfs interface `/sys/class/hwmon` and
|
/// Get temperature sensors from the linux sysfs interface `/sys/class/hwmon`
|
||||||
/// `/sys/devices/platform/coretemp.*`. It returns all found temperature sensors, and the number
|
/// and `/sys/devices/platform/coretemp.*`. It returns all found temperature
|
||||||
/// of checked hwmon directories (not coretemp directories).
|
/// sensors, and the number of checked hwmon directories (not coretemp
|
||||||
|
/// directories).
|
||||||
///
|
///
|
||||||
/// For more details, see the relevant Linux kernel documentation:
|
/// For more details, see the relevant Linux kernel documentation:
|
||||||
/// - [`/sys/class/hwmon`](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-hwmon)
|
/// - [`/sys/class/hwmon`](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-hwmon)
|
||||||
|
@ -233,7 +243,8 @@ fn hwmon_temperatures(temp_type: &TemperatureType, filter: &Option<Filter>) -> H
|
||||||
// will not wake the device, and thus not block,
|
// will not wake the device, and thus not block,
|
||||||
// and meaning no sensors have to be hidden depending on `power_state`
|
// and meaning no sensors have to be hidden depending on `power_state`
|
||||||
//
|
//
|
||||||
// It would probably be more ideal to use a proper async runtime; this would also allow easy cancellation/timeouts.
|
// It would probably be more ideal to use a proper async runtime; this would
|
||||||
|
// also allow easy cancellation/timeouts.
|
||||||
for file_path in dirs {
|
for file_path in dirs {
|
||||||
let sensor_name = read_to_string_lossy(file_path.join("name"));
|
let sensor_name = read_to_string_lossy(file_path.join("name"));
|
||||||
|
|
||||||
|
@ -264,18 +275,19 @@ fn hwmon_temperatures(temp_type: &TemperatureType, filter: &Option<Filter>) -> H
|
||||||
|
|
||||||
// Do some messing around to get a more sensible name for sensors:
|
// Do some messing around to get a more sensible name for sensors:
|
||||||
// - For GPUs, this will use the kernel device name, ex `card0`
|
// - For GPUs, this will use the kernel device name, ex `card0`
|
||||||
// - For nvme drives, this will also use the kernel name, ex `nvme0`.
|
// - For nvme drives, this will also use the kernel name, ex `nvme0`. This is
|
||||||
// This is found differently than for GPUs
|
// found differently than for GPUs
|
||||||
// - For whatever acpitz is, on my machine this is now `thermal_zone0`.
|
// - For whatever acpitz is, on my machine this is now `thermal_zone0`.
|
||||||
// - For k10temp, this will still be k10temp, but it has to be handled special.
|
// - For k10temp, this will still be k10temp, but it has to be handled special.
|
||||||
let hwmon_name = {
|
let hwmon_name = {
|
||||||
let device = file_path.join("device");
|
let device = file_path.join("device");
|
||||||
|
|
||||||
// This will exist for GPUs but not others, this is how we find their kernel name.
|
// This will exist for GPUs but not others, this is how we find their kernel
|
||||||
|
// name.
|
||||||
let drm = device.join("drm");
|
let drm = device.join("drm");
|
||||||
if drm.exists() {
|
if drm.exists() {
|
||||||
// This should never actually be empty. If it is though, we'll fall back to the sensor name
|
// This should never actually be empty. If it is though, we'll fall back to
|
||||||
// later on.
|
// the sensor name later on.
|
||||||
let mut gpu = None;
|
let mut gpu = None;
|
||||||
|
|
||||||
if let Ok(cards) = drm.read_dir() {
|
if let Ok(cards) = drm.read_dir() {
|
||||||
|
@ -294,10 +306,11 @@ fn hwmon_temperatures(temp_type: &TemperatureType, filter: &Option<Filter>) -> H
|
||||||
|
|
||||||
gpu
|
gpu
|
||||||
} else {
|
} else {
|
||||||
// This little mess is to account for stuff like k10temp. This is needed because the
|
// This little mess is to account for stuff like k10temp. This is needed
|
||||||
// `device` symlink points to `nvme*` for nvme drives, but to PCI buses for anything
|
// because the `device` symlink points to `nvme*`
|
||||||
// else. If the first character is alphabetic, it's an actual name like k10temp or
|
// for nvme drives, but to PCI buses for anything
|
||||||
// nvme0, not a PCI bus.
|
// else. If the first character is alphabetic, it's an actual name like
|
||||||
|
// k10temp or nvme0, not a PCI bus.
|
||||||
fs::read_link(device).ok().and_then(|link| {
|
fs::read_link(device).ok().and_then(|link| {
|
||||||
let link = link
|
let link = link
|
||||||
.file_name()
|
.file_name()
|
||||||
|
@ -316,7 +329,8 @@ fn hwmon_temperatures(temp_type: &TemperatureType, filter: &Option<Filter>) -> H
|
||||||
|
|
||||||
let name = finalize_name(hwmon_name, sensor_label, &sensor_name, &mut seen_names);
|
let name = finalize_name(hwmon_name, sensor_label, &sensor_name, &mut seen_names);
|
||||||
|
|
||||||
// TODO: It's possible we may want to move the filter check further up to avoid probing hwmon if not needed?
|
// TODO: It's possible we may want to move the filter check further up to avoid
|
||||||
|
// probing hwmon if not needed?
|
||||||
if is_temp_filtered(filter, &name) {
|
if is_temp_filtered(filter, &name) {
|
||||||
if let Ok(temp_celsius) = parse_temp(&temp_path) {
|
if let Ok(temp_celsius) = parse_temp(&temp_path) {
|
||||||
temperatures.push(TempHarvest {
|
temperatures.push(TempHarvest {
|
||||||
|
@ -335,8 +349,9 @@ fn hwmon_temperatures(temp_type: &TemperatureType, filter: &Option<Filter>) -> H
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets data from `/sys/class/thermal/thermal_zone*`. This should only be used if
|
/// Gets data from `/sys/class/thermal/thermal_zone*`. This should only be used
|
||||||
/// [`hwmon_temperatures`] doesn't return anything to avoid duplicate sensor results.
|
/// if [`hwmon_temperatures`] doesn't return anything to avoid duplicate sensor
|
||||||
|
/// results.
|
||||||
///
|
///
|
||||||
/// See [the Linux kernel documentation](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-thermal)
|
/// See [the Linux kernel documentation](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-thermal)
|
||||||
/// for more details.
|
/// for more details.
|
||||||
|
|
|
@ -21,7 +21,8 @@ pub fn get_temperature_data(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For RockPro64 boards on FreeBSD, they apparently use "hw.temperature" for sensors.
|
// For RockPro64 boards on FreeBSD, they apparently use "hw.temperature" for
|
||||||
|
// sensors.
|
||||||
#[cfg(target_os = "freebsd")]
|
#[cfg(target_os = "freebsd")]
|
||||||
{
|
{
|
||||||
use sysctl::Sysctl;
|
use sysctl::Sysctl;
|
||||||
|
|
|
@ -74,7 +74,8 @@ pub struct ConvertedData {
|
||||||
pub cache_labels: Option<(String, String)>,
|
pub cache_labels: Option<(String, String)>,
|
||||||
pub swap_labels: Option<(String, String)>,
|
pub swap_labels: Option<(String, String)>,
|
||||||
|
|
||||||
pub mem_data: Vec<Point>, /* TODO: Switch this and all data points over to a better data structure... */
|
pub mem_data: Vec<Point>, /* TODO: Switch this and all data points over to a better data
|
||||||
|
* structure... */
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
pub cache_data: Vec<Point>,
|
pub cache_data: Vec<Point>,
|
||||||
pub swap_data: Vec<Point>,
|
pub swap_data: Vec<Point>,
|
||||||
|
@ -169,7 +170,8 @@ impl ConvertedData {
|
||||||
data,
|
data,
|
||||||
last_entry,
|
last_entry,
|
||||||
} => {
|
} => {
|
||||||
// A bit faster to just update all the times, so we just clear the vector.
|
// A bit faster to just update all the times, so we just clear the
|
||||||
|
// vector.
|
||||||
data.clear();
|
data.clear();
|
||||||
*last_entry = *cpu_usage;
|
*last_entry = *cpu_usage;
|
||||||
}
|
}
|
||||||
|
@ -177,8 +179,8 @@ impl ConvertedData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [Opt] Can probably avoid data deduplication - store the shift + data + original once.
|
// TODO: [Opt] Can probably avoid data deduplication - store the shift + data +
|
||||||
// Now push all the data.
|
// original once. Now push all the data.
|
||||||
for (itx, mut cpu) in &mut self.cpu_data.iter_mut().skip(1).enumerate() {
|
for (itx, mut cpu) in &mut self.cpu_data.iter_mut().skip(1).enumerate() {
|
||||||
match &mut cpu {
|
match &mut cpu {
|
||||||
CpuWidgetData::All => unreachable!(),
|
CpuWidgetData::All => unreachable!(),
|
||||||
|
@ -262,10 +264,12 @@ pub fn convert_swap_data_points(data: &DataCollection) -> Vec<Point> {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the most appropriate binary prefix unit type (e.g. kibibyte) and denominator for the given amount of bytes.
|
/// Returns the most appropriate binary prefix unit type (e.g. kibibyte) and
|
||||||
|
/// denominator for the given amount of bytes.
|
||||||
///
|
///
|
||||||
/// The expected usage is to divide out the given value with the returned denominator in order to be able to use it
|
/// The expected usage is to divide out the given value with the returned
|
||||||
/// with the returned binary unit (e.g. divide 3000 bytes by 1024 to have a value in KiB).
|
/// denominator in order to be able to use it with the returned binary unit
|
||||||
|
/// (e.g. divide 3000 bytes by 1024 to have a value in KiB).
|
||||||
#[inline]
|
#[inline]
|
||||||
fn get_binary_unit_and_denominator(bytes: u64) -> (&'static str, f64) {
|
fn get_binary_unit_and_denominator(bytes: u64) -> (&'static str, f64) {
|
||||||
match bytes {
|
match bytes {
|
||||||
|
@ -277,7 +281,8 @@ fn get_binary_unit_and_denominator(bytes: u64) -> (&'static str, f64) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the unit type and denominator for given total amount of memory in kibibytes.
|
/// Returns the unit type and denominator for given total amount of memory in
|
||||||
|
/// kibibytes.
|
||||||
pub fn convert_mem_label(harvest: &MemHarvest) -> Option<(String, String)> {
|
pub fn convert_mem_label(harvest: &MemHarvest) -> Option<(String, String)> {
|
||||||
if harvest.total_bytes > 0 {
|
if harvest.total_bytes > 0 {
|
||||||
Some((format!("{:3.0}%", harvest.use_percent.unwrap_or(0.0)), {
|
Some((format!("{:3.0}%", harvest.use_percent.unwrap_or(0.0)), {
|
||||||
|
@ -371,7 +376,9 @@ pub fn convert_network_points(
|
||||||
let (rx_converted_result, total_rx_converted_result): ((f64, String), (f64, &'static str)) =
|
let (rx_converted_result, total_rx_converted_result): ((f64, String), (f64, &'static str)) =
|
||||||
if use_binary_prefix {
|
if use_binary_prefix {
|
||||||
(
|
(
|
||||||
get_binary_prefix(rx_data, unit), /* If this isn't obvious why there's two functions, one you can configure the unit, the other is always bytes */
|
get_binary_prefix(rx_data, unit), /* If this isn't obvious why there's two
|
||||||
|
* functions, one you can configure the unit,
|
||||||
|
* the other is always bytes */
|
||||||
get_binary_bytes(total_rx_data),
|
get_binary_bytes(total_rx_data),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -464,8 +471,9 @@ pub fn convert_network_points(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a string given a value that is converted to the closest binary variant.
|
/// Returns a string given a value that is converted to the closest binary
|
||||||
/// If the value is greater than a gibibyte, then it will return a decimal place.
|
/// variant. If the value is greater than a gibibyte, then it will return a
|
||||||
|
/// decimal place.
|
||||||
pub fn binary_byte_string(value: u64) -> String {
|
pub fn binary_byte_string(value: u64) -> String {
|
||||||
let converted_values = get_binary_bytes(value);
|
let converted_values = get_binary_bytes(value);
|
||||||
if value >= GIBI_LIMIT {
|
if value >= GIBI_LIMIT {
|
||||||
|
@ -486,8 +494,9 @@ pub fn dec_bytes_per_string(value: u64) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a string given a value that is converted to the closest SI-variant, per second.
|
/// Returns a string given a value that is converted to the closest SI-variant,
|
||||||
/// If the value is greater than a giga-X, then it will return a decimal place.
|
/// per second. If the value is greater than a giga-X, then it will return a
|
||||||
|
/// decimal place.
|
||||||
pub fn dec_bytes_per_second_string(value: u64) -> String {
|
pub fn dec_bytes_per_second_string(value: u64) -> String {
|
||||||
let converted_values = get_decimal_bytes(value);
|
let converted_values = get_decimal_bytes(value);
|
||||||
if value >= GIGA_LIMIT {
|
if value >= GIGA_LIMIT {
|
||||||
|
|
40
src/main.rs
40
src/main.rs
|
@ -1,9 +1,11 @@
|
||||||
//! A customizable cross-platform graphical process/system monitor for the terminal.
|
//! A customizable cross-platform graphical process/system monitor for the
|
||||||
//! Supports Linux, macOS, and Windows. Inspired by gtop, gotop, and htop.
|
//! terminal. Supports Linux, macOS, and Windows. Inspired by gtop, gotop, and
|
||||||
|
//! htop.
|
||||||
//!
|
//!
|
||||||
//! **Note:** The following documentation is primarily intended for people to refer to for development purposes rather
|
//! **Note:** The following documentation is primarily intended for people to
|
||||||
//! than the actual usage of the application. If you are instead looking for documentation regarding the *usage* of
|
//! refer to for development purposes rather than the actual usage of the
|
||||||
//! bottom, refer to [here](https://clementtsang.github.io/bottom/stable/).
|
//! application. If you are instead looking for documentation regarding the
|
||||||
|
//! *usage* of bottom, refer to [here](https://clementtsang.github.io/bottom/stable/).
|
||||||
|
|
||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod utils {
|
pub mod utils {
|
||||||
|
@ -48,8 +50,7 @@ use crossterm::{
|
||||||
};
|
};
|
||||||
use data_conversion::*;
|
use data_conversion::*;
|
||||||
use event::{handle_key_event_or_break, handle_mouse_event, BottomEvent, CollectionThreadEvent};
|
use event::{handle_key_event_or_break, handle_mouse_event, BottomEvent, CollectionThreadEvent};
|
||||||
use options::args;
|
use options::{args, get_color_scheme, get_config_path, get_or_create_config, init_app};
|
||||||
use options::{get_color_scheme, get_config_path, get_or_create_config, init_app};
|
|
||||||
use tui::{backend::CrosstermBackend, Terminal};
|
use tui::{backend::CrosstermBackend, Terminal};
|
||||||
use utils::error;
|
use utils::error;
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
|
@ -156,9 +157,11 @@ fn create_input_thread(
|
||||||
if let Ok(event) = read() {
|
if let Ok(event) = read() {
|
||||||
match event {
|
match event {
|
||||||
Event::Resize(_, _) => {
|
Event::Resize(_, _) => {
|
||||||
// TODO: Might want to debounce this in the future, or take into account the actual resize
|
// TODO: Might want to debounce this in the future, or take into
|
||||||
// values. Maybe we want to keep the current implementation in case the resize event might
|
// account the actual resize values.
|
||||||
// not fire... not sure.
|
// Maybe we want to keep the current implementation in case the
|
||||||
|
// resize event might not fire...
|
||||||
|
// not sure.
|
||||||
|
|
||||||
if sender.send(BottomEvent::Resize).is_err() {
|
if sender.send(BottomEvent::Resize).is_err() {
|
||||||
break;
|
break;
|
||||||
|
@ -170,7 +173,8 @@ fn create_input_thread(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Key(key) if key.kind == KeyEventKind::Press => {
|
Event::Key(key) if key.kind == KeyEventKind::Press => {
|
||||||
// For now, we only care about key down events. This may change in the future.
|
// For now, we only care about key down events. This may change in
|
||||||
|
// the future.
|
||||||
if sender.send(BottomEvent::KeyInput(key)).is_err() {
|
if sender.send(BottomEvent::KeyInput(key)).is_err() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -302,7 +306,8 @@ fn main() -> anyhow::Result<()> {
|
||||||
CanvasStyling::new(colour_scheme, &config)?
|
CanvasStyling::new(colour_scheme, &config)?
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create an "app" struct, which will control most of the program and store settings/state
|
// Create an "app" struct, which will control most of the program and store
|
||||||
|
// settings/state
|
||||||
let (mut app, widget_layout) = init_app(args, config, &styling)?;
|
let (mut app, widget_layout) = init_app(args, config, &styling)?;
|
||||||
|
|
||||||
// Create painter and set colours.
|
// Create painter and set colours.
|
||||||
|
@ -311,14 +316,16 @@ fn main() -> anyhow::Result<()> {
|
||||||
// Check if the current environment is in a terminal.
|
// Check if the current environment is in a terminal.
|
||||||
check_if_terminal();
|
check_if_terminal();
|
||||||
|
|
||||||
// Create termination mutex and cvar. We use this setup because we need to sleep at some points in the update
|
// Create termination mutex and cvar. We use this setup because we need to sleep
|
||||||
// thread, but we want to be able to interrupt the "sleep" if a termination occurs.
|
// at some points in the update thread, but we want to be able to interrupt
|
||||||
|
// the "sleep" if a termination occurs.
|
||||||
let termination_lock = Arc::new(Mutex::new(false));
|
let termination_lock = Arc::new(Mutex::new(false));
|
||||||
let termination_cvar = Arc::new(Condvar::new());
|
let termination_cvar = Arc::new(Condvar::new());
|
||||||
|
|
||||||
let (sender, receiver) = mpsc::channel();
|
let (sender, receiver) = mpsc::channel();
|
||||||
|
|
||||||
// Set up the event loop thread; we set this up early to speed up first-time-to-data.
|
// Set up the event loop thread; we set this up early to speed up
|
||||||
|
// first-time-to-data.
|
||||||
let (collection_thread_ctrl_sender, collection_thread_ctrl_receiver) = mpsc::channel();
|
let (collection_thread_ctrl_sender, collection_thread_ctrl_receiver) = mpsc::channel();
|
||||||
let _collection_thread = create_collection_thread(
|
let _collection_thread = create_collection_thread(
|
||||||
sender.clone(),
|
sender.clone(),
|
||||||
|
@ -394,7 +401,8 @@ fn main() -> anyhow::Result<()> {
|
||||||
|
|
||||||
let mut first_run = true;
|
let mut first_run = true;
|
||||||
|
|
||||||
// Draw once first to initialize the canvas, so it doesn't feel like it's frozen.
|
// Draw once first to initialize the canvas, so it doesn't feel like it's
|
||||||
|
// frozen.
|
||||||
try_drawing(&mut terminal, &mut app, &mut painter)?;
|
try_drawing(&mut terminal, &mut app, &mut painter)?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
|
|
@ -16,7 +16,7 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
pub use colours::ConfigColours;
|
pub use colours::ColoursConfig;
|
||||||
pub use config::ConfigV1;
|
pub use config::ConfigV1;
|
||||||
use hashbrown::{HashMap, HashSet};
|
use hashbrown::{HashMap, HashSet};
|
||||||
use indexmap::IndexSet;
|
use indexmap::IndexSet;
|
||||||
|
@ -62,9 +62,10 @@ macro_rules! is_flag_enabled {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the config path to use. If `override_config_path` is specified, then we will use
|
/// Returns the config path to use. If `override_config_path` is specified, then
|
||||||
/// that. If not, then return the "default" config path, which is:
|
/// we will use that. If not, then return the "default" config path, which is:
|
||||||
/// - If a path already exists at `<HOME>/bottom/bottom.toml`, then use that for legacy reasons.
|
/// - If a path already exists at `<HOME>/bottom/bottom.toml`, then use that for
|
||||||
|
/// legacy reasons.
|
||||||
/// - Otherwise, use `<SYSTEM_CONFIG_FOLDER>/bottom/bottom.toml`.
|
/// - Otherwise, use `<SYSTEM_CONFIG_FOLDER>/bottom/bottom.toml`.
|
||||||
///
|
///
|
||||||
/// For more details on this, see [dirs](https://docs.rs/dirs/latest/dirs/fn.config_dir.html)'
|
/// For more details on this, see [dirs](https://docs.rs/dirs/latest/dirs/fn.config_dir.html)'
|
||||||
|
@ -93,9 +94,10 @@ pub fn get_config_path(override_config_path: Option<&Path>) -> Option<PathBuf> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the config at `config_path`. If there is no config file at the specified path, it will
|
/// Get the config at `config_path`. If there is no config file at the specified
|
||||||
/// try to create a new file with the default settings, and return the default config. If bottom
|
/// path, it will try to create a new file with the default settings, and return
|
||||||
/// fails to write a new config, it will silently just return the default config.
|
/// the default config. If bottom fails to write a new config, it will silently
|
||||||
|
/// just return the default config.
|
||||||
pub fn get_or_create_config(config_path: Option<&Path>) -> error::Result<ConfigV1> {
|
pub fn get_or_create_config(config_path: Option<&Path>) -> error::Result<ConfigV1> {
|
||||||
match &config_path {
|
match &config_path {
|
||||||
Some(path) => {
|
Some(path) => {
|
||||||
|
@ -111,7 +113,8 @@ pub fn get_or_create_config(config_path: Option<&Path>) -> error::Result<ConfigV
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// If we somehow don't have any config path, then just assume the default config but don't write to any file.
|
// If we somehow don't have any config path, then just assume the default config
|
||||||
|
// but don't write to any file.
|
||||||
//
|
//
|
||||||
// TODO: Maybe make this "show" an error, but don't crash.
|
// TODO: Maybe make this "show" an error, but don't crash.
|
||||||
Ok(ConfigV1::default())
|
Ok(ConfigV1::default())
|
||||||
|
@ -124,7 +127,8 @@ pub fn init_app(
|
||||||
) -> Result<(App, BottomLayout)> {
|
) -> Result<(App, BottomLayout)> {
|
||||||
use BottomWidgetType::*;
|
use BottomWidgetType::*;
|
||||||
|
|
||||||
// Since everything takes a reference, but we want to take ownership here to drop matches/config later...
|
// Since everything takes a reference, but we want to take ownership here to
|
||||||
|
// drop matches/config later...
|
||||||
let args = &args;
|
let args = &args;
|
||||||
let config = &config;
|
let config = &config;
|
||||||
|
|
||||||
|
@ -175,18 +179,13 @@ pub fn init_app(
|
||||||
is_flag_enabled!(network_use_binary_prefix, args.network, config);
|
is_flag_enabled!(network_use_binary_prefix, args.network, config);
|
||||||
|
|
||||||
let proc_columns: Option<IndexSet<ProcWidgetColumn>> = {
|
let proc_columns: Option<IndexSet<ProcWidgetColumn>> = {
|
||||||
let columns = config.processes.as_ref().map(|cfg| cfg.columns.clone());
|
config.processes.as_ref().and_then(|cfg| {
|
||||||
|
if cfg.columns.is_empty() {
|
||||||
match columns {
|
None
|
||||||
Some(columns) => {
|
} else {
|
||||||
if columns.is_empty() {
|
Some(IndexSet::from_iter(cfg.columns.clone()))
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(IndexSet::from_iter(columns))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None => None,
|
})
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let network_legend_position = get_network_legend_position(args, config)?;
|
let network_legend_position = get_network_legend_position(args, config)?;
|
||||||
|
@ -384,14 +383,29 @@ pub fn init_app(
|
||||||
use_battery: used_widget_set.get(&Battery).is_some(),
|
use_battery: used_widget_set.get(&Battery).is_some(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let disk_filter =
|
let (disk_name_filter, disk_mount_filter) = {
|
||||||
get_ignore_list(&config.disk_filter).context("Update 'disk_filter' in your config file")?;
|
match &config.disk {
|
||||||
let mount_filter = get_ignore_list(&config.mount_filter)
|
Some(cfg) => {
|
||||||
.context("Update 'mount_filter' in your config file")?;
|
let df = get_ignore_list(&cfg.name_filter)
|
||||||
let temp_filter =
|
.context("Update 'disk.name_filter' in your config file")?;
|
||||||
get_ignore_list(&config.temp_filter).context("Update 'temp_filter' in your config file")?;
|
let mf = get_ignore_list(&cfg.mount_filter)
|
||||||
let net_filter =
|
.context("Update 'disk.mount_filter' in your config file")?;
|
||||||
get_ignore_list(&config.net_filter).context("Update 'net_filter' in your config file")?;
|
|
||||||
|
(df, mf)
|
||||||
|
}
|
||||||
|
None => (None, None),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let temp_sensor_filter = match &config.temperature {
|
||||||
|
Some(cfg) => get_ignore_list(&cfg.sensor_filter)
|
||||||
|
.context("Update 'temperature.sensor_filter' in your config file")?,
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
let net_interface_filter = match &config.network {
|
||||||
|
Some(cfg) => get_ignore_list(&cfg.interface_filter)
|
||||||
|
.context("Update 'network.interface_filter' in your config file")?,
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
let states = AppWidgetStates {
|
let states = AppWidgetStates {
|
||||||
cpu_state: CpuState::init(cpu_state_map),
|
cpu_state: CpuState::init(cpu_state_map),
|
||||||
|
@ -406,10 +420,10 @@ pub fn init_app(
|
||||||
|
|
||||||
let current_widget = widget_map.get(&initial_widget_id).unwrap().clone();
|
let current_widget = widget_map.get(&initial_widget_id).unwrap().clone();
|
||||||
let filters = DataFilters {
|
let filters = DataFilters {
|
||||||
disk_filter,
|
disk_filter: disk_name_filter,
|
||||||
mount_filter,
|
mount_filter: disk_mount_filter,
|
||||||
temp_filter,
|
temp_filter: temp_sensor_filter,
|
||||||
net_filter,
|
net_filter: net_interface_filter,
|
||||||
};
|
};
|
||||||
let is_expanded = expanded && !use_basic_mode;
|
let is_expanded = expanded && !use_basic_mode;
|
||||||
|
|
||||||
|
@ -686,7 +700,8 @@ fn get_default_widget_and_count(
|
||||||
fn get_use_battery(args: &BottomArgs, config: &ConfigV1) -> bool {
|
fn get_use_battery(args: &BottomArgs, config: &ConfigV1) -> bool {
|
||||||
#[cfg(feature = "battery")]
|
#[cfg(feature = "battery")]
|
||||||
{
|
{
|
||||||
// TODO: Move this so it's dynamic in the app itself and automatically hide if there are no batteries?
|
// TODO: Move this so it's dynamic in the app itself and automatically hide if
|
||||||
|
// there are no batteries?
|
||||||
if let Ok(battery_manager) = Manager::new() {
|
if let Ok(battery_manager) = Manager::new() {
|
||||||
if let Ok(batteries) = battery_manager.batteries() {
|
if let Ok(batteries) = battery_manager.batteries() {
|
||||||
if batteries.count() == 0 {
|
if batteries.count() == 0 {
|
||||||
|
@ -907,7 +922,7 @@ mod test {
|
||||||
args::BottomArgs,
|
args::BottomArgs,
|
||||||
canvas::styling::CanvasStyling,
|
canvas::styling::CanvasStyling,
|
||||||
options::{
|
options::{
|
||||||
config::ConfigFlags, get_default_time_value, get_retention, get_update_rate,
|
config::FlagConfig, get_default_time_value, get_retention, get_update_rate,
|
||||||
try_parse_ms,
|
try_parse_ms,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -986,7 +1001,7 @@ mod test {
|
||||||
let args = BottomArgs::parse_from(["btm"]);
|
let args = BottomArgs::parse_from(["btm"]);
|
||||||
|
|
||||||
let mut config = ConfigV1::default();
|
let mut config = ConfigV1::default();
|
||||||
let flags = ConfigFlags {
|
let flags = FlagConfig {
|
||||||
time_delta: Some("2 min".to_string().into()),
|
time_delta: Some("2 min".to_string().into()),
|
||||||
default_time_value: Some("300s".to_string().into()),
|
default_time_value: Some("300s".to_string().into()),
|
||||||
rate: Some("1s".to_string().into()),
|
rate: Some("1s".to_string().into()),
|
||||||
|
@ -1016,7 +1031,7 @@ mod test {
|
||||||
let args = BottomArgs::parse_from(["btm"]);
|
let args = BottomArgs::parse_from(["btm"]);
|
||||||
|
|
||||||
let mut config = ConfigV1::default();
|
let mut config = ConfigV1::default();
|
||||||
let flags = ConfigFlags {
|
let flags = FlagConfig {
|
||||||
time_delta: Some("120000".to_string().into()),
|
time_delta: Some("120000".to_string().into()),
|
||||||
default_time_value: Some("300000".to_string().into()),
|
default_time_value: Some("300000".to_string().into()),
|
||||||
rate: Some("1000".to_string().into()),
|
rate: Some("1000".to_string().into()),
|
||||||
|
@ -1046,7 +1061,7 @@ mod test {
|
||||||
let args = BottomArgs::parse_from(["btm"]);
|
let args = BottomArgs::parse_from(["btm"]);
|
||||||
|
|
||||||
let mut config = ConfigV1::default();
|
let mut config = ConfigV1::default();
|
||||||
let flags = ConfigFlags {
|
let flags = FlagConfig {
|
||||||
time_delta: Some(120000.into()),
|
time_delta: Some(120000.into()),
|
||||||
default_time_value: Some(300000.into()),
|
default_time_value: Some(300000.into()),
|
||||||
rate: Some(1000.into()),
|
rate: Some(1000.into()),
|
||||||
|
@ -1079,15 +1094,17 @@ mod test {
|
||||||
super::init_app(args, config, &styling).unwrap().0
|
super::init_app(args, config, &styling).unwrap().0
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: There's probably a better way to create clap options AND unify together to avoid the possibility of
|
// TODO: There's probably a better way to create clap options AND unify together
|
||||||
// typos/mixing up. Use proc macros to unify on one struct?
|
// to avoid the possibility of typos/mixing up. Use proc macros to unify on
|
||||||
|
// one struct?
|
||||||
#[test]
|
#[test]
|
||||||
fn verify_cli_options_build() {
|
fn verify_cli_options_build() {
|
||||||
let app = crate::args::build_cmd();
|
let app = crate::args::build_cmd();
|
||||||
|
|
||||||
let default_app = create_app(BottomArgs::parse_from(["btm"]));
|
let default_app = create_app(BottomArgs::parse_from(["btm"]));
|
||||||
|
|
||||||
// Skip battery since it's tricky to test depending on the platform/features we're testing with.
|
// Skip battery since it's tricky to test depending on the platform/features
|
||||||
|
// we're testing with.
|
||||||
let skip = ["help", "version", "celsius", "battery"];
|
let skip = ["help", "version", "celsius", "battery"];
|
||||||
|
|
||||||
for arg in app.get_arguments().collect::<Vec<_>>() {
|
for arg in app.get_arguments().collect::<Vec<_>>() {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
//! Argument parsing via clap.
|
//! Argument parsing via clap.
|
||||||
//!
|
//!
|
||||||
//! Note that you probably want to keep this as a single file so the build script doesn't
|
//! Note that you probably want to keep this as a single file so the build
|
||||||
//! trip all over itself.
|
//! script doesn't trip all over itself.
|
||||||
|
|
||||||
// TODO: New sections are misaligned! See if we can get that fixed.
|
// TODO: New sections are misaligned! See if we can get that fixed.
|
||||||
|
|
||||||
|
@ -557,7 +557,8 @@ pub struct StyleArgs {
|
||||||
pub color: Option<String>,
|
pub color: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Other arguments. This just handle options that are for help/version displaying.
|
/// Other arguments. This just handle options that are for help/version
|
||||||
|
/// displaying.
|
||||||
#[derive(Args, Clone, Debug)]
|
#[derive(Args, Clone, Debug)]
|
||||||
#[command(next_help_heading = "Other Options", rename_all = "snake_case")]
|
#[command(next_help_heading = "Other Options", rename_all = "snake_case")]
|
||||||
pub struct OtherArgs {
|
pub struct OtherArgs {
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::borrow::Cow;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||||
pub struct ConfigColours {
|
pub struct ColoursConfig {
|
||||||
pub table_header_color: Option<Cow<'static, str>>,
|
pub table_header_color: Option<Cow<'static, str>>,
|
||||||
pub all_cpu_color: Option<Cow<'static, str>>,
|
pub all_cpu_color: Option<Cow<'static, str>>,
|
||||||
pub avg_cpu_color: Option<Cow<'static, str>>,
|
pub avg_cpu_color: Option<Cow<'static, str>>,
|
||||||
|
@ -31,8 +31,9 @@ pub struct ConfigColours {
|
||||||
pub low_battery_color: Option<Cow<'static, str>>,
|
pub low_battery_color: Option<Cow<'static, str>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigColours {
|
impl ColoursConfig {
|
||||||
/// Returns `true` if there is a [`ConfigColours`] that is empty or there isn't one at all.
|
/// Returns `true` if there is a [`ConfigColours`] that is empty or there
|
||||||
|
/// isn't one at all.
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
if let Ok(serialized_string) = toml_edit::ser::to_string(self) {
|
if let Ok(serialized_string) = toml_edit::ser::to_string(self) {
|
||||||
return serialized_string.is_empty();
|
return serialized_string.is_empty();
|
||||||
|
|
|
@ -1,24 +1,29 @@
|
||||||
pub mod cpu;
|
pub mod cpu;
|
||||||
|
pub mod disk;
|
||||||
mod ignore_list;
|
mod ignore_list;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
pub mod process_columns;
|
pub mod network;
|
||||||
|
pub mod process;
|
||||||
|
pub mod temperature;
|
||||||
|
|
||||||
|
use disk::DiskConfig;
|
||||||
|
use network::NetworkConfig;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use temperature::TempConfig;
|
||||||
|
|
||||||
pub use self::ignore_list::IgnoreList;
|
pub use self::ignore_list::IgnoreList;
|
||||||
use self::{cpu::CpuConfig, layout::Row, process_columns::ProcessConfig};
|
use self::{cpu::CpuConfig, layout::Row, process::ProcessesConfig};
|
||||||
use super::ConfigColours;
|
use super::ColoursConfig;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize)]
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
pub struct ConfigV1 {
|
pub struct ConfigV1 {
|
||||||
pub(crate) flags: Option<ConfigFlags>,
|
pub(crate) flags: Option<FlagConfig>,
|
||||||
pub(crate) colors: Option<ConfigColours>,
|
pub(crate) colors: Option<ColoursConfig>,
|
||||||
pub(crate) row: Option<Vec<Row>>,
|
pub(crate) row: Option<Vec<Row>>,
|
||||||
pub(crate) disk_filter: Option<IgnoreList>,
|
pub(crate) processes: Option<ProcessesConfig>,
|
||||||
pub(crate) mount_filter: Option<IgnoreList>,
|
pub(crate) disk: Option<DiskConfig>,
|
||||||
pub(crate) temp_filter: Option<IgnoreList>,
|
pub(crate) temperature: Option<TempConfig>,
|
||||||
pub(crate) net_filter: Option<IgnoreList>,
|
pub(crate) network: Option<NetworkConfig>,
|
||||||
pub(crate) processes: Option<ProcessConfig>,
|
|
||||||
pub(crate) cpu: Option<CpuConfig>,
|
pub(crate) cpu: Option<CpuConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +47,7 @@ impl From<u64> for StringOrNum {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||||
pub(crate) struct ConfigFlags {
|
pub(crate) struct FlagConfig {
|
||||||
pub(crate) hide_avg_cpu: Option<bool>,
|
pub(crate) hide_avg_cpu: Option<bool>,
|
||||||
pub(crate) dot_marker: Option<bool>,
|
pub(crate) dot_marker: Option<bool>,
|
||||||
pub(crate) temperature_type: Option<String>,
|
pub(crate) temperature_type: Option<String>,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
/// The default selection of the CPU widget. If the given selection is invalid, we will fall back to all.
|
/// The default selection of the CPU widget. If the given selection is invalid,
|
||||||
|
/// we will fall back to all.
|
||||||
#[derive(Clone, Copy, Debug, Default, Deserialize)]
|
#[derive(Clone, Copy, Debug, Default, Deserialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum CpuDefault {
|
pub enum CpuDefault {
|
||||||
|
|
13
src/options/config/disk.rs
Normal file
13
src/options/config/disk.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use super::IgnoreList;
|
||||||
|
|
||||||
|
/// Disk configuration.
|
||||||
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
|
pub struct DiskConfig {
|
||||||
|
/// A filter over the disk names.
|
||||||
|
pub name_filter: Option<IgnoreList>,
|
||||||
|
|
||||||
|
/// A filter over the mount names.
|
||||||
|
pub mount_filter: Option<IgnoreList>,
|
||||||
|
}
|
|
@ -9,7 +9,8 @@ fn default_as_true() -> bool {
|
||||||
pub struct IgnoreList {
|
pub struct IgnoreList {
|
||||||
#[serde(default = "default_as_true")]
|
#[serde(default = "default_as_true")]
|
||||||
// TODO: Deprecate and/or rename, current name sounds awful.
|
// TODO: Deprecate and/or rename, current name sounds awful.
|
||||||
// Maybe to something like "deny_entries"? Currently it defaults to a denylist anyways, so maybe "allow_entries"?
|
// Maybe to something like "deny_entries"? Currently it defaults to a denylist anyways, so
|
||||||
|
// maybe "allow_entries"?
|
||||||
pub is_list_ignored: bool,
|
pub is_list_ignored: bool,
|
||||||
pub list: Vec<String>,
|
pub list: Vec<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
10
src/options/config/network.rs
Normal file
10
src/options/config/network.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use super::IgnoreList;
|
||||||
|
|
||||||
|
/// Network configuration.
|
||||||
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
|
pub struct NetworkConfig {
|
||||||
|
/// A filter over the network interface names.
|
||||||
|
pub interface_filter: Option<IgnoreList>,
|
||||||
|
}
|
|
@ -2,22 +2,23 @@ use serde::Deserialize;
|
||||||
|
|
||||||
use crate::widgets::ProcWidgetColumn;
|
use crate::widgets::ProcWidgetColumn;
|
||||||
|
|
||||||
/// Process column settings.
|
/// Process configuration.
|
||||||
#[derive(Clone, Debug, Default, Deserialize)]
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
pub struct ProcessConfig {
|
pub struct ProcessesConfig {
|
||||||
|
/// A list of process widget columns.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub columns: Vec<ProcWidgetColumn>,
|
pub columns: Vec<ProcWidgetColumn>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::ProcessConfig;
|
use super::ProcessesConfig;
|
||||||
use crate::widgets::ProcWidgetColumn;
|
use crate::widgets::ProcWidgetColumn;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_column_setting() {
|
fn empty_column_setting() {
|
||||||
let config = "";
|
let config = "";
|
||||||
let generated: ProcessConfig = toml_edit::de::from_str(config).unwrap();
|
let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap();
|
||||||
assert!(generated.columns.is_empty());
|
assert!(generated.columns.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +28,7 @@ mod test {
|
||||||
columns = ["CPU%", "PiD", "user", "MEM", "Tread", "T.Write", "Rps", "W/s", "tiMe", "USER", "state"]
|
columns = ["CPU%", "PiD", "user", "MEM", "Tread", "T.Write", "Rps", "W/s", "tiMe", "USER", "state"]
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let generated: ProcessConfig = toml_edit::de::from_str(config).unwrap();
|
let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
generated.columns,
|
generated.columns,
|
||||||
vec![
|
vec![
|
||||||
|
@ -49,25 +50,25 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn process_column_settings_2() {
|
fn process_column_settings_2() {
|
||||||
let config = r#"columns = ["MEM", "TWrite", "Cpuz", "read", "wps"]"#;
|
let config = r#"columns = ["MEM", "TWrite", "Cpuz", "read", "wps"]"#;
|
||||||
toml_edit::de::from_str::<ProcessConfig>(config).expect_err("Should error out!");
|
toml_edit::de::from_str::<ProcessesConfig>(config).expect_err("Should error out!");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn process_column_settings_3() {
|
fn process_column_settings_3() {
|
||||||
let config = r#"columns = ["Twrite", "T.Write"]"#;
|
let config = r#"columns = ["Twrite", "T.Write"]"#;
|
||||||
let generated: ProcessConfig = toml_edit::de::from_str(config).unwrap();
|
let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap();
|
||||||
assert_eq!(generated.columns, vec![ProcWidgetColumn::TotalWrite; 2]);
|
assert_eq!(generated.columns, vec![ProcWidgetColumn::TotalWrite; 2]);
|
||||||
|
|
||||||
let config = r#"columns = ["Tread", "T.read"]"#;
|
let config = r#"columns = ["Tread", "T.read"]"#;
|
||||||
let generated: ProcessConfig = toml_edit::de::from_str(config).unwrap();
|
let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap();
|
||||||
assert_eq!(generated.columns, vec![ProcWidgetColumn::TotalRead; 2]);
|
assert_eq!(generated.columns, vec![ProcWidgetColumn::TotalRead; 2]);
|
||||||
|
|
||||||
let config = r#"columns = ["read", "rps", "r/s"]"#;
|
let config = r#"columns = ["read", "rps", "r/s"]"#;
|
||||||
let generated: ProcessConfig = toml_edit::de::from_str(config).unwrap();
|
let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap();
|
||||||
assert_eq!(generated.columns, vec![ProcWidgetColumn::ReadPerSecond; 3]);
|
assert_eq!(generated.columns, vec![ProcWidgetColumn::ReadPerSecond; 3]);
|
||||||
|
|
||||||
let config = r#"columns = ["write", "wps", "w/s"]"#;
|
let config = r#"columns = ["write", "wps", "w/s"]"#;
|
||||||
let generated: ProcessConfig = toml_edit::de::from_str(config).unwrap();
|
let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap();
|
||||||
assert_eq!(generated.columns, vec![ProcWidgetColumn::WritePerSecond; 3]);
|
assert_eq!(generated.columns, vec![ProcWidgetColumn::WritePerSecond; 3]);
|
||||||
}
|
}
|
||||||
}
|
}
|
10
src/options/config/temperature.rs
Normal file
10
src/options/config/temperature.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use super::IgnoreList;
|
||||||
|
|
||||||
|
/// Temperature configuration.
|
||||||
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
|
pub struct TempConfig {
|
||||||
|
/// A filter over the sensor names.
|
||||||
|
pub sensor_filter: Option<IgnoreList>,
|
||||||
|
}
|
|
@ -38,9 +38,9 @@ pub const LOG_MEBI_LIMIT_U32: u32 = 20;
|
||||||
pub const LOG_GIBI_LIMIT_U32: u32 = 30;
|
pub const LOG_GIBI_LIMIT_U32: u32 = 30;
|
||||||
pub const LOG_TEBI_LIMIT_U32: u32 = 40;
|
pub const LOG_TEBI_LIMIT_U32: u32 = 40;
|
||||||
|
|
||||||
/// Returns a tuple containing the value and the unit in bytes. In units of 1024.
|
/// Returns a tuple containing the value and the unit in bytes. In units of
|
||||||
/// This only supports up to a tebi. Note the "single" unit will have a space appended to match the others if
|
/// 1024. This only supports up to a tebi. Note the "single" unit will have a
|
||||||
/// `spacing` is true.
|
/// space appended to match the others if `spacing` is true.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn get_binary_bytes(bytes: u64) -> (f64, &'static str) {
|
pub fn get_binary_bytes(bytes: u64) -> (f64, &'static str) {
|
||||||
match bytes {
|
match bytes {
|
||||||
|
@ -52,9 +52,9 @@ pub fn get_binary_bytes(bytes: u64) -> (f64, &'static str) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a tuple containing the value and the unit in bytes. In units of 1000.
|
/// Returns a tuple containing the value and the unit in bytes. In units of
|
||||||
/// This only supports up to a tera. Note the "single" unit will have a space appended to match the others if
|
/// 1000. This only supports up to a tera. Note the "single" unit will have a
|
||||||
/// `spacing` is true.
|
/// space appended to match the others if `spacing` is true.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn get_decimal_bytes(bytes: u64) -> (f64, &'static str) {
|
pub fn get_decimal_bytes(bytes: u64) -> (f64, &'static str) {
|
||||||
match bytes {
|
match bytes {
|
||||||
|
@ -67,8 +67,8 @@ pub fn get_decimal_bytes(bytes: u64) -> (f64, &'static str) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a tuple containing the value and the unit. In units of 1024.
|
/// Returns a tuple containing the value and the unit. In units of 1024.
|
||||||
/// This only supports up to a tebi. Note the "single" unit will have a space appended to match the others if
|
/// This only supports up to a tebi. Note the "single" unit will have a space
|
||||||
/// `spacing` is true.
|
/// appended to match the others if `spacing` is true.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn get_binary_prefix(quantity: u64, unit: &str) -> (f64, String) {
|
pub fn get_binary_prefix(quantity: u64, unit: &str) -> (f64, String) {
|
||||||
match quantity {
|
match quantity {
|
||||||
|
@ -81,8 +81,8 @@ pub fn get_binary_prefix(quantity: u64, unit: &str) -> (f64, String) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a tuple containing the value and the unit. In units of 1000.
|
/// Returns a tuple containing the value and the unit. In units of 1000.
|
||||||
/// This only supports up to a tera. Note the "single" unit will have a space appended to match the others if
|
/// This only supports up to a tera. Note the "single" unit will have a space
|
||||||
/// `spacing` is true.
|
/// appended to match the others if `spacing` is true.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn get_decimal_prefix(quantity: u64, unit: &str) -> (f64, String) {
|
pub fn get_decimal_prefix(quantity: u64, unit: &str) -> (f64, String) {
|
||||||
match quantity {
|
match quantity {
|
||||||
|
|
|
@ -26,12 +26,12 @@ pub fn partial_ordering_desc<T: PartialOrd>(a: T, b: T) -> Ordering {
|
||||||
|
|
||||||
/// A trait for additional clamping functions on numeric types.
|
/// A trait for additional clamping functions on numeric types.
|
||||||
pub trait ClampExt {
|
pub trait ClampExt {
|
||||||
/// Restrict a value by a lower bound. If the current value is _lower_ than `lower_bound`,
|
/// Restrict a value by a lower bound. If the current value is _lower_ than
|
||||||
/// it will be set to `_lower_bound`.
|
/// `lower_bound`, it will be set to `_lower_bound`.
|
||||||
fn clamp_lower(&self, lower_bound: Self) -> Self;
|
fn clamp_lower(&self, lower_bound: Self) -> Self;
|
||||||
|
|
||||||
/// Restrict a value by an upper bound. If the current value is _greater_ than `upper_bound`,
|
/// Restrict a value by an upper bound. If the current value is _greater_
|
||||||
/// it will be set to `upper_bound`.
|
/// than `upper_bound`, it will be set to `upper_bound`.
|
||||||
fn clamp_upper(&self, upper_bound: Self) -> Self;
|
fn clamp_upper(&self, upper_bound: Self) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,13 +13,16 @@ pub fn init_logger(
|
||||||
let offset = OFFSET.get_or_init(|| {
|
let offset = OFFSET.get_or_init(|| {
|
||||||
use time::util::local_offset::Soundness;
|
use time::util::local_offset::Soundness;
|
||||||
|
|
||||||
// SAFETY: We only invoke this once, quickly, and it should be invoked in a single-thread context.
|
// SAFETY: We only invoke this once, quickly, and it should be invoked in a
|
||||||
// We also should only ever hit this logging at all in a debug context which is generally fine,
|
// single-thread context. We also should only ever hit this
|
||||||
|
// logging at all in a debug context which is generally fine,
|
||||||
// release builds should have this logging disabled entirely for now.
|
// release builds should have this logging disabled entirely for now.
|
||||||
unsafe {
|
unsafe {
|
||||||
// XXX: If we ever DO add general logging as a release feature, evaluate this again and whether this is
|
// XXX: If we ever DO add general logging as a release feature, evaluate this
|
||||||
// something we want enabled in release builds! What might be safe is falling back to the non-set-soundness
|
// again and whether this is something we want enabled in
|
||||||
// mode when specifically using certain feature flags (e.g. dev-logging feature enables this behaviour).
|
// release builds! What might be safe is falling back to the non-set-soundness
|
||||||
|
// mode when specifically using certain feature flags (e.g. dev-logging feature
|
||||||
|
// enables this behaviour).
|
||||||
|
|
||||||
time::util::local_offset::set_soundness(Soundness::Unsound);
|
time::util::local_offset::set_soundness(Soundness::Unsound);
|
||||||
let res =
|
let res =
|
||||||
|
@ -39,8 +42,8 @@ pub fn init_logger(
|
||||||
"{}[{}][{}] {}",
|
"{}[{}][{}] {}",
|
||||||
offset_time
|
offset_time
|
||||||
.format(&time::macros::format_description!(
|
.format(&time::macros::format_description!(
|
||||||
// The weird "[[[" is because we need to escape a bracket ("[[") to show one "[".
|
// The weird "[[[" is because we need to escape a bracket ("[[") to show
|
||||||
// See https://time-rs.github.io/book/api/format-description.html
|
// one "[". See https://time-rs.github.io/book/api/format-description.html
|
||||||
"[[[year]-[month]-[day]][[[hour]:[minute]:[second][subsecond digits:9]]"
|
"[[[year]-[month]-[day]][[[hour]:[minute]:[second][subsecond digits:9]]"
|
||||||
))
|
))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
|
|
@ -9,11 +9,12 @@ pub fn truncate_to_text<'a, U: Into<usize>>(content: &str, width: U) -> Text<'a>
|
||||||
Text::raw(truncate_str(content, width.into()))
|
Text::raw(truncate_str(content, width.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks that the first string is equal to any of the other ones in a ASCII case-insensitive match.
|
/// Checks that the first string is equal to any of the other ones in a ASCII
|
||||||
|
/// case-insensitive match.
|
||||||
///
|
///
|
||||||
/// The generated code is the same as writing:
|
/// The generated code is the same as writing:
|
||||||
/// `to_ascii_lowercase(a) == to_ascii_lowercase(b) || to_ascii_lowercase(a) == to_ascii_lowercase(c)`,
|
/// `to_ascii_lowercase(a) == to_ascii_lowercase(b) || to_ascii_lowercase(a) ==
|
||||||
/// but without allocating and copying temporaries.
|
/// to_ascii_lowercase(c)`, but without allocating and copying temporaries.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
|
|
|
@ -15,7 +15,8 @@ pub use process_table::*;
|
||||||
pub use temperature_table::*;
|
pub use temperature_table::*;
|
||||||
use tui::{layout::Rect, Frame};
|
use tui::{layout::Rect, Frame};
|
||||||
|
|
||||||
/// A [`Widget`] converts raw data into something that a user can see and interact with.
|
/// A [`Widget`] converts raw data into something that a user can see and
|
||||||
|
/// interact with.
|
||||||
pub trait Widget<Data> {
|
pub trait Widget<Data> {
|
||||||
/// How to actually draw the widget to the terminal.
|
/// How to actually draw the widget to the terminal.
|
||||||
fn draw(&self, f: &mut Frame<'_>, draw_location: Rect, widget_id: u64);
|
fn draw(&self, f: &mut Frame<'_>, draw_location: Rect, widget_id: u64);
|
||||||
|
|
|
@ -87,13 +87,13 @@ impl DataToCell<CpuWidgetColumn> for CpuWidgetTableData {
|
||||||
|
|
||||||
let calculated_width = calculated_width.get();
|
let calculated_width = calculated_width.get();
|
||||||
|
|
||||||
// This is a bit of a hack, but apparently we can avoid having to do any fancy checks
|
// This is a bit of a hack, but apparently we can avoid having to do any fancy
|
||||||
// of showing the "All" on a specific column if the other is hidden by just always
|
// checks of showing the "All" on a specific column if the other is
|
||||||
// showing it on the CPU (first) column - if there isn't room for it, it will just collapse
|
// hidden by just always showing it on the CPU (first) column - if there
|
||||||
// down.
|
// isn't room for it, it will just collapse down.
|
||||||
//
|
//
|
||||||
// This is the same for the use percentages - we just *always* show them, and *always* hide the CPU column if
|
// This is the same for the use percentages - we just *always* show them, and
|
||||||
// it is too small.
|
// *always* hide the CPU column if it is too small.
|
||||||
match &self {
|
match &self {
|
||||||
CpuWidgetTableData::All => match column {
|
CpuWidgetTableData::All => match column {
|
||||||
CpuWidgetColumn::CPU => Some("All".into()),
|
CpuWidgetColumn::CPU => Some("All".into()),
|
||||||
|
|
|
@ -28,7 +28,8 @@ use crate::{
|
||||||
data_collection::processes::{Pid, ProcessHarvest},
|
data_collection::processes::{Pid, ProcessHarvest},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// ProcessSearchState only deals with process' search's current settings and state.
|
/// ProcessSearchState only deals with process' search's current settings and
|
||||||
|
/// state.
|
||||||
pub struct ProcessSearchState {
|
pub struct ProcessSearchState {
|
||||||
pub search_state: AppSearchState,
|
pub search_state: AppSearchState,
|
||||||
pub is_ignoring_case: bool,
|
pub is_ignoring_case: bool,
|
||||||
|
@ -172,7 +173,8 @@ pub struct ProcWidgetState {
|
||||||
/// The state of the togglable table that controls sorting.
|
/// The state of the togglable table that controls sorting.
|
||||||
pub sort_table: SortTable,
|
pub sort_table: SortTable,
|
||||||
|
|
||||||
/// The internal column mapping as an [`IndexSet`], to allow us to do quick mappings of column type -> index.
|
/// The internal column mapping as an [`IndexSet`], to allow us to do quick
|
||||||
|
/// mappings of column type -> index.
|
||||||
pub column_mapping: IndexSet<ProcWidgetColumn>,
|
pub column_mapping: IndexSet<ProcWidgetColumn>,
|
||||||
|
|
||||||
/// A name-to-pid mapping.
|
/// A name-to-pid mapping.
|
||||||
|
@ -426,8 +428,9 @@ impl ProcWidgetState {
|
||||||
|
|
||||||
/// Update the current table data.
|
/// Update the current table data.
|
||||||
///
|
///
|
||||||
/// This function *only* updates the displayed process data. If there is a need to update the actual *stored* data,
|
/// This function *only* updates the displayed process data. If there is a
|
||||||
/// call it before this function.
|
/// need to update the actual *stored* data, call it before this
|
||||||
|
/// function.
|
||||||
pub fn set_table_data(&mut self, data_collection: &DataCollection) {
|
pub fn set_table_data(&mut self, data_collection: &DataCollection) {
|
||||||
let data = match &self.mode {
|
let data = match &self.mode {
|
||||||
ProcWidgetMode::Grouped | ProcWidgetMode::Normal => {
|
ProcWidgetMode::Grouped | ProcWidgetMode::Normal => {
|
||||||
|
@ -495,7 +498,8 @@ impl ProcWidgetState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A process is shown under the filtered tree if at least one of these conditions hold:
|
// A process is shown under the filtered tree if at least one of these
|
||||||
|
// conditions hold:
|
||||||
// - The process itself matches.
|
// - The process itself matches.
|
||||||
// - The process contains some descendant that matches.
|
// - The process contains some descendant that matches.
|
||||||
// - The process's parent (and only parent, not any ancestor) matches.
|
// - The process's parent (and only parent, not any ancestor) matches.
|
||||||
|
@ -524,7 +528,8 @@ impl ProcWidgetState {
|
||||||
|
|
||||||
// Show the entry if it is:
|
// Show the entry if it is:
|
||||||
// - Matches the filter.
|
// - Matches the filter.
|
||||||
// - Has at least one child (doesn't have to be direct) that matches the filter.
|
// - Has at least one child (doesn't have to be direct) that matches the
|
||||||
|
// filter.
|
||||||
// - Is the child of a shown process.
|
// - Is the child of a shown process.
|
||||||
let is_shown = is_process_matching
|
let is_shown = is_process_matching
|
||||||
|| !shown_children.is_empty()
|
|| !shown_children.is_empty()
|
||||||
|
@ -724,7 +729,8 @@ impl ProcWidgetState {
|
||||||
if let Some(grouped_process_harvest) = id_process_mapping.get_mut(id) {
|
if let Some(grouped_process_harvest) = id_process_mapping.get_mut(id) {
|
||||||
grouped_process_harvest.add(process);
|
grouped_process_harvest.add(process);
|
||||||
} else {
|
} else {
|
||||||
// FIXME: [PERF] could maybe eliminate an allocation here in the grouped mode... or maybe just avoid the entire transformation step, making an alloc fine.
|
// FIXME: [PERF] could maybe eliminate an allocation here in the grouped mode...
|
||||||
|
// or maybe just avoid the entire transformation step, making an alloc fine.
|
||||||
id_process_mapping.insert(id, process.clone());
|
id_process_mapping.insert(id, process.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -813,8 +819,8 @@ impl ProcWidgetState {
|
||||||
self.force_update_data = true;
|
self.force_update_data = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marks the selected column as hidden, and automatically resets the selected column to the default
|
/// Marks the selected column as hidden, and automatically resets the
|
||||||
/// sort index and order.
|
/// selected column to the default sort index and order.
|
||||||
fn hide_column(&mut self, column: ProcWidgetColumn) {
|
fn hide_column(&mut self, column: ProcWidgetColumn) {
|
||||||
if let Some(index) = self.column_mapping.get_index_of(&column) {
|
if let Some(index) = self.column_mapping.get_index_of(&column) {
|
||||||
if let Some(col) = self.table.columns.get_mut(index) {
|
if let Some(col) = self.table.columns.get_mut(index) {
|
||||||
|
@ -837,7 +843,8 @@ impl ProcWidgetState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Select a column. If the column is already selected, then just toggle the sort order.
|
/// Select a column. If the column is already selected, then just toggle the
|
||||||
|
/// sort order.
|
||||||
pub fn select_column(&mut self, column: ProcWidgetColumn) {
|
pub fn select_column(&mut self, column: ProcWidgetColumn) {
|
||||||
if let Some(index) = self.column_mapping.get_index_of(&column) {
|
if let Some(index) = self.column_mapping.get_index_of(&column) {
|
||||||
self.table.set_sort_index(index);
|
self.table.set_sort_index(index);
|
||||||
|
@ -891,12 +898,15 @@ impl ProcWidgetState {
|
||||||
|
|
||||||
/// Toggles the appropriate columns/settings when tab is pressed.
|
/// Toggles the appropriate columns/settings when tab is pressed.
|
||||||
///
|
///
|
||||||
/// If count is enabled, we should set the mode to [`ProcWidgetMode::Grouped`], and switch off the User and State
|
/// If count is enabled, we should set the mode to
|
||||||
/// columns. We should also move the user off of the columns if they were selected, as those columns are now hidden
|
/// [`ProcWidgetMode::Grouped`], and switch off the User and State
|
||||||
/// (handled by internal method calls), and go back to the "defaults".
|
/// columns. We should also move the user off of the columns if they were
|
||||||
|
/// selected, as those columns are now hidden (handled by internal
|
||||||
|
/// method calls), and go back to the "defaults".
|
||||||
///
|
///
|
||||||
/// Otherwise, if count is disabled, then if the columns exist, the User and State columns should be re-enabled,
|
/// Otherwise, if count is disabled, then if the columns exist, the User and
|
||||||
/// and the mode switched to [`ProcWidgetMode::Normal`].
|
/// State columns should be re-enabled, and the mode switched to
|
||||||
|
/// [`ProcWidgetMode::Normal`].
|
||||||
pub fn toggle_tab(&mut self) {
|
pub fn toggle_tab(&mut self) {
|
||||||
if !matches!(self.mode, ProcWidgetMode::Tree { .. }) {
|
if !matches!(self.mode, ProcWidgetMode::Tree { .. }) {
|
||||||
if let Some(index) = self
|
if let Some(index) = self
|
||||||
|
@ -1005,14 +1015,15 @@ impl ProcWidgetState {
|
||||||
self.proc_search.search_state.walk_backward();
|
self.proc_search.search_state.walk_backward();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the number of columns *enabled*. Note this differs from *visible* - a column may be enabled but not
|
/// Returns the number of columns *enabled*. Note this differs from
|
||||||
/// visible (e.g. off screen).
|
/// *visible* - a column may be enabled but not visible (e.g. off
|
||||||
|
/// screen).
|
||||||
pub fn num_enabled_columns(&self) -> usize {
|
pub fn num_enabled_columns(&self) -> usize {
|
||||||
self.table.columns.iter().filter(|c| !c.is_hidden).count()
|
self.table.columns.iter().filter(|c| !c.is_hidden).count()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the [`ProcWidgetState`]'s current sort index to whatever was in the sort table if possible, then closes the
|
/// Sets the [`ProcWidgetState`]'s current sort index to whatever was in the
|
||||||
/// sort table.
|
/// sort table if possible, then closes the sort table.
|
||||||
pub(crate) fn use_sort_table_value(&mut self) {
|
pub(crate) fn use_sort_table_value(&mut self) {
|
||||||
self.table.set_sort_index(self.sort_table.current_index());
|
self.table.set_sort_index(self.sort_table.current_index());
|
||||||
|
|
||||||
|
@ -1425,8 +1436,9 @@ mod test {
|
||||||
|
|
||||||
/// Tests toggling if both mem and mem% columns are configured.
|
/// Tests toggling if both mem and mem% columns are configured.
|
||||||
///
|
///
|
||||||
/// Currently, this test doesn't really do much, since we treat these two columns as the same - this test is
|
/// Currently, this test doesn't really do much, since we treat these two
|
||||||
/// intended for use later when we might allow both at the same time.
|
/// columns as the same - this test is intended for use later when we
|
||||||
|
/// might allow both at the same time.
|
||||||
#[test]
|
#[test]
|
||||||
fn double_memory_sim_toggle() {
|
fn double_memory_sim_toggle() {
|
||||||
let init_columns = [
|
let init_columns = [
|
||||||
|
@ -1461,8 +1473,9 @@ mod test {
|
||||||
|
|
||||||
/// Tests toggling if both pid and count columns are configured.
|
/// Tests toggling if both pid and count columns are configured.
|
||||||
///
|
///
|
||||||
/// Currently, this test doesn't really do much, since we treat these two columns as the same - this test is
|
/// Currently, this test doesn't really do much, since we treat these two
|
||||||
/// intended for use later when we might allow both at the same time.
|
/// columns as the same - this test is intended for use later when we
|
||||||
|
/// might allow both at the same time.
|
||||||
#[test]
|
#[test]
|
||||||
fn pid_and_count_sim_toggle() {
|
fn pid_and_count_sim_toggle() {
|
||||||
let init_columns = [
|
let init_columns = [
|
||||||
|
@ -1498,8 +1511,9 @@ mod test {
|
||||||
|
|
||||||
/// Tests toggling if both command and name columns are configured.
|
/// Tests toggling if both command and name columns are configured.
|
||||||
///
|
///
|
||||||
/// Currently, this test doesn't really do much, since we treat these two columns as the same - this test is
|
/// Currently, this test doesn't really do much, since we treat these two
|
||||||
/// intended for use later when we might allow both at the same time.
|
/// columns as the same - this test is intended for use later when we
|
||||||
|
/// might allow both at the same time.
|
||||||
#[test]
|
#[test]
|
||||||
fn command_name_sim_toggle() {
|
fn command_name_sim_toggle() {
|
||||||
let init_columns = [
|
let init_columns = [
|
||||||
|
|
|
@ -41,9 +41,9 @@ impl From<&'static str> for Id {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Id {
|
impl Id {
|
||||||
/// Returns the ID as a lowercase [`String`], with no prefix. This is primarily useful for
|
/// Returns the ID as a lowercase [`String`], with no prefix. This is
|
||||||
/// cases like sorting where we treat everything as the same case (e.g. `Discord` comes before
|
/// primarily useful for cases like sorting where we treat everything as
|
||||||
/// `dkms`).
|
/// the same case (e.g. `Discord` comes before `dkms`).
|
||||||
pub fn to_lowercase(&self) -> String {
|
pub fn to_lowercase(&self) -> String {
|
||||||
match &self.id_type {
|
match &self.id_type {
|
||||||
IdType::Name(name) => name.to_lowercase(),
|
IdType::Name(name) => name.to_lowercase(),
|
||||||
|
@ -306,7 +306,8 @@ impl DataToCell<ProcColumn> for ProcWidgetData {
|
||||||
let calculated_width = calculated_width.get();
|
let calculated_width = calculated_width.get();
|
||||||
|
|
||||||
// TODO: Optimize the string allocations here...
|
// TODO: Optimize the string allocations here...
|
||||||
// TODO: Also maybe just pull in the to_string call but add a variable for the differences.
|
// TODO: Also maybe just pull in the to_string call but add a variable for the
|
||||||
|
// differences.
|
||||||
Some(match column {
|
Some(match column {
|
||||||
ProcColumn::CpuPercent => format!("{:.1}%", self.cpu_usage_percent).into(),
|
ProcColumn::CpuPercent => format!("{:.1}%", self.cpu_usage_percent).into(),
|
||||||
ProcColumn::MemoryVal | ProcColumn::MemoryPercent => self.mem_usage.to_string().into(),
|
ProcColumn::MemoryVal | ProcColumn::MemoryPercent => self.mem_usage.to_string().into(),
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
//! These tests are mostly here just to ensure that invalid results will be caught when passing arguments.
|
//! These tests are mostly here just to ensure that invalid results will be
|
||||||
|
//! caught when passing arguments.
|
||||||
|
|
||||||
use assert_cmd::prelude::*;
|
use assert_cmd::prelude::*;
|
||||||
use predicates::prelude::*;
|
use predicates::prelude::*;
|
||||||
|
|
|
@ -19,13 +19,14 @@ fn get_qemu_target(arch: &str) -> &str {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This is required since running binary tests via cross can cause be tricky! We need to basically "magically" grab
|
/// This is required since running binary tests via cross can cause be tricky!
|
||||||
/// the correct runner in some cases, which can be done by inspecting env variables that should only show up while
|
/// We need to basically "magically" grab the correct runner in some cases,
|
||||||
|
/// which can be done by inspecting env variables that should only show up while
|
||||||
/// using cross.
|
/// using cross.
|
||||||
///
|
///
|
||||||
/// Originally inspired by [ripgrep's test files](https://cs.github.com/BurntSushi/ripgrep/blob/9f0e88bcb14e02da1b88872435b17d74786640b5/tests/util.rs#L470),
|
/// Originally inspired by [ripgrep's test files](https://cs.github.com/BurntSushi/ripgrep/blob/9f0e88bcb14e02da1b88872435b17d74786640b5/tests/util.rs#L470),
|
||||||
/// but adapted to work more generally with the architectures supported by bottom after looking through cross'
|
/// but adapted to work more generally with the architectures supported by
|
||||||
/// [linux-runner](https://github.com/cross-rs/cross/blob/main/docker/linux-runner) file.
|
/// bottom after looking through cross' [linux-runner](https://github.com/cross-rs/cross/blob/main/docker/linux-runner) file.
|
||||||
fn cross_runner() -> Option<String> {
|
fn cross_runner() -> Option<String> {
|
||||||
const TARGET_RUNNER: &str = "CARGO_TARGET_RUNNER";
|
const TARGET_RUNNER: &str = "CARGO_TARGET_RUNNER";
|
||||||
const CROSS_RUNNER: &str = "CROSS_RUNNER";
|
const CROSS_RUNNER: &str = "CROSS_RUNNER";
|
||||||
|
|
Loading…
Reference in a new issue