[Toybox] top: pressing SHIFT-RIGHT and RIGHT confuses [sort key column]
enh
enh at google.com
Tue Apr 28 12:12:56 PDT 2026
On Mon, Apr 27, 2026 at 9:31 PM Mark Hansen <markhansen at google.com> wrote:
>
> 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.
rob: note that this isn't actually android-specific ... i reproduced
the behavior and tested the fix on debian.
> 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