Skip to content
6 changes: 6 additions & 0 deletions Action.c
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ static Htop_Reaction actionToggleKernelThreads(State* st) {
settings->hideKernelThreads = !settings->hideKernelThreads;
settings->lastUpdate++;

Table_preserveSelection(st->host->activeTable);

Machine_scanTables(st->host); // needed to not have a visible delay showing wrong data

return HTOP_RECALCULATE | HTOP_SAVE_SETTINGS | HTOP_KEEP_FOLLOWING;
Expand All @@ -260,6 +262,8 @@ static Htop_Reaction actionToggleUserlandThreads(State* st) {
settings->hideUserlandThreads = !settings->hideUserlandThreads;
settings->lastUpdate++;

Table_preserveSelection(st->host->activeTable);

Machine_scanTables(st->host); // needed to not have a visible delay showing wrong data

return HTOP_RECALCULATE | HTOP_SAVE_SETTINGS | HTOP_KEEP_FOLLOWING;
Expand All @@ -270,6 +274,8 @@ static Htop_Reaction actionToggleRunningInContainer(State* st) {
settings->hideRunningInContainer = !settings->hideRunningInContainer;
settings->lastUpdate++;

Table_preserveSelection(st->host->activeTable);

return HTOP_RECALCULATE | HTOP_SAVE_SETTINGS | HTOP_KEEP_FOLLOWING;
}

Expand Down
86 changes: 61 additions & 25 deletions Header.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
htop - Header.c
(C) 2004-2011 Hisham H. Muhammad
(C) 2020-2026 htop dev team
Released under the GNU GPLv2+, see the COPYING file
in the source distribution for its full text.
*/
Expand All @@ -10,7 +11,6 @@ in the source distribution for its full text.
#include "Header.h"

#include <assert.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
Expand Down Expand Up @@ -180,6 +180,17 @@ Meter* Header_addMeterByClass(Header* this, const MeterClass* type, unsigned int
return meter;
}

/* Meters in text mode may span into empty neighboring columns; keep the
* drawn and clickable widths consistent. */
static int Header_meterWidth(const Header* this, const HeaderLayoutDimensions* colDims, const Meter* meter, size_t col, const int width) {
if (meter->mode == TEXT_METERMODE && !Meter_isMultiColumn(meter) && meter->columnWidthCount > 1) {
size_t spanCol = col + meter->columnWidthCount - 1;
return HeaderLayout_getColumnDimensions(this->headerLayout, this->pad, width, spanCol).x2 - colDims->x1;
}

return colDims->x2 - colDims->x1;
}

void Header_reinit(Header* this) {
Header_forEachColumn(this, col) {
for (int i = 0; i < Vector_size(this->columns[col]); i++) {
Expand All @@ -200,40 +211,19 @@ void Header_draw(const Header* this) {
}
const size_t numCols = HeaderLayout_getColumns(this->headerLayout);
const int width = COLS - 2 * pad - ((int)numCols - 1);
int x = pad;
float roundingLoss = 0.0F;

Header_forEachColumn(this, col) {
Vector* meters = this->columns[col];
float colWidth = (float)width * HeaderLayout_layouts[this->headerLayout].widths[col] / 100.0F;

roundingLoss += colWidth - floorf(colWidth);
if (roundingLoss >= 1.0F) {
colWidth += 1.0F;
roundingLoss -= 1.0F;
}
HeaderLayoutDimensions colDims = HeaderLayout_getColumnDimensions(this->headerLayout, pad, width, col);

for (int y = (pad / 2), i = 0; i < Vector_size(meters); i++) {
Meter* meter = (Meter*) Vector_get(meters, i);

float actualWidth = colWidth;

/* Let meters in text mode expand to the right on empty neighbors;
except for multi column meters. */
if (meter->mode == TEXT_METERMODE && !Meter_isMultiColumn(meter)) {
for (int j = 1; j < meter->columnWidthCount; j++) {
actualWidth++; /* separator column */
actualWidth += (float)width * HeaderLayout_layouts[this->headerLayout].widths[col + j] / 100.0F;
}
}
int drawWidth = Header_meterWidth(this, &colDims, meter, col, width);

assert(meter->draw);
meter->draw(meter, x, y, floorf(actualWidth));
meter->draw(meter, colDims.x1, y, drawWidth);
y += meter->h;
}

x += floorf(colWidth);
x++; /* separator column */
}
}

Expand Down Expand Up @@ -307,3 +297,49 @@ int Header_calculateHeight(Header* this) {

return maxHeight;
}

int Header_click(const Header* this, int x, int y) {
const size_t numCols = HeaderLayout_getColumns(this->headerLayout);
const int width = COLS - 2 * this->pad - ((int)numCols - 1);

Header_forEachColumn(this, col) {
HeaderLayoutDimensions colDims = HeaderLayout_getColumnDimensions(this->headerLayout, this->pad, width, col);

if (x < colDims.x1) {
continue;
}

const bool columnOwnsX = (x < colDims.x2);
bool hit = false;

Vector* meters = this->columns[col];
for (int meterY = (this->pad / 2), i = 0; i < Vector_size(meters); i++) {
Meter* meter = (Meter*) Vector_get(meters, i);

if (y < meterY || y >= meterY + meter->h) {
meterY += meter->h;
continue;
}

const int meterHitWidth = Header_meterWidth(this, &colDims, meter, col, width);
if (x >= colDims.x1 + meterHitWidth) {
/* beyond this meter's drawn extent -> try the next column */
break;
}

Meter_Click clickFn = Meter_clickFn(meter);
if (clickFn) {
return clickFn(meter, x - colDims.x1, y - meterY);
}

/* meter at this position has no click handler: stop the search */
hit = true;
break;
}

if (hit || columnOwnsX)
break;
}

return HTOP_OK;
}
2 changes: 2 additions & 0 deletions Header.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,6 @@ void Header_updateData(Header* this);

int Header_calculateHeight(Header* this);

int Header_click(const Header* this, int x, int y);

#endif
37 changes: 36 additions & 1 deletion HeaderLayout.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#define HEADER_HeaderLayout
/*
htop - HeaderLayout.h
(C) 2021 htop dev team
(C) 2021-2026 htop dev team
Released under the GNU GPLv2+, see the COPYING file
in the source distribution for its full text.
*/
Expand Down Expand Up @@ -81,4 +81,39 @@ static inline HeaderLayout HeaderLayout_fromName(const char* name) {
return LAST_HEADER_LAYOUT;
}

typedef struct HeaderLayoutDimensions_ {
int x1;
int x2;
} HeaderLayoutDimensions;

/*
* Calculate the rectangle covered by the given column of the layout,
* needed (consistently) for header drawing and click handling.
* The rectangle is computed using integer arithmetic, distributing
* rounding remainders onto later columns. No FP math needed here.
*/
static inline HeaderLayoutDimensions HeaderLayout_getColumnDimensions(HeaderLayout hLayout, int x, int width, size_t col) {
assert(0 <= hLayout);
assert(hLayout < LAST_HEADER_LAYOUT);

int carried = 0;
for (size_t i = 0; i < HeaderLayout_layouts[hLayout].columns; i++) {
int colWidth = width * HeaderLayout_layouts[hLayout].widths[i] / 100;
carried += width * HeaderLayout_layouts[hLayout].widths[i] % 100;
if (carried >= 100) {
carried -= 100;
colWidth++;
}

if (i == col) {
return (HeaderLayoutDimensions) { .x1 = x, .x2 = x + colWidth };
}

x += colWidth + 1; // separator column
}

assert(col < HeaderLayout_layouts[hLayout].columns);
return (HeaderLayoutDimensions) { .x1 = x, .x2 = x };
}

#endif /* HEADER_HeaderLayout */
3 changes: 3 additions & 0 deletions Meter.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ typedef ATTR_NONNULL void (*Meter_UpdateValues)(Meter*);
typedef ATTR_NONNULL void (*Meter_Draw)(Meter*, int, int, int);
typedef ATTR_NONNULL const char* (*Meter_GetCaption)(const Meter*);
typedef ATTR_NONNULL ATTR_ACCESS3_W(2, 3) void (*Meter_GetUiName)(const Meter*, char*, size_t);
typedef ATTR_NONNULL int (*Meter_Click)(Meter*, int relX, int relY);

typedef struct MeterClass_ {
const ObjectClass super;
Expand All @@ -65,6 +66,7 @@ typedef struct MeterClass_ {
const Meter_Draw draw;
const Meter_GetCaption getCaption;
const Meter_GetUiName getUiName;
const Meter_Click click;
const MeterModeId defaultMode;
const uint32_t supportedModes; /* bitset of supported modes, 1<<mode_id */
const double total;
Expand Down Expand Up @@ -100,6 +102,7 @@ typedef struct MeterClass_ {
#define Meter_attributes(this_) As_Meter(this_)->attributes
#define Meter_name(this_) As_Meter(this_)->name
#define Meter_uiName(this_) As_Meter(this_)->uiName
#define Meter_clickFn(this_) As_Meter(this_)->click
#define Meter_isMultiColumn(this_) As_Meter(this_)->isMultiColumn
#define Meter_isPercentChart(this_) As_Meter(this_)->isPercentChart

Expand Down
18 changes: 18 additions & 0 deletions ScreenManager.c
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,24 @@ void ScreenManager_run(ScreenManager* this, Panel** lastFocus, int* lastKey, con
panelFocus->lastMouseBarClickX = mevent.x;
ch = KEY_MOUSE_BAR_CLICK;
}
} else if (!this->state->hideMeters && this->header &&
mevent.x >= this->x1 && mevent.x < COLS + this->x2 &&
mevent.y >= this->y1 && mevent.y < this->y1 + header_height(this) - (settings->screenTabs ? 1 : 0)) {
/* Click in the meters/header area (excluding the screen-tab row) */
int meterReaction = Header_click(this->header, mevent.x - this->x1, mevent.y - this->y1);
if (meterReaction & HTOP_RECALCULATE) {
rescan = true;
sortTimeout = 0;
}
if (meterReaction & HTOP_SAVE_SETTINGS) {
this->host->settings->changed = true;
}
if (meterReaction & HTOP_REFRESH) {
sortTimeout = 0;
}
if (meterReaction) {
force_redraw = true;
}
} else {
for (size_t i = 0; i < this->panelCount; i++) {
Panel* panel = (Panel*) Vector_get(this->panels, i);
Expand Down
81 changes: 79 additions & 2 deletions Table.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,21 @@ Table* Table_init(Table* this, const ObjectClass* klass, Machine* host) {
this->following = -1;
this->stableId = -1;
this->stableLastIdx = 0;
this->pendingSelection = -1;
this->host = host;
return this;
}

void Table_preserveSelection(Table* this) {
Panel* panel = this->panel;
if (panel == NULL)
return;

const Row* selected = (const Row*) Panel_getSelected(panel);
if (selected)
this->pendingSelection = selected->id;
}

void Table_done(Table* this) {
Hashtable_delete(this->table);
Vector_delete(this->displayList);
Expand Down Expand Up @@ -275,19 +286,60 @@ void Table_rebuildPanel(Table* this) {
}
}

/* One-shot: keep the selection on the currently shown process when a
* visibility toggle (H/K/O) just happened, redirecting to the process a
* now-hidden row belongs to. If no visible target remains, fall back to
* anchoring the nearest surviving row below. Ignored while following. */
int wantId = this->pendingSelection;
int pendingSel = -1;
bool pendingFallback = false;
if (this->following == -1 && wantId != -1) {
const Row* pending = (const Row*) Hashtable_get(this->table, wantId);
if (pending != NULL) {
if (pending->show) {
pendingSel = wantId;
} else {
int parentId = Row_getGroupOrParent(pending);
const Row* redirect = (const Row*) Hashtable_get(this->table, parentId);
if (parentId != wantId
&& redirect != NULL && redirect->show
&& !Row_matchesFilter(redirect, this)) {
pendingSel = parentId;
} else {
pendingFallback = true;
}
}
}
}
this->pendingSelection = -1;

const int rowCount = Vector_size(this->displayList);
bool foundFollowed = false;
bool foundPendingSelection = false;
bool fallbackNext = false;
int fallbackIdx = -1;
int stableFoundIdx = -1;
int idx = 0;

for (int i = 0; i < rowCount; i++) {
Row* row = (Row*) Vector_get(this->displayList, i);

if (pendingFallback && row->id == wantId) {
/* The wanted row is hidden; anchor the next surviving row instead */
pendingFallback = false;
fallbackNext = true;
}

if ( !row->show || (Row_matchesFilter(row, this) == true) )
continue;

Panel_set(this->panel, idx, (Object*)row);

if (fallbackNext) {
fallbackNext = false;
fallbackIdx = idx;
}

if (this->following != -1 && row->id == this->following) {
foundFollowed = true;
Panel_setSelected(this->panel, idx);
Expand All @@ -306,6 +358,19 @@ void Table_rebuildPanel(Table* this) {
this->panel->allowExcessScrollV = true;
}

if (pendingSel != -1 && row->id == pendingSel) {
foundPendingSelection = true;
Panel_setSelected(this->panel, idx);
/* Keep scroll position relative to the selected row */
int newScrollV = idx - stableOffset;
if (!hardMode || idx == 0) {
if (newScrollV < 0)
newScrollV = 0;
}
this->panel->scrollV = newScrollV;
this->panel->allowExcessScrollV = true;
}

if (stableActive && row->id == this->stableId) {
stableFoundIdx = idx;
}
Expand All @@ -319,8 +384,20 @@ void Table_rebuildPanel(Table* this) {
Panel_setSelectionColor(this->panel, PANEL_SELECTION_FOCUS);
}

if (fallbackIdx == -1 && fallbackNext && idx > 0)
fallbackIdx = idx - 1; /* hidden row was last; anchor the new last row */

if (this->following == -1) {
if (stableActive && stableFoundIdx != -1) {
if (fallbackIdx != -1) {
/* No visible target for the pending selection: anchor the nearest
surviving row at the same screen position */
Panel_setSelected(this->panel, fallbackIdx);
int newScrollV = fallbackIdx - stableOffset;
if (newScrollV < 0)
newScrollV = 0;
this->panel->scrollV = newScrollV;
this->panel->allowExcessScrollV = true;
} else if (!foundPendingSelection && stableActive && stableFoundIdx != -1) {
/* Stable tree view: keep the anchor row at the same screen line.
In hard mode, scrollV may go negative to render empty lines above row 0,
but only when the root is not selected (reset to 0 when root is at the top).
Expand All @@ -339,7 +416,7 @@ void Table_rebuildPanel(Table* this) {
this->panel->scrollV = newScrollV;
this->panel->allowExcessScrollV = true;
this->stableLastIdx = stableFoundIdx;
} else {
} else if (!foundPendingSelection) {
/* Normal behavior: restore position by index */
if (currPos > 0 && currPos == currSize - 1)
Panel_setSelected(this->panel, Panel_size(this->panel) - 1);
Expand Down
Loading
Loading