Skip to content

Commit 4db39da

Browse files
authored
feature: Add mouse support to sorting columns (#413)
Adds mouse support for sorting columns within the process widget. You can now click on the column header to sort (or invert the sort).
1 parent ce9818d commit 4db39da

File tree

8 files changed

+135
-26
lines changed

8 files changed

+135
-26
lines changed

.vscode/settings.json

+1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"nuget",
9898
"nvme",
9999
"paren",
100+
"pcpu",
100101
"pids",
101102
"pmem",
102103
"powerpc",

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121

2222
- [#409](https://github.com/ClementTsang/bottom/pull/409): Adds `Ctrl-w` and `Ctrl-h` shortcuts in search, to delete a word and delete a character respectively.
2323

24+
- [#413](https://github.com/ClementTsang/bottom/pull/413): Adds mouse support for sorting process columns.
25+
2426
## Changes
2527

2628
- [#372](https://github.com/ClementTsang/bottom/pull/372): Hides the SWAP graph and legend in normal mode if SWAP is 0.

README.md

+4-3
Original file line numberDiff line numberDiff line change
@@ -445,9 +445,10 @@ Note that the `and` operator takes precedence over the `or` operator.
445445

446446
#### Process bindings
447447

448-
| | |
449-
| ----- | --------------------------------------------------------------------------------------------------- |
450-
| Click | If in tree mode and you click on a selected entry, it toggles whether the branch is expanded or not |
448+
| | |
449+
| ---------------------- | --------------------------------------------------------------------------------------------------- |
450+
| Click on process entry | If in tree mode and you click on a selected entry, it toggles whether the branch is expanded or not |
451+
| Click on table header | Sorts the widget by that column, or inverts the sort if already selected |
451452

452453
## Features
453454

src/app.rs

+67-17
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ impl App {
399399
// If it just opened, move left
400400
proc_widget_state
401401
.columns
402-
.set_to_sorted_index(&proc_widget_state.process_sorting_type);
402+
.set_to_sorted_index_from_type(&proc_widget_state.process_sorting_type);
403403
self.move_widget_selection(&WidgetDirection::Left);
404404
} else {
405405
// Otherwise, move right if currently on the sort widget
@@ -469,6 +469,7 @@ impl App {
469469
}
470470
}
471471

472+
proc_widget_state.requires_redraw = true;
472473
self.proc_state.force_update = Some(self.current_widget.widget_id);
473474
}
474475
}
@@ -692,15 +693,17 @@ impl App {
692693
self.delete_dialog_state.is_showing_dd = false;
693694
}
694695
self.is_force_redraw = true;
695-
} else if let BottomWidgetType::ProcSort = self.current_widget.widget_type {
696-
if let Some(proc_widget_state) = self
697-
.proc_state
698-
.widget_states
699-
.get_mut(&(self.current_widget.widget_id - 2))
700-
{
701-
self.proc_state.force_update = Some(self.current_widget.widget_id - 2);
702-
proc_widget_state.update_sorting_with_columns();
703-
self.toggle_sort();
696+
} else if !self.is_in_dialog() {
697+
if let BottomWidgetType::ProcSort = self.current_widget.widget_type {
698+
if let Some(proc_widget_state) = self
699+
.proc_state
700+
.widget_states
701+
.get_mut(&(self.current_widget.widget_id - 2))
702+
{
703+
self.proc_state.force_update = Some(self.current_widget.widget_id - 2);
704+
proc_widget_state.update_sorting_with_columns();
705+
self.toggle_sort();
706+
}
704707
}
705708
}
706709
}
@@ -1470,6 +1473,8 @@ impl App {
14701473
}
14711474
'c' => {
14721475
if let BottomWidgetType::Proc = self.current_widget.widget_type {
1476+
// FIXME: There's a mismatch bug with this and all sorting types when using the keybind vs the sorting menu.
1477+
// If the sorting menu is open, it won't update when using this!
14731478
if let Some(proc_widget_state) = self
14741479
.proc_state
14751480
.get_mut_widget_state(self.current_widget.widget_id)
@@ -2932,13 +2937,13 @@ impl App {
29322937
// Get our index...
29332938
let clicked_entry = y - *tlc_y;
29342939
// + 1 so we start at 0.
2935-
let offset = 1
2936-
+ if self.is_drawing_border() { 1 } else { 0 }
2937-
+ if self.is_drawing_gap(&self.current_widget) {
2938-
self.app_config_fields.table_gap
2939-
} else {
2940-
0
2941-
};
2940+
let border_offset = if self.is_drawing_border() { 1 } else { 0 };
2941+
let header_gap_offset = 1 + if self.is_drawing_gap(&self.current_widget) {
2942+
self.app_config_fields.table_gap
2943+
} else {
2944+
0
2945+
};
2946+
let offset = border_offset + header_gap_offset;
29422947
if clicked_entry >= offset {
29432948
let offset_clicked_entry = clicked_entry - offset;
29442949
match &self.current_widget.widget_type {
@@ -3030,6 +3035,51 @@ impl App {
30303035
}
30313036
_ => {}
30323037
}
3038+
} else {
3039+
// We might have clicked on a header! Check if we only exceeded the table + border offset, and
3040+
// it's implied we exceeded the gap offset.
3041+
if clicked_entry == border_offset {
3042+
#[allow(clippy::single_match)]
3043+
match &self.current_widget.widget_type {
3044+
BottomWidgetType::Proc => {
3045+
if let Some(proc_widget_state) = self
3046+
.proc_state
3047+
.get_mut_widget_state(self.current_widget.widget_id)
3048+
{
3049+
// Let's now check if it's a column header.
3050+
if let (Some(y_loc), Some(x_locs)) = (
3051+
&proc_widget_state.columns.column_header_y_loc,
3052+
&proc_widget_state.columns.column_header_x_locs,
3053+
) {
3054+
// debug!("x, y: {}, {}", x, y);
3055+
// debug!("y_loc: {}", y_loc);
3056+
// debug!("x_locs: {:?}", x_locs);
3057+
3058+
if y == *y_loc {
3059+
for (itx, (x_left, x_right)) in
3060+
x_locs.iter().enumerate()
3061+
{
3062+
if x >= *x_left && x <= *x_right {
3063+
// Found our column!
3064+
proc_widget_state
3065+
.columns
3066+
.set_to_sorted_index_from_visual_index(
3067+
itx,
3068+
);
3069+
proc_widget_state
3070+
.update_sorting_with_columns();
3071+
self.proc_state.force_update =
3072+
Some(self.current_widget.widget_id);
3073+
break;
3074+
}
3075+
}
3076+
}
3077+
}
3078+
}
3079+
}
3080+
_ => {}
3081+
}
3082+
}
30333083
}
30343084
}
30353085
BottomWidgetType::Battery => {

src/app/states.rs

+15-3
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@ pub struct ColumnInfo {
178178

179179
pub struct ProcColumn {
180180
pub ordered_columns: Vec<ProcessSorting>,
181+
/// The y location of headers. Since they're all aligned, it's just one value.
182+
pub column_header_y_loc: Option<u16>,
183+
/// The x start and end bounds for each header.
184+
pub column_header_x_locs: Option<Vec<(u16, u16)>>,
181185
pub column_mapping: HashMap<ProcessSorting, ColumnInfo>,
182186
pub longest_header_len: u16,
183187
pub column_state: TableState,
@@ -294,6 +298,8 @@ impl Default for ProcColumn {
294298
current_scroll_position: 0,
295299
previous_scroll_position: 0,
296300
backup_prev_scroll_position: 0,
301+
column_header_y_loc: None,
302+
column_header_x_locs: None,
297303
}
298304
}
299305
}
@@ -335,8 +341,8 @@ impl ProcColumn {
335341
.sum()
336342
}
337343

338-
/// ALWAYS call this when opening the sorted window.
339-
pub fn set_to_sorted_index(&mut self, proc_sorting_type: &ProcessSorting) {
344+
/// NOTE: ALWAYS call this when opening the sorted window.
345+
pub fn set_to_sorted_index_from_type(&mut self, proc_sorting_type: &ProcessSorting) {
340346
// TODO [Custom Columns]: If we add custom columns, this may be needed! Since column indices will change, this runs the risk of OOB. So, when you change columns, CALL THIS AND ADAPT!
341347
let mut true_index = 0;
342348
for column in &self.ordered_columns {
@@ -352,6 +358,12 @@ impl ProcColumn {
352358
self.backup_prev_scroll_position = self.previous_scroll_position;
353359
}
354360

361+
/// This function sets the scroll position based on the index.
362+
pub fn set_to_sorted_index_from_visual_index(&mut self, visual_index: usize) {
363+
self.current_scroll_position = visual_index;
364+
self.backup_prev_scroll_position = self.previous_scroll_position;
365+
}
366+
355367
pub fn get_column_headers(
356368
&self, proc_sorting_type: &ProcessSorting, sort_reverse: bool,
357369
) -> Vec<String> {
@@ -432,7 +444,7 @@ impl ProcWidgetState {
432444

433445
// TODO: If we add customizable columns, this should pull from config
434446
let mut columns = ProcColumn::default();
435-
columns.set_to_sorted_index(&process_sorting_type);
447+
columns.set_to_sorted_index_from_type(&process_sorting_type);
436448
if is_grouped {
437449
// Normally defaults to showing by PID, toggle count on instead.
438450
columns.toggle(&ProcessSorting::Count);

src/canvas.rs

+8-2
Original file line numberDiff line numberDiff line change
@@ -327,13 +327,19 @@ impl Painter {
327327
widget.bottom_right_corner = None;
328328
}
329329

330-
// And reset dd_dialog...
330+
// Reset dd_dialog...
331331
app_state.delete_dialog_state.button_positions = vec![];
332332

333-
// And battery dialog...
333+
// Reset battery dialog...
334334
for battery_widget in app_state.battery_state.widget_states.values_mut() {
335335
battery_widget.tab_click_locs = None;
336336
}
337+
338+
// Reset column headers for sorting in process widget...
339+
for proc_widget in app_state.proc_state.widget_states.values_mut() {
340+
proc_widget.columns.column_header_y_loc = None;
341+
proc_widget.columns.column_header_x_locs = None;
342+
}
337343
}
338344

339345
if app_state.help_dialog_state.is_showing_help {

src/canvas/widgets/process_table.rs

+36
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,42 @@ impl ProcessTableWidget for Painter {
507507
f.render_widget(process_block, margined_draw_loc);
508508
}
509509

510+
// Check if we need to update columnar bounds...
511+
if recalculate_column_widths
512+
|| proc_widget_state.columns.column_header_x_locs.is_none()
513+
|| proc_widget_state.columns.column_header_y_loc.is_none()
514+
{
515+
// y location is just the y location of the widget + border size (1 normally, 0 in basic)
516+
proc_widget_state.columns.column_header_y_loc =
517+
Some(draw_loc.y + if draw_border { 1 } else { 0 });
518+
519+
// x location is determined using the x locations of the widget; just offset from the left bound
520+
// as appropriate, and use the right bound as limiter.
521+
522+
let mut current_x_left = draw_loc.x + 1;
523+
let max_x_right = draw_loc.x + draw_loc.width - 1;
524+
525+
let mut x_locs = vec![];
526+
527+
for width in proc_widget_state
528+
.table_width_state
529+
.calculated_column_widths
530+
.iter()
531+
{
532+
let right_bound = current_x_left + width;
533+
534+
if right_bound < max_x_right {
535+
x_locs.push((current_x_left, right_bound));
536+
current_x_left = right_bound + 1;
537+
} else {
538+
x_locs.push((current_x_left, max_x_right));
539+
break;
540+
}
541+
}
542+
543+
proc_widget_state.columns.column_header_x_locs = Some(x_locs);
544+
}
545+
510546
if app_state.should_get_widget_bounds() {
511547
// Update draw loc in widget map
512548
if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {

src/constants.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ pub const CPU_HELP_TEXT: [&str; 2] = [
254254
"Mouse scroll Scrolling over an CPU core/average shows only that entry on the chart",
255255
];
256256

257-
pub const PROCESS_HELP_TEXT: [&str; 14] = [
257+
pub const PROCESS_HELP_TEXT: [&str; 15] = [
258258
"3 - Process widget",
259259
"dd Kill the selected process",
260260
"c Sort by CPU usage, press again to reverse sorting order",
@@ -269,6 +269,7 @@ pub const PROCESS_HELP_TEXT: [&str; 14] = [
269269
"% Toggle between values and percentages for memory usage",
270270
"t, F5 Toggle tree mode",
271271
"+, -, click Collapse/expand a branch while in tree mode",
272+
"click on header Sorts the entries by that column, click again to invert the sort",
272273
];
273274

274275
pub const SEARCH_HELP_TEXT: [&str; 48] = [

0 commit comments

Comments
 (0)