refactor: run nightly format (#2229)

Run nightly format as of 2026-08-31.
This commit is contained in:
Clement Tsang
2026-09-01 10:02:00 +00:00
committed by GitHub
parent 7c472015b5
commit e4ba8c657b
83 changed files with 920 additions and 695 deletions
+3 -2
View File
@@ -98,8 +98,9 @@ fn nightly_version() {
.args(["rev-parse", "--short=8", "HEAD"])
.output()
{
// If we're not building in either, we do the lazy thing and fall back to
// manually grabbing info using git as a command.
// If we're not building in either, we do the lazy thing and
// fall back to manually grabbing info using git
// as a command.
let hash = String::from_utf8(output.stdout).unwrap();
output_nightly_version(version, &hash);
}
+53 -41
View File
@@ -154,10 +154,10 @@ impl App {
pub fn update_data(&mut self) {
let data_source = self.data_store.get_data();
// FIXME: (points_rework_v1) maybe separate PR but would it make more sense to
// store references of data? Would it also make more sense to move the
// "data set" step to the draw step, and make it only set if force
// update is set here?
// FIXME: (points_rework_v1) maybe separate PR but would it make more
// sense to store references of data? Would it also make more
// sense to move the "data set" step to the draw step, and make
// it only set if force update is set here?
for proc in self.states.proc_state.widget_states.values_mut() {
if proc.force_update_data {
proc.set_table_data(data_source);
@@ -203,7 +203,8 @@ impl App {
self.data_store.reset();
// Reset zoom.
// TODO: Make this suck less... should just make it so that calling reset fixes this all (including above too).
// TODO: Make this suck less... should just make it so that calling
// reset fixes this all (including above too).
for widget_state in self.states.cpu_state.widget_states.values_mut() {
widget_state.graph.state_mut().reset_zoom();
}
@@ -299,7 +300,8 @@ impl App {
}
pub fn is_in_any_search(&self) -> bool {
// TODO: This is really hacky, but is fine until we do some smarter things like putting event catching at a per-widget/dialog state.
// TODO: This is really hacky, but is fine until we do some smarter
// things like putting event catching at a per-widget/dialog state.
self.is_in_search_widget() || (self.help_dialog_state.is_help_searching())
}
@@ -367,8 +369,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);
@@ -997,12 +999,10 @@ impl App {
let use_simple_selection = {
cfg_select! {
any(target_os = "linux", target_os = "macos", target_os = "freebsd") => {
!self.app_config_fields.is_advanced_kill
}
_ => {
true
}
any(target_os = "linux", target_os = "macos", target_os = "freebsd") => {
!self.app_config_fields.is_advanced_kill
}
_ => true,
}
};
@@ -1299,8 +1299,8 @@ impl App {
// 1. Send a movement signal in `direction`.
// 2. Check if this new widget we've landed on is hidden. If not, halt.
// 3. If it hidden, loop and either send:
// - A signal equal to the current direction, if it is opposite of the
// reflection.
// - A signal equal to the current direction, if it is opposite of
// the reflection.
// - Reflection direction.
if !self.ignore_normal_keybinds() && !self.is_expanded {
@@ -1359,8 +1359,10 @@ impl App {
BottomWidgetType::BasicTables => {
match &direction {
WidgetDirection::Up => {
// Note this case would fail if it moved up into a hidden
// widget, but it's for basic so whatever, it's all hard-coded
// Note this case would fail if it moved up into
// a hidden
// widget, but it's for basic so whatever, it's
// all hard-coded
// right now anyways...
if let Some(next_new_widget_id) = new_widget.up_neighbour
&& let Some(next_new_widget) =
@@ -1370,12 +1372,15 @@ impl App {
}
}
WidgetDirection::Down => {
// Assuming we're in basic mode (BasicTables), then
// we want to move DOWN to the currently shown widget.
// Assuming we're in basic mode (BasicTables),
// then
// we want to move DOWN to the currently shown
// widget.
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
// 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
@@ -1399,7 +1404,8 @@ impl App {
// It may be hidden...
if let Some((parent_direction, offset)) = &new_widget.parent_reflector {
if direction.is_opposite(parent_direction) {
// Keep going in the current direction if hidden...
// Keep going in the current direction if
// hidden...
// unless we hit a wall of sorts.
let option_next_neighbour_id = match &direction {
WidgetDirection::Left => new_widget.left_neighbour,
@@ -2048,19 +2054,19 @@ impl App {
/// 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
&& let (
@@ -2106,7 +2112,8 @@ impl App {
}
}
self.move_widget_selection(&WidgetDirection::Right);
// Bit extra logic to ensure you always land on a proc widget, not the sort
// Bit extra logic to ensure you always land on a proc widget,
// not the sort
if let BottomWidgetType::ProcSort = &self.current_widget.widget_type {
self.move_widget_selection(&WidgetDirection::Right);
}
@@ -2114,8 +2121,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.process_kill_dialog.is_open() && self.process_kill_dialog.on_click(x, y) {
return;
}
@@ -2163,8 +2170,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
@@ -2196,9 +2203,11 @@ impl App {
self.change_process_position(change);
// If in tree mode, also check to see if this click is
// If in tree mode, also check to see if
// this click is
// on
// the same entry as the already selected one - if it
// the same entry as the already
// selected one - if it
// is,
// then we minimize.
if is_tree_mode && change == 0 {
@@ -2207,7 +2216,8 @@ impl App {
}
}
BottomWidgetType::ProcSort => {
// TODO: [Feature] This could sort if you double click!
// TODO: [Feature] This could sort if you
// double click!
if let Some(proc_widget_state) = self
.states
.proc_state
@@ -2262,7 +2272,8 @@ impl App {
_ => {}
}
} else {
// We might have clicked on a header! Check if we only exceeded the
// 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 {
@@ -2317,7 +2328,8 @@ impl App {
let num_batteries =
self.data_store.get_data().battery_harvest.len();
if itx >= num_batteries {
// range check to keep within current data
// range check to keep within current
// data
battery_widget_state.currently_selected_battery_index =
num_batteries - 1;
} else {
+2 -1
View File
@@ -43,7 +43,8 @@ impl ProcessData {
// We collect all processes that either:
// - Do not have a parent PID (that is, they are orphan processes)
// - Have a parent PID but we don't have the parent (we promote them as orphans)
// - Have a parent PID but we don't have the parent (we promote them as
// orphans)
self.orphan_pids = self
.process_harvest
.iter()
+6 -4
View File
@@ -23,7 +23,8 @@ use crate::{
widgets::{DiskWidgetData, TempWidgetData},
};
/// Because otherwise you can't do lookups for something like `(String, String)` as a key.
/// Because otherwise you can't do lookups for something like `(String, String)`
/// as a key.
trait PairKey {
fn pair(&self) -> (&str, &str);
}
@@ -138,8 +139,8 @@ impl InnerData {
) {
let harvested_time = data.collection_time;
// We must adjust all the network values to their selected type (defaults to
// bits).
// We must adjust all the network values to their selected type
// (defaults to bits).
if matches!(settings.network_unit_type, DataUnit::Byte)
&& let Some(network) = &mut data.network
{
@@ -334,7 +335,8 @@ impl InnerData {
io_read_rate_bytes = Some(0);
io_write_rate_bytes = Some(0);
// TODO: We probably want to also add some cleanup after a while if unused.
// TODO: We probably want to also add some cleanup after a
// while if unused.
self.prev_io.insert(
(disk.mount_point.clone(), checked_name.to_string()),
(io_device.read_bytes, io_device.write_bytes),
+8 -6
View File
@@ -291,13 +291,13 @@ impl TimeSeriesData {
.time
.partition_point(|then| now.duration_since(*then) > max_age);
// Partition point returns the first index that does not match the predicate, so
// minus one.
// Partition point returns the first index that does not match the
// predicate, so minus one.
if partition_point > 0 {
partition_point - 1
} else {
// If the partition point was 0, then it means all values are too new to be
// pruned.
// If the partition point was 0, then it means all values are
// too new to be pruned.
// crate::info!("Skipping prune.");
return;
}
@@ -330,7 +330,8 @@ impl TimeSeriesData {
self.gpu_mem.retain(|_, gpu| {
let _ = gpu.prune(end);
// Remove the entry if it is empty. We can always add it again later.
// Remove the entry if it is empty. We can always add it again
// later.
if gpu.no_elements() {
false
} else {
@@ -343,7 +344,8 @@ impl TimeSeriesData {
self.temperature.retain(|_, data| {
let _ = data.prune(end);
// Remove the entry if it is empty. We can always add it again later.
// Remove the entry if it is empty. We can always add it again
// later.
if data.no_elements() {
false
} else {
+3 -3
View File
@@ -27,9 +27,9 @@ impl Filter {
#[inline]
pub(crate) fn should_keep(&self, entry: &str) -> bool {
if self.has_match(entry) {
// 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
+4 -2
View File
@@ -175,7 +175,8 @@ impl BottomLayout {
if let Some(current_row) = layout_mapping
.get(&(row_height_percentage_start, row_height_percentage_end))
{
// First check for within the same col_row for left and right
// First check for within the same col_row for left
// and right
if let Some(current_col) = current_row
.1
.get(&(col_width_percentage_start, col_width_percentage_end))
@@ -316,7 +317,8 @@ impl BottomLayout {
)
.next_back()
{
// Now check each widget_width and pick the best
// Now check each widget_width and pick the
// best
for candidate_widget in &(to_up.1).1 {
let mut current_best_distance = 0;
let mut current_best_widget_id = widget.widget_id;
+17 -13
View File
@@ -111,8 +111,8 @@ impl Painter {
self.previous_width = terminal_width;
}
// TODO: We should probably remove this or make it done elsewhere, not the
// responsibility of the app.
// TODO: We should probably remove this or make it done elsewhere,
// not the responsibility of the app.
if app_state.should_get_widget_bounds() {
// If we're force drawing, reset ALL mouse boundaries.
for widget in app_state.widget_map.values_mut() {
@@ -176,11 +176,13 @@ impl Painter {
.constraints([Constraint::Percentage(100)])
.areas(vertical_dialog_chunk)
} else {
// We calculate this so that the margins never have to split an odd number.
// We calculate this so that the margins never have to split
// an odd number.
let len = if (dialog_width.saturating_sub(MAX_TEXT_LENGTH)) % 2 == 0 {
MAX_TEXT_LENGTH
} else {
// It can only be 1 if the difference is greater than 1, so this is fine.
// It can only be 1 if the difference is greater than 1,
// so this is fine.
MAX_TEXT_LENGTH + 1
};
@@ -197,8 +199,9 @@ impl Painter {
f.buffer_mut()
.set_style(area, self.styles.general_widget_style);
// FIXME: For width, just limit to a max size or full width. For height, not
// sure. Maybe pass max and let child handle?
// FIXME: For width, just limit to a max size or full width. For
// height, not sure. Maybe pass max and let
// child handle?
let horizontal_padding = if terminal_width < 100 { 0 } else { 5 };
let vertical_padding = if terminal_height < 100 { 0 } else { 5 };
@@ -300,8 +303,8 @@ impl Painter {
let data = app_state.data_store.get_data();
let actual_cpu_data_len = data.cpu_harvest.len();
// 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.is_multiple_of(4))
@@ -425,9 +428,10 @@ impl Painter {
self.draw_frozen_indicator(f, frozen_draw_loc);
}
// A two-pass algorithm - get layouts using constraints (first pass),
// then pass each layout to the corresponding widget (second pass).
// Note that layouts are already cached in ratatui, so we don't need
// A two-pass algorithm - get layouts using constraints (first
// pass), then pass each layout to the
// corresponding widget (second pass). Note that
// layouts are already cached in ratatui, so we don't need
// to do it manually!
let base = Layout::vertical(self.layout.rows.iter().map(|r| r.constraint))
.split(terminal_size);
@@ -457,8 +461,8 @@ impl Painter {
}
})?;
// We also move it back to the origin to try rand avoid wasting CPU cycles for things like kitty's
// `cursor_trail` calculations.
// We also move it back to the origin to try rand avoid wasting CPU
// cycles for things like kitty's `cursor_trail` calculations.
let backend = terminal.backend_mut();
backend.set_cursor_position(Position::ORIGIN)?;
backend.flush()?;
+6 -4
View File
@@ -222,8 +222,9 @@ where
} else {
total_width_left = total_width_left.saturating_sub(new_width + COLUMN_SPACING);
// SAFETY: This is safe as we call `stop_allocating_space` which checks that
// the value pushed is greater than zero.
// SAFETY: This is safe as we call `stop_allocating_space` which
// checks that the value pushed is greater than
// zero.
unsafe {
calculated_widths.push(NonZeroU16::new_unchecked(new_width));
}
@@ -276,8 +277,9 @@ mod test {
}
}
/// Ensure that the [`DataTableColumn`] implementation for [`Column`] calls the right method.
/// Yes, this is a somewhat meaningless test but it may catch a regression if this happens again in the future
/// Ensure that the [`DataTableColumn`] implementation for [`Column`] calls
/// the right method. Yes, this is a somewhat meaningless test but it
/// may catch a regression if this happens again in the future
/// during a refactor.
///
/// See <https://github.com/ClementTsang/bottom/issues/2159> for details of the issue.
+2 -1
View File
@@ -209,7 +209,8 @@ where
if !self.data.is_empty() || !self.first_draw {
if self.first_draw {
// TODO: Doing it this way is fine, but it could be done better (e.g. showing
// 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;
if let Some(first_index) = self.first_index {
+3 -2
View File
@@ -124,8 +124,9 @@ 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
// 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())
+10 -6
View File
@@ -60,22 +60,26 @@ impl DataTableState {
self.display_start_index = match scroll_direction {
ScrollDirection::Down => {
if current_scroll_position < start_index + num_rows {
// If, using the current scroll position, we can see the element
// (so within that and + num_rows) just reuse the current previously
// If, using the current scroll position, we can see the
// element (so within that and +
// num_rows) just reuse the current previously
// scrolled position.
start_index
} else if current_scroll_position >= num_rows {
// If the current position past the last element visible in the list,
// then skip until we can see that element.
// If the current position past the last element visible in
// the list, then skip until we can see
// that element.
current_scroll_position - num_rows + 1
} else {
// Else, if it is not past the last element visible, do not omit anything.
// Else, if it is not past the last element visible, do not
// omit anything.
0
}
}
ScrollDirection::Up => {
if current_scroll_position <= start_index {
// If it's past the first element, then show from that element downwards
// If it's past the first element, then show from that
// element downwards
current_scroll_position
} else if current_scroll_position >= start_index + num_rows {
current_scroll_position - num_rows + 1
+5 -3
View File
@@ -22,7 +22,8 @@ pub struct SearchInputStyles {
pub hint_style: Style,
}
/// Build a query span from a [`InputFieldState`]. `available_width` is the terminal column width.
/// Build a query span from a [`InputFieldState`]. `available_width` is the
/// terminal column width.
pub fn build_query_spans(
input_field_state: &InputFieldState, available_width: usize, is_on_widget: bool,
cursor_style: Style, text_style: Style,
@@ -30,8 +31,9 @@ pub fn build_query_spans(
let query = input_field_state.current_query();
if !is_on_widget {
// This is easier - we just need to get a range of graphemes, rather than
// dealing with possibly inserting a cursor (as none is shown!)
// This is easier - we just need to get a range of graphemes, rather
// than dealing with possibly inserting a cursor (as none is
// shown!)
return vec![Span::styled(query.to_string(), text_style)];
}
+4 -3
View File
@@ -109,7 +109,8 @@ pub struct TimeGraph<'a> {
impl TimeGraph<'_> {
/// Generates the [`Axis`] for the x-axis.
fn generate_x_axis(&self) -> Axis<'_> {
// Due to how we display things, we need to adjust the time bound values.
// Due to how we display things, we need to adjust the time bound
// values.
let adjusted_x_bounds = AxisBound::Min(self.x_min);
if self.hide_x_labels {
@@ -155,8 +156,8 @@ impl TimeGraph<'_> {
pub fn draw<F: Copy + Default + Into<f64>>(
&self, f: &mut Frame<'_>, draw_loc: Rect, graph_data: Vec<GraphData<'_, F>>,
) {
// TODO: (points_rework_v1) can we reduce allocations in the underlying graph by
// saving some sort of state?
// TODO: (points_rework_v1) can we reduce allocations in the underlying
// graph by saving some sort of state?
let x_axis = self.generate_x_axis();
let y_axis = self.generate_y_axis();
+18 -16
View File
@@ -400,8 +400,8 @@ pub(crate) enum ChartScaling {
impl ChartScaling {
/// Scale a value.
pub(super) fn scale(&self, value: f64) -> f64 {
// Remember to do saturating log checks as otherwise 0.0 becomes inf, and you
// get gaps!
// Remember to do saturating log checks as otherwise 0.0 becomes inf,
// and you get gaps!
match self {
ChartScaling::Linear => value,
ChartScaling::Log10 => saturating_log10(value),
@@ -672,8 +672,9 @@ impl<'a, F: Copy + Default + Into<f64>> TimeChart<'a, F> {
let first_label_width = first_x_label.content.width() as u16;
let width_left_of_y_axis = match self.x_axis.labels_alignment {
Alignment::Left => {
// The last character of the label should be below the Y-Axis when it exists,
// not on its left
// The last character of the label should be below the
// Y-Axis when it exists, not on its
// left
let y_axis_offset = u16::from(has_y_axis);
first_label_width.saturating_sub(y_axis_offset)
}
@@ -682,8 +683,8 @@ impl<'a, F: Copy + Default + Into<f64>> TimeChart<'a, F> {
};
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)
}
@@ -721,8 +722,8 @@ impl<'a, F: Copy + Default + Into<f64>> TimeChart<'a, F> {
Self::render_label(buf, first_label, label_area, label_alignment);
for (i, label) in labels[1..labels.len() - 1].iter().enumerate() {
// We add 1 to x (and width-1 below) to leave at least one space before each
// intermediate labels
// We add 1 to x (and width-1 below) to leave at least one space
// before each intermediate labels
let x = graph_area.left() + (i + 1) as u16 * width_between_ticks + 1;
let label_area = Rect::new(x, y, width_between_ticks.saturating_sub(1), 1);
@@ -731,7 +732,8 @@ impl<'a, F: Copy + Default + Into<f64>> TimeChart<'a, F> {
let x = graph_area.right() - width_between_ticks;
let label_area = Rect::new(x, y, width_between_ticks, 1);
// The last label should be aligned Right to be at the edge of the graph area
// The last label should be aligned Right to be at the edge of the graph
// area
Self::render_label(buf, last_label, label_area, Alignment::Right);
}
@@ -803,9 +805,9 @@ impl<F: Copy + Default + Into<f64>> Widget for TimeChart<'_, F> {
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
// graph 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 graph area (i.e legend and axis names).
let Some(original_style) = buf.cell((area.left(), area.top())).map(|cell| cell.style())
else {
return;
@@ -1490,8 +1492,8 @@ mod tests {
#[test]
fn legend_truncates_entries_by_height() {
// 5 datasets but only room for 3 entries in the legend (height=5, so 5-2=3
// entries).
// 5 datasets but only room for 3 entries in the legend (height=5, so
// 5-2=3 entries).
let datasets: Vec<_> = (0..5)
.map(|i| Dataset::default().name(format!("D{i}")))
.collect();
@@ -1591,8 +1593,8 @@ mod tests {
assert!(layout.legend_area.is_some());
let legend = layout.legend_area.unwrap();
// Width should be based on "AB"/"CD" (2 chars) + 2 borders = 4, not the long
// name.
// Width should be based on "AB"/"CD" (2 chars) + 2 borders = 4, not the
// long name.
assert_eq!(legend.width, 4);
}
}
@@ -138,22 +138,24 @@ impl<const W: usize, const H: usize> Grid for PatternGrid<W, H> {
// The ratatui/tui-rs implementation; this gives a more merged
// look, but it also makes it a bit harder to read in some cases.
//
// using get_mut here because we are indexing the vector with usize values
// and we want to make sure we don't panic if the index is out of bounds
// if let Some(cell) = self.cells.get_mut(index) {
// cell.pattern |= 1u8 << ((x % W) + W * (y % H));
// cell.color = Some(color);
// using get_mut here because we are indexing the vector with usize
// values and we want to make sure we don't panic if the index
// is out of bounds if let Some(cell) =
// self.cells.get_mut(index) { cell.pattern |= 1u8 << ((x %
// W) + W * (y % H)); cell.color = Some(color);
// }
// Custom implementation do distinguish between lines better.
if let Some(cell) = self.cells.get_mut(index) {
if let Some(curr_color) = &mut cell.color {
if *curr_color != color {
// If the colour doesn't match, then reset the colour and cell.
// If the colour doesn't match, then reset the colour and
// cell.
*curr_color = color;
cell.pattern = 1u8 << ((x % W) + W * (y % H));
} else {
// If it does match, then combine it with the previous underlying cell.
// If it does match, then combine it with the previous
// underlying cell.
cell.pattern |= 1u8 << ((x % W) + W * (y % H));
}
} else {
@@ -306,8 +308,9 @@ impl Grid for CharGrid {
fn paint(&mut self, x: usize, y: usize, color: Color) {
let index = y.saturating_mul(self.width as usize).saturating_add(x);
// using get_mut here because we are indexing the vector with usize values
// and we want to make sure we don't panic if the index is out of bounds
// using get_mut here because we are indexing the vector with usize
// values and we want to make sure we don't panic if the index
// is out of bounds
if let Some(c) = self.cells.get_mut(index) {
*c = Some(color);
}
@@ -380,39 +383,41 @@ 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
// cell:
// 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
// 2. upper: reset, lower: color => '▄' fg: lower color / bg: reset
// 3. upper: color, lower: reset => '▀' fg: upper color / bg: reset
// 4. upper: color, lower: color => '▀' fg: upper color / bg: lower color
// 4. upper: color, lower: color => '▀' fg: upper color / bg: lower
// color
//
// Note that because the foreground reset color (i.e. default foreground color)
// is usually 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.
// first we join each adjacent row together to get an iterator that contains
// vertical pairs of pixels, with the lower row being the first element
// in the pair
// first we join each adjacent row together to get an iterator that
// contains vertical pairs of pixels, with the lower row being
// the first element in the pair
let vertical_color_pairs = self
.pixels
.iter()
.tuples()
.flat_map(|(upper_row, lower_row)| zip(upper_row, lower_row));
// Then we determine the character to print for each pair, along with the color
// of the foreground and background.
// Then we determine the character to print for each pair, along with
// the color of the foreground and background.
let contents = vertical_color_pairs
.map(|(upper, lower)| {
let (symbol, fg, bg) = match (upper, lower) {
@@ -16,17 +16,19 @@ impl<F: Copy + Default + Into<f64>> TimeChart<'_, F> {
// - Last point wins for what gets drawn.
// - We set _all_ points for all datasets before actually rendering.
//
// By doing this, it's a bit more efficient from my experience than looping
// over each dataset and rendering a new layer each time.
// By doing this, it's a bit more efficient from my experience than
// looping over each dataset and rendering a new layer each
// time.
//
// 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
// issue but it can happen in some cases).
// 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 {
let Data::Some { times, values } = dataset.data else {
@@ -40,24 +42,27 @@ impl<F: Copy + Default + Into<f64>> TimeChart<'_, F> {
let color = dataset.style.fg.unwrap_or(Color::Reset);
let left_edge = self.x_axis.bounds.get_bounds()[0];
// TODO: (points_rework_v1) Can we instead modify the range so it's based on the
// epoch rather than having to convert? TODO: (points_rework_v1) Is
// this efficient? Or should I prune using take_while first?
// TODO: (points_rework_v1) Can we instead modify the range so it's
// based on the epoch rather than having to convert?
// TODO: (points_rework_v1) Is this efficient? Or should
// I prune using take_while first?
for (curr, next) in values
.iter_along_base(times)
.rev()
.map(|(&time, &val)| {
let from_start = -(current_time.duration_since(time).as_millis() as f64);
// XXX: Should this be generic over dataset.graph_type instead? That would allow
// us to move transformations behind a type - however, that
// XXX: Should this be generic over dataset.graph_type
// instead? That would allow us to move
// transformations behind a type - however, that
// also means that there's some complexity added.
(from_start, self.scaling.scale(val.into()))
})
.tuple_windows()
{
if curr.0 == left_edge {
// The current point hits the left edge. Draw just the current point and halt.
// The current point hits the left edge. Draw just the
// current point and halt.
ctx.draw(&Points {
coords: &[curr],
color,
@@ -65,8 +70,8 @@ impl<F: Copy + Default + Into<f64>> TimeChart<'_, F> {
break;
} else if next.0 < left_edge {
// The next point goes past the left edge. Interpolate a point + the line and
// halt.
// The next point goes past the left edge. Interpolate a
// point + the line and halt.
let interpolated = interpolate_point(&next, &curr, left_edge);
ctx.draw(&CanvasLine {
+10 -9
View File
@@ -115,8 +115,8 @@ impl Painter {
.horizontal_margin(1)
.split(draw_loc);
// Done like this for now since it's easier to just manually paint instead of
// dealing with blocks.
// Done like this for now since it's easier to just manually paint
// instead of dealing with blocks.
f.buffer_mut()
.set_style(draw_loc, self.styles.general_widget_style);
@@ -133,18 +133,19 @@ 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.
// - 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.
// - 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.
// - 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));
+10 -7
View File
@@ -96,7 +96,8 @@ impl Painter {
if !matched_body.is_empty() || header_matches {
if let Some(header) = header_str {
// Don't insert the space if there's nothing above anyway.
// Don't insert the space if there's nothing above
// anyway.
if !lines.is_empty() {
lines.push(Line::from(Span::default()));
}
@@ -166,14 +167,16 @@ impl Painter {
};
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.
let inner = block.inner(content_area);
app_state.help_dialog_state.height = inner.height;
// The overflow buffer is used to account for lines that wrap onto multiple lines,
// so we can properly calculate the max scroll index later.
// The overflow buffer is used to account for lines that wrap onto
// multiple lines, so we can properly calculate the max
// scroll index later.
let mut overflow_buffer = 0;
let paragraph_width: usize = max(inner.width, 1).into();
let mut prev_section_len = 0;
@@ -422,8 +425,8 @@ mod tests {
check_spans(&lines[0], ["Hi 你好!", "🇨🇦", ""]);
}
/// Shows that we only match the first occurrence in the line. This is expected behaviour as we're passing
/// in separate strings.
/// Shows that we only match the first occurrence in the line. This is
/// expected behaviour as we're passing in separate strings.
#[test]
fn test_unicode_3() {
let mut lines = Vec::new();
+49 -24
View File
@@ -269,7 +269,11 @@ impl ProcessKillDialog {
for pid in pids {
if let Err(err) = process_killer::kill_process_given_pid(pid) {
self.state = ProcessKillDialogState::Error { process_name, pid: Some(pid), err: err.to_string() };
self.state = ProcessKillDialogState::Error {
process_name,
pid: Some(pid),
err: err.to_string(),
};
break;
}
}
@@ -279,15 +283,26 @@ impl ProcessKillDialog {
for pid in pids {
// Send a SIGTERM by default.
if let Err(err) = process_killer::kill_process_given_pid(pid, DEFAULT_KILL_SIGNAL) {
self.state = ProcessKillDialogState::Error { process_name, pid: Some(pid), err: err.to_string() };
if let Err(err) = process_killer::kill_process_given_pid(
pid,
DEFAULT_KILL_SIGNAL,
) {
self.state = ProcessKillDialogState::Error {
process_name,
pid: Some(pid),
err: err.to_string(),
};
break;
}
}
}
_ => {
self.state = ProcessKillDialogState::Error { process_name, pid: None, err: "Killing processes is not supported on this platform.".into() };
self.state = ProcessKillDialogState::Error {
process_name,
pid: None,
err: "Killing processes is not supported on this platform."
.into(),
};
}
}
}
@@ -337,7 +352,8 @@ impl ProcessKillDialog {
};
if new >= SIGNAL_TEXT.len() {
// If the new value is too large, then just assume we instead
// If the new value is too large, then just
// assume we instead
// want the value itself.
state.select(Some(value as usize));
self.last_char = Some((c, Instant::now()));
@@ -582,11 +598,16 @@ impl ProcessKillDialog {
} else {
cfg_select! {
any(target_os = "linux", target_os = "macos", target_os = "freebsd") => {
ButtonState::Signals { state: ListState::default().with_selected(Some(DEFAULT_KILL_SIGNAL)), last_button_draw_area: Rect::default() }
}
_ => {
ButtonState::Simple { yes: false, last_yes_button_area: Rect::default(), last_no_button_area: Rect::default()}
ButtonState::Signals {
state: ListState::default().with_selected(Some(DEFAULT_KILL_SIGNAL)),
last_button_draw_area: Rect::default(),
}
}
_ => ButtonState::Simple {
yes: false,
last_yes_button_area: Rect::default(),
last_no_button_area: Rect::default(),
},
}
};
@@ -607,8 +628,8 @@ impl ProcessKillDialog {
}
pub fn handle_redraw(&mut self) {
// FIXME: Not sure if we need this. We can probably handle this better in the
// draw function later.
// FIXME: Not sure if we need this. We can probably handle this better
// in the draw function later.
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
{
@@ -695,16 +716,17 @@ impl ProcessKillDialog {
// A list of options, displayed vertically.
const SIGNAL_TEXT_LEN: u16 = SIGNAL_TEXT.len() as u16;
// Make the rect only as big as it needs to be, which is the height of the text,
// the buttons, and up to 2 spaces (margin and space between), and the size of
// Make the rect only as big as it needs to be, which is the
// height of the text, the buttons, and up to 2
// spaces (margin and space between), and the size of
// the block.
let [draw_area] =
Layout::vertical([Constraint::Max(num_lines + SIGNAL_TEXT_LEN + 2 + 3)])
.flex(Flex::Center)
.areas(draw_area);
// Now we need to divide the block into one area for the paragraph,
// and one for the buttons.
// Now we need to divide the block into one area for the
// paragraph, and one for the buttons.
let [text_draw_area, button_draw_area] = Layout::vertical([
Constraint::Max(num_lines),
Constraint::Max(SIGNAL_TEXT_LEN),
@@ -733,7 +755,8 @@ impl ProcessKillDialog {
Span::styled(signal, style)
}));
// This is kinda dumb how you have to set the constraint, but ok.
// This is kinda dumb how you have to set the constraint, but
// ok.
const LONGEST_SIGNAL_TEXT_LENGTH: u16 = const {
let mut i = 0;
let mut max = 0;
@@ -771,14 +794,15 @@ impl ProcessKillDialog {
last_yes_button_area,
last_no_button_area,
} => {
// Make the rect only as big as it needs to be, which is the height of the text,
// the buttons, and up to 3 spaces (margin and space between) + 2 for block.
// Make the rect only as big as it needs to be, which is the
// height of the text, the buttons, and up to 3
// spaces (margin and space between) + 2 for block.
let [draw_area] = Layout::vertical([Constraint::Max(num_lines + 1 + 3 + 2)])
.flex(Flex::Center)
.areas(draw_area);
// Now we need to divide the block into one area for the paragraph,
// and one for the buttons.
// Now we need to divide the block into one area for the
// paragraph, and one for the buttons.
let [text_area, button_area] =
Layout::vertical([Constraint::Max(num_lines), Constraint::Length(1)])
.flex(Flex::SpaceEvenly)
@@ -849,8 +873,8 @@ impl ProcessKillDialog {
pub fn draw(&mut self, f: &mut Frame<'_>, draw_area: Rect, styles: &Styles) {
// The idea is:
// - Use as big of a dialog box as needed (within the maximal draw loc)
// - So the non-button ones are going to be smaller... probably whatever the
// height of the text is.
// - So the non-button ones are going to be smaller... probably
// whatever the height of the text is.
// - Meanwhile for the button one, it'll likely be full height if it's
// "advanced" kill.
@@ -863,7 +887,8 @@ impl ProcessKillDialog {
match &mut self.state {
ProcessKillDialogState::NotEnabled => {}
ProcessKillDialogState::Selecting(state) => {
// Draw a text box. If buttons are yes/no, fit it, otherwise, use max space.
// Draw a text box. If buttons are yes/no, fit it, otherwise,
// use max space.
Self::draw_selecting(f, draw_area, styles, state);
}
ProcessKillDialogState::Error {
+2 -1
View File
@@ -110,7 +110,8 @@ 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
// +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;
+7 -6
View File
@@ -28,10 +28,11 @@ impl Painter {
// of columns to draw all CPUs. Ideally, as well, we want to not have
// to ever scroll.
//
// **General logic** - count number of elements in cpu_data. Then see how
// 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
// **General logic** - count number of elements in cpu_data. Then see
// how 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.
f.render_widget(
@@ -59,8 +60,8 @@ impl Painter {
let [cores_loc, mut avg_loc] =
Layout::vertical([Constraint::Min(0), Constraint::Length(1)]).areas(draw_loc);
// The cores section all have horizontal margin, so to line up with the cores we
// need to add some margin ourselves.
// The cores section all have horizontal margin, so to line up with
// the cores we need to add some margin ourselves.
avg_loc.x += 1;
avg_loc.width -= 2;
+7 -6
View File
@@ -127,7 +127,8 @@ impl Painter {
let time = &data.time_series_data.time;
if current_scroll_position == ALL_POSITION {
// This case ensures the other cases cannot have the position be equal to 0.
// This case ensures the other cases cannot have the position be
// equal to 0.
cpu_points
.iter()
@@ -145,9 +146,9 @@ impl Painter {
.rev()
.collect()
} else if let Some(CpuData { .. }) = cpu_entries.get(current_scroll_position - 1) {
// We generally subtract one from current scroll position because of the all
// entry. TODO: Do this a bit better (e.g. we can just do if let
// Some(_) = cpu_points.get())
// We generally subtract one from current scroll position because of
// the all entry. TODO: Do this a bit better (e.g. we
// can just do if let Some(_) = cpu_points.get())
let style = if show_avg_cpu && current_scroll_position == AVG_POSITION {
self.styles.avg_cpu_colour
@@ -241,8 +242,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;
+22 -13
View File
@@ -67,8 +67,9 @@ impl Painter {
.is_some_and(|d| has_data_in_window(d, times, current_display_time))
};
// If there is a mount point and we're in mount legend mode, it must be non-empty
// (i.e. actually mounted), or we will short-circuit and ignore it.
// If there is a mount point and we're in mount legend mode,
// it must be non-empty (i.e. actually
// mounted), or we will short-circuit and ignore it.
if let Some(mount_point) = mount_map.get(name.as_str()) {
match legend_type {
DiskGraphLegend::Disk => true,
@@ -77,11 +78,14 @@ impl Painter {
} else {
match legend_type {
DiskGraphLegend::Disk => {
// Otherwise, it may have _previously_ been a valid mount point, so keep showing it until it ages out.
// Otherwise, it may have _previously_ been a
// valid mount point, so keep showing it until
// it ages out.
has_read_data()
}
DiskGraphLegend::Mount => {
// Since it would be misleading in this case, just skip it in mount mode.
// Since it would be misleading in this case,
// just skip it in mount mode.
false
}
}
@@ -114,12 +118,14 @@ impl Painter {
};
// Removed devices still visible in the window show "N/A".
// TODO: Maybe should make it so the colour is based on entry name? As then it may shift.
// TODO: Maybe should make it so the colour is based on entry name?
// As then it may shift.
let read_colours = &self.styles.disk_io_read_colour_styles;
let write_colours = &self.styles.disk_io_write_colour_styles;
// Pad the device/mount labels to the widest visible one so the rate columns
// line up in the legend (the rate itself is already fixed-width).
// Pad the device/mount labels to the widest visible one so the rate
// columns line up in the legend (the rate itself is
// already fixed-width).
let name_width = device_names
.iter()
.map(|name| match legend_type {
@@ -140,7 +146,8 @@ impl Painter {
DiskGraphLegend::Disk => name.as_str(),
DiskGraphLegend::Mount => match mount_map.get(name.as_str()).copied() {
Some(mount) => mount,
// This wouldn't trigger anyway, we filter out devices without mount points in mount legend mode.
// This wouldn't trigger anyway, we filter out devices without mount points
// in mount legend mode.
None => continue,
},
};
@@ -161,7 +168,8 @@ impl Painter {
write_colours[idx % write_colours.len()]
};
// TODO: Combine into one line; probably need to add some kind of multi-styled GraphData.
// TODO: Combine into one line; probably need to add some kind
// of multi-styled GraphData.
if let Some(values) = read_values {
let rate = if is_active {
format_rate_fixed(values.last().copied().unwrap_or(0.0))
@@ -237,8 +245,8 @@ impl Painter {
}
/// Returns true if `data` has at least one real (non-gap) data point within the
/// visible time window defined by `current_display_time` milliseconds from the end
/// of `times`.
/// visible time window defined by `current_display_time` milliseconds from the
/// end of `times`.
fn has_data_in_window<F: Copy + Default + Into<f64>>(
data: &ChunkedData<F>, times: &[Instant], current_display_time: u64,
) -> bool {
@@ -252,8 +260,9 @@ fn has_data_in_window<F: Copy + Default + Into<f64>>(
.is_some_and(|(t, _)| *t >= oldest)
}
/// Format a byte/s rate as a fixed-width string (always 11 chars, right-aligned)
/// to keep legend labels a stable width and prevent legend box shifting between frames.
/// Format a byte/s rate as a fixed-width string (always 11 chars,
/// right-aligned) to keep legend labels a stable width and prevent legend box
/// shifting between frames.
fn format_rate_fixed(bytes_per_sec: f64) -> String {
let (val, unit) = if bytes_per_sec < KIBI_LIMIT_F64 {
(bytes_per_sec, "B/s")
+5 -3
View File
@@ -68,8 +68,9 @@ impl Painter {
let mut size = 1;
let data = app_state.data_store.get_data();
// TODO: is this optimization really needed...? This just pre-allocates a vec,
// but it'll probably never be that big...
// TODO: is this optimization really needed...? This just
// pre-allocates a vec, but it'll probably never
// be that big...
if data.swap_harvest.is_some() {
size += 1; // add capacity for SWAP
@@ -89,7 +90,8 @@ impl Painter {
let time_series = &data.time_series_data;
let time = &time_series.time;
// TODO: Add a "no data" option here/to time graph if there is no entries
// TODO: Add a "no data" option here/to time graph if there is
// no entries
graph_data(
&mut points,
"RAM",
+5 -3
View File
@@ -56,8 +56,9 @@ impl Painter {
avg_tx_packet_size,
} = calculate_packet_info(network_data, use_binary_prefix);
// TODO: Stylize packet stuff later with something else? Or maybe make it so
// total is now (by default) just bolded RX/TX? I doubt anyone cares...
// TODO: Stylize packet stuff later with something else? Or maybe
// make it so total is now (by default) just bolded
// RX/TX? I doubt anyone cares...
let rx_packet_rate_label = format!("RX Pkt: {}pkt/s", rx_packet_rate);
let tx_packet_rate_label = format!("TX Pkt: {}pkt/s", tx_packet_rate);
let avg_rx_packet_size_label = format!(
@@ -72,7 +73,8 @@ impl Painter {
if draw_loc.width < 60 {
// 4 rows x 2 columns layout
// Column 1: RX, TX, Total RX, Total TX (top to bottom)
// Column 2: RX Packets, TX Packets, AVG RX, AVG TX (top to bottom)
// Column 2: RX Packets, TX Packets, AVG RX, AVG TX (top to
// bottom)
let grid_loc = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
+36 -29
View File
@@ -45,8 +45,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 =
@@ -84,8 +84,8 @@ impl Painter {
let use_old_network_legend = app_state.app_config_fields.use_old_network_legend;
let legend_constraints = if use_old_network_legend {
// Always hide it. Note that I could pass in `None` to the position as well but
// eh this works.
// Always hide it. Note that I could pass in `None` to the
// position as well but eh this works.
LegendConstraints {
width: Constraint::Length(0),
height: Constraint::Length(0),
@@ -99,7 +99,8 @@ impl Painter {
}
};
// TODO: Add support for clicking on legend to only show that value on chart.
// TODO: Add support for clicking on legend to only show that value
// on chart.
let use_binary_prefix = app_state.app_config_fields.network_use_binary_prefix;
let unit_type = app_state.app_config_fields.network_unit_type;
let unit = match unit_type {
@@ -140,9 +141,10 @@ impl Painter {
const MAX_LEGEND_WIDTH: u16 = 70;
let approx_legend_width = draw_loc.width * 3 / 4;
// FIXME: I'm not really a huge fan of this - I think it may be better to just
// not support this and allow for more easily spawning a
// separate legend table (basically old legend).
// FIXME: I'm not really a huge fan of this - I think it may be
// better to just not support this and allow for
// more easily spawning a separate legend table
// (basically old legend).
if app_state.app_config_fields.network_show_packets
&& approx_legend_width > MAX_LEGEND_WIDTH
{
@@ -329,20 +331,21 @@ impl Painter {
fn adjust_network_data_point(max_entry: f64, config: &AppConfigFields) -> (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.
// 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.
//
@@ -382,8 +385,9 @@ fn adjust_network_data_point(max_entry: f64, config: &AppConfigFields) -> (f64,
};
let max_entry_upper = if max_entry == 0.0 {
// If it's 0, then just use a very low value so the labels aren't just "0.0" 4
// times. This _also_ prevents the y-axis height range ever
// If it's 0, then just use a very low value so the labels
// aren't just "0.0" 4 times. This _also_
// prevents the y-axis height range ever
// being 0.
1.0
} else {
@@ -420,9 +424,10 @@ fn adjust_network_data_point(max_entry: f64, config: &AppConfigFields) -> (f64,
}
};
// 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![
format!("0{unit_prefix}{unit_type}"),
@@ -432,8 +437,9 @@ fn adjust_network_data_point(max_entry: f64, config: &AppConfigFields) -> (f64,
]
.into_iter()
.map(|s| {
// Pull 5 as the longest legend value is generally going to be 5 digits (if they
// somehow hit over 5 terabits per second)
// Pull 5 as the longest legend value is generally going to be 5
// digits (if they somehow hit over 5 terabits
// per second)
format!("{s:>5}")
})
.collect();
@@ -447,8 +453,8 @@ fn adjust_network_data_point(max_entry: f64, config: &AppConfigFields) -> (f64,
(LOG_MEGA_LIMIT, LOG_GIGA_LIMIT, LOG_TERA_LIMIT)
};
// Remember to do saturating log checks as otherwise 0.0 becomes inf, and you
// get gaps!
// Remember to do saturating log checks as otherwise 0.0 becomes
// inf, and you get gaps!
let max_entry = if use_binary_prefix {
saturating_log2(max_entry)
} else {
@@ -534,7 +540,8 @@ fn adjust_network_data_point(max_entry: f64, config: &AppConfigFields) -> (f64,
],
)
} else {
// I really doubt anyone's transferring beyond petabyte speeds...
// I really doubt anyone's transferring beyond petabyte
// speeds...
(
if use_binary_prefix {
LOG_PEBI_LIMIT
+2 -6
View File
@@ -177,12 +177,8 @@ impl Painter {
// TODO: [MOVEMENT] Movement support for these in search
let (case, whole, regex) = {
cfg_select! {
target_os = "macos" => {
("Case(F1)", "Whole(F2)", "Regex(F3)")
}
_ => {
("Case(Alt+C)", "Whole(Alt+W)", "Regex(Alt+R)")
}
target_os = "macos" => ("Case(F1)", "Whole(F2)", "Regex(F3)"),
_ => ("Case(Alt+C)", "Whole(Alt+W)", "Regex(Alt+R)"),
}
};
let option_text = Line::from(vec![
+8 -6
View File
@@ -314,8 +314,8 @@ impl DataCollector {
/// - Disk (Windows, FreeBSD)
/// - 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.
if self.widgets_to_harvest.use_cpu || self.widgets_to_harvest.use_proc {
self.sys.system.refresh_cpu_all();
@@ -480,9 +480,10 @@ impl DataCollector {
if self.widgets_to_harvest.use_proc
&& 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);
}
@@ -524,7 +525,8 @@ impl DataCollector {
if arc.0.used_bytes > arc.1 {
#[cfg(target_os = "linux")]
{
// Keep arc min like htop; the subtraction below won't underflow because of
// Keep arc min like htop; the subtraction
// below won't underflow because of
// the above check.
mem.used_bytes =
mem.used_bytes.saturating_sub(arc.0.used_bytes - arc.1);
+2 -2
View File
@@ -70,8 +70,8 @@ fn get_amd_devs() -> Option<Vec<PathBuf>> {
continue;
}
// 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 test_path = device_path.join("drm");
if test_path.as_path().exists() {
devices.push(device_path);
+10 -9
View File
@@ -14,15 +14,16 @@ pub fn get_cpu_data_list(collector: &DataCollector) -> CollectionResult<CpuHarve
target_os = "linux" => {
cpus.push(CpuData {
data_type: CpuDataType::Avg,
usage: collector.cgroup_cpu_data.avg_cpu_percent.unwrap_or_else(|| sys.global_cpu_usage()),
usage: collector
.cgroup_cpu_data
.avg_cpu_percent
.unwrap_or_else(|| sys.global_cpu_usage()),
});
}
_ => {
cpus.push(CpuData {
data_type: CpuDataType::Avg,
usage: sys.global_cpu_usage(),
})
}
_ => cpus.push(CpuData {
data_type: CpuDataType::Avg,
usage: sys.global_cpu_usage(),
}),
}
}
@@ -42,8 +43,8 @@ pub fn get_cpu_data_list(collector: &DataCollector) -> CollectionResult<CpuHarve
#[cfg(unix)]
pub(crate) fn get_load_avg() -> crate::collection::cpu::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 sysinfo::LoadAvg { one, five, fifteen } = sysinfo::System::load_average();
[one as f32, five as f32, fifteen as f32]
+2 -1
View File
@@ -9,6 +9,7 @@ cfg_select! {
mod zfs_io_counters;
#[cfg(feature = "zfs")]
pub use io_counters::IoCounters;
pub(crate) use self::freebsd::*;
}
target_os = "windows" => {
@@ -64,13 +65,13 @@ cfg_select! {
any(target_os = "linux", target_os = "macos", target_os = "windows") => {
mod io_counters;
pub use io_counters::IoCounters;
use crate::collection::DataCollector;
/// Returns the I/O usage of certain mount points.
pub fn get_io_usage(_collector: &DataCollector) -> anyhow::Result<IoHarvest> {
let mut io_hash: HashMap<String, Option<IoData>> = HashMap::default();
// TODO: Maybe rewrite this to not do a result of vec of result...
for io in io_stats()?.into_iter() {
let mount_point = io.device_name().to_string_lossy();
+12 -12
View File
@@ -49,21 +49,21 @@ pub fn get_disk_usage(collector: &DataCollector) -> anyhow::Result<Vec<DiskHarve
mounted_names.insert(base.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();
@@ -88,8 +88,8 @@ pub fn get_disk_usage(collector: &DataCollector) -> anyhow::Result<Vec<DiskHarve
#[cfg(target_os = "linux")]
{
// If we're including unmounted disks, then we'll add any disks that we previously
// did not mark as mounted.
// If we're including unmounted disks, then we'll add any disks that we
// previously did not mark as mounted.
if collector.include_unmounted_disks {
for disk in unmounted_disks(&mounted_names) {
if keep_disk_entry(&disk.name, &disk.mount_point, disk_filter, &None) {
+2 -2
View File
@@ -72,8 +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) {
+8 -8
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()
@@ -88,8 +88,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 {
@@ -161,8 +161,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) {
@@ -187,8 +187,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)
+7 -6
View File
@@ -14,9 +14,9 @@ const PARTITION_BLOCK_SIZE: u64 = 1024;
/// Returns [`DiskHarvest`] entries for block devices that aren't in `mounted`.
///
/// These come from `/proc/partitions`, which lists every block device (so even
/// devices with no I/O activity are covered) along with its size in terms of blocks.
/// Note this also filters out some devices, like `loop*`, `ram*`, `zram*`, etc., as
/// these are not "disks".
/// devices with no I/O activity are covered) along with its size in terms of
/// blocks. Note this also filters out some devices, like `loop*`, `ram*`,
/// `zram*`, etc., as these are not "disks".
pub(crate) fn unmounted_disks(mounted: &HashSet<String>) -> Vec<DiskHarvest> {
const PROC_PARTITIONS: &str = "/proc/partitions";
@@ -27,7 +27,8 @@ pub(crate) fn unmounted_disks(mounted: &HashSet<String>) -> Vec<DiskHarvest> {
parse_unmounted_disks(BufReader::new(file), mounted)
}
/// Parses `/proc/partitions` into [`DiskHarvest`] entries for block devices not present in `mounted`.
/// Parses `/proc/partitions` into [`DiskHarvest`] entries for block devices not
/// present in `mounted`.
fn parse_unmounted_disks<R: BufRead>(mut reader: R, mounted: &HashSet<String>) -> Vec<DiskHarvest> {
let mut disks = Vec::new();
let mut line = String::new();
@@ -37,8 +38,8 @@ fn parse_unmounted_disks<R: BufRead>(mut reader: R, mounted: &HashSet<String>) -
break;
}
// Format: `major minor #blocks name`. The header line and the blank line
// after it simply fail to parse here and get skipped.
// Format: `major minor #blocks name`. The header line and the blank
// line after it simply fail to parse here and get skipped.
let mut parts = line.split_whitespace();
let blocks = parts.nth(2).and_then(|b| b.parse::<u64>().ok());
let name = parts.next();
+9 -9
View File
@@ -10,8 +10,8 @@ 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
@@ -19,15 +19,15 @@ fn get_device_io(device: io_kit::IoObject) -> anyhow::Result<IoCounters> {
// 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()?;
@@ -6,7 +6,8 @@ use super::{IoIterator, bindings::*};
pub fn get_disks() -> anyhow::Result<IoIterator> {
let mut media_iter: io_iterator_t = 0;
// SAFETY: This is a safe syscall via IOKit, all the arguments should be safe.
// SAFETY: This is a safe syscall via IOKit, all the arguments should be
// safe.
let result = unsafe {
IOServiceGetMatchingServices(
kIOMasterPortDefault,
@@ -37,8 +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)),
@@ -48,8 +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);
}
@@ -81,8 +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);
}
@@ -95,12 +95,14 @@ pub fn get_dict(
dict.find(&key)
.map(|value_ref| {
// SAFETY: Only used for debug asserts, system API call that should be safe.
// SAFETY: Only used for debug asserts, system API call that should
// be safe.
unsafe {
debug_assert!(value_ref.type_of() == CFDictionaryGetTypeID());
}
// "Casting" `CFDictionary<*const void, *const void>` into a needed dict type
// "Casting" `CFDictionary<*const void, *const void>` into a needed
// dict type
let ptr = value_ref.to_void() as CFDictionaryRef;
// SAFETY: System API call, it should be safe?
@@ -116,7 +118,8 @@ pub fn get_i64(
dict.find(&key)
.and_then(|value_ref| {
// SAFETY: Only used for debug asserts, system API call that should be safe.
// SAFETY: Only used for debug asserts, system API call that should
// be safe.
unsafe {
debug_assert!(value_ref.type_of() == CFNumberGetTypeID());
}
@@ -133,7 +136,8 @@ pub fn get_string(
dict.find(&key)
.and_then(|value_ref| {
// SAFETY: Only used for debug asserts, system API call that should be safe.
// SAFETY: Only used for debug asserts, system API call that should
// be safe.
unsafe {
debug_assert!(value_ref.type_of() == CFStringGetTypeID());
}
+3 -3
View File
@@ -35,9 +35,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 {
+2 -2
View File
@@ -38,8 +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:?}")
+8 -7
View File
@@ -41,8 +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()),
@@ -114,8 +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());
@@ -151,15 +151,16 @@ 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 accommodate 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 accommodate the
// volume path https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumenameforvolumemountpointw
const VOLUME_MAX_LEN: usize = 50;
let mount = {
let mount_path = Path::new(mount);
let mut wide_path = mount_path.as_os_str().encode_wide().collect::<Vec<_>>();
// Always push on a \0 character, without this it will occasionally break.
// Always push on a \0 character, without this it will occasionally
// break.
wide_path.push(0x0000);
wide_path
+26 -14
View File
@@ -44,7 +44,8 @@ pub(crate) struct CgroupMemCollector {
pub swap: Option<CgroupMemData>,
}
/// Computes the cgroup v1 swap limit. Calculated by getting memsw and subtracting the memory limits.
/// Computes the cgroup v1 swap limit. Calculated by getting memsw and
/// subtracting the memory limits.
#[inline]
fn cgroup_v1_swap_limit(
memsw_limit: u64, mem_limit: Option<u64>, total_memory: u64, total_swap: u64,
@@ -68,7 +69,8 @@ impl CgroupMemCollector {
}
}
/// Try and update the memory using cgroup v1 semantics. If successful, returns `true`.
/// Try and update the memory using cgroup v1 semantics. If successful,
/// returns `true`.
fn try_update_memory_cgroup_v1(&mut self, total_memory: u64, total_swap: u64) -> bool {
if let Some(mem_usage) = read_u64("/sys/fs/cgroup/memory/memory.usage_in_bytes") {
// --- Memory ---
@@ -80,7 +82,8 @@ impl CgroupMemCollector {
};
// Technically if it's some insanely high value (https://unix.stackexchange.com/a/421182),
// then it's "unlimited", but we can just make it so we take the max of the main and this anyway.
// then it's "unlimited", but we can just make it so we take the max
// of the main and this anyway.
let mem_limit_raw = read_u64("/sys/fs/cgroup/memory/memory.limit_in_bytes");
let mem_limit = mem_limit_raw.map(CgroupMemLimit::Bytes);
@@ -90,7 +93,8 @@ impl CgroupMemCollector {
});
// --- Swap ---
// Since swap is dependent on the normal memory usage, we couple it together.
// Since swap is dependent on the normal memory usage, we couple it
// together.
if let Some(memsw_usage) = read_u64("/sys/fs/cgroup/memory/memory.memsw.usage_in_bytes")
{
let used_bytes = memsw_usage.saturating_sub(mem_usage);
@@ -114,7 +118,8 @@ impl CgroupMemCollector {
}
}
/// Try and update the memory using cgroup v2 semantics. If successful, returns `true`.
/// Try and update the memory using cgroup v2 semantics. If successful,
/// returns `true`.
fn try_update_memory_cgroup_v2(&mut self) -> bool {
let mut could_update = false;
@@ -186,13 +191,15 @@ fn parse_cpu_quota(cpu_max: String) -> Option<f64> {
/// Gathers CPU data from cgroup sources.
#[derive(Default, Debug)]
pub(crate) struct CgroupCpuCollector {
/// A maximum number of CPUs (cores) that can be used, as defined by the cgroup.
/// A maximum number of CPUs (cores) that can be used, as defined by the
/// cgroup.
pub cpu_quota: Option<f64>,
/// Computed average CPU usage percent (only set when a quota is active).
pub avg_cpu_percent: Option<f32>,
/// The previous CPU microsecond time (usage) and when it was last updated. Used to compute average cgroup CPU usage.
/// The previous CPU microsecond time (usage) and when it was last updated.
/// Used to compute average cgroup CPU usage.
prev_cpu: Option<(u64, Instant)>,
}
@@ -205,7 +212,8 @@ impl CgroupCpuCollector {
}
}
/// Try to update CPU data using cgroup v1 semantics. Returns `true` on success.
/// Try to update CPU data using cgroup v1 semantics. Returns `true` on
/// success.
fn try_update_cpu_cgroup_v1(&mut self) -> bool {
let quota_raw: i64 = {
let quota_str = match fs::read_to_string("/sys/fs/cgroup/cpu/cpu.cfs_quota_us") {
@@ -230,11 +238,13 @@ impl CgroupCpuCollector {
}
})
} else {
// If it's less than 0 (-1) then it's representing "unlimited" quota (AKA use the full CPU).
// If it's less than 0 (-1) then it's representing "unlimited" quota
// (AKA use the full CPU).
None
};
// cpuacct.usage is in nanoseconds; convert to microseconds for consistency with v2
// cpuacct.usage is in nanoseconds; convert to microseconds for
// consistency with v2
if let Some(usage_nsec) = read_u64("/sys/fs/cgroup/cpuacct/cpuacct.usage") {
self.try_compute_avg_and_update(usage_nsec / 1000);
} else {
@@ -244,7 +254,8 @@ impl CgroupCpuCollector {
true
}
/// Try to update CPU data using cgroup v2 semantics. Returns `true` on success.
/// Try to update CPU data using cgroup v2 semantics. Returns `true` on
/// success.
fn try_update_cpu_cgroup_v2(&mut self) -> bool {
let cpu_max = match fs::read_to_string("/sys/fs/cgroup/cpu.max") {
Ok(s) => s,
@@ -263,9 +274,10 @@ impl CgroupCpuCollector {
true
}
/// Try and compute average CPU usage based on the current microseconds. Note that this requires _two_ invocations
/// to compute a value, as the first invocation just sets the baseline for the next CPU time and timestamp to
/// compare with.
/// Try and compute average CPU usage based on the current microseconds.
/// Note that this requires _two_ invocations to compute a value, as the
/// first invocation just sets the baseline for the next CPU time and
/// timestamp to compare with.
fn try_compute_avg_and_update(&mut self, current_microseconds: u64) {
let now = Instant::now();
+7 -6
View File
@@ -4,7 +4,8 @@ use std::{fs, path::Path};
/// Will return false if the state is not D0/unknown, or if it does not support
/// `device/power_state` (e.g. the path does not exist).
///
/// `path` is a path to the device itself (e.g. `/sys/class/hwmon/hwmon1/device`).
/// `path` is a path to the device itself (e.g.
/// `/sys/class/hwmon/hwmon1/device`).
#[inline]
pub fn is_device_awake(device: &Path) -> bool {
// Whether the temperature should *actually* be read during enumeration.
@@ -14,12 +15,12 @@ pub fn is_device_awake(device: &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 can't be
// reported either.
// Their self-hosted git instance has disabled sign up, so this bug
// can't be reported either.
state == "D0" || state == "unknown"
} else {
true
+12 -7
View File
@@ -32,7 +32,8 @@ pub(crate) fn get_arc_usage() -> Option<(MemData, u64)> {
// Parse the value, remember it's in bytes!
if let Ok(number) = number.parse::<u64>() {
*to_write = number;
// We only need a few keys, so we can bail early.
// We only need a few keys, so we can bail
// early.
zfs_keys_read += 1;
if zfs_keys_read == ZFS_KEYS_NEEDED {
break;
@@ -53,9 +54,15 @@ pub(crate) fn get_arc_usage() -> Option<(MemData, u64)> {
sysctl::Ctl::new("kstat.zfs.misc.arcstats.c_max"),
sysctl::Ctl::new("kstat.zfs.misc.arcstats.c_min"),
) {
if let (Ok(sysctl::CtlValue::U64(arc)), Ok(sysctl::CtlValue::U64(mem)), Ok(sysctl::CtlValue::U64(min))) =
(mem_arc_value.value(), mem_sys_value.value(), mem_min_value.value())
{
if let (
Ok(sysctl::CtlValue::U64(arc)),
Ok(sysctl::CtlValue::U64(mem)),
Ok(sysctl::CtlValue::U64(min)),
) = (
mem_arc_value.value(),
mem_sys_value.value(),
mem_min_value.value(),
) {
(mem, arc, min)
} else {
(0, 0, 0)
@@ -64,9 +71,7 @@ pub(crate) fn get_arc_usage() -> Option<(MemData, u64)> {
(0, 0, 0)
}
}
_ => {
(0, 0, 0)
}
_ => (0, 0, 0),
}
};
+7 -10
View File
@@ -14,11 +14,12 @@ fn get_usage(used: u64, total: u64) -> Option<MemData> {
})
}
/// Resolves the total memory to report given an optional cgroup limit and the physical total.
/// Resolves the total memory to report given an optional cgroup limit and the
/// physical total.
///
/// cgroup v1 reports an "unlimited" limit as a very large value, which causes problems if taken
/// literally. This function caps it to the minimum of the cgroup total or the actual total to
/// avoid this problem.
/// cgroup v1 reports an "unlimited" limit as a very large value, which causes
/// problems if taken literally. This function caps it to the minimum of the
/// cgroup total or the actual total to avoid this problem.
#[cfg(target_os = "linux")]
#[inline]
fn resolve_cgroup_total(limit: Option<&CgroupMemLimit>, base_total: u64) -> u64 {
@@ -51,9 +52,7 @@ pub(crate) fn get_ram_usage(collector: &DataCollector) -> Option<MemData> {
get_usage(used, total)
}
_ => {
get_usage(sys.used_memory(), sys.total_memory())
}
_ => get_usage(sys.used_memory(), sys.total_memory()),
}
}
@@ -81,9 +80,7 @@ pub(crate) fn get_swap_usage(collector: &DataCollector) -> Option<MemData> {
get_usage(used, total)
}
_ => {
get_usage(sys.used_swap(), sys.total_swap())
}
_ => get_usage(sys.used_swap(), sys.total_swap()),
}
}
+4 -4
View File
@@ -36,8 +36,8 @@ fn get_swap_usage_inner(sys: &System) -> Option<MemData> {
// See https://kennykerr.ca/rust-getting-started/string-tutorial.html
let query = w!("\\Paging File(_Total)\\% Usage");
// SAFETY: Hits a few Windows APIs; this should be safe as we check each step,
// and we clean up at the end.
// SAFETY: Hits a few Windows APIs; this should be safe as we check each
// step, and we clean up at the end.
unsafe {
let mut query_handle: PDH_HQUERY = zeroed();
let mut counter_handle: PDH_HCOUNTER = zeroed();
@@ -92,8 +92,8 @@ mod tests {
let swap_usage = get_swap_usage_inner(&sys);
if sys.total_swap() > 0 {
// Not sure if we can guarantee this to always pass on a machine, so I'll just
// print out.
// Not sure if we can guarantee this to always pass on a machine, so
// I'll just print out.
println!("swap: {swap_usage:?}");
} else {
println!("No swap, skipping.");
+30 -14
View File
@@ -64,9 +64,10 @@ fn is_gpu_class(class_code: &str) -> bool {
class_code.starts_with(PCI_BASE_CLASS_DISPLAY)
}
/// Get a list of PCI bus IDs for Linux. This will handle whether the device is awake or not.
/// We do this separately to avoid the possibility of NVML waking up the device at all;
/// this is particularly useful for things like laptops with hybrid graphics (e.g. NVIDIA Optimus).
/// Get a list of PCI bus IDs for Linux. This will handle whether the device is
/// awake or not. We do this separately to avoid the possibility of NVML waking
/// up the device at all; this is particularly useful for things like laptops
/// with hybrid graphics (e.g. NVIDIA Optimus).
///
/// Note this is somewhat expensive, so it may be worth caching this result.
///
@@ -77,9 +78,10 @@ fn is_gpu_class(class_code: &str) -> bool {
/// - <https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-power_state>
#[cfg(target_os = "linux")]
fn get_active_pci_bus_ids() -> Vec<String> {
use crate::collection::linux::utils::is_device_awake;
use std::fs;
use crate::collection::linux::utils::is_device_awake;
let Ok(entries) = fs::read_dir("/sys/bus/pci/devices") else {
return Vec::new();
};
@@ -154,24 +156,38 @@ pub fn get_nvidia_gpu_data(collector: &mut DataCollector) -> Option<GpusData> {
use itertools::Either;
// Refresh every ~10 seconds.
if let Some((cached_list, cached_time)) = &collector.nvidia_gpu_list_cache && cached_time.elapsed().as_secs() < 10 {
let devices = Either::Left(cached_list.iter().filter_map(|id| nvml.device_by_pci_bus_id(id.as_str()).ok()));
if let Some((cached_list, cached_time)) = &collector.nvidia_gpu_list_cache
&& cached_time.elapsed().as_secs() < 10
{
let devices = Either::Left(
cached_list
.iter()
.filter_map(|id| nvml.device_by_pci_bus_id(id.as_str()).ok()),
);
(devices, cached_list.len())
}
else {
} else {
let pci_bus_ids = get_active_pci_bus_ids();
let num_gpus = pci_bus_ids.len();
collector.nvidia_gpu_list_cache = Some((pci_bus_ids.clone(), std::time::Instant::now()));
collector.nvidia_gpu_list_cache =
Some((pci_bus_ids.clone(), std::time::Instant::now()));
let devices = Either::Right(pci_bus_ids.into_iter().filter_map(|id| nvml.device_by_pci_bus_id(id).ok()));
let devices = Either::Right(
pci_bus_ids
.into_iter()
.filter_map(|id| nvml.device_by_pci_bus_id(id).ok()),
);
(devices, num_gpus)
}
},
}
_ => {
// The fallback behaviour (the old one) is to just list all nvml devices blindly.
// Note this has the risk of waking up sleeping devices.
// The fallback behaviour (the old one) is to just list all nvml
// devices blindly. Note this has the risk of
// waking up sleeping devices.
let num_gpus = nvml.device_count().ok()?;
((0..num_gpus).flat_map(|i| nvml.device_by_index(i)), num_gpus as usize)
(
(0..num_gpus).flat_map(|i| nvml.device_by_index(i)),
num_gpus as usize,
)
}
}
};
+49 -58
View File
@@ -172,21 +172,22 @@ impl DataCollector {
pub(crate) fn get_processes(&mut self) -> CollectionResult<Vec<ProcessHarvest>> {
cfg_select! {
target_os = "linux" => {
let time_diff = self.data.collection_time
let time_diff = self
.data
.collection_time
.duration_since(self.last_collection_time)
.as_secs();
linux_process_data(
self,
time_diff,
)
}
any(target_os = "freebsd", target_os = "macos", target_os = "windows", target_os = "android", target_os = "ios") => {
sysinfo_process_data(self)
}
_ => {
Err(crate::collection::error::CollectionError::Unsupported)
linux_process_data(self, time_diff)
}
any(
target_os = "freebsd",
target_os = "macos",
target_os = "windows",
target_os = "android",
target_os = "ios"
) => sysinfo_process_data(self),
_ => Err(crate::collection::error::CollectionError::Unsupported),
}
}
}
@@ -194,52 +195,42 @@ impl DataCollector {
/// Pulled from [`ProcessStatus::to_string`] to avoid an alloc.
pub(super) fn process_status_str(status: ProcessStatus) -> &'static str {
cfg_select! {
target_os = "linux" => {
match status {
ProcessStatus::Idle => "Idle",
ProcessStatus::Run => "Runnable",
ProcessStatus::Sleep => "Sleeping",
ProcessStatus::Stop => "Stopped",
ProcessStatus::Zombie => "Zombie",
ProcessStatus::Tracing => "Tracing",
ProcessStatus::Dead => "Dead",
ProcessStatus::Wakekill => "Wakekill",
ProcessStatus::Waking => "Waking",
ProcessStatus::Parked => "Parked",
ProcessStatus::UninterruptibleDiskSleep => "UninterruptibleDiskSleep",
_ => "Unknown",
}
}
target_os = "windows" => {
match status {
ProcessStatus::Run => "Runnable",
_ => "Unknown",
}
}
target_os = "macos" => {
match status {
ProcessStatus::Idle => "Idle",
ProcessStatus::Run => "Runnable",
ProcessStatus::Sleep => "Sleeping",
ProcessStatus::Stop => "Stopped",
ProcessStatus::Zombie => "Zombie",
_ => "Unknown",
}
}
target_os = "freebsd" => {
match status {
ProcessStatus::Idle => "Idle",
ProcessStatus::Run => "Runnable",
ProcessStatus::Sleep => "Sleeping",
ProcessStatus::Stop => "Stopped",
ProcessStatus::Zombie => "Zombie",
ProcessStatus::Dead => "Dead",
ProcessStatus::LockBlocked => "LockBlocked",
_ => "Unknown",
}
}
_ => {
"Unknown"
}
target_os = "linux" => match status {
ProcessStatus::Idle => "Idle",
ProcessStatus::Run => "Runnable",
ProcessStatus::Sleep => "Sleeping",
ProcessStatus::Stop => "Stopped",
ProcessStatus::Zombie => "Zombie",
ProcessStatus::Tracing => "Tracing",
ProcessStatus::Dead => "Dead",
ProcessStatus::Wakekill => "Wakekill",
ProcessStatus::Waking => "Waking",
ProcessStatus::Parked => "Parked",
ProcessStatus::UninterruptibleDiskSleep => "UninterruptibleDiskSleep",
_ => "Unknown",
},
target_os = "windows" => match status {
ProcessStatus::Run => "Runnable",
_ => "Unknown",
},
target_os = "macos" => match status {
ProcessStatus::Idle => "Idle",
ProcessStatus::Run => "Runnable",
ProcessStatus::Sleep => "Sleeping",
ProcessStatus::Stop => "Stopped",
ProcessStatus::Zombie => "Zombie",
_ => "Unknown",
},
target_os = "freebsd" => match status {
ProcessStatus::Idle => "Idle",
ProcessStatus::Run => "Runnable",
ProcessStatus::Sleep => "Sleeping",
ProcessStatus::Stop => "Stopped",
ProcessStatus::Zombie => "Zombie",
ProcessStatus::Dead => "Dead",
ProcessStatus::LockBlocked => "LockBlocked",
_ => "Unknown",
},
_ => "Unknown",
}
}
+18 -15
View File
@@ -222,19 +222,21 @@ fn read_proc(
(concat_string!("[", comm, "]"), comm)
} else {
// If the comm fits then we'll default to whatever is set.
// If it doesn't, we need to do some magic to determine what it's
// supposed to be.
// If it doesn't, we need to do some magic to determine what
// it's supposed to be.
// TODO: We might want to re-evaluate if we want to do it like this,
// as it turns out I was dumb and sometimes comm != process name...
// TODO: We might want to re-evaluate if we want to do it like
// this, as it turns out I was dumb and
// sometimes comm != process name...
//
// What we should do is store:
// - basename (what we're kinda doing now, except we're gating on comm length)
// - basename (what we're kinda doing now, except we're gating
// on comm length)
// - command (full thing)
// - comm (as a separate thing)
//
// Stuff like htop also offers the option to "highlight" basename and comm in
// command. Might be neat?
// Stuff like htop also offers the option to "highlight"
// basename and comm in command. Might be neat?
let name = if comm.len() >= MAX_STAT_NAME_LEN {
binary_name_from_cmdline(&cmdline)
} else {
@@ -249,8 +251,8 @@ fn read_proc(
};
// We have moved command processing here.
// SAFETY: We are only replacing a single char (NUL) with another single char
// (space).
// SAFETY: We are only replacing a single char (NUL) with another single
// char (space).
let mut command = command;
let buf_mut = unsafe { command.as_mut_vec() };
@@ -389,13 +391,14 @@ pub(crate) fn linux_process_data(
.cpu_quota
.unwrap_or_else(|| 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;
}
// TODO: Could maybe use a double buffer hashmap to avoid allocating this each
// time? e.g. we swap which is prev and which is new.
// TODO: Could maybe use a double buffer hashmap to avoid allocating this
// each time? e.g. we swap which is prev and which is new.
let mut seen_pids: HashSet<Pid> = HashSet::default();
// Note this will only return PIDs of _processes_, not threads. You can get
@@ -497,8 +500,8 @@ pub(crate) fn linux_process_data(
prev_process_details.shrink_to_fit();
}
// TODO: This might be more efficient to just separate threads into their own
// list, but for now this works so it fits with existing code.
// TODO: This might be more efficient to just separate threads into their
// own list, but for now this works so it fits with existing code.
Ok(process_vector)
}
+18 -15
View File
@@ -76,24 +76,26 @@ impl Stat {
/// `/proc/<PID>/stat`. For documentation, see
/// [here](https://manpages.ubuntu.com/manpages/noble/man5/proc_pid_stat.5.html) as a reference.
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
// (technically) 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 (technically) might have non-utf8 characters, we
// can't just use read_to_string.
f.read_to_end(unsafe { buffer.as_mut_vec() })?;
// TODO: Is this needed?
let line = buffer.trim();
// Comm is represented by a string in parentheses (e.g. `(foo)`, `((bar))`).
// To handle that second case, we need to find the "last" closing parentheses.
// Comm is represented by a string in parentheses (e.g. `(foo)`,
// `((bar))`). To handle that second case, we need to find the
// "last" closing parentheses.
let (comm, rest) = {
let start_paren = line
.find('(')
.ok_or_else(|| anyhow!("start paren missing"))?;
// So, we _could_ try and be smart and only parse a limited slice of the string - however,
// there appears to be no ABI guarantees of comm length anymore, so we just take the hit and do an rsplit
// over the full string.
// So, we _could_ try and be smart and only parse a limited slice of
// the string - however, there appears to be no ABI
// guarantees of comm length anymore, so we just take the hit and do
// an rsplit over the full string.
//
// Sources/discussion:
// - https://man.archlinux.org/man/proc_pid_stat.5.en
@@ -189,8 +191,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() {
@@ -225,7 +227,8 @@ impl Io {
}
}
// Quick short circuit if we have already read all the required fields.
// Quick short circuit if we have already read all the required
// fields.
if read_fields == NUM_FIELDS {
break;
}
@@ -307,8 +310,8 @@ impl Process {
let mut root = pid_path;
// 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!
// Stat is pretty long, do this first to pre-allocate up-front.
let stat =
@@ -316,8 +319,8 @@ impl Process {
reset(&mut root, buffer);
let cmdline = if cmdline(&mut root, &pid_dir, buffer).is_ok() {
// The clone will give a string with the capacity of the length of buffer, don't
// worry.
// The clone will give a string with the capacity of the length of
// buffer, don't worry.
Some(buffer.clone())
} else {
None
@@ -288,8 +288,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()) }
+25 -14
View File
@@ -10,11 +10,11 @@ cfg_select! {
pub(crate) use process_ext::*;
use super::ProcessHarvest;
use crate::collection::{DataCollector, error::CollectionResult, processes::*};
use crate::collection::{DataCollector, processes::*};
use crate::collection::error::CollectionResult;
pub fn sysinfo_process_data(collector: &mut DataCollector) -> CollectionResult<Vec<ProcessHarvest>> {
pub fn sysinfo_process_data(
collector: &mut DataCollector,
) -> CollectionResult<Vec<ProcessHarvest>> {
let sys = &collector.sys.system;
let use_current_cpu_total = collector.use_current_cpu_total;
let unnormalized_cpu = collector.unnormalized_cpu;
@@ -22,18 +22,29 @@ cfg_select! {
let user_table = &mut collector.user_table;
cfg_select! {
target_os = "macos" => {
MacOSProcessExt::sysinfo_process_data(sys, use_current_cpu_total, unnormalized_cpu, total_memory, user_table)
}
target_os = "freebsd" => {
FreeBSDProcessExt::sysinfo_process_data(sys, use_current_cpu_total, unnormalized_cpu, total_memory, user_table)
}
_ => {
GenericProcessExt::sysinfo_process_data(sys, use_current_cpu_total, unnormalized_cpu, total_memory, user_table)
}
target_os = "macos" => MacOSProcessExt::sysinfo_process_data(
sys,
use_current_cpu_total,
unnormalized_cpu,
total_memory,
user_table,
),
target_os = "freebsd" => FreeBSDProcessExt::sysinfo_process_data(
sys,
use_current_cpu_total,
unnormalized_cpu,
total_memory,
user_table,
),
_ => GenericProcessExt::sysinfo_process_data(
sys,
use_current_cpu_total,
unnormalized_cpu,
total_memory,
user_table,
),
}
}
}
_ => {}
}
+25 -21
View File
@@ -17,15 +17,9 @@ fn get_nice(pid: Pid) -> i32 {
// SAFETY: getpriority takes no user pointers; pid is passed as a value
// and errors are reported via the return value.
cfg_select! {
target_os = "freebsd" => {
unsafe { libc::getpriority(libc::PRIO_PROCESS, pid) }
}
target_os = "macos" => {
unsafe { libc::getpriority(libc::PRIO_PROCESS, pid as u32) }
}
_ => {
0
}
target_os = "freebsd" => unsafe { libc::getpriority(libc::PRIO_PROCESS, pid) },
target_os = "macos" => unsafe { libc::getpriority(libc::PRIO_PROCESS, pid as u32) },
_ => 0,
}
}
@@ -39,10 +33,16 @@ fn get_priority(pid: Pid) -> i32 {
}
}
target_os = "freebsd" => {
use libc::{c_int, c_void};
use std::{mem, ptr};
let mib = [libc::CTL_KERN, libc::KERN_PROC, libc::KERN_PROC_PID, pid as c_int];
use libc::{c_int, c_void};
let mib = [
libc::CTL_KERN,
libc::KERN_PROC,
libc::KERN_PROC_PID,
pid as c_int,
];
let mut kp: libc::kinfo_proc = unsafe { mem::zeroed() };
let mut size = mem::size_of::<libc::kinfo_proc>();
@@ -61,11 +61,13 @@ fn get_priority(pid: Pid) -> i32 {
)
};
if ret == 0 { kp.ki_pri.pri_level as i32 } else { 0 }
}
_ => {
0
if ret == 0 {
kp.ki_pri.pri_level as i32
} else {
0
}
}
_ => 0,
}
}
@@ -230,12 +232,16 @@ fn convert_process_status_to_char(status: ProcessStatus) -> char {
ProcessStatus::Sleep => SSLEEP,
ProcessStatus::Stop => SSTOP,
ProcessStatus::Zombie => SZOMB,
_ => '?'
_ => '?',
}
}
target_os = "freebsd" => {
const fn assert_u8(val: libc::c_char) -> u8 {
if val < 0 { panic!("there was an invalid i8 constant that is supposed to be a char") } else { val as u8 }
if val < 0 {
panic!("there was an invalid i8 constant that is supposed to be a char")
} else {
val as u8
}
}
const SIDL: u8 = assert_u8(libc::SIDL);
@@ -254,11 +260,9 @@ fn convert_process_status_to_char(status: ProcessStatus) -> char {
ProcessStatus::Zombie => SZOMB as char,
ProcessStatus::Dead => SWAIT as char,
ProcessStatus::LockBlocked => SLOCK as char,
_ => '?'
_ => '?',
}
}
_ => {
'?'
}
_ => '?',
}
}
+2 -2
View File
@@ -16,8 +16,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 which we check.
// SAFETY: getpwuid returns a null pointer if no passwd entry is
// found for the uid which we check.
let passwd = unsafe { libc::getpwuid(uid) };
if passwd.is_null() {
+6 -5
View File
@@ -16,8 +16,8 @@ use crate::collection::{DataCollector, error::CollectionResult};
/// for more information on the core Windows API being called and the meaning of
/// the priorities, as well as the access rights needed.
fn get_priority(pid: u32) -> anyhow::Result<i32> {
// SAFETY: We check validity of each step and bail on errors. We also close the
// handle.
// SAFETY: We check validity of each step and bail on errors. We also close
// the handle.
unsafe {
let process_handle: HANDLE = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid)?;
if process_handle.is_invalid() {
@@ -146,9 +146,10 @@ pub fn sysinfo_process_data(
.user_id()
.and_then(|uid| users.get_user_by_id(uid).map(|user| user.name().into())),
time: if process.start_time() == 0 {
// Workaround for sysinfo 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 sysinfo 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.
//
// TODO: Maybe return an option instead?
Duration::ZERO
+32 -22
View File
@@ -37,9 +37,10 @@ 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
@@ -48,7 +49,8 @@ fn get_hwmon_candidates() -> (HashSet<PathBuf>, usize) {
// - https://github.com/giampaolo/psutil/issues/971
// - https://github.com/giampaolo/psutil/blob/642438375e685403b4cd60b0c0e25b80dd5a813d/psutil/_pslinux.py#L1316
//
// If it does match, then add the `device/` directory to the path.
// If it does match, then add the `device/` directory to the
// path.
if path.join("device/temp1_input").exists() {
path.push("device");
}
@@ -69,8 +71,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
// 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.
//
@@ -212,8 +215,8 @@ fn hwmon_temperatures(filter: &Option<Filter>, graph_filter: &Option<Filter>) ->
// `/sys/class/hwmon/hwmon*/device/power_state` == `D3cold` will
// wake the device up, and will block until it initializes.
//
// Reading the `hwmon*/device/power_state` or `hwmon*/temp*_label` properties
// will not wake the device, and thus not block,
// Reading the `hwmon*/device/power_state` or `hwmon*/temp*_label`
// properties 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
@@ -247,21 +250,25 @@ fn hwmon_temperatures(filter: &Option<Filter>, graph_filter: &Option<Filter>) ->
let sensor_label_path = file_path.join(name.replace("input", "label"));
let sensor_label = read_to_string_lossy(sensor_label_path);
// Do some messing around to get a more sensible name for sensors:
// Do some messing around to get a more sensible name for
// sensors:
// - For GPUs, this will use the kernel device name, ex `card0`
// - For 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.
// - 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.
#[cfg(feature = "gpu")]
{
@@ -295,11 +302,13 @@ fn hwmon_temperatures(filter: &Option<Filter>, graph_filter: &Option<Filter>) ->
}
}
} else {
// This little mess is to account for stuff like k10temp. This is needed
// 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.
// 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().and_then(|f| f.to_str()).map(|s| s.trim());
@@ -317,8 +326,9 @@ fn hwmon_temperatures(filter: &Option<Filter>, graph_filter: &Option<Filter>) ->
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 (Filter::optional_should_keep(filter, &name)
|| Filter::optional_should_keep(graph_filter, &name))
&& let Ok(temp_celsius) = parse_temp(&temp_path)
+4 -2
View File
@@ -17,7 +17,8 @@ use crate::canvas::components::time_series::{
const STALE_MIN_MILLISECONDS: u64 = Duration::from_secs(30).as_millis() as u64;
/// Configuration values for a [`TimeseriesState`], sourced from [`crate::app::AppConfigFields`].
/// Configuration values for a [`TimeseriesState`], sourced from
/// [`crate::app::AppConfigFields`].
#[derive(Copy, Clone, Debug)]
pub struct TimeseriesConfig {
pub time_interval: u64,
@@ -34,7 +35,8 @@ pub struct TimeseriesState {
}
impl TimeseriesState {
/// Create a new [`TimeseriesState`] using the given config and `autohide_timer` setting.
/// Create a new [`TimeseriesState`] using the given config and
/// `autohide_timer` setting.
pub fn new(config: TimeseriesConfig, autohide_timer: Option<Instant>) -> Self {
Self {
current_display_time: config.default_time_value,
+10 -7
View File
@@ -47,13 +47,15 @@ impl GraphHeightCache {
let visible_left_bound = match last_time.checked_sub(visible_duration) {
Some(v) => v,
None => {
// On some systems (like Windows) it can be possible that the
// current display time causes subtraction to fail if, for example,
// the uptime of the system is too low and current_display_time is
// too high. See https://github.com/ClementTsang/bottom/issues/1825.
// On some systems (like Windows) it can be possible that
// the current display time causes
// subtraction to fail if, for example,
// the uptime of the system is too low and
// current_display_time is too high. See https://github.com/ClementTsang/bottom/issues/1825.
//
// As such, we instead take the oldest visible time. This is a bit
// inefficient, but since it should only happen rarely, it's fine.
// As such, we instead take the oldest visible time. This is
// a bit inefficient, but since it
// should only happen rarely, it's fine.
times
.iter()
.take_while(|t| last_time.duration_since(**t) < visible_duration)
@@ -90,7 +92,8 @@ impl GraphHeightCache {
}
}
/// A time series graph that automatically adjusts the y-axis based on the data provided.
/// A time series graph that automatically adjusts the y-axis based on the data
/// provided.
pub struct AutoYAxisTimeGraph {
state: TimeseriesState,
height_cache: GraphHeightCache,
+4 -3
View File
@@ -766,7 +766,8 @@ mod test {
#[test]
fn help_menu_matches_entry_len() {
// Subtract 2 to account for the extra newline + search instructions at the bottom.
// Subtract 2 to account for the extra newline + search instructions at
// the bottom.
const HELP_CONTENTS_TEXT_LEN: usize = HELP_CONTENTS_TEXT.len() - 2;
assert_eq!(
@@ -800,8 +801,8 @@ mod test {
.unwrap()
.replace_all(CONFIG_TEXT, "$1");
// Then, trim off anything that has more than 2 spaces + alphabetical character
// or '[' following a "#".
// Then, trim off anything that has more than 2 spaces + alphabetical
// character or '[' following a "#".
let default_config = Regex::new(r"(?m)^#(\s\s+)([a-zA-Z\[])")
.unwrap()
.replace_all(&default_config, "$2");
+17 -14
View File
@@ -136,9 +136,9 @@ fn panic_hook(panic_info: &PanicHookInfo<'_>) {
println!("thread '<unnamed>' panicked at '{msg}', {panic_info}\n\r{backtrace}")
}
// TODO: Might be cleaner in the future to use a cancellation token, but that
// causes some fun issues with lifetimes; for now if it panics then shut
// down the main program entirely ASAP.
// TODO: Might be cleaner in the future to use a cancellation token, but
// that causes some fun issues with lifetimes; for now if it panics then
// shut down the main program entirely ASAP.
std::process::exit(1);
}
@@ -166,9 +166,10 @@ fn create_input_thread(
{
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
// 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.
@@ -182,8 +183,8 @@ fn create_input_thread(
}
}
Event::Key(key) if !keys_disabled && 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;
}
@@ -244,8 +245,8 @@ fn create_collection_thread(
data_collector.update_data();
data_collector.data = Data::default();
// Tiny sleep I guess? To go between the first update above and the first update
// in the loop.
// Tiny sleep I guess? To go between the first update above and the
// first update in the loop.
std::thread::sleep(Duration::from_millis(5));
loop {
@@ -371,8 +372,9 @@ pub fn start_bottom(enable_error_hook: &mut bool) -> anyhow::Result<()> {
let mut terminal = Terminal::new(CrosstermBackend::new(stdout_val))?;
// This may fail in some environments, like tests, since it may fail to get the cursor position.
// In that case, fall back to just manually clearing it with backend.
// This may fail in some environments, like tests, since it may fail to get
// the cursor position. In that case, fall back to just manually
// clearing it with backend.
if terminal.clear().is_err() {
terminal.backend_mut().clear()?;
}
@@ -422,8 +424,9 @@ pub fn start_bottom(enable_error_hook: &mut bool) -> anyhow::Result<()> {
BottomEvent::Update(data) => {
app.data_store.eat_data(data, &app.app_config_fields);
// This thing is required as otherwise, some widgets can't draw correctly w/o
// some data (or they need to be re-drawn).
// This thing is required as otherwise, some widgets can't
// draw correctly w/o some data (or they
// need to be re-drawn).
if first_run {
first_run = false;
app.is_force_redraw = true;
+36 -29
View File
@@ -39,7 +39,8 @@ use crate::{
widgets::*,
};
/// Macro to check whether a flag is enabled, either as an arg or in the config file.
/// Macro to check whether a flag is enabled, either as an arg or in the config
/// file.
macro_rules! is_flag_enabled {
($flag_name:ident, $arg:expr, $config:expr) => {
if $arg.$flag_name {
@@ -77,7 +78,8 @@ macro_rules! is_flag_enabled_in {
};
}
/// A version of [`is_flag_enabled`] which should be used to deprecate old config flags.
/// A version of [`is_flag_enabled`] which should be used to deprecate old
/// config flags.
macro_rules! enabled_option_with_deprecated {
($arg:expr, $config:expr, $section:ident . $field:ident, $flags:ident . $deprecated_flag:ident $(,)?) => {
if $arg {
@@ -117,7 +119,8 @@ macro_rules! enabled_option_with_deprecated {
};
}
/// Get the value of a field for a specific section in the config file if set. If not set, the default is used.
/// Get the value of a field for a specific section in the config file if set.
/// If not set, the default is used.
macro_rules! config_or_default {
($config:expr, $section:ident . $field:ident) => {
$config
@@ -128,8 +131,8 @@ macro_rules! config_or_default {
};
}
/// Get the value of a field for a specific section in the config file if set. If not set,
/// the provided default is used.
/// Get the value of a field for a specific section in the config file if set.
/// If not set, the provided default is used.
macro_rules! config_or {
($config:expr, $section:ident . $field:ident, $default:expr) => {
$config
@@ -165,9 +168,10 @@ fn get_config_path(override_config_path: Option<&Path>) -> Option<PathBuf> {
if let Ok(res) = old_home_path.try_exists()
&& res
{
// We used to create it at `<HOME>/DEFAULT_CONFIG_FILE_PATH`, but changed it
// to be more correct later. However, for legacy reasons, if it already exists,
// use the old one.
// We used to create it at `<HOME>/DEFAULT_CONFIG_FILE_PATH`, but
// changed it to be more correct later. However, for
// legacy reasons, if it already exists, use the old
// one.
return Some(old_home_path);
}
}
@@ -181,9 +185,9 @@ fn get_config_path(override_config_path: Option<&Path>) -> Option<PathBuf> {
&& let Ok(xdg_config_path) = std::env::var("XDG_CONFIG_HOME")
&& !xdg_config_path.is_empty()
{
// If XDG_CONFIG_HOME exists and is non-empty, _but_ we previously used the
// Library-based path for a config and it exists, then use that
// instead for backwards-compatibility.
// If XDG_CONFIG_HOME exists and is non-empty, _but_ we previously used
// the Library-based path for a config and it exists, then use
// that instead for backwards-compatibility.
if let Some(old_macos_path) = &config_path
&& let Ok(res) = old_macos_path.try_exists()
&& res
@@ -257,11 +261,11 @@ pub(crate) fn get_or_create_config(config_path: Option<&Path>) -> anyhow::Result
}
}
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: For now, just print a message to stderr indicating this. In the future,
// probably show in-app (too).
// TODO: For now, just print a message to stderr indicating this. In
// the future, probably show in-app (too).
eprintln!(
"Note: bottom couldn't find a location to create or read a config file, so \
@@ -395,7 +399,8 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL
let network_unit_type = get_network_unit_type(args, config);
let network_scale_type = get_network_scale_type(args, config);
// Use + update this again after deprecation
// let network_use_binary_prefix = is_flag_enabled!(network_use_binary_prefix, args.network, config);
// let network_use_binary_prefix =
// is_flag_enabled!(network_use_binary_prefix, args.network, config);
let network_use_binary_prefix = enabled_option_with_deprecated!(
args.network.network_use_binary_prefix,
config,
@@ -998,7 +1003,8 @@ fn deprecated_warning(deprecated_field: &str, new_field: &str) {
);
}
/// Mark a config option field as deprecated, and what to use instead with an alias.
/// Mark a config option field as deprecated, and what to use instead with an
/// alias.
#[inline]
fn deprecated_warning_with_alias(deprecated_field: &str, new_field: &str, alias: &str) {
eprintln!(
@@ -1151,8 +1157,8 @@ fn get_default_widget_and_count(
#[cfg(feature = "battery")]
fn get_use_battery(args: &BottomArgs, config: &Config) -> bool {
// 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()
&& let Ok(batteries) = battery_manager.batteries()
&& batteries.count() == 0
@@ -1348,7 +1354,8 @@ fn parse_legend_position(
} else if let Some(s) = cfg {
parse_position_or_err(s, setting, OptionError::invalid_config_value)
} else if let Some((s, name)) = deprecated_cfg {
// Remove the deprecated args/code paths and copy back to the function above later.
// Remove the deprecated args/code paths and copy back to the function
// above later.
if let Some(alias) = alias {
deprecated_warning_with_alias(name, setting, alias);
} else {
@@ -1655,17 +1662,17 @@ mod test {
super::init_app(args, config).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", "generate_schema"];
for arg in app.get_arguments().collect::<Vec<_>>() {
@@ -1710,8 +1717,8 @@ mod test {
use super::{DEFAULT_CONFIG_FILE_LOCATION, get_config_path};
// Case three: no previous config, no XDG var.
// SAFETY: This is fine, this is just a test, and no other test affects env
// vars.
// SAFETY: This is fine, this is just a test, and no other test affects
// env vars.
unsafe {
std::env::remove_var("XDG_CONFIG_HOME");
}
@@ -1729,8 +1736,8 @@ mod test {
}
// Case two: no previous config, XDG var exists.
// SAFETY: This is fine, this is just a test, and no other test affects env
// vars.
// SAFETY: This is fine, this is just a test, and no other test affects
// env vars.
unsafe {
std::env::set_var("XDG_CONFIG_HOME", "/tmp");
}
+4 -2
View File
@@ -14,7 +14,8 @@ pub(crate) struct DiskConfig {
/// A filter over the mount names.
pub(crate) mount_filter: Option<IgnoreList>,
/// Whether to include block devices that aren't currently mounted (currently Linux only). Defaults to false.
/// Whether to include block devices that aren't currently mounted
/// (currently Linux only). Defaults to false.
pub(crate) include_unmounted: Option<bool>,
/// A list of disk widget columns.
@@ -62,7 +63,8 @@ mod test {
toml_edit::de::from_str::<DiskConfig>(config).expect_err("Should error out!");
}
/// Test that disk enum variants that are advertised in the schema are valid.
/// Test that disk enum variants that are advertised in the schema are
/// valid.
#[cfg(feature = "generate_schema")]
#[test]
fn ensure_disk_column_schema_is_accepted() {
+4 -2
View File
@@ -25,7 +25,8 @@ pub(crate) struct DiskIoGraphConfig {
/// Whether to show the write rate line. Defaults to true.
pub(crate) show_write: Option<bool>,
/// Whether to label legend entries by device name or mount point. Defaults to disk name.
/// Whether to label legend entries by device name or mount point. Defaults
/// to disk name.
pub(crate) legend: Option<DiskGraphLegend>,
/// Whether to use a logarithmic scale on the y-axis. Defaults to false.
@@ -37,6 +38,7 @@ pub(crate) struct DiskIoGraphConfig {
/// An optional list of device names to include or exclude.
pub(crate) name_filter: Option<IgnoreList>,
/// Whether to include block devices that aren't currently mounted (currently Linux only). Defaults to false.
/// Whether to include block devices that aren't currently mounted
/// (currently Linux only). Defaults to false.
pub(crate) include_unmounted: Option<bool>,
}
+2 -1
View File
@@ -353,7 +353,8 @@ mod test {
Some(3)
);
// Test disk -> processes, processes -> process sort, process sort -> network
// Test disk -> processes, processes -> process sort, process sort ->
// network
assert_eq!(
ret_bottom_layout.rows[1].children[1].children[1].children[0].down_neighbour,
Some(7)
+2 -1
View File
@@ -12,7 +12,8 @@ pub struct MemoryGraphConfig {
/// Where to place the legend for the memory chart widget.
pub(crate) legend_position: Option<String>,
/// Whether to collect and display cache and buffer memory. Not available on Windows.
/// Whether to collect and display cache and buffer memory. Not available on
/// Windows.
#[cfg_attr(target_os = "windows", allow(dead_code))]
pub(crate) cache_memory: Option<bool>,
+7 -5
View File
@@ -21,14 +21,16 @@ pub(crate) struct NetworkGraphConfig {
/// Displays the network widget using bytes. Defaults to bits.
pub(crate) use_bytes: Option<bool>,
/// Displays the network widget with a log scale. Defaults to a non-log scale.
/// Displays the network widget with a log scale. Defaults to a non-log
/// scale.
pub(crate) use_log: Option<bool>,
/// Displays the network widget with a binary prefix (e.g. kibibits) rather than a decimal
/// prefix (e.g. kilobits). Defaults to decimal prefixes.
/// Displays the network widget with a binary prefix (e.g. kibibits) rather
/// than a decimal prefix (e.g. kilobits). Defaults to decimal prefixes.
pub(crate) use_binary_prefix: Option<bool>,
/// Zeroes out the total network usage ("All") counters so it shows the total usage
/// since the app is started, rather than the total usage since boot.
/// Zeroes out the total network usage ("All") counters so it shows the
/// total usage since the app is started, rather than the total usage
/// since boot.
pub(crate) start_zeroed: Option<bool>,
}
+10 -5
View File
@@ -9,7 +9,8 @@ use crate::{canvas::components::data_table::SortOrder, widgets::ProcColumn};
pub(crate) struct ProcessesConfig {
/// A list of process widget columns.
#[serde(default)]
pub columns: Vec<ProcColumn>, // TODO: make this more composable(?) in the future, we might need to rethink how it's done for custom widgets
pub columns: Vec<ProcColumn>, /* TODO: make this more composable(?) in the future, we might
* need to rethink how it's done for custom widgets */
/// The default sort column.
#[serde(default)]
@@ -37,7 +38,8 @@ pub(crate) struct ProcessesConfig {
// across platforms.
//
// #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
/// Disable the advanced kill dialog and just show the basic one with no options.
/// Disable the advanced kill dialog and just show the basic one with no
/// options.
#[cfg_attr(
not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")),
allow(dead_code)
@@ -47,7 +49,8 @@ pub(crate) struct ProcessesConfig {
/// Defaults to showing process memory usage by value.
pub default_memory_value: Option<bool>,
/// Groups processes with the same name by default. No effect if `--tree` is set.
/// Groups processes with the same name by default. No effect if `--tree` is
/// set.
pub default_grouped: Option<bool>,
/// Enables regex by default while searching.
@@ -62,7 +65,8 @@ pub(crate) struct ProcessesConfig {
/// Makes the process widget use tree mode by default.
pub default_tree: Option<bool>,
/// Calculates process CPU usage as a percentage of current usage rather than total usage.
/// Calculates process CPU usage as a percentage of current usage rather
/// than total usage.
pub current_usage: Option<bool>,
/// Show process CPU% usage without averaging over the number of CPU cores.
@@ -181,7 +185,8 @@ mod test {
);
}
/// Test that process enum variants that are advertised in the schema are valid.
/// Test that process enum variants that are advertised in the schema are
/// valid.
#[cfg(feature = "generate_schema")]
#[test]
fn ensure_process_column_schema_is_accepted() {
+2 -2
View File
@@ -281,8 +281,8 @@ mod test {
#[test]
fn color_spelling_aliases_work() {
// Parse all_styling_color.toml (which uses the "color" alias spelling throughout)
// and assert every field was actually parsed.
// Parse all_styling_color.toml (which uses the "color" alias spelling
// throughout) and assert every field was actually parsed.
let toml_str = include_str!("../../../tests/valid_configs/all_styling_color.toml");
let config = toml_edit::de::from_str::<Config>(toml_str)
.expect("config file should parse")
+2 -2
View File
@@ -4,8 +4,8 @@ pub(super) mod default;
pub(super) mod gruvbox;
pub(super) mod nord;
/// Convert a [`ratatui::style::Color`] into a [`ratatui::style::Style`] with the colour
/// as the foreground.
/// Convert a [`ratatui::style::Color`] into a [`ratatui::style::Style`] with
/// the colour as the foreground.
macro_rules! colour {
($value:expr) => {
ratatui::style::Style::new().fg($value)
+2 -1
View File
@@ -22,7 +22,8 @@ pub(crate) struct TempConfig {
#[cfg(test)]
mod tests {
/// Test that temp enum variants that are advertised in the schema are valid.
/// Test that temp enum variants that are advertised in the schema are
/// valid.
#[cfg(feature = "generate_schema")]
#[test]
fn ensure_temp_column_schema_is_accepted() {
+2 -2
View File
@@ -14,8 +14,8 @@ pub(crate) struct TempGraphConfig {
#[serde(default)]
pub(crate) legend_position: Option<String>,
/// An upper temperature value for the graph; entries higher than this will be hidden. If not set,
/// there is no limit.
/// An upper temperature value for the graph; entries higher than this will
/// be hidden. If not set, there is no limit.
///
/// Is in the configured temperature unit.
#[serde(default)]
+18 -14
View File
@@ -74,8 +74,8 @@ impl InputFieldState {
/// such that this is clear this only matters for drawing... but it also
/// changes states...
pub(crate) fn get_start_position(&mut self, available_width: usize, is_force_redraw: bool) {
// Remember - the number of columns != the number of grapheme slots/sizes, you
// cannot use index to determine this reliably!
// Remember - the number of columns != the number of grapheme
// slots/sizes, you cannot use index to determine this reliably!
let start_index = if is_force_redraw {
0
@@ -100,7 +100,8 @@ impl InputFieldState {
// - The current start index can show the cursor's word.
// - The current start index cannot show the cursor's word.
//
// What differs is how we "scroll" based on the cursor movement direction.
// What differs is how we "scroll" based on the cursor movement
// direction.
self.display_start_index = match self.cursor_direction {
CursorDirection::Right => {
@@ -108,7 +109,8 @@ impl InputFieldState {
// 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
// If the current position is past the last visible
// element, skip until we
// see it.
let mut index = 0;
@@ -159,8 +161,8 @@ impl InputFieldState {
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_query[0..ctx], 0);
@@ -182,8 +184,8 @@ impl InputFieldState {
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_query[0..ctx], 0);
@@ -717,18 +719,19 @@ mod tests {
/// producing the wrong result or panicking.
#[test]
fn delete_previous_word_unicode() {
// "你好 world" - '你'=3 bytes, '好'=3 bytes, ' '=1, "world"=5, so 12 bytes
// total
// "你好 world" - '你'=3 bytes, '好'=3 bytes, ' '=1, "world"=5, so 12
// bytes total
let mut state = InputFieldState::default();
state.insert_string("你好 world".to_string());
// Cursor is at the end (byte 12). Deleting the previous word should remove
// "world", leaving "你好 " (7 bytes).
// Cursor is at the end (byte 12). Deleting the previous word should
// remove "world", leaving "你好 " (7 bytes).
state.delete_previous_word();
assert_eq!(state.current_query(), "你好 ");
assert_eq!(state.cursor_index(), 7);
// Deleting again skips the trailing space then removes "你好", leaving "".
// Deleting again skips the trailing space then removes "你好", leaving
// "".
state.delete_previous_word();
assert_eq!(state.current_query(), "");
assert_eq!(state.cursor_index(), 0);
@@ -821,7 +824,8 @@ mod tests {
assert_eq!(state.grapheme_cursor.cur_cursor(), 12);
assert_eq!(state.display_start_index, 10);
// Move past the flag emoji (🇨🇦 = 8 bytes) to the end of string (byte 20).
// Move past the flag emoji (🇨🇦 = 8 bytes) to the end of string (byte
// 20).
state.move_right();
state.get_start_position(4, false);
assert_eq!(state.grapheme_cursor.cur_cursor(), 20);
+4 -2
View File
@@ -27,7 +27,8 @@ impl Process {
}
fn kill(self) -> anyhow::Result<()> {
// SAFETY: Windows API call, this is safe as we are passing in the handle.
// SAFETY: Windows API call, this is safe as we are passing in the
// handle.
let result = unsafe { TerminateProcess(self.0, 1) };
if result.is_err() {
bail!("process may have already been terminated.");
@@ -40,7 +41,8 @@ impl Process {
#[cfg(target_os = "windows")]
impl Drop for Process {
fn drop(&mut self) {
// SAFETY: Windows API call, this is safe as we are passing in the handle.
// SAFETY: Windows API call, this is safe as we are passing in the
// handle.
unsafe {
let _ = CloseHandle(self.0);
}
+7 -6
View File
@@ -53,13 +53,14 @@ 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()),
+5 -3
View File
@@ -333,7 +333,8 @@ impl DiskTableWidget {
},
sort_index: match &config.default_disk_sort_column {
Some(column) => {
// Must check that the column used exists. If not, fall back to 0.
// Must check that the column used exists. If not, fall back
// to 0.
let existing_columns = match columns {
Some(c) => c,
@@ -377,8 +378,9 @@ impl DiskTableWidget {
/// Update the current table data.
pub fn set_table_data(&mut self, data: &InnerData) {
// Note that the data may contain unmounted disks (e.g. we enable it for another disk widget),
// so we have to potentially filter it out here too.
// Note that the data may contain unmounted disks (e.g. we enable it for
// another disk widget), so we have to potentially filter it out
// here too.
let mut data: Vec<DiskWidgetData> = if self.show_unmounted {
data.disk_harvest.clone()
} else {
+6 -5
View File
@@ -581,7 +581,8 @@ impl ProcWidgetState {
let filtered_tree = {
let mut filtered_tree: IntHashMap<Pid, Vec<Pid>> = IntHashMap::default();
// We do a simple DFS traversal to build our filtered parent-to-tree mappings.
// We do a simple DFS traversal to build our filtered parent-to-tree
// mappings.
let mut visited_pids: IntHashMap<Pid, bool> = IntHashMap::default();
let mut stack = orphan_pids
.iter()
@@ -603,8 +604,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()
@@ -711,8 +712,8 @@ impl ProcWidgetState {
has_children = !children_pids.is_empty();
}
// This is so that if an entry is "collapsed" but there are no children, avoid
// drawing the "+".
// This is so that if an entry is "collapsed" but there are no
// children, avoid drawing the "+".
let prefix = if has_children {
if prefixes.is_empty() {
"+ ".to_string()
+2 -2
View File
@@ -169,8 +169,8 @@ impl SortsRow for ProcColumn {
}
}
ProcColumn::User => {
// FIXME: Is there a better way here to keep the to_lowercase? Usually it
// shouldn't matter but...
// FIXME: Is there a better way here to keep the to_lowercase?
// Usually it shouldn't matter but...
if descending {
data.sort_by_cached_key(|pd| Reverse(pd.user.clone()));
} else {
+2 -2
View File
@@ -351,8 +351,8 @@ impl DataToCell<ProcColumn> for ProcWidgetData {
&self, column: &ProcColumn, calculated_width: NonZeroU16,
) -> Option<Cow<'static, str>> {
// 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 {
#[cfg(unix)]
ProcColumn::Nice => self.nice.to_string().into(),
+16 -9
View File
@@ -127,7 +127,8 @@ pub(crate) fn parse_query(search_query: &str, options: &QueryOptions) -> QueryRe
let mut last = 0;
for (index, matched) in s.match_indices(|x| DELIMITER_LIST.contains(&x)) {
if matched == "\"" {
// Always split on quote delimiters to open/close quoted sections.
// Always split on quote delimiters to open/close quoted
// sections.
if last != index {
split_query.push_back(s[last..index].to_owned());
}
@@ -211,7 +212,8 @@ impl std::str::FromStr for PrefixType {
// TODO: Didn't add mem_bytes, total_read, and total_write
// for now as it causes help to be clogged.
// TODO: Add a `name` keyword alias so something like `name = blah` is valid.
// TODO: Add a `name` keyword alias so something like `name = blah` is
// valid.
let mut result = Name;
if multi_eq_ignore_ascii_case!(s, "cpu" | "cpu%") {
@@ -940,7 +942,8 @@ mod tests {
assert!(query.check(&other, false));
}
/// Test state queries with the non-equality operator. This also tests that string comparisons.
/// Test state queries with the non-equality operator. This also tests that
/// string comparisons.
#[test]
fn state_not_equal_query() {
let query = parse_query_no_options("state != sleeping").unwrap();
@@ -1084,8 +1087,8 @@ mod tests {
assert!(!query.check(&other, false));
}
/// A quoted `"!"` as the RHS of a prefixed string query with "!=" should not match the
/// literal `!`, nor should it be treated as an operator.
/// A quoted `"!"` as the RHS of a prefixed string query with "!=" should
/// not match the literal `!`, nor should it be treated as an operator.
#[test]
fn user_negated_equals_quoted_bang() {
let query = parse_query_no_options("user != \"!\"").unwrap();
@@ -1112,7 +1115,8 @@ mod tests {
assert!(!query.check(&without, false));
}
/// Quoted angle brackets should be treated as literal characters in name matches.
/// Quoted angle brackets should be treated as literal characters in name
/// matches.
#[test]
fn quoted_angle_brackets_match_literal() {
let query = parse_query_no_options("\"Thread<15>\"").unwrap();
@@ -1136,7 +1140,8 @@ mod tests {
assert!(!query.check(&non_matching, false));
}
/// A quoted `!=` should be treated as a literal character sequence in name matches.
/// A quoted `!=` should be treated as a literal character sequence in name
/// matches.
#[test]
fn quoted_not_equals_matches_literal() {
let query = parse_query_no_options("\"a!=b\"").unwrap();
@@ -1148,7 +1153,8 @@ mod tests {
assert!(!query.check(&non_matching, false));
}
/// Quoted parentheses should be treated as literal characters in name matches.
/// Quoted parentheses should be treated as literal characters in name
/// matches.
#[test]
fn quoted_parens_match_literal() {
let query = parse_query_no_options("\"a(b)\"").unwrap();
@@ -1168,7 +1174,8 @@ mod tests {
parse_query_no_options("time !=").unwrap_err();
}
/// Some miscellaneous invalid string searches involving negation (`!`) parsing.
/// Some miscellaneous invalid string searches involving negation (`!`)
/// parsing.
#[test]
fn misc_invalid_bang_search() {
parse_query_no_options("user = !").unwrap_err();
+35 -22
View File
@@ -82,17 +82,18 @@ impl Prefix {
) -> QueryResult<Self> {
if let Some(queue_top) = query.pop_front() {
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());
Ok(Prefix::Attribute(ProcessAttribute::Empty))
} else {
let mut intern_string = vec![queue_top];
// TODO: I think this should consume the quote...? Might need to check the other
// spot we process quotes.
// TODO: I think this should consume the quote...? Might need to
// check the other spot we process quotes.
while let Some(next_str) = query.front() {
if next_str == "\"" {
break;
@@ -110,7 +111,8 @@ impl Prefix {
)?))
}
} else {
// Uh oh, there's nothing left in the stack, but we're inside quotes!
// Uh oh, there's nothing left in the stack, but we're inside
// quotes!
Err(QueryError::new("Missing closing quotation"))
}
}
@@ -195,9 +197,9 @@ impl QueryProcessor for Prefix {
let inner = Prefix::process(query, options)?;
return Ok(Prefix::Negate(Box::new(inner)));
} else if curr == "\"" {
// 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 = Prefix::process_in_quotes(query, options)?;
return if let Some(close_quote) = query.pop_front() {
@@ -230,7 +232,8 @@ impl QueryProcessor for Prefix {
)?));
}
PrefixType::Pid | PrefixType::State | PrefixType::User => {
// We have to check if someone put an (in)equality check...
// We have to check if someone put an (in)equality
// check...
if content == "=" || content == "!=" {
let negate = content.starts_with('!');
@@ -241,22 +244,27 @@ impl QueryProcessor for Prefix {
"`!` is reserved; use `\"!\"` to match the literal character",
));
}
// TODO: [Query] Need to consider the following cases:
// TODO: [Query] Need to consider the
// following cases:
// - (test)
// - (test
// - test)
// These are split into 2 to 3 different strings due to
// 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,
// Do we want these to be valid? They
// should, as a string,
// right?
// We also must check if this value is wrapped in quotes!
// We also must check if this value is
// wrapped in quotes!
let final_value = if string_value == "\"" {
let mut intern_string = vec![];
// Keep parsing until we either hit another quotation or we
// Keep parsing until we either hit
// another quotation or we
// error.
while let Some(next_string) = query.pop_front() {
if next_string == "\"" {
@@ -343,13 +351,16 @@ impl QueryProcessor for Prefix {
}
}
_ => {
// Assume it's some numerical value. Now we gotta parse the content... yay.
// Note that for numerical parsing, we handle unit parsing later, not here.
// Assume it's some numerical value. Now we gotta
// parse the content... yay.
// Note that for numerical parsing, we handle unit
// parsing later, not here.
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
// 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);
@@ -366,7 +377,8 @@ impl QueryProcessor for Prefix {
return Err(QueryError::missing_value());
}
} else if content == ">" || content == "<" {
// We also have to check if the next string is an "="...
// We also have to check if the next string is
// an "="...
if let Some(queue_next) = query.pop_front() {
if queue_next == "=" {
condition = Some(if content == ">" {
@@ -395,7 +407,8 @@ impl QueryProcessor for Prefix {
if let Some(condition) = condition
&& let Some(read_value) = value
{
// Note that the values *might* have a unit or need to be parsed
// Note that the values *might* have a unit or
// need to be parsed
// differently based on the
// prefix type!
@@ -431,8 +444,8 @@ impl QueryProcessor for Prefix {
}
}
// TODO: Give more information here (e.g. closest query?), though this is moreso
// meant as a fallback.
// TODO: Give more information here (e.g. closest query?), though this
// is moreso meant as a fallback.
Err(QueryError::new("Invalid query"))
}
}
+12 -7
View File
@@ -243,25 +243,29 @@ fn test_network_alias() {
run_and_kill_cfg("./tests/valid_configs/network_alias.toml");
}
/// This uses deprecated network settings - once they are removed, this test file should be moved to invalid configs.
/// This uses deprecated network settings - once they are removed, this test
/// file should be moved to invalid configs.
#[test]
fn test_deprecated_network() {
run_and_kill_cfg("./tests/valid_configs/deprecated/network.toml");
}
/// This uses deprecated process settings - once they are removed, this test file should be moved to invalid configs.
/// This uses deprecated process settings - once they are removed, this test
/// file should be moved to invalid configs.
#[test]
fn test_deprecated_processes() {
run_and_kill_cfg("./tests/valid_configs/deprecated/processes.toml");
}
/// This uses deprecated CPU settings - once they are removed, this test file should be moved to invalid configs.
/// This uses deprecated CPU settings - once they are removed, this test file
/// should be moved to invalid configs.
#[test]
fn test_deprecated_cpu() {
run_and_kill_cfg("./tests/valid_configs/deprecated/cpu.toml");
}
/// This uses deprecated memory settings - once they are removed, this test file should be moved to invalid configs.
/// This uses deprecated memory settings - once they are removed, this test file
/// should be moved to invalid configs.
#[test]
fn test_deprecated_memory() {
run_and_kill_cfg("./tests/valid_configs/deprecated/memory.toml");
@@ -277,14 +281,15 @@ fn test_disk_io_graph() {
run_and_kill_cfg("./tests/valid_configs/widget/disk_io_graph.toml");
}
/// This uses deprecated temperature settings - once they are removed, this test file should be moved to invalid configs.
/// This uses deprecated temperature settings - once they are removed, this test
/// file should be moved to invalid configs.
#[test]
fn test_deprecated_temperature() {
run_and_kill_cfg("./tests/valid_configs/deprecated/temperature.toml");
}
/// Test that deprecated warnings are not shown for config options that are not actually set,
/// even when a `[flags]` section is present.
/// Test that deprecated warnings are not shown for config options that are not
/// actually set, even when a `[flags]` section is present.
#[test]
fn test_no_spurious_deprecated_warnings() {
let mut child = btm_command(&["-C", "./tests/valid_configs/empty_flags.toml"])