File the keystroke ring's missing modifier state as item 10
Some checks failed
libakgl CI Build / cmake_build (push) Failing after 20s
libakgl CI Build / mutation_test (push) Failing after 17s

akbasic built a line editor on akgl_controller_poll_key() and found the ring
carries a keycode and nothing else. No shifted character is reachable, so a
BASIC string literal cannot be typed at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 11:19:56 -04:00
parent 55675cc9de
commit 54df954ed6

47
TODO.md
View File

@@ -883,6 +883,11 @@ Doing that turned up **items 5 through 9**, filed below. Four are things `akbasi
around to build at all; each workaround is commented at its site over there with the words
"filed upstream", so they can be found and deleted when these land.
Building the *standalone* SDL frontend on top of all that — a real window, a real event pump
and a line editor — turned up **item 10**, and re-confirmed items 6 and 7: both workarounds
had to be written a second time, in `src/frontend_akgl.c`, because a host that owns a window is
exactly the caller they inconvenience.
These are filed here rather than worked around in `akbasic` because growing `libakgl` to serve
a consumer is the wanted outcome. Each entry says what the BASIC verb needs, what the
`akgl_*` entry point should look like, and what would cover it.
@@ -1099,6 +1104,48 @@ a consumer is the wanted outcome. Each entry says what the BASIC verb needs, wha
hand and assert the frame at which the pitch has moved, exactly as `tests/audio.c` already
does for the envelope stages.
10. **The keystroke ring carries a keycode and nothing else, so Shift is invisible.**
`akgl_controller_handle_event()` (`src/controller.c:104-105`) pushes `event->key.key` into
the ring and drops the rest of the event, and `akgl_controller_poll_key(int *keycode, bool
*available)` hands back only that. Item 4 asked for a non-blocking keystroke read and got
one; what it did not ask for, and what a *line editor* turns out to need, is which
character the keystroke actually produced.
`akbasic` has now built one — an `INPUT` and a REPL prompt drawn in the window, in
`src/sink_akgl.c` — and the consequence is concrete: no shifted character is reachable, so
`"`, `!`, `(`, `)`, `:` and `;` cannot be typed at all, and lower case cannot be typed
either. A BASIC line editor that cannot type a double quote cannot enter a string literal.
The interpreter folds letters to upper case, which is what a C128 does anyway and is the
right resolution for letters; it is not a resolution for punctuation, and there is nothing
the caller can do about it because the modifier state was discarded two layers down.
Note this is not solvable by the caller polling `SDL_GetModState()` at read time: by then
the key is long released, and the whole point of a ring is that reads are decoupled from
events.
**Fix:** widen the ring entry to carry the modifier state and the composed text SDL already
computes. Either
```c
typedef struct akgl_Keystroke {
SDL_Keycode key;
SDL_Keymod mod;
char text[8]; /* UTF-8, from SDL_EVENT_TEXT_INPUT; "" for a non-printing key */
} akgl_Keystroke;
akerr_ErrorContext AKERR_NOIGNORE *akgl_controller_poll_keystroke(akgl_Keystroke *dest, bool *available);
```
alongside the existing `akgl_controller_poll_key()`, which stays as it is so nothing
breaks — a game asking "was the up arrow pressed" wants exactly what it already gets.
Filling `text` means handling `SDL_EVENT_TEXT_INPUT` as well as `SDL_EVENT_KEY_DOWN`, which
is the only correct way to get a character out of SDL anyway: it is what makes a keyboard
layout, a compose key and a dead key work, none of which a keycode can express.
Tests would push a shifted key-down plus its text-input event through
`akgl_controller_handle_event()` and assert both the keycode and the composed character
come back, mirroring what `tests/controller.c` already does for the plain ring.
## Carried over
1. **Make character-to-sprite state bindings release their references symmetrically.**