Skip to content

Remember meter positions across layout changes#1993

Open
ugolnikovE wants to merge 1 commit into
htop-dev:mainfrom
ugolnikovE:issue-1941
Open

Remember meter positions across layout changes#1993
ugolnikovE wants to merge 1 commit into
htop-dev:mainfrom
ugolnikovE:issue-1941

Conversation

@ugolnikovE

@ugolnikovE ugolnikovE commented May 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #1941

Based on @Explorer09's suggestion: instead of collapsing meters into the last visible column when the layout is reduced, keep all columns allocated in memory. The rendering layer merges hidden columns visually. Switching back to the original layout restores meter positions.

Hidden columns are permanently collapsed if the user modifies meters in the editor or exits htop.

Summary by CodeRabbit

  • Bug Fixes

    • Temporary header/column reorganizations are now reliably undone, preventing duplicated or hidden meters.
    • Header layout consistently collapses after adds/removes, avoiding stale header state.
    • Moving or deleting meters only triggers layout recalculation when the meter list actually changes, reducing unnecessary redraws.
  • Refactor

    • Internal header and panel behaviors reorganized for more predictable layout and serialization.

Review Change Stack

@BenBE BenBE left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting approach, but something doesn't quite feel right yet. Haven't looked into it too deeply though, thus mostly first impression rn.

Comment thread CategoriesPanel.c Outdated
Comment on lines +50 to +57
bool didCopy = false;
for (size_t i = columns; i < this->header->maxColumns; i++) {
for (int j = 0; j < Vector_size(this->header->columns[i]); j++) {
Vector_add(this->header->columns[columns - 1], Vector_get(this->header->columns[i], j));
didCopy = true;
}
}
this->header->metersCopied = didCopy;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repeatedly assigning didCopy here feels a bit wasteful, especially if we could get the same value by simply or'ing the Vector_size of each column instead. Also given Vector_add may repeatedly resize the backing array it might be preferable if we could somehow forward the needed amount of memory and reserve a properly sized array beforehand. Not much of an performance issue in practice probably, but at least feels cleaner.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, good point. Replaced didCopy with a pre-count of hidden meters and switched from manual Vector_add loop to Vector_splice. I considered dropping the count loop entirely (just checking columns < maxColumns), but kept it for clarity. It makes the intent explicit and avoids unnecessary owner flag toggling when there's nothing to copy

