Skip to content

Add Shift+Tab to toggle select all/deselect all in MultiChoice prompts#54

Merged
keynmol merged 2 commits intoneandertech:mainfrom
kevin-lee:add-toggle-all-multi-selection
Apr 8, 2026
Merged

Add Shift+Tab to toggle select all/deselect all in MultiChoice prompts#54
keynmol merged 2 commits intoneandertech:mainfrom
kevin-lee:add-toggle-all-multi-selection

Conversation

@kevin-lee
Copy link
Copy Markdown
Contributor

@kevin-lee kevin-lee commented Apr 1, 2026

Close #55: Add Shift+Tab to toggle select all/deselect all in MultiChoice prompts

Add SHIFT_TAB key event that maps to the CSI sequence ESC[Z (backtab), supported universally across iTerm2, Apple Terminal, and Linux terminals.

In MultiChoice prompts, Shift+Tab now toggles all items at once:

  • If all currently filtered items are selected, it deselects them all
  • Otherwise, it selects them all
  • Respects active text filter (only affects filtered items)
  • Preserves selection state of non-filtered items

This also fixes a possible latent bug where pressing Shift+Tab would cause a MatchError in CharCollector since CSI_Started had no case for 'Z'.

The instruction text is updated to show the new keybinding: "Tab to toggle, Shift+Tab to toggle all, Enter to submit."


Update for Windows:

Add Windows support for SHIFT_TAB in CharCollector and Scala Native

The previous commit added Shift+Tab support via the ANSI ESC[Z sequence, which works on macOS and Linux.

On Windows, terminal input uses scan codes via _getch(), and Shift+Tab produces a 0x00 prefix followed by scan code 15 (0x0F), which was not handled.

  • Add case 15 => SHIFT_TAB to ScanCode_Started in CharCollector so the Windows scan code path correctly decodes Shift+Tab.
  • Normalize 0x00 to 0xE0 in Scala Native ChangeModeWindows.getchar() to match the existing JVM ChangeModeWindows behavior. Without this, the 0x00 prefix byte is silently skipped by KeyboardReadingThread's if lastRead != 0 guard, causing the subsequent scan code to be misinterpreted as a regular character.
  • Add CharCollectorTests to verify SHIFT_TAB decoding through both the ANSI CSI path (ESC[Z) and the Windows scan code path (ScanCode_Started + 15).

Screenshot 2026-04-02 at 9 52 48 am

Shift+Tab:
Screenshot 2026-04-02 at 9 53 00 am
Screenshot 2026-04-02 at 9 54 12 am
Screenshot 2026-04-02 at 9 54 43 am
Screenshot 2026-04-02 at 9 55 02 am

…t all` in `MultiChoice` prompts

Add `SHIFT_TAB` key event that maps to the CSI sequence `ESC[Z` (`backtab`), supported universally across iTerm2, Apple Terminal, and Linux terminals.

In `MultiChoice` prompts, `Shift+Tab` now toggles all items at once:
- If all currently filtered items are selected, it deselects them all
- Otherwise, it selects them all
- Respects active text filter (only affects filtered items)
- Preserves selection state of non-filtered items

This also fixes a possible latent bug where pressing `Shift+Tab` would cause a `MatchError` in `CharCollector` since `CSI_Started` had no case for 'Z'.

The instruction text is updated to show the new keybinding:
"Tab to toggle, Shift+Tab to toggle all, Enter to submit."
@kevin-lee kevin-lee force-pushed the add-toggle-all-multi-selection branch from 34048d6 to 1fbff82 Compare April 2, 2026 12:17
@kevin-lee kevin-lee changed the title Add Shift+Tab toggle all/deselect all for MultiChoice prompts Add Shift+Tab to toggle select all/deselect all in MultiChoice prompts Apr 2, 2026
@keynmol
Copy link
Copy Markdown
Contributor

keynmol commented Apr 4, 2026

Hi!

Directionally I'm okay with this, it's useful.

My only question is whether this will work on windows (which we currently support somewhat) - otherwise I'll take a closer look the PR once I'm back at my laptop in a couple of days.

…t all` in `MultiChoice` prompts for Windows

Add Windows support for `SHIFT_TAB` in `CharCollector` and Scala Native

The previous commit added `Shift+Tab` support via the ANSI `ESC[Z` sequence,
which works on macOS and Linux. On Windows, terminal input uses scan codes
via `_getch()`, and `Shift+Tab` produces a `0x00` prefix
followed by scan code `15` (`0x0F`), which was not handled.

- Add `case 15 => SHIFT_TAB` to `ScanCode_Started` in `CharCollector`
  so the Windows scan code path correctly decodes `Shift+Tab`.
- Normalize `0x00` to `0xE0` in Scala Native `ChangeModeWindows.getchar()`
  to match the existing JVM `ChangeModeWindows` behavior. Without this,
  the `0x00` prefix byte is silently skipped by
  `KeyboardReadingThread`'s `if lastRead != 0` guard, causing the
  subsequent scan code to be misinterpreted as a regular character.
- Add `CharCollectorTests` to verify `SHIFT_TAB` decoding through both
  the ANSI CSI path (`ESC[Z`) and the Windows scan code path
  (`ScanCode_Started` + `15`).
@kevin-lee
Copy link
Copy Markdown
Contributor Author

kevin-lee commented Apr 5, 2026

Hi!

Directionally I'm okay with this, it's useful.

My only question is whether this will work on windows (which we currently support somewhat) - otherwise I'll take a closer look the PR once I'm back at my laptop in a couple of days.

@keynmol Thank you! Oh, that’s a very good point. Sorry, I hadn’t done anything for Windows because my app was targeting macOS and Linux for now. My bad.

I’ve just made some updates for Windows and committed them.

My Windows machine is only for gaming, so it doesn't have a build env for this yet, but I'll still try building it there and see how it goes.


FYI, here is what I found.

Platform analysis:

Platform Input mechanism Shift+Tab encoding Status
macOS / Linux (JVM, Native) POSIX read() / getchar() → ANSI sequences ESC[Z Already works via CSI_Startedcase 'Z'
Scala.js (Node.js, all OS) Node.js readline keypress events ESC[Z in key.sequence Already works — Node.js abstracts platform differences
JVM Windows _getch() via JNA → scan codes 0x00 + 0x0F (15) Partially broken0x000xE0 mapping exists in ChangeModeWindows.java:54, but ScanCode_Started has no case 15
Scala Native Windows _getch() via @extern → scan codes 0x00 + 0x0F (15) Broken0x00 not normalized, gets skipped by KeyboardReadingThread; no case 15 in ScanCode_Started
  1. CharCollector.ScanCode_Started has no mapping for scan code 15 (Shift+Tab)
  2. ChangeModeWindows.scala (Scala Native) returns raw _getch() without normalising 0x00 -> 0xE0, unlike the JVM ChangeModeWindows.java which already does this

Why 0x000xE0 is safe on Windows:

  • _getch() has no error return (Microsoft docs)

    Return value
    Returns the character read. There's no error return.

  • 0x00 from _getch() is always a scan code prefix, never EOF
  • The scan codes following 0x00 and 0xE0 prefixes don't conflict
  • The JVM implementation already does this, and it's proven

@keynmol
Copy link
Copy Markdown
Contributor

keynmol commented Apr 8, 2026

Looked at the PR:

  1. It works fine on my Mac, JVM, JS and Native
  2. I assume it works on Linux as well
  3. It doesn't work on Windows – on JVM Shift+Tab just doesn't work, on Native it's completely broken (but it's not the fault of this PR, I messed something up)

I recorded a video in my windows VM:

CleanShot.2026-04-07.at.09.06.49.mp4

Given I don't have a windows machine to comfortably develop this, I will merge and release this PR with a windows caveat and address it later.

Thanks for your work!

@keynmol keynmol merged commit 9487e16 into neandertech:main Apr 8, 2026
4 checks passed
@kevin-lee
Copy link
Copy Markdown
Contributor Author

@keynmol Thank you!

I had a really long journey building it on Windows, and finally found that even the main branch (Native) isn't working on Windows.

main branch

After exampleNative/nativeLink:

TicTacToe:

cue4s>modules\example\target\native-3\example.exe
�[?25l�[0A�[0GException in thread "cue4s-keyboard-input-thread" scala.NotImplementedError: an implementation is missing
        at java.lang.Throwable.fillInStackTrace(Unknown Source)
        at scala.scalanative.runtime.Throwable.<init>(Unknown Source)
        at scala.Predef$.$qmark$qmark$qmark(Unknown Source)
        at cue4s.ChangeModeWindows$.read(Unknown Source)
        at cue4s.InputProviderImpl.$anonfun$6(Unknown Source)
        at cue4s.InputProviderImpl$$Lambda$8.apply(Unknown Source)
        at scala.Function0.apply$mcI$sp(Unknown Source)
        at cue4s.KeyboardReadingThread.run(Unknown Source)
        at scala.scalanative.runtime.NativeThread$.threadEntryPoint(Unknown Source)
        at scala.scalanative.runtime.NativeThread$.threadRoutine$$anonfun$1(Unknown Source)
        at <none>.SM49scala.scalanative.runtime.NativeThread$$$Lambda$1G17$extern$forwarder(Unknown Source)
        at <none>.ProxyThreadStartRoutine(ImmixGC.c:142)
        at <none>.BaseThreadInitThunk(Unknown Source)
        at <none>.RtlUserThreadStart(Unknown Source)
�[?25l�[0A�[0G�[?25l�[0A�[0G�[?25l�[0A�[0G�[?25l�[0A�[0G�[?25l�[0A�[0G�[?25l�[0A�[0G�[?25l�[0A�[0G

sync with only multiple selection code

I used ('A' to 'Z').zip('B' to 'Z') to have more than one char.

cue4s>modules\example\target\native-3\example.exe
�[?25l�[32m? �[39m�[36mWhat are your favourite letters?�[39m�[36m 쨩 �[39m
�[0G�[1mTab�[0m to toggle, �[1mEnter�[0m to submit.
�[0G�[32m _ (A,B)�[39m
�[0G _ (B,C)
�[0G _ (C,D)
�[0G _ (D,E)
�[0G _ (E,F)
�[0G _ (F,G)
�[0G v (G,H)
�[0G�[9A�[0GException in thread "cue4s-keyboard-input-thread" scala.NotImplementedError: an implementation is missing
        at java.lang.Throwable.fillInStackTrace(Unknown Source)
        at scala.scalanative.runtime.Throwable.<init>(Unknown Source)
        at scala.Predef$.$qmark$qmark$qmark(Unknown Source)
        at cue4s.ChangeModeWindows$.read(Unknown Source)
        at cue4s.InputProviderImpl.$anonfun$6(Unknown Source)
        at cue4s.InputProviderImpl$$Lambda$8.apply(Unknown Source)
        at scala.Function0.apply$mcI$sp(Unknown Source)
        at cue4s.KeyboardReadingThread.run(Unknown Source)
        at scala.scalanative.runtime.NativeThread$.threadEntryPoint(Unknown Source)
        at scala.scalanative.runtime.NativeThread$.threadRoutine$$anonfun$1(Unknown Source)
        at <none>.SM49scala.scalanative.runtime.NativeThread$$$Lambda$1G17$extern$forwarder(Unknown Source)
        at <none>.ProxyThreadStartRoutine(ImmixGC.c:142)
        at <none>.BaseThreadInitThunk(Unknown Source)
        at <none>.RtlUserThreadStart(Unknown Source)

My branch

After exampleNative/nativeLink:

TicTacToe:

cue4s>modules\example\target\native-3\example.exe
�[?25l�[0A�[0GException in thread "cue4s-keyboard-input-thread" scala.NotImplementedError: an implementation is missing
        at java.lang.Throwable.fillInStackTrace(Unknown Source)
        at scala.scalanative.runtime.Throwable.<init>(Unknown Source)
        at scala.Predef$.$qmark$qmark$qmark(Unknown Source)
        at cue4s.ChangeModeWindows$.read(Unknown Source)
        at cue4s.InputProviderImpl.$anonfun$6(Unknown Source)
        at cue4s.InputProviderImpl$$Lambda$8.apply(Unknown Source)
        at scala.Function0.apply$mcI$sp(Unknown Source)
        at cue4s.KeyboardReadingThread.run(Unknown Source)
        at scala.scalanative.runtime.NativeThread$.threadEntryPoint(Unknown Source)
        at scala.scalanative.runtime.NativeThread$.threadRoutine$$anonfun$1(Unknown Source)
        at <none>.SM49scala.scalanative.runtime.NativeThread$$$Lambda$1G17$extern$forwarder(Unknown Source)
        at <none>.ProxyThreadStartRoutine(ImmixGC.c:142)
        at <none>.BaseThreadInitThunk(Unknown Source)
        at <none>.RtlUserThreadStart(Unknown Source)
�[?25l�[0A�[0G�[?25l�[0A�[0G�[?25l�[0A�[0G�[?25l�[0A�[0G�[?25l�[0A�[0G�[?25l�[0A�[0G

sync with only multiple selection code

cue4s>modules\example\target\native-3\example.exe
�[?25l�[32m? �[39m�[36mWhat are your favourite letters?�[39m�[36m 쨩 �[39m
�[0G�[1mTab�[0m to toggle, �[1mShift+Tab�[0m to toggle all, �[1mEnter�[0m to submit.
�[0G�[32m _ (A,B)�[39m
�[0G _ (B,C)
�[0G _ (C,D)
�[0G _ (D,E)
�[0G _ (E,F)
�[0G _ (F,G)
�[0G v (G,H)
�[0G�[9A�[0GException in thread "cue4s-keyboard-input-thread" scala.NotImplementedError: an implementation is missing
        at java.lang.Throwable.fillInStackTrace(Unknown Source)
        at scala.scalanative.runtime.Throwable.<init>(Unknown Source)
        at scala.Predef$.$qmark$qmark$qmark(Unknown Source)
        at cue4s.ChangeModeWindows$.read(Unknown Source)
        at cue4s.InputProviderImpl.$anonfun$6(Unknown Source)
        at cue4s.InputProviderImpl$$Lambda$8.apply(Unknown Source)
        at scala.Function0.apply$mcI$sp(Unknown Source)
        at cue4s.KeyboardReadingThread.run(Unknown Source)
        at scala.scalanative.runtime.NativeThread$.threadEntryPoint(Unknown Source)
        at scala.scalanative.runtime.NativeThread$.threadRoutine$$anonfun$1(Unknown Source)
        at <none>.SM49scala.scalanative.runtime.NativeThread$$$Lambda$1G17$extern$forwarder(Unknown Source)
        at <none>.ProxyThreadStartRoutine(ImmixGC.c:142)
        at <none>.BaseThreadInitThunk(Unknown Source)
        at <none>.RtlUserThreadStart(Unknown Source)

Sorry, I haven't tried the JVM one.

@kevin-lee
Copy link
Copy Markdown
Contributor Author

@keynmol Oh, it’s already out?! 😃 That’s awesome! Thanks so much!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Can cue4s support toggles to select all and deselect all in MultiChoice prompts?

2 participants