[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