Skip to content

Commit 31b1a15

Browse files
authored
Merge pull request #1957 from fasterit/stable-tree-view
Stable tree view
2 parents d061937 + 06849fd commit 31b1a15

12 files changed

Lines changed: 235 additions & 35 deletions

File tree

Action.c

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -373,18 +373,27 @@ static Htop_Reaction actionExpandCollapseOrSortColumn(State* st) {
373373
return st->host->settings->ss->treeView ? actionExpandOrCollapse(st) : actionSetSortColumn(st);
374374
}
375375

376-
static inline void setActiveScreen(Settings* settings, State* st, unsigned int ssIdx) {
376+
static inline bool setActiveScreen(Settings* settings, State* st, unsigned int ssIdx) {
377377
assert(settings->ssIndex == ssIdx);
378378
Machine* host = st->host;
379379

380+
// Save following state from current table before switching screens
381+
int following = host->activeTable->following;
382+
380383
settings->ss = settings->screens[ssIdx];
381384
if (!settings->ss->table)
382385
settings->ss->table = host->processTable;
383386
host->activeTable = settings->ss->table;
384387

388+
// Transfer following state to new table if it doesn't already have one
389+
if (following != -1 && host->activeTable->following == -1)
390+
host->activeTable->following = following;
391+
385392
// set correct functionBar - readonly if requested, and/or with non-process screens
386393
bool readonly = Settings_isReadonly() || (host->activeTable != host->processTable);
387394
MainPanel_setFunctionBar(st->mainPanel, readonly);
395+
396+
return host->activeTable->following != -1;
388397
}
389398

390399
static Htop_Reaction actionNextScreen(State* st) {
@@ -393,8 +402,10 @@ static Htop_Reaction actionNextScreen(State* st) {
393402
if (settings->ssIndex == settings->nScreens) {
394403
settings->ssIndex = 0;
395404
}
396-
setActiveScreen(settings, st, settings->ssIndex);
397-
return HTOP_UPDATE_PANELHDR | HTOP_REFRESH | HTOP_REDRAW_BAR;
405+
Htop_Reaction reaction = HTOP_UPDATE_PANELHDR | HTOP_REFRESH | HTOP_REDRAW_BAR;
406+
if (setActiveScreen(settings, st, settings->ssIndex))
407+
reaction |= HTOP_KEEP_FOLLOWING;
408+
return reaction;
398409
}
399410

400411
static Htop_Reaction actionPrevScreen(State* st) {
@@ -404,8 +415,10 @@ static Htop_Reaction actionPrevScreen(State* st) {
404415
} else {
405416
settings->ssIndex--;
406417
}
407-
setActiveScreen(settings, st, settings->ssIndex);
408-
return HTOP_UPDATE_PANELHDR | HTOP_REFRESH | HTOP_REDRAW_BAR;
418+
Htop_Reaction reaction = HTOP_UPDATE_PANELHDR | HTOP_REFRESH | HTOP_REDRAW_BAR;
419+
if (setActiveScreen(settings, st, settings->ssIndex))
420+
reaction |= HTOP_KEEP_FOLLOWING;
421+
return reaction;
409422
}
410423

411424
Htop_Reaction Action_setScreenTab(State* st, int x) {
@@ -422,8 +435,10 @@ Htop_Reaction Action_setScreenTab(State* st, int x) {
422435
int width = rem >= bracketWidth ? (int)strnlen(tab, rem - bracketWidth + 1) : 0;
423436
if (width >= rem - bracketWidth + 1) {
424437
settings->ssIndex = i;
425-
setActiveScreen(settings, st, i);
426-
return HTOP_UPDATE_PANELHDR | HTOP_REFRESH | HTOP_REDRAW_BAR;
438+
Htop_Reaction reaction = HTOP_UPDATE_PANELHDR | HTOP_REFRESH | HTOP_REDRAW_BAR;
439+
if (setActiveScreen(settings, st, i))
440+
reaction |= HTOP_KEEP_FOLLOWING;
441+
return reaction;
427442
}
428443

429444
rem -= bracketWidth + width;
@@ -566,7 +581,14 @@ static Htop_Reaction actionFilterByUser(State* st) {
566581
}
567582

568583
Htop_Reaction Action_follow(State* st) {
569-
st->host->activeTable->following = MainPanel_selectedRow(st->mainPanel);
584+
int selectedRow = MainPanel_selectedRow(st->mainPanel);
585+
if (st->host->activeTable->following == selectedRow) {
586+
/* Toggle: unfollow when F is pressed on the already-followed process */
587+
st->host->activeTable->following = -1;
588+
Panel_setSelectionColor((Panel*)st->mainPanel, PANEL_SELECTION_FOCUS);
589+
return HTOP_OK;
590+
}
591+
st->host->activeTable->following = selectedRow;
570592
Panel_setSelectionColor((Panel*)st->mainPanel, PANEL_SELECTION_FOLLOW);
571593
return HTOP_KEEP_FOLLOWING;
572594
}

CRT.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,6 +1243,11 @@ IGNORE_WCASTQUAL_BEGIN
12431243
/* These are a bit of a gamble: */
12441244
define_key("\033[1;5D", KEY_CTRL_LEFT);
12451245
define_key("\033[1;5C", KEY_CTRL_RIGHT);
1246+
define_key("\033[1;2A", KEY_SR); // SR = scroll reverse, Shift-UP
1247+
define_key("\033[1;2B", KEY_SF); // SF = scroll forward, Shift-DOWN
1248+
define_key("\033[1;3A", KEY_SR); // SR = scroll reverse, Alt-UP
1249+
define_key("\033[1;3B", KEY_SF); // SF = scroll forward, Alt-DOWN
1250+
12461251

12471252
char sequence[3] = "\033a";
12481253
for (char c = 'a'; c <= 'z'; c++) {

CommandLine.c

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ static void printHelpFlag(const char* name) {
6868
"-p --pid=PID[,PID,PID...] Show only the given PIDs\n"
6969
" --readonly Disable all system and process changing features\n"
7070
"-s --sort-key=COLUMN Sort by COLUMN in list view (try --sort-key=help for a list)\n"
71-
"-t --tree Show the tree view (can be combined with -s)\n"
71+
"-t --tree[=MODE] Show the tree view (MODE: classic|soft|hard); can be combined with -s\n"
7272
"-u --user[=USERNAME] Show only processes for a given user (or $USER)\n"
7373
"-U --no-unicode Do not use unicode but plain ASCII\n"
7474
"-V --version Print version info\n");
@@ -92,6 +92,7 @@ typedef struct CommandLineSettings_ {
9292
bool enableMouse;
9393
#endif
9494
bool treeView;
95+
int stableTreeView;
9596
bool allowUnicode;
9697
bool highlightChanges;
9798
int highlightDelaySecs;
@@ -100,6 +101,25 @@ typedef struct CommandLineSettings_ {
100101
bool hideFunctionBar;
101102
} CommandLineSettings;
102103

104+
static bool parseTreeStableMode(const char* arg, int* stableTreeView) {
105+
if (String_eq(arg, "0") || String_eq(arg, "classic") || String_eq(arg, "legacy") || String_eq(arg, "jumpy")) {
106+
*stableTreeView = 0;
107+
return true;
108+
}
109+
110+
if (String_eq(arg, "1") || String_eq(arg, "soft") || String_eq(arg, "stable")) {
111+
*stableTreeView = 1;
112+
return true;
113+
}
114+
115+
if (String_eq(arg, "2") || String_eq(arg, "hard") || String_eq(arg, "STABLE")) {
116+
*stableTreeView = 2;
117+
return true;
118+
}
119+
120+
return false;
121+
}
122+
103123
static CommandLineStatus parseArguments(int argc, char** argv, CommandLineSettings* flags) {
104124

105125
*flags = (CommandLineSettings) {
@@ -115,6 +135,7 @@ static CommandLineStatus parseArguments(int argc, char** argv, CommandLineSettin
115135
#endif
116136
.treeView = false,
117137
.allowUnicode = true,
138+
.stableTreeView = -1,
118139
.highlightChanges = false,
119140
.highlightDelaySecs = -1,
120141
.readonly = false,
@@ -143,7 +164,7 @@ static CommandLineStatus parseArguments(int argc, char** argv, CommandLineSettin
143164
{"no-mouse", no_argument, 0, 'M'},
144165
{"no-unicode", no_argument, 0, 'U'},
145166
{"no-meters", no_argument, 0, 129},
146-
{"tree", no_argument, 0, 't'},
167+
{"tree", optional_argument, 0, 't'},
147168
{"pid", required_argument, 0, 'p'},
148169
{"filter", required_argument, 0, 'F'},
149170
{"no-functionbar", no_argument, 0, 130},
@@ -156,7 +177,7 @@ static CommandLineStatus parseArguments(int argc, char** argv, CommandLineSettin
156177

157178
int opt, opti = 0;
158179
/* Parse arguments */
159-
while ((opt = getopt_long(argc, argv, "hVMCs:td:n:u::Up:F:H::", long_opts, &opti))) {
180+
while ((opt = getopt_long(argc, argv, "hVMCs:t::d:n:u::Up:F:H::", long_opts, &opti))) {
160181
if (opt == EOF)
161182
break;
162183

@@ -250,6 +271,20 @@ static CommandLineStatus parseArguments(int argc, char** argv, CommandLineSettin
250271
flags->hideMeters = true;
251272
break;
252273
case 't':
274+
if (!optarg && optind < argc &&
275+
(argv[optind][0] != '\0' && argv[optind][0] != '-')) {
276+
int stableTreeView = -1;
277+
if (parseTreeStableMode(argv[optind], &stableTreeView)) {
278+
flags->stableTreeView = stableTreeView;
279+
optarg = argv[optind++];
280+
}
281+
}
282+
if (optarg) {
283+
if (!parseTreeStableMode(optarg, &flags->stableTreeView)) {
284+
fprintf(stderr, "Error: invalid tree mode \"%s\" (expected: classic, soft, hard (or 0, 1, 2)).\n", optarg);
285+
return STATUS_ERROR_EXIT;
286+
}
287+
}
253288
flags->treeView = true;
254289
break;
255290
case 'p': {
@@ -382,6 +417,8 @@ int CommandLine_run(int argc, char** argv) {
382417
#endif
383418
if (flags.treeView)
384419
settings->ss->treeView = true;
420+
if (flags.stableTreeView != -1)
421+
settings->ss->stableTreeView = flags.stableTreeView;
385422
if (flags.highlightChanges)
386423
settings->highlightChanges = true;
387424
if (flags.highlightDelaySecs != -1)

DisplayOptionsPanel.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ DisplayOptionsPanel* DisplayOptionsPanel_new(Settings* settings, ScreenManager*
270270
Panel_add(super, (Object*) CheckItem_newByRef("Tree view", &(settings->ss->treeView)));
271271
Panel_add(super, (Object*) CheckItem_newByRef("- Tree view is always sorted by PID (htop 2 behavior)", &(settings->ss->treeViewAlwaysByPID)));
272272
Panel_add(super, (Object*) CheckItem_newByRef("- Tree view is collapsed by default", &(settings->ss->allBranchesCollapsed)));
273+
Panel_add(super, (Object*) NumberItem_newByRef("- Tree view is kept visually stable (0 - off, 1 - soft, 2 - hard)", &(settings->ss->stableTreeView), 0, 0, 2));
273274
Panel_add(super, (Object*) TextItem_new("Global options:"));
274275
Panel_add(super, (Object*) CheckItem_newByRef("Show tabs for screens", &(settings->screenTabs)));
275276
Panel_add(super, (Object*) CheckItem_newByRef("Shadow other users' processes", &(settings->shadowOtherUsers)));

MainPanel.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ static HandlerResult MainPanel_eventHandler(Panel* super, int ch) {
112112
result = HANDLED;
113113
} else if (ch == 27) {
114114
this->state->hideSelection = true;
115+
if (host->activeTable->following != -1) {
116+
host->activeTable->following = -1;
117+
Panel_setSelectionColor(super, PANEL_SELECTION_FOCUS);
118+
}
115119
return HANDLED;
116120
} else if (ch != ERR && ch > 0 && ch < KEY_MAX && this->keys[ch]) {
117121
reaction |= (this->keys[ch])(this->state);
@@ -149,8 +153,7 @@ static HandlerResult MainPanel_eventHandler(Panel* super, int ch) {
149153
if ((reaction & HTOP_QUIT) == HTOP_QUIT) {
150154
return BREAK_LOOP;
151155
}
152-
if ((reaction & HTOP_KEEP_FOLLOWING) != HTOP_KEEP_FOLLOWING) {
153-
host->activeTable->following = -1;
156+
if (host->activeTable->following == -1) {
154157
Panel_setSelectionColor(super, PANEL_SELECTION_FOCUS);
155158
}
156159
return result;

Panel.c

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ void Panel_init(Panel* this, int x, int y, int w, int h, const ObjectClass* type
6464
this->needsRedraw = true;
6565
this->cursorOn = false;
6666
this->wasFocus = false;
67+
this->allowExcessScrollV = false;
6768
RichString_beginAllocated(this->header);
6869
this->defaultBar = fuBar;
6970
this->currentBar = fuBar;
@@ -118,6 +119,7 @@ void Panel_prune(Panel* this) {
118119
this->selected = 0;
119120
this->oldSelected = 0;
120121
this->needsRedraw = true;
122+
this->allowExcessScrollV = false;
121123
}
122124

123125
void Panel_add(Panel* this, Object* o) {
@@ -262,11 +264,20 @@ void Panel_draw(Panel* this, bool force_redraw, bool focus, bool highlightSelect
262264
h--;
263265
}
264266

265-
// ensure scroll area is on screen
266-
if (this->scrollV < 0) {
267-
this->scrollV = 0;
268-
this->needsRedraw = true;
269-
} else if (this->scrollV > size - h) {
267+
/* ensure scroll area is on screen
268+
Note: when allowExcessScrollV is set, negative scrollV is allowed to display
269+
empty lines above the first row, and scrollV > size-h is allowed to display
270+
empty lines below the last row. Both are used by stable tree view hard mode
271+
to keep the selected row at a fixed screen position. */
272+
if (!this->allowExcessScrollV) {
273+
if (this->scrollV < 0) {
274+
this->scrollV = 0;
275+
this->needsRedraw = true;
276+
} else if (this->scrollV > size - h) {
277+
this->scrollV = MAXIMUM(size - h, 0);
278+
this->needsRedraw = true;
279+
}
280+
} if (!this->allowExcessScrollV && this->scrollV > size - h) {
270281
this->scrollV = MAXIMUM(size - h, 0);
271282
this->needsRedraw = true;
272283
}
@@ -279,8 +290,10 @@ void Panel_draw(Panel* this, bool force_redraw, bool focus, bool highlightSelect
279290
this->needsRedraw = true;
280291
}
281292

282-
int first = this->scrollV;
283-
int upTo = MINIMUM(first + h, size);
293+
// topPad: empty screen lines above the first row (non-zero when scrollV is negative)
294+
int topPad = (this->scrollV < 0) ? -this->scrollV : 0;
295+
int first = this->scrollV + topPad;
296+
int upTo = MINIMUM(first + h - topPad, size);
284297

285298
int selectionColor = focus
286299
? CRT_colors[this->selectionColorId]
@@ -289,6 +302,10 @@ void Panel_draw(Panel* this, bool force_redraw, bool focus, bool highlightSelect
289302
RichString_begin(item);
290303
if (this->needsRedraw || force_redraw) {
291304
int line = 0;
305+
while (line < topPad) {
306+
mvhline(y + line, x, ' ', this->w);
307+
line++;
308+
}
292309
for (int i = first; line < h && i < upTo; i++) {
293310
const Object* itemObj = Vector_get(this->items, i);
294311
RichString_rewind(&item, RichString_size(&item));
@@ -321,9 +338,9 @@ void Panel_draw(Panel* this, bool force_redraw, bool focus, bool highlightSelect
321338
RichString_rewind(&item, RichString_size(&item));
322339
Object_display(oldObj, &item);
323340
int oldLen = RichString_sizeVal(item);
324-
mvhline(y + this->oldSelected - first, x + 0, ' ', this->w);
341+
mvhline(y + this->oldSelected - this->scrollV, x + 0, ' ', this->w);
325342
if (scrollH < oldLen)
326-
RichString_printoffnVal(item, y + this->oldSelected - first, x,
343+
RichString_printoffnVal(item, y + this->oldSelected - this->scrollV, x,
327344
scrollH, MINIMUM(oldLen - scrollH, this->w));
328345

329346
const Object* newObj = Vector_get(this->items, this->selected);
@@ -333,10 +350,10 @@ void Panel_draw(Panel* this, bool force_redraw, bool focus, bool highlightSelect
333350
int newLen = RichString_sizeVal(item);
334351
this->selectedLen = newLen;
335352
attrset(selectionColor);
336-
mvhline(y + this->selected - first, x + 0, ' ', this->w);
353+
mvhline(y + this->selected - this->scrollV, x + 0, ' ', this->w);
337354
RichString_setAttr(&item, selectionColor);
338355
if (scrollH < newLen)
339-
RichString_printoffnVal(item, y + this->selected - first, x,
356+
RichString_printoffnVal(item, y + this->selected - this->scrollV, x,
340357
scrollH, MINIMUM(newLen - scrollH, this->w));
341358
attrset(CRT_colors[RESET_COLOR]);
342359
}
@@ -364,6 +381,8 @@ bool Panel_onKey(Panel* this, int key) {
364381
assert(this != NULL);
365382

366383
const int size = Vector_size(this->items);
384+
const int availableHeight = this->h - Panel_headerHeight(this);
385+
const int maxScroll = MAXIMUM(0, size - availableHeight);
367386

368387
#define PANEL_SCROLL(amount) \
369388
do { \
@@ -419,6 +438,26 @@ bool Panel_onKey(Panel* this, int key) {
419438
PANEL_SCROLL(+CRT_scrollWheelVAmount);
420439
break;
421440

441+
case KEY_SR:
442+
if (this->scrollV > 0) {
443+
// keep selection within the now-visible area
444+
if (this->selected < this->scrollV + availableHeight) {
445+
this->scrollV--;
446+
this->needsRedraw = true;
447+
}
448+
}
449+
break;
450+
451+
case KEY_SF:
452+
if (this->scrollV < maxScroll) {
453+
// keep selection within the now-visible area
454+
if (this->selected >= this->scrollV) {
455+
this->scrollV++;
456+
this->needsRedraw = true;
457+
}
458+
}
459+
break;
460+
422461
case KEY_HOME:
423462
this->selected = 0;
424463
break;

Panel.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ struct Panel_ {
7676
bool needsRedraw;
7777
bool cursorOn;
7878
bool wasFocus;
79+
bool allowExcessScrollV; /* when true, scrollV > size-h is permitted (blank lines at bottom) */
7980
int lastMouseBarClickX; /* X position of last mouse click on function bar (LINES-1) */
8081
FunctionBar* currentBar;
8182
FunctionBar* defaultBar;

Settings.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ ScreenSettings* Settings_newScreen(Settings* this, const ScreenDefaults* default
279279
.treeView = false,
280280
.treeViewAlwaysByPID = false,
281281
.allBranchesCollapsed = false,
282+
.stableTreeView = 0,
282283
};
283284
return Settings_initScreenSettings(ss, this, defaults->columns);
284285
}
@@ -554,6 +555,9 @@ static bool Settings_read(Settings* this, const char* fileName, const Machine* h
554555
} else if (String_eq(option[0], ".all_branches_collapsed")) {
555556
if (screen)
556557
screen->allBranchesCollapsed = atoi(option[1]);
558+
} else if (String_eq(option[0], ".stable_tree_view")) {
559+
if (screen)
560+
screen->stableTreeView = atoi(option[1]);
557561
} else if (String_eq(option[0], ".dynamic")) {
558562
if (screen) {
559563
free_and_xStrdup(&screen->dynamic, option[1]);
@@ -767,6 +771,7 @@ int Settings_write(const Settings* this, bool onCrash) {
767771
printSettingInteger(".sort_direction", ss->direction);
768772
printSettingInteger(".tree_sort_direction", ss->treeDirection);
769773
printSettingInteger(".all_branches_collapsed", ss->allBranchesCollapsed);
774+
printSettingInteger(".stable_tree_view", ss->stableTreeView);
770775
}
771776

772777
#undef printSettingString

Settings.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ typedef struct ScreenSettings_ {
5252
bool treeView;
5353
bool treeViewAlwaysByPID;
5454
bool allBranchesCollapsed;
55+
int stableTreeView; /* 0=off, 1=soft, 2=hard (allow empty space above PID 1) */
5556
} ScreenSettings;
5657

5758
typedef struct Settings_ {

0 commit comments

Comments
 (0)