From 166b502412966671136d74900634b0721daf43ab Mon Sep 17 00:00:00 2001 From: Daniel Lange Date: Thu, 27 Aug 2026 14:33:32 +0200 Subject: [PATCH 1/2] Keep the previously shown process in view when switching threads / kthreads / container visibility Applies on top of the header-meter-clicks branch (PR #2085) Assisted-by: OpenCode Zen --- Action.c | 6 ++++ Table.c | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++-- Table.h | 3 ++ TasksMeter.c | 6 ++++ 4 files changed, 94 insertions(+), 2 deletions(-) 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/Table.c b/Table.c index 937588e96..ef74cf2a6 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 4d3ee1e0a..90718f536 100644 --- a/TasksMeter.c +++ b/TasksMeter.c @@ -90,12 +90,18 @@ static int TasksMeter_click(Meter* this, int relX, int relY ATTR_UNUSED) { 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; } From 17eb297e9c5c50f3a5e8b7bca7cdc37bdc0f5844 Mon Sep 17 00:00:00 2001 From: Daniel Lange Date: Thu, 27 Aug 2026 15:36:27 +0200 Subject: [PATCH 2/2] Apply review fix from the rabbit (calc out the screenTabs line from header_height) Assisted-by: CodeRabbitAI --- ScreenManager.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ScreenManager.c b/ScreenManager.c index c1aff691b..d41fb7075 100644 --- a/ScreenManager.c +++ b/ScreenManager.c @@ -294,10 +294,10 @@ 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)) { - /* Click in the meters/header area */ +} 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;