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:
Clement Tsang 2024-06-16 02:15:36 -04:00 committed by GitHub
parent 46520d8b4e
commit 982b7181a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
85 changed files with 954 additions and 652 deletions

View file

@ -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`.
- [#1472](https://github.com/ClementTsang/bottom/pull/1472): The following config fields have changed names:
- `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

View file

@ -143,6 +143,7 @@ clap_complete_nushell = "4.5.1"
clap_complete_fig = "4.5.0"
clap_mangen = "0.2.20"
indoc = "2.0.5"
# schemars = "0.8.21"
[package.metadata.deb]
section = "utility"

View file

@ -80,18 +80,53 @@
# How much data is stored at once in terms of time.
#retention = "10m"
# These are flags around the process widget.
# Processes widget configuration
#[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%"]
# [cpu]
# CPU widget configuration
#[cpu]
# One of "all" (default), "average"/"avg"
# 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
# will depend on terminal support.
#[colors] # Uncomment if you want to use custom colors
# Represents the colour of table headers (processes, CPU, disks, temperature).
#table_header_color="LightBlue"
@ -160,33 +195,3 @@
# [[row.child]]
# type="proc"
# 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

View file

@ -421,7 +421,8 @@ impl App {
pws.is_sort_open = !pws.is_sort_open;
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 {
pws.sort_table.set_position(pws.table.sort_index());
self.move_widget_selection(&WidgetDirection::Left);
@ -1054,13 +1055,15 @@ impl App {
.widget_states
.get_mut(&(self.current_widget.widget_id - 1))
{
// Traverse backwards from the current cursor location until you hit non-whitespace characters,
// then continue to traverse (and delete) backwards until you hit a whitespace character. Halt.
// Traverse backwards from the current cursor location until you hit
// 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.
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 mut start_index = 0;
let mut saw_non_whitespace = false;
@ -1617,7 +1620,8 @@ impl App {
if let Some(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 =
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
/// handled by the widget specifically.
/// Moves the mouse to the widget that was clicked on, then propagates the
/// click down to be handled by the widget specifically.
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
// is within.
// Pretty dead simple - iterate through the widget map and go to the widget
// where the click is within.
// TODO: [REFACTOR] might want to refactor this, it's really ugly.
// TODO: [REFACTOR] Might wanna refactor ALL state things in general, currently everything
// is grouped up as an app state. We should separate stuff like event state and gui state and etc.
// TODO: [REFACTOR] Might wanna refactor ALL state things in general, currently
// 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
// case here...
// Short circuit if we're in basic table... we might have to handle the basic
// table arrow case here...
if let Some(bt) = &mut self.states.basic_table_widget_state {
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
// and bail after.
// Second short circuit --- are we in the dd dialog state? If so, only check
// yes/no/signals and bail after.
if self.is_in_dialog() {
match self.delete_dialog_state.button_positions.iter().find(
|(tl_x, tl_y, br_x, br_y, _idx)| {
@ -2649,7 +2655,8 @@ impl App {
) {
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) {
match &self.current_widget.widget_type {
BottomWidgetType::Proc
@ -2682,8 +2689,10 @@ impl App {
self.change_process_position(change);
// If in tree mode, also check to see if this click is on
// the same entry as the already selected one - if it is,
// If in tree mode, also check to see if this click is
// on
// the same entry as the already selected one - if it
// is,
// then we minimize.
if is_tree_mode && change == 0 {
self.toggle_collapsing_process_branch();
@ -2755,8 +2764,9 @@ impl App {
_ => {}
}
} else {
// We might have clicked on a header! Check if we only exceeded the table + border offset, and
// it's implied we exceeded the gap offset.
// We might have clicked on a header! Check if we only exceeded the
// table + border offset, and it's implied
// we exceeded the gap offset.
if clicked_entry == border_offset {
match &self.current_widget.widget_type {
BottomWidgetType::Proc => {
@ -2851,8 +2861,9 @@ impl App {
/// A quick and dirty way to handle paste events.
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.
// In particular, encapsulate this entire logic and add some tests to make it less potentially error-prone.
// Partially copy-pasted from the single-char variant; should probably clean up
// 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();
if let Some(proc_widget_state) = self
.states

View file

@ -2,10 +2,10 @@
//! a better name for the file. Since I called data collection "harvesting",
//! then this is the farmer I guess.
//!
//! Essentially the main goal is to shift the initial calculation and distribution
//! of joiner points and data to one central location that will only do it
//! *once* upon receiving the data --- as opposed to doing it on canvas draw,
//! which will be a costly process.
//! Essentially the main goal is to shift the initial calculation and
//! distribution of joiner points and data to one central location that will
//! only do it *once* upon receiving the data --- as opposed to doing it on
//! canvas draw, which will be a costly process.
//!
//! 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
@ -102,8 +102,8 @@ impl ProcessData {
/// 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
/// data, keep updating this. As of 2021-09-08, we just clone the current collection
/// when it freezes to have a snapshot floating around.
/// data, keep updating this. As of 2021-09-08, we just clone the current
/// collection when it freezes to have a snapshot floating around.
///
/// Note that with this method, the *app* thread is responsible for cleaning -
/// not the data collector.
@ -355,7 +355,8 @@ impl DataCollection {
#[cfg(feature = "zfs")]
{
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 {
device.name.split('/').last()
}

View file

@ -16,8 +16,9 @@ impl Filter {
#[inline]
pub(crate) fn keep_entry(&self, value: &str) -> bool {
if self.has_match(value) {
// If a match is found, then if we wanted to ignore if we match, return false. If we want
// to keep if we match, return true. Thus, return the inverse of `is_list_ignored`.
// If a match is found, then if we wanted to ignore if we match, return false.
// If we want to keep if we match, return true. Thus, return the
// inverse of `is_list_ignored`.
!self.is_list_ignored
} else {
self.is_list_ignored

View file

@ -1,7 +1,8 @@
use super::DataCollection;
/// The [`FrozenState`] indicates whether the application state should be frozen. It is either not frozen or
/// frozen and containing a copy of the state at the time.
/// The [`FrozenState`] indicates whether the application state should be
/// frozen. It is either not frozen or frozen and containing a copy of the state
/// at the time.
pub enum FrozenState {
NotFrozen,
Frozen(Box<DataCollection>),

View file

@ -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")]
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.
#[cfg(target_family = "unix")]
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) };
if output != 0 {

View file

@ -26,13 +26,15 @@ const OR_LIST: [&str; 2] = ["or", "||"];
const AND_LIST: [&str; 2] = ["and", "&&"];
/// 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.
/// Enclosing anything, including prefixes, in quotes, means we treat it as an entire process
/// rather than a prefix.
/// Enclosing anything, including prefixes, in quotes, means we treat it as an
/// entire process rather than a prefix.
/// - 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.
/// - STATE: Use prefix `state`, 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 write: Use prefix `write`. Can compare.
///
/// For queries, whitespaces are our delimiters. We will merge together any adjacent non-prefixed
/// or quoted elements after splitting to treat as process names.
/// Furthermore, we want to support boolean joiners like AND and OR, and brackets.
/// For queries, whitespaces are our delimiters. We will merge together any
/// adjacent non-prefixed or quoted elements after splitting to treat as process
/// names. Furthermore, we want to support boolean joiners like AND and OR, and
/// brackets.
pub fn parse_query(
search_query: &str, is_searching_whole_word: bool, is_ignoring_case: bool,
is_searching_with_regex: bool,
@ -176,8 +179,9 @@ pub fn parse_query(
if let Some(queue_top) = query.pop_front() {
if inside_quotation {
if queue_top == "\"" {
// This means we hit something like "". Return an empty prefix, and to deal with
// the close quote checker, add one to the top of the stack. Ugly fix but whatever.
// This means we hit something like "". Return an empty prefix, and to deal
// with the close quote checker, add one to the top of the
// stack. Ugly fix but whatever.
query.push_front("\"".to_string());
return Ok(Prefix {
or: None,
@ -268,8 +272,9 @@ pub fn parse_query(
} else if queue_top == ")" {
return Err(QueryError("Missing opening parentheses".into()));
} else if queue_top == "\"" {
// Similar to parentheses, trap and check for missing closing quotes. Note, however, that we
// will DIRECTLY call another process_prefix call...
// Similar to parentheses, trap and check for missing closing quotes. Note,
// however, that we will DIRECTLY call another process_prefix
// call...
let prefix = process_prefix(query, true)?;
if let Some(close_paren) = query.pop_front() {
@ -308,10 +313,12 @@ pub fn parse_query(
// - (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.
//
// 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 {
or: None,
@ -385,8 +392,8 @@ pub fn parse_query(
let mut condition: Option<QueryComparison> = 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
// future.
// TODO: Jeez, what the heck did I write here... add some tests and
// clean this up in the future.
if content == "=" {
condition = Some(QueryComparison::Equal);
if let Some(queue_next) = query.pop_front() {
@ -423,8 +430,9 @@ pub fn parse_query(
if let Some(condition) = condition {
if let Some(read_value) = value {
// Note that the values *might* have a unit or need to be parsed differently
// based on the prefix type!
// Note that the values *might* have a unit or need to be parsed
// differently based on the
// prefix type!
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)]
pub struct Prefix {
pub or: Option<Box<Or>>,

View file

@ -112,7 +112,8 @@ impl Default for 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) {
*self = AppSearchState {
is_enabled: self.is_enabled,
@ -161,7 +162,8 @@ impl AppSearchState {
// Use the current index.
start_index
} 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;
for i in 0..(cursor_index + 1) {
@ -211,7 +213,8 @@ impl AppSearchState {
Ok(_) => {}
Err(err) => match err {
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
.provide_context(&self.current_search_query[0..ctx], 0);
@ -233,7 +236,8 @@ impl AppSearchState {
Ok(_) => {}
Err(err) => match err {
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
.provide_context(&self.current_search_query[0..ctx], 0);

View file

@ -260,7 +260,8 @@ impl Painter {
let middle_dialog_chunk = Layout::default()
.direction(Direction::Horizontal)
.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(100),
@ -386,7 +387,8 @@ impl Painter {
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 c =
(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 {
// TODO: Can I remove this? Does ratatui's layout constraints work properly for fixing
// https://github.com/ClementTsang/bottom/issues/896 now?
// TODO: Can I remove this? Does ratatui's layout constraints work properly for
// fixing https://github.com/ClementTsang/bottom/issues/896 now?
fn get_constraints(
direction: Direction, constraints: &[LayoutConstraint], area: Rect,
) -> Vec<Rect> {
// Order of operations:
// - Ratios first + canvas-handled (which is just zero)
// - Then any flex-grows to take up remaining space; divide amongst remaining
// hand out any remaining space
// - Then any flex-grows to take up remaining space; divide amongst
// remaining hand out any remaining space
#[derive(Debug, Default, Clone, Copy)]
struct Size {
@ -688,7 +690,8 @@ impl Painter {
&col_rows.children
)
.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(
Direction::Horizontal,
col_row_constraint_vec.as_slice(),

View file

@ -20,13 +20,15 @@ use crate::utils::general::ClampExt;
/// 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
/// expects sorted data or not, with two expected types:
/// Note that [`DataTable`] takes a generic type `S`, bounded by [`SortType`].
/// 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.
/// - [`Sortable`]: This table expects sorted data, and there are helper functions to
/// facilitate things like sorting based on a selected column, shortcut column selection support, mouse column
/// selection support, etc.
/// - [`Unsortable`]: The default if otherwise not specified. This table does
/// not expect sorted data.
/// - [`Sortable`]: This table expects sorted data, and there are helper
/// 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 columns: Vec<C>,
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
/// valid change, this function will also return the new position wrapped in an [`Option`].
/// Increments the scroll position if possible by a positive/negative
/// 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> {
let max_index = self.data.len();
let current_index = self.state.current_index;

View file

@ -7,17 +7,20 @@ use std::{
/// A bound on the width of a column.
#[derive(Clone, Copy, Debug)]
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 {
/// 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,
/// The max width, as a percentage of the total width available. If [`None`],
/// then it can grow as desired.
/// The max width, as a percentage of the total width available. If
/// [`None`], then it can grow as desired.
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),
/// 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.
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)]
fn header(&self) -> Cow<'static, str> {
self.text()
@ -63,8 +67,9 @@ pub trait DataTableColumn<H: ColumnHeader> {
/// The actually displayed "header".
fn header(&self) -> Cow<'static, str>;
/// The header length, along with any required additional lengths for things like arrows.
/// Defaults to getting the length of [`DataTableColumn::header`].
/// The header length, along with any required additional lengths for things
/// like arrows. Defaults to getting the length of
/// [`DataTableColumn::header`].
fn header_len(&self) -> usize {
self.header().len()
}
@ -78,7 +83,8 @@ pub struct Column<H> {
/// A restriction on this column's width.
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,
}
@ -148,10 +154,13 @@ impl<H: ColumnHeader> Column<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.
/// * `left_to_right` is whether to size from left-to-right (`true`) or right-to-left (`false`).
/// * `total_width` is the total width on the canvas that the columns can
/// 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>;
}

View file

@ -9,8 +9,9 @@ pub trait DataToCell<H>
where
H: ColumnHeader,
{
/// Given data, a column, and its corresponding width, return the string in the cell that will
/// be displayed in the [`DataTable`](super::DataTable).
/// Given data, a column, and its corresponding width, return the string in
/// the cell that will be displayed in the
/// [`DataTable`](super::DataTable).
fn to_cell(&self, column: &H, calculated_width: NonZeroU16) -> Option<Cow<'static, str>>;
/// Apply styling to the generated [`Row`] of cells.

View file

@ -202,7 +202,8 @@ where
if !self.data.is_empty() || !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 {
self.set_position(first_index);
}

View file

@ -46,7 +46,8 @@ pub struct Sortable {
}
/// 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),
/// and therefore only [`Unsortable`] and [`Sortable`] can implement it.
@ -97,9 +98,10 @@ impl SortType for Sortable {
SortOrder::Ascending => UP_ARROW,
SortOrder::Descending => DOWN_ARROW,
};
// TODO: I think I can get away with removing the truncate_to_text call since
// I almost always bind to at least the header size...
// TODO: Or should we instead truncate but ALWAYS leave the arrow at the end?
// TODO: I think I can get away with removing the truncate_to_text call
// since I almost always bind to at least the header
// 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())
} else {
truncate_to_text(&c.header(), width.get())
@ -127,7 +129,8 @@ pub struct SortColumn<T> {
/// A restriction on this column's width.
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,
}
@ -178,8 +181,9 @@ impl<D, T> SortColumn<T>
where
T: ColumnHeader + SortsRow<DataType = D>,
{
/// Creates a new [`SortColumn`] with a width that follows the header width, which has no shortcut and sorts by
/// default in ascending order ([`SortOrder::Ascending`]).
/// Creates a new [`SortColumn`] with a width that follows the header width,
/// which has no shortcut and sorts by default in ascending order
/// ([`SortOrder::Ascending`]).
pub fn new(inner: T) -> Self {
Self {
inner,
@ -189,8 +193,8 @@ where
}
}
/// Creates a new [`SortColumn`] with a hard width, which has no shortcut and sorts by default in
/// ascending order ([`SortOrder::Ascending`]).
/// Creates a new [`SortColumn`] with a hard width, which has no shortcut
/// and sorts by default in ascending order ([`SortOrder::Ascending`]).
pub fn hard(inner: T, width: u16) -> Self {
Self {
inner,
@ -200,8 +204,8 @@ where
}
}
/// Creates a new [`SortColumn`] with a soft width, which has no shortcut and sorts by default in
/// ascending order ([`SortOrder::Ascending`]).
/// Creates a new [`SortColumn`] with a soft width, which has no shortcut
/// and sorts by default in ascending order ([`SortOrder::Ascending`]).
pub fn soft(inner: T, max_percentage: Option<f32>) -> Self {
Self {
inner,
@ -226,7 +230,8 @@ where
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) {
let descending = matches!(order, SortOrder::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,
/// and otherwise do nothing.
/// Given some `x` and `y`, if possible, select the corresponding column or
/// 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
/// returned.
/// If there was some update, the corresponding column type will be
/// returned. If nothing happens, [`None`] is returned.
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 let Some(index) = self.get_range(x) {
@ -304,10 +309,11 @@ where
/// 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
/// to the prescribed default sort order.
/// If the index is different from the previous one, it will move to the new
/// 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) {
if self.sort_type.sort_index == index {
self.toggle_order();

View file

@ -23,7 +23,8 @@ pub struct GraphData<'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],
/// 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<'_> {
if self.is_expanded {
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
/// throughout time in the x-axis.
/// Draws a time graph at [`Rect`] location provided by `draw_loc`. A time
/// graph is used to display data points throughout time in the x-axis.
///
/// This time graph:
/// - 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 `graph_data`, which represents *what* data to draw, and various details like style and optional legends.
/// - Expects a [`TimeGraph`] to be passed in, which details how to draw the
/// 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<'_>]) {
let x_axis = self.generate_x_axis();
let y_axis = self.generate_y_axis();

View file

@ -47,8 +47,8 @@ impl<'a> Default for PipeGauge<'a> {
}
impl<'a> PipeGauge<'a> {
/// The ratio, a value from 0.0 to 1.0 (any other greater or less will be clamped)
/// represents the portion of the pipe gauge to fill.
/// The ratio, a value from 0.0 to 1.0 (any other greater or less will be
/// clamped) represents the portion of the pipe gauge to fill.
///
/// Note: passing in NaN will potentially cause problems.
pub fn ratio(mut self, ratio: f64) -> Self {
@ -87,7 +87,8 @@ impl<'a> PipeGauge<'a> {
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 {
self.hide_parts = hide_parts;
self

View file

@ -1,5 +1,5 @@
//! A [`tui::widgets::Chart`] but slightly more specialized to show right-aligned timeseries
//! data.
//! A [`tui::widgets::Chart`] but slightly more specialized to show
//! 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);
//! the specializations are factored out to `time_chart/points.rs`.
@ -31,7 +31,8 @@ pub type Point = (f64, f64);
pub struct Axis<'a> {
/// Title displayed next to axis end
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],
/// A list of labels to put to the left or below the axis
pub(crate) labels: Option<Vec<Span<'a>>>,
@ -44,10 +45,11 @@ pub struct Axis<'a> {
impl<'a> Axis<'a> {
/// 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,
/// this is the top.
/// It will be displayed at the end of the axis. For an X axis this is the
/// 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"]
pub fn title<T>(mut self, title: T) -> Axis<'a>
where
@ -61,7 +63,8 @@ impl<'a> Axis<'a> {
///
/// 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"]
pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a> {
self.bounds = bounds;
@ -238,11 +241,13 @@ impl FromStr for LegendPosition {
///
/// 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
/// (`(f64, f64)`), the first element being X and the second Y. It's also worth noting that, unlike
/// the [`Rect`], here the Y axis is bottom to top, as in math.
/// After that, you can pass it data with [`Dataset::data`]. Data is an array of
/// `f64` tuples (`(f64, f64)`), the first element being X and the second Y.
/// 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)]
pub struct Dataset<'a> {
/// 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
///
/// Points will then either be rendered as scrattered points or with lines between them
/// depending on [`Dataset::graph_type`].
/// Points will then either be rendered as scrattered points or with lines
/// between them depending on [`Dataset::graph_type`].
///
/// Data consist in an array of `f64` tuples (`(f64, f64)`), the first element being X and the
/// second Y. It's also worth noting that, unlike the [`Rect`], here the Y axis is bottom to
/// top, as in math.
/// Data consist in an array of `f64` tuples (`(f64, f64)`), the first
/// element being X and the second Y. It's also worth noting that,
/// 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"]
pub fn data(mut self, data: &'a [(f64, f64)]) -> Dataset<'a> {
self.data = data;
@ -284,12 +289,15 @@ impl<'a> Dataset<'a> {
/// Sets the kind of character to use to display this dataset
///
/// You can use dots (`•`), blocks (`█`), bars (`▄`), braille (`⠓`, `⣇`, `⣿`) or half-blocks
/// (`█`, `▄`, and `▀`). See [symbols::Marker] for more details.
/// You can use dots (`•`), blocks (`█`), bars (`▄`), braille (`⠓`, `⣇`,
/// `⣿`) 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"]
pub fn marker(mut self, marker: symbols::Marker) -> Dataset<'a> {
self.marker = marker;
@ -298,9 +306,10 @@ impl<'a> Dataset<'a> {
/// Sets how the dataset should be drawn
///
/// [`TimeChart`] can draw either a [scatter](GraphType::Scatter) or [line](GraphType::Line) charts.
/// A scatter will draw only the points in the dataset while a line will also draw a line
/// between them. See [`GraphType`] for more details
/// [`TimeChart`] can draw either a [scatter](GraphType::Scatter) or
/// [line](GraphType::Line) charts. A scatter will draw only the points
/// 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"]
pub fn graph_type(mut self, graph_type: GraphType) -> Dataset<'a> {
self.graph_type = graph_type;
@ -309,11 +318,13 @@ impl<'a> Dataset<'a> {
/// Sets the style of this dataset
///
/// The given style will be used to draw the legend and the data points. Currently the legend
/// will use the entire style whereas the data points will only use the foreground.
/// The given style will be used to draw the legend and the data points.
/// 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
/// your own type that implements [`Into<Style>`]).
/// `style` accepts any type that is convertible to [`Style`] (e.g.
/// [`Style`], [`Color`], or your own type that implements
/// [`Into<Style>`]).
#[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> {
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,
/// labels, legend, ...).
/// A container that holds all the infos about where to display each elements of
/// the chart (axis, labels, legend, ...).
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
struct ChartLayout {
/// Location of the title of the x axis
@ -343,8 +354,9 @@ struct ChartLayout {
graph_area: Rect,
}
/// A "custom" chart, just a slightly tweaked [`tui::widgets::Chart`] from ratatui, but with greater control over the
/// legend, and built with the idea of drawing data points relative to a time-based x-axis.
/// A "custom" chart, just a slightly tweaked [`tui::widgets::Chart`] from
/// 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:
/// - Styling option for the legend box
@ -368,8 +380,8 @@ pub struct TimeChart<'a> {
legend_style: Style,
/// Constraints used to determine whether the legend should be shown or not
hidden_legend_constraints: (Constraint, Constraint),
/// The position detnermine where the legenth is shown or hide regaurdless of
/// `hidden_legend_constraints`
/// The position detnermine where the legenth is shown or hide regaurdless
/// of `hidden_legend_constraints`
legend_position: Option<LegendPosition>,
/// The marker type.
marker: Marker,
@ -402,8 +414,9 @@ impl<'a> TimeChart<'a> {
/// Sets the style of the entire chart
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
/// `style` accepts any type that is convertible to [`Style`] (e.g.
/// [`Style`], [`Color`], or your own type that implements
/// [`Into<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"]
@ -444,14 +457,16 @@ impl<'a> TimeChart<'a> {
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
/// legend takes more space than what is allowed by any constraint, the legend is hidden.
/// [`Constraint::Min`] is an exception and will always show the legend.
/// The tuple's first constraint is used for the width and the second for
/// the height. If the legend takes more space than what is allowed by
/// 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
/// the chart, either horizontally or vertically.
/// If this is not set, the default behavior is to hide the legend if it is
/// greater than 25% of the chart, either horizontally or vertically.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn hidden_legend_constraints(
mut self, constraints: (Constraint, Constraint),
@ -464,8 +479,9 @@ impl<'a> TimeChart<'a> {
///
/// The default is [`LegendPosition::TopRight`].
///
/// If [`None`] is given, hide the legend even if [`hidden_legend_constraints`] determines it
/// should be shown. In contrast, if `Some(...)` is given, [`hidden_legend_constraints`] might
/// If [`None`] is given, hide the legend even if
/// [`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.
///
/// See [`LegendPosition`] for all available positions.
@ -477,8 +493,8 @@ impl<'a> TimeChart<'a> {
self
}
/// Compute the internal layout of the chart given the area. If the area is too small some
/// elements may be automatically hidden
/// Compute the internal layout of the chart given the area. If the area is
/// too small some elements may be automatically hidden
fn layout(&self, area: Rect) -> ChartLayout {
let mut layout = ChartLayout::default();
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);
}
// 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)
}
@ -703,9 +720,9 @@ impl Widget for TimeChart<'_> {
return;
}
// Sample the style of the entire widget. This sample will be used to reset the style of
// the cells that are part of the components put on top of the grah area (i.e legend and
// axis names).
// Sample the style of the entire widget. This sample will be used to reset the
// style of the cells that are part of the components put on top of the
// grah area (i.e legend and axis names).
let original_style = buf.get(area.left(), area.top()).style();
let layout = self.layout(chart_area);
@ -1020,7 +1037,8 @@ mod tests {
let layout = widget.layout(buffer.area);
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]

View file

@ -1,10 +1,12 @@
//! 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>.
//!
//! The main thing this is pulled in for is overriding how `BrailleGrid`'s draw logic works, as changing it is
//! needed in order to draw all datasets in only one layer back in [`super::TimeChart::render`]. More specifically,
//! 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
//! The main thing this is pulled in for is overriding how `BrailleGrid`'s draw
//! logic works, as changing it is needed in order to draw all datasets in only
//! one layer back in [`super::TimeChart::render`]. More specifically,
//! 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.
//!
//! 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),
}
/// 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
/// vertical half block characters, the upper half block '▀' and lower half block '▄' which take up
/// half the height of a normal character but the full width. Together with an empty space ' ' and a
/// full block '█', we can effectively double the resolution of a single cell. In addition, because
/// each character can have a foreground and background color, we can control 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.
/// In terminals, each character is usually twice as tall as it is wide. Unicode
/// has a couple of vertical half block characters, the upper half block '▀' and
/// lower half block '▄' which take up half the height of a normal character but
/// the full width. Together with an empty space ' ' and a full block '█', we
/// can effectively double the resolution of a single cell. In addition, because
/// each character can have a foreground and background color, we can control
/// 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
/// foreground color for each 2x4 dots cell, and the CharGrid which only supports a single
/// character for each cell.
/// This allows for more flexibility than the BrailleGrid which only supports a
/// single foreground color for each 2x4 dots cell, and the CharGrid which only
/// supports a single character for each cell.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
struct HalfBlockGrid {
/// width of the grid in number of terminal columns
@ -309,8 +313,8 @@ struct HalfBlockGrid {
}
impl HalfBlockGrid {
/// Create a new [`HalfBlockGrid`] with the given width and height measured in terminal columns
/// and rows respectively.
/// Create a new [`HalfBlockGrid`] with the given width and height measured
/// in terminal columns and rows respectively.
fn new(width: u16, height: u16) -> HalfBlockGrid {
HalfBlockGrid {
width,
@ -334,10 +338,11 @@ impl Grid for HalfBlockGrid {
}
fn save(&self) -> Layer {
// Given that we store the pixels in a grid, and that we want to use 2 pixels arranged
// vertically to form a single terminal cell, which can be either empty, upper half block,
// lower half block or full block, we 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
// Given that we store the pixels in a grid, and that we want to use 2 pixels
// arranged vertically to form a single terminal cell, which can be
// either empty, upper half block, lower half block or full block, we
// 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:
//
// 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
// 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
// not the same as the background reset color (i.e. default background color), we need to
// swap around the colors for that state (2 reset/color).
// Note that because the foreground reset color (i.e. default foreground color)
// is usually not the same as the background reset color (i.e. default
// 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
// block, but we choose to use a full block instead. This allows us to write unit tests that
// treat the cell as a single character instead of two half block characters.
// When the upper and lower colors are the same, we could continue to use an
// upper half block, but we choose to use a full block instead. This
// 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,
// 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
// time_chart drawing-layer-thing.
// Note we implement this slightly differently to what is done in ratatui's
// 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 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
.pixels
.iter()
@ -503,8 +511,8 @@ impl<'a> Context<'a> {
}
}
/// The Canvas widget may be used to draw more detailed figures using braille patterns (each
/// cell can have a braille character in 8 different positions).
/// The Canvas widget may be used to draw more detailed figures using braille
/// patterns (each cell can have a braille character in 8 different positions).
pub struct Canvas<'a, F>
where
F: Fn(&mut Context<'_>),
@ -558,9 +566,10 @@ where
self
}
/// Change the type of points used to draw the shapes. By default the braille patterns are used
/// as they provide a more fine grained result but you might want to use the simple dot or
/// block instead if the targeted terminal does not support those symbols.
/// Change the type of points used to draw the shapes. By default the
/// braille patterns are used as they provide a more fine grained result
/// but you might want to use the simple dot or block instead if the
/// targeted terminal does not support those symbols.
///
/// # Examples
///

View file

@ -22,9 +22,10 @@ impl TimeChart<'_> {
// See <https://github.com/ClementTsang/bottom/pull/918> and <https://github.com/ClementTsang/bottom/pull/937>
// for the original motivation.
//
// We also additionally do some interpolation logic because we may get caught missing some points
// when drawing, but we generally want to avoid 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
// We also additionally do some interpolation logic because we may get caught
// missing some points when drawing, but we generally want to avoid
// 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).
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>) {
match dataset
.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>) {
match dataset
.data
.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
// expect to use the returned index as part of a (m..n) range.
// In the success case, this means we found an index. Add one since we want to include this
// index and we expect to use the returned index as part of a (m..n) range.
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*
// the location. This index is where one would insert to fit inside the dataset - and since this is an end
// bound, index is, in a sense, already "+1" for our range later.
// In the fail case, this means we did not find an index, and the returned index is where
// one would *insert* the location. This index is where one would insert to fit
// inside the dataset - and since this is an end bound, index is, in a sense,
// already "+1" for our range later.
Err(index) => (index, {
let sum = index.checked_add(1);
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 {
let delta_x = newer_point.0 - older_point.0;
let delta_y = newer_point.1 - older_point.1;

View file

@ -129,15 +129,18 @@ impl Painter {
if app_state.should_get_widget_bounds() {
// 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.
// - 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
// 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
// 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).
// - 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 {
basic_table.left_tlc =
Some((margined_draw_loc[0].x, margined_draw_loc[0].y + 1));

View file

@ -249,14 +249,15 @@ impl Painter {
const SIGNAL: usize = if cfg!(target_os = "windows") { 1 } else { 15 };
// 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
// of the left section and 3 characters back. We then give it a buffer size of 1 on the x-coordinate.
// - Same for the "no" button, except it is the right section and we do it from the start of the right
// section.
// - We have three sections; we put our mouse bounding box for the "yes" button
// at the very right edge of the left section and 3 characters back. We then
// give it a buffer size of 1 on the x-coordinate.
// - 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
// them here and check with >= and <= mouse bound checks, as opposed to how we do it elsewhere with
// >= and <. See https://github.com/ClementTsang/bottom/pull/459 for details.
// Lastly, note that mouse detection for the dd buttons assume correct widths.
// As such, we correct them here and check with >= and <= mouse
// 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![
// Yes
(

View file

@ -63,8 +63,8 @@ impl Painter {
.border_style(self.colours.border_style);
if app_state.should_get_widget_bounds() {
// We must also recalculate how many lines are wrapping to properly get scrolling to work on
// small terminal sizes... oh joy.
// We must also recalculate how many lines are wrapping to properly get
// scrolling to work on small terminal sizes... oh joy.
app_state.help_dialog_state.height = block.inner(draw_loc).height;

View file

@ -6,7 +6,7 @@ use tui::style::{Color, Style};
use super::ColourScheme;
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 currently_selected_text_colour: Color,
@ -154,7 +154,7 @@ impl CanvasStyling {
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
try_set_colour!(self.avg_colour_style, colours, avg_cpu_color);
try_set_colour!(self.all_colour_style, colours, all_cpu_color);

View file

@ -111,8 +111,9 @@ impl Painter {
tab_click_locs
.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
// to start at the blank space before the tab label.
// +4 because we want to go one space, then one space past to get to the
// '|', then 2 more to start at the blank space
// before the tab label.
current_x += width + 4;
}
battery_widget_state.tab_click_locs = Some(tab_click_locs);

View file

@ -33,8 +33,8 @@ impl Painter {
// 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.
// If not, then add a new column.
// Then, from this, split the row space across ALL columns. From there, generate
// the desired lengths.
// Then, from this, split the row space across ALL columns. From there,
// generate the desired lengths.
if app_state.current_widget.widget_id == widget_id {
f.render_widget(

View file

@ -251,7 +251,8 @@ impl Painter {
.widget_states
.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;
let is_on_widget = widget_id == app_state.current_widget.widget_id;

View file

@ -42,8 +42,8 @@ impl Painter {
if app_state.should_get_widget_bounds() {
// Update draw loc in widget map
// Note that in both cases, we always go to the same widget id so it's fine to do it like
// this lol.
// Note that in both cases, we always go to the same widget id so it's fine to
// do it like this lol.
if let Some(network_widget) = app_state.widget_map.get_mut(&widget_id) {
network_widget.top_left_corner = Some((draw_loc.x, draw_loc.y));
network_widget.bottom_right_corner =
@ -74,7 +74,8 @@ impl Painter {
// TODO: Cache network results: Only update if:
// - Force update (includes time interval change)
// - 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.
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,
network_use_binary_prefix: bool,
) -> 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(
network_scale_type: &AxisScaling, network_use_binary_prefix: bool,
) -> 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
// are sorted, so we can short-circuit our search to filter out only the relevant data points...
// First, let's shorten our ranges to actually look. We can abuse the fact that
// 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)) = (
rx.iter().position(|(time, _data)| *time >= time_start),
rx.iter().rposition(|(time, _data)| *time <= 0.0),
@ -337,26 +340,31 @@ fn adjust_network_data_point(
network_use_binary_prefix: bool,
) -> (f64, Vec<String>) {
// 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.
// We don't want to drown out some data if the ranges are too 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.
// - Main goal is to maximize the amount of information displayed given a
// specific height. We don't want to drown out some data if the ranges are too
// 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.
//
// The idea is we take the top value, build our scale such that each "point" is 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
// The idea is we take the top value, build our scale such that each "point" is
// 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?
//
// So, how do we do this in ratatui? Well, if we are using intervals that tie in perfectly to the max
// value we want... then it's actually not that 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.
// So, how do we do this in ratatui? Well, if we are using intervals that tie
// in perfectly to the max value we want... then it's actually not that
// 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.
//
// ===
//
// 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 {
DataUnit::Byte => "B",
@ -411,8 +419,9 @@ fn adjust_network_data_point(
)
};
// Finally, build an acceptable range starting from there, using the given height!
// Note we try to put more of a weight on the bottom section vs. the top, since the top has less data.
// Finally, build an acceptable range starting from there, using the given
// 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 labels: Vec<String> = vec![
@ -422,7 +431,8 @@ fn adjust_network_data_point(
format!("{:.1}", base_unit * 1.5),
]
.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();
(bumped_max_entry, labels)

View file

@ -20,7 +20,8 @@ const SORT_MENU_WIDTH: u16 = 7;
impl Painter {
/// 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(
&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, draw_border: bool,
widget_id: u64,
@ -106,7 +107,8 @@ impl Painter {
}
/// 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.
fn draw_search_field(
&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.
/// - `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.
fn draw_sort_table(
&self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,

View file

@ -1,6 +1,6 @@
use tui::widgets::Borders;
use crate::options::ConfigColours;
use crate::options::ColoursConfig;
// Default widget ID
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 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 TIME_LABEL_HEIGHT_LIMIT: u16 = 7;
@ -25,8 +26,8 @@ pub const SIDE_BORDERS: Borders = Borders::LEFT.union(Borders::RIGHT);
// Colour profiles
// TODO: Generate these with a macro or something...
pub fn default_light_mode_colour_palette() -> ConfigColours {
ConfigColours {
pub fn default_light_mode_colour_palette() -> ColoursConfig {
ColoursConfig {
text_color: Some("black".into()),
border_color: Some("black".into()),
table_header_color: Some("black".into()),
@ -61,12 +62,12 @@ pub fn default_light_mode_colour_palette() -> ConfigColours {
"Blue".into(),
"Red".into(),
]),
..ConfigColours::default()
..ColoursConfig::default()
}
}
pub fn gruvbox_colour_palette() -> ConfigColours {
ConfigColours {
pub fn gruvbox_colour_palette() -> ColoursConfig {
ColoursConfig {
table_header_color: Some("#83a598".into()),
all_cpu_color: Some("#8ec07c".into()),
avg_cpu_color: Some("#fb4934".into()),
@ -124,8 +125,8 @@ pub fn gruvbox_colour_palette() -> ConfigColours {
}
}
pub fn gruvbox_light_colour_palette() -> ConfigColours {
ConfigColours {
pub fn gruvbox_light_colour_palette() -> ColoursConfig {
ColoursConfig {
table_header_color: Some("#076678".into()),
all_cpu_color: Some("#8ec07c".into()),
avg_cpu_color: Some("#fb4934".into()),
@ -183,8 +184,8 @@ pub fn gruvbox_light_colour_palette() -> ConfigColours {
}
}
pub fn nord_colour_palette() -> ConfigColours {
ConfigColours {
pub fn nord_colour_palette() -> ColoursConfig {
ColoursConfig {
table_header_color: Some("#81a1c1".into()),
all_cpu_color: Some("#88c0d0".into()),
avg_cpu_color: Some("#8fbcbb".into()),
@ -230,8 +231,8 @@ pub fn nord_colour_palette() -> ConfigColours {
}
}
pub fn nord_light_colour_palette() -> ConfigColours {
ConfigColours {
pub fn nord_light_colour_palette() -> ColoursConfig {
ColoursConfig {
table_header_color: Some("#5e81ac".into()),
all_cpu_color: Some("#81a1c1".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".
#network_legend = "TopRight".
# These are flags around the process widget.
# Processes widget configuration
#[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%
# 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%"]
# [cpu]
# CPU widget configuration
#[cpu]
# One of "all" (default), "average"/"avg"
# 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
# will depend on terminal support.
#[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]]
# type="proc"
# 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.
@ -773,8 +778,8 @@ mod test {
}
}
/// This test exists because previously, [`SIDE_BORDERS`] was set incorrectly after I moved from
/// tui-rs to ratatui.
/// This test exists because previously, [`SIDE_BORDERS`] was set
/// incorrectly after I moved from tui-rs to ratatui.
#[test]
fn assert_side_border_bits_match() {
assert_eq!(

View file

@ -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
/// - Memory usage
/// - Network usage
@ -183,7 +184,7 @@ impl DataCollector {
temperature_type: TemperatureType::Celsius,
use_current_cpu_total: 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_tx: 0,
show_average_cpu: false,
@ -255,7 +256,8 @@ impl DataCollector {
/// - Disk (Windows)
/// - Temperatures (non-Linux)
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);
let refresh_start = Instant::now();
@ -374,9 +376,9 @@ impl DataCollector {
fn update_processes(&mut self) {
if self.widgets_to_harvest.use_proc {
if let Ok(mut process_list) = self.get_processes() {
// NB: To avoid duplicate sorts on rerenders/events, we sort the processes by PID here.
// We also want to avoid re-sorting *again* later on if we're sorting by PID, since we already
// did it here!
// NB: To avoid duplicate sorts on rerenders/events, we sort the processes by
// PID here. We also want to avoid re-sorting *again* later on
// if we're sorting by PID, since we already did it here!
process_list.sort_unstable_by_key(|p| p.pid);
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
/// accuracy on for the first refresh), and we bound the lower end just to avoid the off-chance that
/// refreshing too quickly causes 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 bound the upper end to avoid waiting too long (e.g. FreeBSD is 1s, which
/// I'm fine with losing accuracy on for the first refresh), and we bound the
/// lower end just to avoid the off-chance that refreshing too quickly causes
/// 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.
const fn get_sleep_duration() -> Duration {

View file

@ -1,6 +1,7 @@
//! 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! {
if #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "ios"))] {

View file

@ -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> {
// 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();
Ok([one as f32, five as f32, fifteen as f32])

View file

@ -86,16 +86,18 @@ cfg_if! {
}
}
/// Whether to keep the current disk entry given the filters, disk name, and disk mount.
/// Precedence ordering in the case where name and mount filters disagree, "allow"
/// takes precedence over "deny".
/// Whether to keep the current disk entry given the filters, disk name, and
/// disk mount. Precedence ordering in the case where name and mount filters
/// disagree, "allow" takes precedence over "deny".
///
/// For implementation, we do this as follows:
///
/// 1. Is the entry allowed through any filter? That is, does it match an entry in a
/// filter where `is_list_ignored` is `false`? If so, we always keep this entry.
/// 2. Is the entry denied through any filter? That is, does it match an entry in a
/// filter where `is_list_ignored` is `true`? If so, we always deny this entry.
/// 1. Is the entry allowed through any filter? That is, does it match an entry
/// in a filter where `is_list_ignored` is `false`? If so, we always keep
/// this entry.
/// 2. Is the entry denied through any filter? That is, does it match an entry
/// in a filter where `is_list_ignored` is `true`? If so, we always deny this
/// entry.
/// 3. Anything else is allowed.
pub fn keep_disk_entry(
disk_name: &str, mount_point: &str, disk_filter: &Option<Filter>, mount_filter: &Option<Filter>,

View file

@ -28,7 +28,8 @@ struct FileSystem {
}
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)]
let mut io_harvest: HashMap<String, Option<IoData>> =
get_disk_info().map(|storage_system_information| {

View file

@ -1,5 +1,5 @@
//! Disk stats for Unix-like systems that aren't supported through other means. Officially,
//! for now, this means Linux and macOS.
//! Disk stats for Unix-like systems that aren't supported through other means.
//! Officially, for now, this means Linux and macOS.
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 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:
// 1. Is the entry allowed through any filter? That is, does it match an entry in a filter where `is_list_ignored` is `false`? If so, we always keep this entry.
// 2. Is the entry denied through any filter? That is, does it match an entry in a filter where `is_list_ignored` is `true`? If so, we always deny this entry.
// 1. Is the entry allowed through any filter? That is, does it match an entry
// in a filter where `is_list_ignored` is `false`? If so, we always keep this
// entry.
// 2. Is the entry denied through any filter? That is, does it match an entry in
// a filter where `is_list_ignored` is `true`? If so, we always deny this
// entry.
// 3. Anything else is allowed.
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,
// see https://github.com/ClementTsang/bottom/issues/419 for details).
// The usage line can fail in some cases (for example, if you use Void Linux +
// LUKS, see https://github.com/ClementTsang/bottom/issues/419 for details).
if let Ok(usage) = partition.usage() {
let total = usage.total();

View file

@ -5,8 +5,8 @@ use crate::multi_eq_ignore_ascii_case;
/// Known filesystems. Original list from
/// [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
/// the [`FileSystem::Other`] element.
/// All physical filesystems should have their own enum element and all virtual
/// filesystems will go into the [`FileSystem::Other`] element.
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
#[non_exhaustive]
pub enum FileSystem {
@ -81,7 +81,8 @@ impl FileSystem {
!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]
pub fn is_virtual(&self) -> bool {
matches!(self, FileSystem::Other(..))

View file

@ -28,7 +28,8 @@ impl FromStr for 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+
/// - 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 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) {
if bytes > 0 {
if let Ok(counters) = IoCounters::from_str(&line) {

View file

@ -43,8 +43,8 @@ impl Partition {
/// Returns the device name for the partition.
pub fn get_device_name(&self) -> String {
if let Some(device) = self.device() {
// See if this disk is actually mounted elsewhere on Linux. This is a workaround properly map I/O
// in some cases (i.e. disk encryption, https://github.com/ClementTsang/bottom/issues/419).
// See if this disk is actually mounted elsewhere on Linux. This is a workaround
// 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 path.is_absolute() {
path.into_os_string()
@ -87,7 +87,8 @@ impl Partition {
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()) };
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 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) {
if bytes > 0 {
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 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) {
if bytes > 0 {
if let Ok(partition) = Partition::from_str(&line) {

View file

@ -10,23 +10,24 @@ fn get_device_io(device: io_kit::IoObject) -> anyhow::Result<IoCounters> {
//
// Okay, so this is weird.
//
// The problem is that if I have this check - this is what sources like psutil use, for
// example (see https://github.com/giampaolo/psutil/blob/7eadee31db2f038763a3a6f978db1ea76bbc4674/psutil/_psutil_osx.c#LL1422C20-L1422C20)
// The problem is that if I have this check - this is what sources like psutil
// 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.
//
// The problem with this is that there is *never* a disk0 *disk* entry to correspond to this,
// so there will be entries like disk1 or whatnot. Someone's done some digging on the gopsutil
// repo (https://github.com/shirou/gopsutil/issues/855#issuecomment-610016435), and it seems
// The problem with this is that there is *never* a disk0 *disk* entry to
// correspond to this, so there will be entries like disk1 or whatnot.
// 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.
//
// So with all that said, what I've found is that I *can* still get a mapping - but I have
// to disable the conform check, which... is weird. I'm not sure if this is valid at all. But
// it *does* seem to match Activity Monitor with regards to disk activity, so... I guess we
// can leave this for now...?
// So with all that said, what I've found is that I *can* still get a mapping -
// but I have to disable the conform check, which... is weird. I'm not sure
// if this is valid at all. But it *does* seem to match Activity Monitor
// with regards to disk activity, so... I guess we can leave this for
// now...?
// 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 parent_props = parent.properties()?;

View file

@ -49,7 +49,8 @@ extern "C" {
entry: io_registry_entry_t, plane: *const libc::c_char, parent: *mut io_registry_entry_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(
entry: io_registry_entry_t, properties: *mut CFMutableDictionaryRef,

View file

@ -37,7 +37,8 @@ impl Iterator for IoIterator {
fn next(&mut self) -> Option<Self::Item> {
// 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) } {
0 => None,
io_object => Some(IoObject::from(io_object)),
@ -47,7 +48,8 @@ impl Iterator for IoIterator {
impl Drop for IoIterator {
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) };
assert_eq!(result, kern_return::KERN_SUCCESS);
}

View file

@ -24,8 +24,9 @@ pub struct IoObject(io_object_t);
impl IoObject {
/// Returns a typed dictionary with this object's properties.
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
// we guard against it with a check against `result` to ensure it succeeded.
// SAFETY: The IOKit call should be fine, the arguments are safe. The
// `assume_init` should also be fine, as we guard against it with a
// check against `result` to ensure it succeeded.
unsafe {
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
/// is one.
/// Gets the [`kIOServicePlane`] parent [`io_object_t`] for this
/// [`io_object_t`], if there is one.
pub fn service_parent(&self) -> anyhow::Result<IoObject> {
let mut parent: io_registry_entry_t = 0;
@ -65,7 +66,8 @@ impl IoObject {
// pub fn conforms_to_block_storage_driver(&self) -> bool {
// // SAFETY: IOKit call, the arguments should be safe.
// let result =
// unsafe { IOObjectConformsTo(self.0, "IOBlockStorageDriver\0".as_ptr().cast()) };
// unsafe { IOObjectConformsTo(self.0,
// "IOBlockStorageDriver\0".as_ptr().cast()) };
// result != 0
// }
@ -79,7 +81,8 @@ impl From<io_object_t> for IoObject {
impl Drop for IoObject {
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) };
assert_eq!(result, kern_return::KERN_SUCCESS);
}

View file

@ -34,8 +34,9 @@ pub(crate) fn mounts() -> anyhow::Result<Vec<libc::statfs>> {
"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.
// Otherwise, getfsstat64 should return the number of statfs structures if it succeeded.
// SAFETY: We have a debug assert check, and if `result` is not correct (-1), we
// 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
unsafe {

View file

@ -38,7 +38,8 @@ impl Partition {
let result = unsafe { libc::statvfs(path.as_ptr(), vfs.as_mut_ptr()) };
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() }))
} else {
bail!("statvfs failed to get the disk usage for disk {path:?}")

View file

@ -1,7 +1,7 @@
pub struct Usage(libc::statvfs);
// Note that x86 returns `u32` values while x86-64 returns `u64`s, so we convert everything
// to `u64` for consistency.
// Note that x86 returns `u32` values while x86-64 returns `u64`s, so we convert
// everything to `u64` for consistency.
#[allow(clippy::useless_conversion)]
impl Usage {
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)
}
/// 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 {
u64::from(self.0.f_bfree) * u64::from(self.0.f_frsize)
}
#[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 {
let avail_to_root = u64::from(self.0.f_bfree) * u64::from(self.0.f_frsize);
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 {
u64::from(self.0.f_bavail) * u64::from(self.0.f_frsize)
}

View file

@ -41,7 +41,8 @@ fn volume_io(volume: &Path) -> anyhow::Result<DISK_PERFORMANCE> {
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 {
CreateFileW(
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 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 {
DeviceIoControl(
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];
// 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) }?;
if handle.is_invalid() {
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.
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
// https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumenameforvolumemountpointw
// According to winapi docs 50 is a reasonable length to accomodate the volume
// path https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumenameforvolumemountpointw
const VOLUME_MAX_LEN: usize = 50;
let mount = {

View file

@ -1,6 +1,7 @@
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")]
pub fn zfs_io_stats() -> anyhow::Result<Vec<IoCounters>> {
use sysctl::Sysctl;

View file

@ -19,5 +19,6 @@ pub mod arc;
pub struct MemHarvest {
pub used_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. */
}

View file

@ -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
/// between the available and free memory. Free memory is defined as memory not containing any data,
/// which means cache and buffer memory are not "free". Available memory is defined as memory able
/// 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)
/// Returns cache usage. sysinfo has no way to do this directly but it should
/// equal the difference between the available and free memory. Free memory is
/// defined as memory not containing any data, which means cache and buffer
/// memory are not "free". Available memory is defined as memory able
/// 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)
#[cfg(not(target_os = "windows"))]
pub(crate) fn get_cache_usage(sys: &System) -> Option<MemHarvest> {

View file

@ -7,7 +7,8 @@ use sysinfo::Networks;
use super::NetworkHarvest;
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(
networks: &Networks, prev_net_access_time: Instant, prev_net_rx: &mut u64,
prev_net_tx: &mut u64, curr_time: Instant, filter: &Option<Filter>,

View file

@ -87,7 +87,8 @@ pub struct ProcessHarvest {
/// Cumulative process uptime.
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")]
pub uid: Option<libc::uid_t>,

View file

@ -29,11 +29,11 @@ pub struct PrevProcDetails {
cpu_time: u64,
}
/// Given `/proc/stat` file contents, determine the idle and non-idle values of the CPU
/// used to calculate CPU usage.
/// Given `/proc/stat` file contents, determine the idle and non-idle values of
/// the CPU used to calculate CPU usage.
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
/// will return `0_f64`.
/// Converts a `Option<&str>` value to an f64. If it fails to parse or is
/// `None`, it will return `0_f64`.
fn str_to_f64(val: Option<&str>) -> 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 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
// See https://github.com/htop-dev/htop/blob/main/linux/LinuxProcessList.c
// Note we do not get guest/guest_nice, as they are calculated as part of
// user/nice respectively See https://github.com/htop-dev/htop/blob/main/linux/LinuxProcessList.c
let idle = idle + iowait;
let non_idle = user + nice + system + irq + softirq + steal;
@ -331,8 +331,8 @@ pub(crate) fn linux_process_data(
if unnormalized_cpu {
let num_processors = collector.sys.system.cpus().len() as f64;
// Note we *divide* here because the later calculation divides `cpu_usage` - in effect,
// multiplying over the number of cores.
// Note we *divide* here because the later calculation divides `cpu_usage` - in
// effect, multiplying over the number of cores.
cpu_usage /= num_processors;
}

View file

@ -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
/// [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 {
/// The filename of the executable without parentheses.
pub comm: String,
@ -40,13 +41,16 @@ pub(crate) struct Stat {
/// The parent process 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,
/// 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,
/// 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,
/// The start time of the process, represented in clock ticks.
@ -56,8 +60,8 @@ pub(crate) struct Stat {
impl Stat {
#[inline]
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,
// we can't just use read_to_string.
// Since this is just one line, we can read it all at once. However, since it
// might have non-utf8 characters, we can't just use read_to_string.
f.read_to_end(unsafe { buffer.as_mut_vec() })?;
let line = buffer.to_string_lossy();
@ -82,12 +86,14 @@ impl Stat {
.ok_or_else(|| anyhow!("missing state"))?;
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 utime: 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 start_time: u64 = next_part(&mut rest)?.parse()?;
@ -115,7 +121,8 @@ impl Stat {
/// 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 read_bytes: u64,
pub write_bytes: u64,
@ -136,7 +143,8 @@ impl Io {
let mut read_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) {
if bytes > 0 {
if buffer.is_empty() {
@ -207,12 +215,13 @@ fn reset(root: &mut PathBuf, buffer: &mut String) {
}
impl Process {
/// Creates a new [`Process`] given a `/proc/<PID>` path. This may fail if the process
/// no longer exists or there are permissions issues.
/// Creates a new [`Process`] given a `/proc/<PID>` path. This may fail if
/// the process no longer exists or there are permissions issues.
///
/// Note that this pre-allocates fields on **creation**! As such, some data might end
/// up "outdated" depending on when you call some of the methods. Therefore, this struct
/// is only useful for either fields that are unlikely to change, or are short-lived and
/// Note that this pre-allocates fields on **creation**! As such, some data
/// might end up "outdated" depending on when you call some of the
/// methods. Therefore, this struct is only useful for either fields
/// that are unlikely to change, or are short-lived and
/// will be discarded quickly.
pub(crate) fn from_path(pid_path: PathBuf) -> anyhow::Result<Process> {
// TODO: Pass in a buffer vec/string to share?
@ -247,7 +256,8 @@ impl Process {
let mut root = pid_path;
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 =
open_at(&mut root, "stat", &fd).and_then(|file| Stat::from_file(file, &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)
}
/// Opens a path. Note that this function takes in a mutable root - this will mutate it to avoid allocations. You
/// probably will want to pop the most recent child after if you need to use the buffer again.
/// Opens a path. Note that this function takes in a mutable root - this will
/// 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]
fn open_at(root: &mut PathBuf, child: &str, fd: &OwnedFd) -> anyhow::Result<File> {
root.push(child);

View file

@ -22,7 +22,8 @@ impl UnixProcessExt for MacOSProcessExt {
let output = Command::new("ps")
.args(["-o", "pid=,pcpu=", "-p"])
.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())
.collect::<String>(),
)

View file

@ -1,5 +1,5 @@
//! Partial bindings from Apple's open source code for getting process 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).
//! Partial bindings from Apple's open source code for getting process
//! 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;
@ -152,7 +152,8 @@ pub(crate) struct extern_proc {
/// Pointer to process group. Originally a pointer to a `pgrp`.
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,
/// Exit status for wait; also stop signal.
@ -207,10 +208,12 @@ pub(crate) struct vmspace {
#[allow(non_camel_case_types)]
#[repr(C)]
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,
/// 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,
/// Process credentials
@ -237,7 +240,8 @@ pub(crate) struct eproc {
/// tty process group id
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,
/// wchan message
@ -291,8 +295,8 @@ pub(crate) fn kinfo_process(pid: Pid) -> Result<kinfo_proc> {
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
// -1 with errno set.
// SAFETY: info is initialized if result succeeded and returned a non-negative
// 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
unsafe { Ok(info.assume_init()) }

View file

@ -6,7 +6,10 @@ use hashbrown::HashMap;
use sysinfo::{ProcessStatus, System};
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 {
fn sysinfo_process_data(

View file

@ -12,7 +12,8 @@ impl UserTable {
if let Some(user) = self.uid_user_mapping.get(&uid) {
Ok(user.clone())
} 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) };
if passwd.is_null() {

View file

@ -103,8 +103,9 @@ pub fn sysinfo_process_data(
.and_then(|uid| users.get_user_by_id(uid))
.map_or_else(|| "N/A".into(), |user| user.name().to_owned().into()),
time: if process_val.start_time() == 0 {
// Workaround for Windows occasionally returning a start time equal to UNIX epoch, giving a run time
// in the range of 50+ years. We just return a time of zero in this case for simplicity.
// Workaround for Windows occasionally returning a start time equal to UNIX
// 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
} else {
Duration::from_secs(process_val.run_time())

View file

@ -47,7 +47,8 @@ impl FromStr for 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 {
fn convert_celsius_to_kelvin(celsius: f32) -> f32 {
celsius + 273.15

View file

@ -13,13 +13,15 @@ use crate::{app::filter::Filter, utils::error::BottomError};
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 {
temperatures: Vec<TempHarvest>,
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> {
Ok(fs::read_to_string(path)?
.trim_end()
@ -28,7 +30,8 @@ fn parse_temp(path: &Path) -> Result<f32> {
/ 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) {
let mut dirs = HashSet::default();
@ -36,11 +39,13 @@ fn get_hwmon_candidates() -> (HashSet<PathBuf>, usize) {
for entry in read_dir.flatten() {
let mut path = entry.path();
// hwmon includes many sensors, we only want ones with at least one temperature sensor
// Reading this file will wake the device, but we're only checking existence, so it should be fine.
// hwmon includes many sensors, we only want ones with at least one temperature
// 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() {
// Note we also check for a `device` subdirectory (e.g. `/sys/class/hwmon/hwmon*/device/`).
// This is needed for CentOS, which adds this extra `/device` directory. See:
// Note we also check for a `device` subdirectory (e.g.
// `/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/giampaolo/psutil/issues/971
// - 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();
if path.join("temp1_input").exists() {
// It's possible that there are dupes (represented by symlinks) - the easy
// way is to just substitute the parent directory and check if the hwmon
// It's possible that there are dupes (represented by symlinks) - the
// easy way is to just substitute the parent
// directory and check if the hwmon
// variant exists already in a set.
//
// 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.
/// 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]
fn is_device_awake(path: &Path) -> bool {
// 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 let Ok(state) = fs::read_to_string(power_state) {
let state = state.trim();
// The zenpower3 kernel module (incorrectly?) reports "unknown", causing this check
// to fail and temperatures to appear as zero instead of having the file not exist.
// The zenpower3 kernel module (incorrectly?) reports "unknown", causing this
// 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"
} else {
true
@ -199,9 +208,10 @@ fn is_device_awake(path: &Path) -> bool {
}
}
/// Get temperature sensors from the linux sysfs interface `/sys/class/hwmon` and
/// `/sys/devices/platform/coretemp.*`. It returns all found temperature sensors, and the number
/// of checked hwmon directories (not coretemp directories).
/// Get temperature sensors from the linux sysfs interface `/sys/class/hwmon`
/// and `/sys/devices/platform/coretemp.*`. It returns all found temperature
/// sensors, and the number of checked hwmon directories (not coretemp
/// directories).
///
/// For more details, see the relevant Linux kernel documentation:
/// - [`/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,
// 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 {
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:
// - For GPUs, this will use the kernel device name, ex `card0`
// - For nvme drives, this will also use the kernel name, ex `nvme0`.
// This is found differently than for GPUs
// - For nvme drives, this will also use the kernel name, ex `nvme0`. This is
// found differently than for GPUs
// - 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.
let hwmon_name = {
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");
if drm.exists() {
// This should never actually be empty. If it is though, we'll fall back to the sensor name
// later on.
// This should never actually be empty. If it is though, we'll fall back to
// the sensor name later on.
let mut gpu = None;
if let Ok(cards) = drm.read_dir() {
@ -294,10 +306,11 @@ fn hwmon_temperatures(temp_type: &TemperatureType, filter: &Option<Filter>) -> H
gpu
} else {
// This little mess is to account for stuff like k10temp. This is needed because the
// `device` symlink points to `nvme*` for nvme drives, but to PCI buses for anything
// else. If the first character is alphabetic, it's an actual name like k10temp or
// nvme0, not a PCI bus.
// This little mess is to account for stuff like k10temp. This is needed
// because the `device` symlink points to `nvme*`
// for nvme drives, but to PCI buses for anything
// 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| {
let link = link
.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);
// 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 let Ok(temp_celsius) = parse_temp(&temp_path) {
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
/// [`hwmon_temperatures`] doesn't return anything to avoid duplicate sensor results.
/// Gets data from `/sys/class/thermal/thermal_zone*`. This should only be used
/// 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)
/// for more details.

View file

@ -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")]
{
use sysctl::Sysctl;

View file

@ -74,7 +74,8 @@ pub struct ConvertedData {
pub cache_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"))]
pub cache_data: Vec<Point>,
pub swap_data: Vec<Point>,
@ -169,7 +170,8 @@ impl ConvertedData {
data,
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();
*last_entry = *cpu_usage;
}
@ -177,8 +179,8 @@ impl ConvertedData {
}
}
// TODO: [Opt] Can probably avoid data deduplication - store the shift + data + original once.
// Now push all the data.
// TODO: [Opt] Can probably avoid data deduplication - store the shift + data +
// original once. Now push all the data.
for (itx, mut cpu) in &mut self.cpu_data.iter_mut().skip(1).enumerate() {
match &mut cpu {
CpuWidgetData::All => unreachable!(),
@ -262,10 +264,12 @@ pub fn convert_swap_data_points(data: &DataCollection) -> Vec<Point> {
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
/// with the returned binary unit (e.g. divide 3000 bytes by 1024 to have a value in KiB).
/// The expected usage is to divide out the given value with the returned
/// 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]
fn get_binary_unit_and_denominator(bytes: u64) -> (&'static str, f64) {
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)> {
if harvest.total_bytes > 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)) =
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),
)
} else {
@ -464,8 +471,9 @@ pub fn convert_network_points(
}
}
/// Returns a string given a value that is converted to the closest binary variant.
/// If the value is greater than a gibibyte, then it will return a decimal place.
/// Returns a string given a value that is converted to the closest binary
/// variant. If the value is greater than a gibibyte, then it will return a
/// decimal place.
pub fn binary_byte_string(value: u64) -> String {
let converted_values = get_binary_bytes(value);
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.
/// If the value is greater than a giga-X, then it will return a decimal place.
/// Returns a string given a value that is converted to the closest SI-variant,
/// 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 {
let converted_values = get_decimal_bytes(value);
if value >= GIGA_LIMIT {

View file

@ -1,9 +1,11 @@
//! A customizable cross-platform graphical process/system monitor for the terminal.
//! Supports Linux, macOS, and Windows. Inspired by gtop, gotop, and htop.
//! A customizable cross-platform graphical process/system monitor for the
//! 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
//! than the actual usage of the application. If you are instead looking for documentation regarding the *usage* of
//! bottom, refer to [here](https://clementtsang.github.io/bottom/stable/).
//! **Note:** The following documentation is primarily intended for people to
//! refer to for development purposes rather than the actual usage of the
//! 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 utils {
@ -48,8 +50,7 @@ use crossterm::{
};
use data_conversion::*;
use event::{handle_key_event_or_break, handle_mouse_event, BottomEvent, CollectionThreadEvent};
use options::args;
use options::{get_color_scheme, get_config_path, get_or_create_config, init_app};
use options::{args, get_color_scheme, get_config_path, get_or_create_config, init_app};
use tui::{backend::CrosstermBackend, Terminal};
use utils::error;
#[allow(unused_imports)]
@ -156,9 +157,11 @@ fn create_input_thread(
if let Ok(event) = read() {
match event {
Event::Resize(_, _) => {
// TODO: Might want to debounce this in the future, or take into account the actual resize
// values. Maybe we want to keep the current implementation in case the resize event might
// not fire... not sure.
// TODO: Might want to debounce this in the future, or take into
// account the actual resize values.
// 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() {
break;
@ -170,7 +173,8 @@ fn create_input_thread(
}
}
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() {
break;
}
@ -302,7 +306,8 @@ fn main() -> anyhow::Result<()> {
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)?;
// Create painter and set colours.
@ -311,14 +316,16 @@ fn main() -> anyhow::Result<()> {
// Check if the current environment is in a terminal.
check_if_terminal();
// Create termination mutex and cvar. We use this setup because we need to sleep at some points in the update
// thread, but we want to be able to interrupt the "sleep" if a termination occurs.
// Create termination mutex and cvar. We use this setup because we need to sleep
// 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_cvar = Arc::new(Condvar::new());
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 = create_collection_thread(
sender.clone(),
@ -394,7 +401,8 @@ fn main() -> anyhow::Result<()> {
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)?;
loop {

View file

@ -16,7 +16,7 @@ use std::{
};
use anyhow::{Context, Result};
pub use colours::ConfigColours;
pub use colours::ColoursConfig;
pub use config::ConfigV1;
use hashbrown::{HashMap, HashSet};
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
/// 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.
/// Returns the config path to use. If `override_config_path` is specified, then
/// 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.
/// - 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)'
@ -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
/// try to create a new file with the default settings, and return the default config. If bottom
/// fails to write a new config, it will silently just return the default config.
/// Get the config at `config_path`. If there is no config file at the specified
/// path, it will try to create a new file with the default settings, and return
/// 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> {
match &config_path {
Some(path) => {
@ -111,7 +113,8 @@ pub fn get_or_create_config(config_path: Option<&Path>) -> error::Result<ConfigV
}
}
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.
Ok(ConfigV1::default())
@ -124,7 +127,8 @@ pub fn init_app(
) -> Result<(App, BottomLayout)> {
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 config = &config;
@ -175,18 +179,13 @@ pub fn init_app(
is_flag_enabled!(network_use_binary_prefix, args.network, config);
let proc_columns: Option<IndexSet<ProcWidgetColumn>> = {
let columns = config.processes.as_ref().map(|cfg| cfg.columns.clone());
match columns {
Some(columns) => {
if columns.is_empty() {
None
} else {
Some(IndexSet::from_iter(columns))
}
config.processes.as_ref().and_then(|cfg| {
if cfg.columns.is_empty() {
None
} else {
Some(IndexSet::from_iter(cfg.columns.clone()))
}
None => None,
}
})
};
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(),
};
let disk_filter =
get_ignore_list(&config.disk_filter).context("Update 'disk_filter' in your config file")?;
let mount_filter = get_ignore_list(&config.mount_filter)
.context("Update 'mount_filter' in your config file")?;
let temp_filter =
get_ignore_list(&config.temp_filter).context("Update 'temp_filter' in your config file")?;
let net_filter =
get_ignore_list(&config.net_filter).context("Update 'net_filter' in your config file")?;
let (disk_name_filter, disk_mount_filter) = {
match &config.disk {
Some(cfg) => {
let df = get_ignore_list(&cfg.name_filter)
.context("Update 'disk.name_filter' in your config file")?;
let mf = get_ignore_list(&cfg.mount_filter)
.context("Update 'disk.mount_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 {
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 filters = DataFilters {
disk_filter,
mount_filter,
temp_filter,
net_filter,
disk_filter: disk_name_filter,
mount_filter: disk_mount_filter,
temp_filter: temp_sensor_filter,
net_filter: net_interface_filter,
};
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 {
#[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(batteries) = battery_manager.batteries() {
if batteries.count() == 0 {
@ -907,7 +922,7 @@ mod test {
args::BottomArgs,
canvas::styling::CanvasStyling,
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,
},
};
@ -986,7 +1001,7 @@ mod test {
let args = BottomArgs::parse_from(["btm"]);
let mut config = ConfigV1::default();
let flags = ConfigFlags {
let flags = FlagConfig {
time_delta: Some("2 min".to_string().into()),
default_time_value: Some("300s".to_string().into()),
rate: Some("1s".to_string().into()),
@ -1016,7 +1031,7 @@ mod test {
let args = BottomArgs::parse_from(["btm"]);
let mut config = ConfigV1::default();
let flags = ConfigFlags {
let flags = FlagConfig {
time_delta: Some("120000".to_string().into()),
default_time_value: Some("300000".to_string().into()),
rate: Some("1000".to_string().into()),
@ -1046,7 +1061,7 @@ mod test {
let args = BottomArgs::parse_from(["btm"]);
let mut config = ConfigV1::default();
let flags = ConfigFlags {
let flags = FlagConfig {
time_delta: Some(120000.into()),
default_time_value: Some(300000.into()),
rate: Some(1000.into()),
@ -1079,15 +1094,17 @@ mod test {
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
// typos/mixing up. Use proc macros to unify on one struct?
// TODO: There's probably a better way to create clap options AND unify together
// to avoid the possibility of typos/mixing up. Use proc macros to unify on
// one struct?
#[test]
fn verify_cli_options_build() {
let app = crate::args::build_cmd();
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"];
for arg in app.get_arguments().collect::<Vec<_>>() {

View file

@ -1,7 +1,7 @@
//! Argument parsing via clap.
//!
//! Note that you probably want to keep this as a single file so the build script doesn't
//! trip all over itself.
//! Note that you probably want to keep this as a single file so the build
//! script doesn't trip all over itself.
// TODO: New sections are misaligned! See if we can get that fixed.
@ -557,7 +557,8 @@ pub struct StyleArgs {
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)]
#[command(next_help_heading = "Other Options", rename_all = "snake_case")]
pub struct OtherArgs {

View file

@ -3,7 +3,7 @@ use std::borrow::Cow;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct ConfigColours {
pub struct ColoursConfig {
pub table_header_color: Option<Cow<'static, str>>,
pub all_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>>,
}
impl ConfigColours {
/// Returns `true` if there is a [`ConfigColours`] that is empty or there isn't one at all.
impl ColoursConfig {
/// Returns `true` if there is a [`ConfigColours`] that is empty or there
/// isn't one at all.
pub fn is_empty(&self) -> bool {
if let Ok(serialized_string) = toml_edit::ser::to_string(self) {
return serialized_string.is_empty();

View file

@ -1,24 +1,29 @@
pub mod cpu;
pub mod disk;
mod ignore_list;
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 temperature::TempConfig;
pub use self::ignore_list::IgnoreList;
use self::{cpu::CpuConfig, layout::Row, process_columns::ProcessConfig};
use super::ConfigColours;
use self::{cpu::CpuConfig, layout::Row, process::ProcessesConfig};
use super::ColoursConfig;
#[derive(Clone, Debug, Default, Deserialize)]
pub struct ConfigV1 {
pub(crate) flags: Option<ConfigFlags>,
pub(crate) colors: Option<ConfigColours>,
pub(crate) flags: Option<FlagConfig>,
pub(crate) colors: Option<ColoursConfig>,
pub(crate) row: Option<Vec<Row>>,
pub(crate) disk_filter: Option<IgnoreList>,
pub(crate) mount_filter: Option<IgnoreList>,
pub(crate) temp_filter: Option<IgnoreList>,
pub(crate) net_filter: Option<IgnoreList>,
pub(crate) processes: Option<ProcessConfig>,
pub(crate) processes: Option<ProcessesConfig>,
pub(crate) disk: Option<DiskConfig>,
pub(crate) temperature: Option<TempConfig>,
pub(crate) network: Option<NetworkConfig>,
pub(crate) cpu: Option<CpuConfig>,
}
@ -42,7 +47,7 @@ impl From<u64> for StringOrNum {
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub(crate) struct ConfigFlags {
pub(crate) struct FlagConfig {
pub(crate) hide_avg_cpu: Option<bool>,
pub(crate) dot_marker: Option<bool>,
pub(crate) temperature_type: Option<String>,

View file

@ -1,6 +1,7 @@
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)]
#[serde(rename_all = "lowercase")]
pub enum CpuDefault {

View 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>,
}

View file

@ -9,7 +9,8 @@ fn default_as_true() -> bool {
pub struct IgnoreList {
#[serde(default = "default_as_true")]
// 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 list: Vec<String>,
#[serde(default)]

View 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>,
}

View file

@ -2,22 +2,23 @@ use serde::Deserialize;
use crate::widgets::ProcWidgetColumn;
/// Process column settings.
/// Process configuration.
#[derive(Clone, Debug, Default, Deserialize)]
pub struct ProcessConfig {
pub struct ProcessesConfig {
/// A list of process widget columns.
#[serde(default)]
pub columns: Vec<ProcWidgetColumn>,
}
#[cfg(test)]
mod test {
use super::ProcessConfig;
use super::ProcessesConfig;
use crate::widgets::ProcWidgetColumn;
#[test]
fn empty_column_setting() {
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());
}
@ -27,7 +28,7 @@ mod test {
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!(
generated.columns,
vec![
@ -49,25 +50,25 @@ mod test {
#[test]
fn process_column_settings_2() {
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]
fn process_column_settings_3() {
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]);
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]);
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]);
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]);
}
}

View 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>,
}

View file

@ -38,9 +38,9 @@ pub const LOG_MEBI_LIMIT_U32: u32 = 20;
pub const LOG_GIBI_LIMIT_U32: u32 = 30;
pub const LOG_TEBI_LIMIT_U32: u32 = 40;
/// Returns a tuple containing the value and the unit in bytes. 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
/// `spacing` is true.
/// Returns a tuple containing the value and the unit in bytes. 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 `spacing` is true.
#[inline]
pub fn get_binary_bytes(bytes: u64) -> (f64, &'static str) {
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.
/// This only supports up to a tera. Note the "single" unit will have a space appended to match the others if
/// `spacing` is true.
/// Returns a tuple containing the value and the unit in bytes. 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 `spacing` is true.
#[inline]
pub fn get_decimal_bytes(bytes: u64) -> (f64, &'static str) {
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.
/// This only supports up to a tebi. Note the "single" unit will have a space appended to match the others if
/// `spacing` is true.
/// This only supports up to a tebi. Note the "single" unit will have a space
/// appended to match the others if `spacing` is true.
#[inline]
pub fn get_binary_prefix(quantity: u64, unit: &str) -> (f64, String) {
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.
/// This only supports up to a tera. Note the "single" unit will have a space appended to match the others if
/// `spacing` is true.
/// This only supports up to a tera. Note the "single" unit will have a space
/// appended to match the others if `spacing` is true.
#[inline]
pub fn get_decimal_prefix(quantity: u64, unit: &str) -> (f64, String) {
match quantity {

View file

@ -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.
pub trait ClampExt {
/// Restrict a value by a lower bound. If the current value is _lower_ than `lower_bound`,
/// it will be set to `_lower_bound`.
/// Restrict a value by a lower bound. If the current value is _lower_ than
/// `lower_bound`, it will be set to `_lower_bound`.
fn clamp_lower(&self, lower_bound: Self) -> Self;
/// Restrict a value by an upper bound. If the current value is _greater_ than `upper_bound`,
/// it will be set to `upper_bound`.
/// Restrict a value by an upper bound. If the current value is _greater_
/// than `upper_bound`, it will be set to `upper_bound`.
fn clamp_upper(&self, upper_bound: Self) -> Self;
}

View file

@ -13,13 +13,16 @@ pub fn init_logger(
let offset = OFFSET.get_or_init(|| {
use time::util::local_offset::Soundness;
// SAFETY: We only invoke this once, quickly, and it should be invoked in a single-thread context.
// We also should only ever hit this logging at all in a debug context which is generally fine,
// SAFETY: We only invoke this once, quickly, and it should be invoked in a
// 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.
unsafe {
// XXX: If we ever DO add general logging as a release feature, evaluate this again and whether this is
// something we want enabled in 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).
// XXX: If we ever DO add general logging as a release feature, evaluate this
// again and whether this is something we want enabled in
// 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);
let res =
@ -39,8 +42,8 @@ pub fn init_logger(
"{}[{}][{}] {}",
offset_time
.format(&time::macros::format_description!(
// The weird "[[[" is because we need to escape a bracket ("[[") to show one "[".
// See https://time-rs.github.io/book/api/format-description.html
// The weird "[[[" is because we need to escape a bracket ("[[") to show
// one "[". See https://time-rs.github.io/book/api/format-description.html
"[[[year]-[month]-[day]][[[hour]:[minute]:[second][subsecond digits:9]]"
))
.unwrap(),

View file

@ -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()))
}
/// 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:
/// `to_ascii_lowercase(a) == to_ascii_lowercase(b) || to_ascii_lowercase(a) == to_ascii_lowercase(c)`,
/// but without allocating and copying temporaries.
/// `to_ascii_lowercase(a) == to_ascii_lowercase(b) || to_ascii_lowercase(a) ==
/// to_ascii_lowercase(c)`, but without allocating and copying temporaries.
///
/// # Examples
///

View file

@ -15,7 +15,8 @@ pub use process_table::*;
pub use temperature_table::*;
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> {
/// How to actually draw the widget to the terminal.
fn draw(&self, f: &mut Frame<'_>, draw_location: Rect, widget_id: u64);

View file

@ -87,13 +87,13 @@ impl DataToCell<CpuWidgetColumn> for CpuWidgetTableData {
let calculated_width = calculated_width.get();
// This is a bit of a hack, but apparently we can avoid having to do any fancy checks
// of showing the "All" on a specific column if the other is hidden by just always
// showing it on the CPU (first) column - if there isn't room for it, it will just collapse
// down.
// This is a bit of a hack, but apparently we can avoid having to do any fancy
// checks of showing the "All" on a specific column if the other is
// hidden by just always showing it on the CPU (first) column - if there
// 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
// it is too small.
// This is the same for the use percentages - we just *always* show them, and
// *always* hide the CPU column if it is too small.
match &self {
CpuWidgetTableData::All => match column {
CpuWidgetColumn::CPU => Some("All".into()),

View file

@ -28,7 +28,8 @@ use crate::{
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 search_state: AppSearchState,
pub is_ignoring_case: bool,
@ -172,7 +173,8 @@ pub struct ProcWidgetState {
/// The state of the togglable table that controls sorting.
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>,
/// A name-to-pid mapping.
@ -426,8 +428,9 @@ impl ProcWidgetState {
/// Update the current table data.
///
/// This function *only* updates the displayed process data. If there is a need to update the actual *stored* data,
/// call it before this function.
/// This function *only* updates the displayed process data. If there is a
/// need to update the actual *stored* data, call it before this
/// function.
pub fn set_table_data(&mut self, data_collection: &DataCollection) {
let data = match &self.mode {
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 contains some descendant that matches.
// - The process's parent (and only parent, not any ancestor) matches.
@ -524,7 +528,8 @@ impl ProcWidgetState {
// Show the entry if it is:
// - 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.
let is_shown = is_process_matching
|| !shown_children.is_empty()
@ -724,7 +729,8 @@ impl ProcWidgetState {
if let Some(grouped_process_harvest) = id_process_mapping.get_mut(id) {
grouped_process_harvest.add(process);
} 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());
}
}
@ -813,8 +819,8 @@ impl ProcWidgetState {
self.force_update_data = true;
}
/// Marks the selected column as hidden, and automatically resets the selected column to the default
/// sort index and order.
/// Marks the selected column as hidden, and automatically resets the
/// selected column to the default sort index and order.
fn hide_column(&mut self, column: ProcWidgetColumn) {
if let Some(index) = self.column_mapping.get_index_of(&column) {
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) {
if let Some(index) = self.column_mapping.get_index_of(&column) {
self.table.set_sort_index(index);
@ -891,12 +898,15 @@ impl ProcWidgetState {
/// 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
/// 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".
/// If count is enabled, we should set the mode to
/// [`ProcWidgetMode::Grouped`], and switch off the User and State
/// 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,
/// and the mode switched to [`ProcWidgetMode::Normal`].
/// Otherwise, if count is disabled, then if the columns exist, the User and
/// State columns should be re-enabled, and the mode switched to
/// [`ProcWidgetMode::Normal`].
pub fn toggle_tab(&mut self) {
if !matches!(self.mode, ProcWidgetMode::Tree { .. }) {
if let Some(index) = self
@ -1005,14 +1015,15 @@ impl ProcWidgetState {
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
/// visible (e.g. off screen).
/// Returns the number of columns *enabled*. Note this differs from
/// *visible* - a column may be enabled but not visible (e.g. off
/// screen).
pub fn num_enabled_columns(&self) -> usize {
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
/// sort table.
/// Sets the [`ProcWidgetState`]'s current sort index to whatever was in the
/// sort table if possible, then closes the sort table.
pub(crate) fn use_sort_table_value(&mut self) {
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.
///
/// Currently, this test doesn't really do much, since we treat these two columns as the same - this test is
/// intended for use later when we might allow both at the same time.
/// Currently, this test doesn't really do much, since we treat these two
/// columns as the same - this test is intended for use later when we
/// might allow both at the same time.
#[test]
fn double_memory_sim_toggle() {
let init_columns = [
@ -1461,8 +1473,9 @@ mod test {
/// 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
/// intended for use later when we might allow both at the same time.
/// Currently, this test doesn't really do much, since we treat these two
/// columns as the same - this test is intended for use later when we
/// might allow both at the same time.
#[test]
fn pid_and_count_sim_toggle() {
let init_columns = [
@ -1498,8 +1511,9 @@ mod test {
/// 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
/// intended for use later when we might allow both at the same time.
/// Currently, this test doesn't really do much, since we treat these two
/// columns as the same - this test is intended for use later when we
/// might allow both at the same time.
#[test]
fn command_name_sim_toggle() {
let init_columns = [

View file

@ -41,9 +41,9 @@ impl From<&'static str> for Id {
}
impl Id {
/// Returns the ID as a lowercase [`String`], with no prefix. This is primarily useful for
/// cases like sorting where we treat everything as the same case (e.g. `Discord` comes before
/// `dkms`).
/// Returns the ID as a lowercase [`String`], with no prefix. This is
/// primarily useful for cases like sorting where we treat everything as
/// the same case (e.g. `Discord` comes before `dkms`).
pub fn to_lowercase(&self) -> String {
match &self.id_type {
IdType::Name(name) => name.to_lowercase(),
@ -306,7 +306,8 @@ impl DataToCell<ProcColumn> for ProcWidgetData {
let calculated_width = calculated_width.get();
// 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 {
ProcColumn::CpuPercent => format!("{:.1}%", self.cpu_usage_percent).into(),
ProcColumn::MemoryVal | ProcColumn::MemoryPercent => self.mem_usage.to_string().into(),

View file

@ -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 predicates::prelude::*;

View file

@ -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
/// the correct runner in some cases, which can be done by inspecting env variables that should only show up while
/// This is required since running binary tests via cross can cause be tricky!
/// 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.
///
/// 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'
/// [linux-runner](https://github.com/cross-rs/cross/blob/main/docker/linux-runner) file.
/// but adapted to work more generally with the architectures supported by
/// bottom after looking through cross' [linux-runner](https://github.com/cross-rs/cross/blob/main/docker/linux-runner) file.
fn cross_runner() -> Option<String> {
const TARGET_RUNNER: &str = "CARGO_TARGET_RUNNER";
const CROSS_RUNNER: &str = "CROSS_RUNNER";