feat(ui): CardKB (I2C keyboard) support with full keyboard-only navigation

Auto-detected at boot on Wire1/Grove (addr 0x5F) -- no setting to flip, and a
no-op on boards without that bus or with nothing attached. This UI's key
codes (KEY_LEFT/UP/DOWN/RIGHT/ENTER/CANCEL) already match CardKB's own byte
protocol, so most input needs zero translation and flows through the same
key queue as physical buttons.

Two bytes get remapped in UITask::pollCardKB():
- Enter, only when the on-screen keyboard's plain grid state is active (no
  placeholder/accent popup, not in cursor-mode), becomes a new KEY_KB_ENTER
  sentinel meaning "submit the field" -- reusing plain KEY_ENTER there would
  insert a stray character, since a CardKB typist's row/col never reflect an
  intentional grid selection. Everywhere else Enter is untouched, so
  selecting a placeholder or committing an accent still works normally.
- Tab (otherwise unused) becomes KEY_CONTEXT_MENU, standing in for the
  "Hold-Enter" long-press gesture CardKB has no way to produce -- without it,
  ~30 context menus across the UI (message reply/navigate, Bot/Admin/
  Repeater, ...) would be unreachable from the keyboard alone.

KeyboardWidget gains a direct-typing path: printable ASCII inserts straight
at the cursor bypassing the grid, Backspace deletes, KEY_KB_ENTER submits.

Build-verified: WioTrackerL1_companion_solo_dual and
WioTrackerL1Eink_companion_solo_dual both compile and link clean; also
smoke-tested Heltec_mesh_solar_companion_radio_ble (no ENV_PIN_SDA/SCL) to
confirm zero regression on boards without the feature.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-07-21 23:43:33 +02:00
co-authored by Claude Sonnet 5
parent a57d5d67bb
commit 7cae6470bf
5 changed files with 92 additions and 0 deletions
@@ -721,6 +721,30 @@ struct KeyboardWidget {
if (c == KEY_CANCEL) return CANCELLED;
// Direct-typing passthrough (CardKB or similar literal-ASCII input
// source, see UITask::pollCardKB()). Printable characters insert
// straight at the cursor, bypassing the on-screen grid entirely -- no
// caps re-application, the source already sends the correct case.
// Backspace deletes the previous character. KEY_KB_ENTER (only ever
// emitted when the grid is in this exact plain state -- no popup, no
// cursor-mode, see pollCardKB()) submits the field, instead of
// KEY_ENTER's usual "commit grid cell (row,col)".
if (c == KEY_KB_ENTER) return DONE;
if (c == 0x08) {
if (cursor_pos > 0) {
int n = kbUtf8LastCharBytes(buf, cursor_pos);
memmove(buf + cursor_pos - n, buf + cursor_pos, len - cursor_pos);
len -= n; cursor_pos -= n;
buf[len] = '\0';
}
return NONE;
}
if (c >= 0x20 && c <= 0x7E) {
char one[2] = { c, '\0' };
insertGlyph(one, false);
return NONE;
}
const int rows = gridRows();
const int cols = gridCols();