[Toybox] top: pressing SHIFT-RIGHT and RIGHT confuses [sort key column]
Mark Hansen
markhansen at google.com
Mon Apr 27 18:30:40 PDT 2026
Hi, first up, thanks for toybox. I'm always debugging Android apps
using Toybox builtins and I appreciate all your work.
I just had some confusing behaviour in toybox top in Android.
Background: top lets you press left/right to move the list, and
shift-left/shift-right to change sort column:
https://cs.android.com/android/platform/superproject/+/android-latest-release:external/toybox/toys/posix/ps.c;l=110-111;drc=b8186ba3c4d9548da2ae8b8aaf388b2a04f1966b
```
Cursor UP/DOWN or LEFT/RIGHT to move list, SHIFT LEFT/RIGHT to change sort,
space to force update, R to reverse sort, Q to exit.
```
And the intention from the source code looks to be to highlight the
sort-order column by putting the column name in square brackets:
https://cs.android.com/android/platform/superproject/+/android-latest-release:external/toybox/toys/posix/ps.c;l=1734-1736;drc=b8186ba3c4d9548da2ae8b8aaf388b2a04f1966b
```
if (isspace(was) && !isspace(is) && i++==TT.sortpos && pos!=toybuf)
pos[-1] = '[';
if (!isspace(was) && isspace(is) && i==TT.sortpos+1) *pos = ']';
```
To reproduce:
```
$ adb shell
tegu:/ # getprop ro.build.fingerprint
google/tegu/tegu:Baklava/MAIN/14957292:userdebug/dev-keys
tegu:/ # top --version
toybox 0.8.13-android
tegu:/ # top
```
To start, `[%CPU]` is selected as the sort column. And we are indeed
sorting by CPU descending. So far, so good:
```
PID USER PR NI VIRT RES SHR S[%CPU] %MEM TIME+ ARGS
12668 root 20 0 11G 11M 3.0M R 5.0 0.1 30:42.28 adbd
--root_seclabel=u:r:su:s0 --tim_seclabel=u:r:adbd_tradeinmode:s0
2329 u0_a233 0 0 17G 209M 130M S 4.6 2.7 12:48.74
com.google.android.apps.nexuslauncher
31948 root 20 0 10G 6.0M 4.2M R 4.3 0.0 0:00.38 top
23706 u0_a185 20 0 18G 303M 186M S 4.3 4.0 42:10.26
com.google.android.gms.persistent
1471 system 18 -2 176G 608M 416M S 4.3 8.0 578:28.53 system_server
```
If I press shift-right, the sort key correctly moves to `[%MEM]`. Both
the data is sorted and the column correctly has square brackets
indicating that it's the sort key:
```
PID USER PR NI VIRT RES SHR S %CPU [%MEM] TIME+ ARGS
1471 system 18 -2 176G 611M 417M S 1.6 8.0 579:06.29 system_server
3862 u0_a340 20 0 19G 510M 366M S 0.0 6.7 3:38.29
com.google.android.apps.gmm.dev
10159 u0_a188 20 0 43G 327M 202M S 0.0 4.3 0:15.90
com.google.android.googlequicksearchbox:search
23706 u0_a185 20 0 18G 299M 188M S 0.6 3.9 42:45.58
com.google.android.gms.persistent
```
Reset back to sorting by `[%CPU]` by pressing shift-left.
Then let's press right arrow: this should scroll right, but keep the
sort order. We're still sorting by `%CPU` but now we incorrectly see
`[%MEM]` in square brackets indicating `%MEM` is the sort column:
```
USER PR NI VIRT RES SHR S %CPU [%MEM] TIME+ ARGS
system 18 -2 176G 620M 420M S 6.0 8.1 578:30.83 system_server
root 20 0 11G 11M 3.0M S 5.3 0.1 30:45.47 adbd
--root_seclabel=u:r:su:s0 --tim_seclabel=u:r:adbd_tradeinmode:s0
root 20 0 10G 6.0M 4.2M R 3.6 0.0 0:02.83 top
u0_a185 20 0 18G 298M 187M S 3.6 3.9 42:12.80
com.google.android.gms.persistent
root 20 0 10G 2.7M 2.6M S 2.6 0.0 3:46.71 top -p 9624
u0_a268 20 0 16G 93M 52M S 1.3 1.2 24:38.92
com.google.android.grilservice
system 20 0 11G 2.9M 1.9M S 1.3 0.0 913:56.90
android.hardware.sensors-service.multihal
system -3 0 11G 5.9M 4.7M S 1.3 0.0 191:39.56
android.hardware.composer.hwc3-service.pixel
bluetooth 20 0 17G 119M 69M S 1.0 1.5 292:37.72
com.google.android.bluetooth
```
We can see the code handling keypresses:
https://cs.android.com/android/platform/superproject/+/android-latest-release:external/toybox/toys/posix/ps.c;l=1791-1793;drc=b8186ba3c4d9548da2ae8b8aaf388b2a04f1966b
```
if (i == (KEY_SHIFT|KEY_LEFT)) setsort(TT.sortpos-1);
else if (i == (KEY_SHIFT|KEY_RIGHT)) setsort(TT.sortpos+1);
else if (i == KEY_RIGHT) TT.scroll++;
else if (i == KEY_LEFT && TT.scroll) TT.scroll--;
```
So the relevant state is `TT.sortpos` and `TT.scroll`
I expect the problem is that here in `get_headers`, we print into a
string buffer starting at header position `TT.scroll`:
https://cs.android.com/android/platform/superproject/+/android-latest-release:external/toybox/toys/posix/ps.c;l=1149-1166;drc=b8186ba3c4d9548da2ae8b8aaf388b2a04f1966b
```
// Write FIELD list into display header string (truncating at blen),
// and return bitfield of which FIELDs are used.
static long long get_headers(struct ofields *field, char *buf, int blen)
{
long long bits = 0;
int len = 0, scroll;
// Skip TT.scroll many fields (but not last one)
for (scroll = TT.scroll; scroll && field->next; scroll--) field = field->next;
for (; field; field = field->next) {
len += snprintf(buf+len, blen-len, " %*s"+!bits, field->len,
field->title);
bits |= 1LL<<field->which;
}
return bits;
}
```
Then where we use `get_headers` here, we have a loop from `i = 0` and
we are comparing the relative index i (where i = 0 is the start of a
window over the headers that starts at `TT.scroll`) with the
absolutely-indexed `TT.sortpos`
```cc
get_headers(TT.fields, pos = toybuf, sizeof(toybuf));
for (i = 0, is = ' '; *pos; pos++) {
was = is;
is = *pos;
if (isspace(was) && !isspace(is) && i++==TT.sortpos && pos!=toybuf)
pos[-1] = '[';
if (!isspace(was) && isspace(is) && i==TT.sortpos+1) *pos = ']';
}
```
enh at google.com said: "i tried your suggested fix, and it seems to work":
```
diff --git a/toys/posix/ps.c b/toys/posix/ps.c
index 86cd2197..8d53e26a 100644
--- a/toys/posix/ps.c
+++ b/toys/posix/ps.c
@@ -1726,7 +1726,7 @@ static void top_common(
lines = header_line(lines, 0);
// print line of header labels for currently displayed fields
get_headers(TT.fields, pos = toybuf, sizeof(toybuf));
- for (i = 0, is = ' '; *pos; pos++) {
+ for (i = TT.scroll, is = ' '; *pos; pos++) {
was = is;
is = *pos;
if (isspace(was) && !isspace(is) && i++==TT.sortpos && pos!=toybuf)
```
Thank you
More information about the Toybox
mailing list