Comment thread Header.c
Comment on lines -45 to +49
Header_forEachColumn(this, i) {
for (size_t i = 0; i < this->maxColumns; i++) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Header_forEachColumn only iterates over visible columns. Since hidden columns now stay allocated in memory, Header_delete needs to free all of them up to maxColumns

Comment thread MetersPanel.c Outdated

#include "MetersPanel.h"

#include <stdbool.h>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already included via MetersPanel.h

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@BenBE BenBE added the enhancement Extension or improvement to existing feature label May 10, 2026
@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Remember original meter column assignments during header-layout previews by splicing hidden internal columns into the last visible column, tracking that with metersCopied, and providing helpers to undo or collapse the splice; header updates run only when meters are actually modified or panels finalize.

Changes

Meter Column Position Memory During Layout Changes

Layer / File(s) Summary
Header struct state and helper declarations
Header.h
Add maxColumns and metersCopied fields and declare Header_undoMetersCopy and Header_collapseLayout.
Header lifecycle and init state
Header.c
Include <stddef.h>, initialize maxColumns from HeaderLayout, and free/allocate columns using maxColumns.
Header layout resizing and helpers
Header.c
Change Header_setLayout to grow-only and introduce Header_undoMetersCopy / Header_collapseLayout to manage ownership and collapse of extra columns.
Header serialization, draw and height calculation
Header.c
Refactor Header_writeBackToSettings, Header_draw, Header_updateData, and Header_calculateHeight to use a drawCol window and do/while traversal so the last visible column can include internal-column meters when metersCopied is false.
Categories panel meter splicing and cleanup
CategoriesPanel.c
Detect hidden internal columns in makeMetersPage, splice their vectors into the last visible column and set metersCopied; undo the splice in delete and eventHandler when rebuilding pages.
Panel event handlers with selective layout reset
AvailableMetersPanel.c, MetersPanel.c
Collapse header layout and clear metersCopied only after meter-add/delete/move when the meters list actually changed (`modified

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through columns, tucked meters tight,
I spliced the hidden ones for a previewed sight.
When layouts expand, they shuffle back near —
but if you move them, they choose to stay clear. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preserving meter positions when header layouts change, which is the primary objective of this pull request.
Linked Issues check ✅ Passed The pull request implements all coding requirements from issue #1941: preserves original meter column assignments when layout reduces [#1941], restores meters to original columns when layout expands [#1941], and clears preserved state on meter edits or exit [#1941].
Out of Scope Changes check ✅ Passed All changes are directly aligned with the objective to remember meter positions: AvailableMetersPanel and MetersPanel reset the flag on modifications, CategoriesPanel handles the splicing logic, and Header manages storage and restoration of meter positions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CategoriesPanel.c`:
- Around line 39-40: The teardown leaves header->metersCopied true after calling
Header_undoMetersCopy(), so the normal draw path still thinks meters are in
copied state and doesn't merge restored columns; update the teardown to clear
the flag by setting this->header->metersCopied = 0 (or false) immediately after
calling Header_undoMetersCopy(), ensuring Header_undoMetersCopy(this->header) is
invoked then the metersCopied field is reset so the draw/layout logic treats
meters as un-copied.

In `@Header.c`:
- Around line 179-183: The loop that accumulates len currently always adds sizes
of hidden columns when iterating past the last visible column (using variables
drawCol, col, numCols, this->maxColumns, and Vector_size), which double-counts
those entries if metersCopied is true; update the loop condition to skip
counting hidden columns when metersCopied is true (e.g., require &&
!metersCopied in the do/while condition) and apply the same fix to the analogous
block that computes colSettings->len later (lines referenced around drawCol use
at 193-210) so colSettings->len matches the actual serialized entries.
- Around line 98-106: The assert in Header_collapseLayout
(assert(this->metersCopied || currentColumns == this->maxColumns)) is too strict
for new call paths (AvailableMetersPanel / MetersPanel clear metersCopied before
invoking collapse); remove or relax that assertion so collapsing proceeds even
when metersCopied is false. Replace the assert with no-op or a runtime guard
that does not abort (e.g., drop the assert entirely) and let the existing
cleanup loop and xReallocArray behavior run normally so reduced layouts with
empty/hidden columns are collapsed without triggering an assertion failure.

In `@MetersPanel.c`:
- Around line 140-143: The code sets modified/result unconditionally after
calling Vector_moveUp/Vector_moveDown, but those functions are no-ops at list
boundaries; change the logic in the handlers that call
Vector_moveUp/Panel_moveSelectedUp and Vector_moveDown/Panel_moveSelectedDown so
you only set modified = true and result = HANDLED when a real move
occurred—detect this by checking whether the item's index changed (compare the
selected index before and after, or use the move function's boolean return if
available) and only mark modified/handled when the index actually differs; apply
the same fix to the symmetric block around the Vector_moveDown calls (also noted
for lines 152-155).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7d88b7fb-52d3-4d81-8e57-bf429d48bab0

📥 Commits

Reviewing files that changed from the base of the PR and between affc210 and 8250e84.

📒 Files selected for processing (5)
  • AvailableMetersPanel.c
  • CategoriesPanel.c
  • Header.c
  • Header.h
  • MetersPanel.c

Comment thread CategoriesPanel.c Outdated
Comment thread Header.c Outdated
Comment thread Header.c Outdated
Comment thread MetersPanel.c Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
MetersPanel.c (1)

9-9: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Unusual include order.

Vector.h is included before MetersPanel.h (line 12), which breaks the typical convention of including the corresponding header first after config.h. If Vector.h is already transitively included via MetersPanel.h, this include may be redundant.

Suggested reordering
-#include "Vector.h"
 `#include` "config.h" // IWYU pragma: keep
 
 `#include` "MetersPanel.h"

If Vector.h is genuinely needed explicitly, move it below MetersPanel.h:

 `#include` "config.h" // IWYU pragma: keep
 
 `#include` "MetersPanel.h"
 
+#include "Vector.h"
 `#include` <stdlib.h>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MetersPanel.c` at line 9, Move the include of Vector.h so the translation
unit's corresponding header MetersPanel.h is included first (after config.h):
either reorder the includes so MetersPanel.h appears before Vector.h, or remove
the Vector.h include entirely if all Vector symbols are already provided
transitively via MetersPanel.h; also verify any use of Vector types/functions in
functions like those in MetersPanel.c require an explicit include and add
Vector.h only if needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@MetersPanel.c`:
- Line 9: Move the include of Vector.h so the translation unit's corresponding
header MetersPanel.h is included first (after config.h): either reorder the
includes so MetersPanel.h appears before Vector.h, or remove the Vector.h
include entirely if all Vector symbols are already provided transitively via
MetersPanel.h; also verify any use of Vector types/functions in functions like
those in MetersPanel.c require an explicit include and add Vector.h only if
needed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6d7b57f7-24c7-4afa-9ee0-4cc70c960058

📥 Commits

Reviewing files that changed from the base of the PR and between 8250e84 and 75738a6.

📒 Files selected for processing (5)
  • AvailableMetersPanel.c
  • CategoriesPanel.c
  • Header.c
  • Header.h
  • MetersPanel.c

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Header.c (1)

245-277: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Mirror the hidden-column traversal in Header_updateData().

These new merged draw paths now surface meters from columns[currentColumns..maxColumns) when !metersCopied, but Header_updateData() still only walks the visible columns. After shrinking the header, preserved hidden meters will keep drawing stale values until something else rebuilds them.

Suggested fix
 void Header_updateData(Header* this) {
+   const size_t numCols = HeaderLayout_getColumns(this->headerLayout);
    Header_forEachColumn(this, col) {
-      Vector* meters = this->columns[col];
-      int items = Vector_size(meters);
-      for (int i = 0; i < items; i++) {
-         Meter* meter = (Meter*) Vector_get(meters, i);
-         Meter_updateValues(meter);
-      }
+      size_t drawCol = col;
+      do {
+         Vector* meters = this->columns[drawCol++];
+         for (int i = 0; i < Vector_size(meters); i++) {
+            Meter* meter = (Meter*) Vector_get(meters, i);
+            Meter_updateValues(meter);
+         }
+      } while ((col == numCols - 1) && (drawCol < this->maxColumns) && !this->metersCopied);
    }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Header.c` around lines 245 - 277, Header_forEachColumn's draw path now
iterates meters in columns[currentColumns..maxColumns) when !metersCopied, but
Header_updateData still only updates visible columns, so hidden preserved meters
keep stale values; update Header_updateData to mirror the same traversal: when
this->metersCopied is false, iterate drawCol from the current column index
through this->maxColumns (using the same loop condition as Header_forEachColumn)
and process/update each Meter in this->columns[drawCol] (honoring
Meter_isMultiColumn and columnWidthCount logic if relevant), otherwise keep the
existing visible-only behavior; reference Header_forEachColumn,
Header_updateData, this->metersCopied, currentColumns, maxColumns, columns, and
Meter_isMultiColumn to align the traversal and avoid stale meters.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Header.h`:
- Around line 18-21: Header.h uses the bool type in the Header_ struct
(metersCopied) but doesn't include <stdbool.h>, creating fragile transitive
include dependencies; update Header.h to directly include <stdbool.h> so the
bool definition is explicit for the Header_ struct and any code including
Header.h (referencing Header_, metersCopied, and the Header.h interface) no
longer relies on other headers to provide bool.

In `@MetersPanel.c`:
- Around line 200-203: The condition that sets header->metersCopied to false
currently only runs when (modified || sideMove), which misses style-only edits
(the F4/space path) that also mutate a meter; update the logic so style-only
changes are treated as a real modification—either expand the condition to
include the style-change flag used by the F4/space path (e.g., styleChanged or
whatever symbol marks style edits) or call Header_collapseLayout(header);
header->metersCopied = false unconditionally after any code path that mutates
meter style; key symbols to touch: the conditional around
Header_collapseLayout(header) and header->metersCopied assignment, and the
style-change handler invoked by F4/space.

---

Outside diff comments:
In `@Header.c`:
- Around line 245-277: Header_forEachColumn's draw path now iterates meters in
columns[currentColumns..maxColumns) when !metersCopied, but Header_updateData
still only updates visible columns, so hidden preserved meters keep stale
values; update Header_updateData to mirror the same traversal: when
this->metersCopied is false, iterate drawCol from the current column index
through this->maxColumns (using the same loop condition as Header_forEachColumn)
and process/update each Meter in this->columns[drawCol] (honoring
Meter_isMultiColumn and columnWidthCount logic if relevant), otherwise keep the
existing visible-only behavior; reference Header_forEachColumn,
Header_updateData, this->metersCopied, currentColumns, maxColumns, columns, and
Meter_isMultiColumn to align the traversal and avoid stale meters.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 74c610ff-b15c-401e-8ce1-e48d89ac25a3

📥 Commits

Reviewing files that changed from the base of the PR and between 75738a6 and f021228.

📒 Files selected for processing (5)
  • AvailableMetersPanel.c
  • CategoriesPanel.c
  • Header.c
  • Header.h
  • MetersPanel.c

Comment thread Header.h
Comment thread MetersPanel.c
Comment on lines +200 to +203
if (modified || sideMove) {
Header_collapseLayout(header);
header->metersCopied = false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Treat style changes as a real meter modification.

The new modified || sideMove gate excludes the F4/space style path even though it mutates a meter. In a reduced layout, that leaves metersCopied intact after an edit, so restoring the layout later revives the remembered hidden column assignment instead of ending the temporary-preservation window described in the PR.

Suggested fix
       case ' ':
       case KEY_F(4):
       case 't': {
          if (!Vector_size(this->meters))
             break;
          Meter* meter = (Meter*) Vector_get(this->meters, selected);
          MeterModeId mode = Meter_nextSupportedMode(meter);
          Meter_setMode(meter, mode);
          Panel_set(super, selected, (Object*) Meter_toListItem(meter, this->moving));
+         modified = true;
          result = HANDLED;
          break;
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (modified || sideMove) {
Header_collapseLayout(header);
header->metersCopied = false;
}
case ' ':
case KEY_F(4):
case 't': {
if (!Vector_size(this->meters))
break;
Meter* meter = (Meter*) Vector_get(this->meters, selected);
MeterModeId mode = Meter_nextSupportedMode(meter);
Meter_setMode(meter, mode);
Panel_set(super, selected, (Object*) Meter_toListItem(meter, this->moving));
modified = true;
result = HANDLED;
break;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MetersPanel.c` around lines 200 - 203, The condition that sets
header->metersCopied to false currently only runs when (modified || sideMove),
which misses style-only edits (the F4/space path) that also mutate a meter;
update the logic so style-only changes are treated as a real modification—either
expand the condition to include the style-change flag used by the F4/space path
(e.g., styleChanged or whatever symbol marks style edits) or call
Header_collapseLayout(header); header->metersCopied = false unconditionally
after any code path that mutates meter style; key symbols to touch: the
conditional around Header_collapseLayout(header) and header->metersCopied
assignment, and the style-change handler invoked by F4/space.

When the user reduces the number of header columns,
keep the hidden columns allocated in memory instead of collapsing
them into the last visible column. This allows restoring meter
positions when switching back to the original layout.

Fixes htop-dev#1941

Signed-off-by: ugolnikovE <evgeny.ugolnik@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
MetersPanel.c (1)

121-131: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Style changes (F4/space) should set modified = true.

The style change handler mutates a meter but doesn't set modified = true. With the current logic, changing a meter's display style won't trigger Header_collapseLayout, so hidden columns will remain allocated and metersCopied will stay intact. Per the PR objectives, any meter modification in the editor should permanently collapse hidden columns.

🐛 Proposed fix
       case ' ':
       case KEY_F(4):
       case 't': {
          if (!Vector_size(this->meters))
             break;
          Meter* meter = (Meter*) Vector_get(this->meters, selected);
          MeterModeId mode = Meter_nextSupportedMode(meter);
          Meter_setMode(meter, mode);
          Panel_set(super, selected, (Object*) Meter_toListItem(meter, this->moving));
+         modified = true;
          result = HANDLED;
          break;
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@MetersPanel.c` around lines 121 - 131, The style-change branch (cases ' ',
KEY_F(4), 't') mutates a meter via Meter_nextSupportedMode/Meter_setMode and
updates the list item with Panel_set/Meter_toListItem but doesn't mark the
editor state as modified; update that branch to set modified = true after
applying the mode change (before result = HANDLED) so Header_collapseLayout and
subsequent collapse logic will run for this mutation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@MetersPanel.c`:
- Around line 121-131: The style-change branch (cases ' ', KEY_F(4), 't')
mutates a meter via Meter_nextSupportedMode/Meter_setMode and updates the list
item with Panel_set/Meter_toListItem but doesn't mark the editor state as
modified; update that branch to set modified = true after applying the mode
change (before result = HANDLED) so Header_collapseLayout and subsequent
collapse logic will run for this mutation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 48f8a479-acc4-4e04-a262-170ec4a91de9

📥 Commits

Reviewing files that changed from the base of the PR and between f021228 and 5a912bd.

📒 Files selected for processing (5)
  • AvailableMetersPanel.c
  • CategoriesPanel.c
  • Header.c
  • Header.h
  • MetersPanel.c

@ugolnikovE

Copy link
Copy Markdown
Contributor Author

@BenBE applied your suggestions (Vector_splice, preallocation, didCopy cleanup). One design note: I kept the current behavior on meter style changes - they change rendering, not layout position, so the collapse path stays bound to position-affecting actions only. Ready for another look when you have time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Extension or improvement to existing feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HeaderLayout UX: Remember the meters' original column numbers until exit

2 participants