[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