diff --git a/Action.c b/Action.c index 673e09b09..eb3e6e8ae 100644 --- a/Action.c +++ b/Action.c @@ -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; @@ -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; @@ -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; } diff --git a/Header.c b/Header.c index 7447f7189..86466be31 100644 --- a/Header.c +++ b/Header.c @@ -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. */ @@ -10,7 +11,6 @@ in the source distribution for its full text. #include "Header.h" #include -#include #include #include #include @@ -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++) { @@ -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 */ } } @@ -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; +} diff --git a/Header.h b/Header.h index b0ffd5921..f14ed4b8d 100644 --- a/Header.h +++ b/Header.h @@ -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 diff --git a/HeaderLayout.h b/HeaderLayout.h index c8d51c845..47f449ef1 100644 --- a/HeaderLayout.h +++ b/HeaderLayout.h @@ -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. */ @@ -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 */ diff --git a/Meter.h b/Meter.h index 552885144..2428fd9b2 100644 --- a/Meter.h +++ b/Meter.h @@ -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; @@ -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<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 diff --git a/ScreenManager.c b/ScreenManager.c index f79596c38..d41fb7075 100644 --- a/ScreenManager.c +++ b/ScreenManager.c @@ -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); diff --git a/Table.c b/Table.c index 8f0903c52..dab87d371 100644 --- a/Table.c +++ b/Table.c @@ -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); @@ -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); @@ -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; } @@ -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). @@ -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); diff --git a/Table.h b/Table.h index 18c5d7516..006e74609 100644 --- a/Table.h +++ b/Table.h @@ -36,6 +36,7 @@ typedef struct Table_ { int following; /* -1 or row being visually tracked in the user interface */ int stableId; /* stable tree view: row ID to keep at fixed screen position (-1 = inactive) */ int stableLastIdx; /* panel index where stableId row was placed in the last rebuild */ + int pendingSelection; /* one-shot: row ID to (re)select in the next rebuild (-1 = inactive) */ struct Panel_* panel; } Table; @@ -66,6 +67,8 @@ extern const TableClass Table_class; void Table_setPanel(Table* this, struct Panel_* panel); +void Table_preserveSelection(Table* this); + void Table_printHeader(const Settings* settings, RichString* header); void Table_add(Table* this, struct Row_* row); diff --git a/TasksMeter.c b/TasksMeter.c index f522b753b..90718f536 100644 --- a/TasksMeter.c +++ b/TasksMeter.c @@ -9,6 +9,7 @@ in the source distribution for its full text. #include "TasksMeter.h" +#include "Action.h" #include "CRT.h" #include "Machine.h" #include "Macros.h" @@ -47,12 +48,12 @@ static void TasksMeter_display(const Object* cast, RichString* out) { len = xSnprintf(buffer, sizeof(buffer), "%d", (int)this->values[2]); RichString_appendnAscii(out, CRT_colors[METER_VALUE], buffer, len); - RichString_appendAscii(out, settings->hideUserlandThreads ? CRT_colors[METER_SHADOW] : CRT_colors[METER_TEXT], ", "); + RichString_appendAscii(out, CRT_colors[METER_TEXT], ", "); len = xSnprintf(buffer, sizeof(buffer), "%d", (int)this->values[1]); RichString_appendnAscii(out, settings->hideUserlandThreads ? CRT_colors[METER_SHADOW] : CRT_colors[TASKS_RUNNING], buffer, len); RichString_appendAscii(out, settings->hideUserlandThreads ? CRT_colors[METER_SHADOW] : CRT_colors[METER_TEXT], " thr"); - RichString_appendAscii(out, settings->hideKernelThreads ? CRT_colors[METER_SHADOW] : CRT_colors[METER_TEXT], ", "); + RichString_appendAscii(out, CRT_colors[METER_TEXT], ", "); len = xSnprintf(buffer, sizeof(buffer), "%d", (int)this->values[0]); RichString_appendnAscii(out, settings->hideKernelThreads ? CRT_colors[METER_SHADOW] : CRT_colors[TASKS_RUNNING], buffer, len); RichString_appendAscii(out, settings->hideKernelThreads ? CRT_colors[METER_SHADOW] : CRT_colors[METER_TEXT], " kthr"); @@ -63,6 +64,50 @@ static void TasksMeter_display(const Object* cast, RichString* out) { RichString_appendAscii(out, CRT_colors[METER_TEXT], " running"); } +static int TasksMeter_click(Meter* this, int relX, int relY ATTR_UNUSED) { + /* Click handling is only implemented in text mode. Go somebody elseā„¢ do that for LED mode :) */ + if (this->mode != TEXT_METERMODE) { + return HTOP_OK; + } + + Settings* settings = this->host->settings; + char tmp[32]; + + /* Layout in text mode (after the "Tasks: " caption): + * {processes}, {thr_count} thr, {kthr_count} kthr; {running} running + */ + static const int captionLen = 7; // strlen("Tasks: ") + int procLen = xSnprintf(tmp, sizeof(tmp), "%d", (int)this->values[2]); + + int thrStart = captionLen + procLen + 2; // +2 for ", " + int thrLen = xSnprintf(tmp, sizeof(tmp), "%d", (int)this->values[1]); + int thrEnd = thrStart + thrLen + 4; // +4 for " thr" + + int kthrStart = thrEnd + 2; // +2 for ", " + int kthrLen = xSnprintf(tmp, sizeof(tmp), "%d", (int)this->values[0]); + int kthrEnd = kthrStart + kthrLen + 5; // +5 for " kthr" + + if (relX >= thrStart && relX < thrEnd) { + settings->hideUserlandThreads = !settings->hideUserlandThreads; + settings->lastUpdate++; + + Table_preserveSelection(this->host->activeTable); + + return HTOP_RECALCULATE | HTOP_SAVE_SETTINGS | HTOP_KEEP_FOLLOWING; + } + + if (relX >= kthrStart && relX < kthrEnd) { + settings->hideKernelThreads = !settings->hideKernelThreads; + settings->lastUpdate++; + + Table_preserveSelection(this->host->activeTable); + + return HTOP_RECALCULATE | HTOP_SAVE_SETTINGS | HTOP_KEEP_FOLLOWING; + } + + return HTOP_OK; +} + const MeterClass TasksMeter_class = { .super = { .extends = Class(Meter), @@ -70,6 +115,7 @@ const MeterClass TasksMeter_class = { .display = TasksMeter_display, }, .updateValues = TasksMeter_updateValues, + .click = TasksMeter_click, .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 4,