Files
MeshCore-Solo/examples/companion_radio/ui-new/KeyboardWidget.h

878 lines
40 KiB
C
Raw Normal View History

#pragma once
#include <helpers/ui/DisplayDriver.h>
#include <Arduino.h>
#include "PopupMenu.h"
#include "icons.h" // mini-icons for the special-key row (⇧ ⌫ ⎵ ✓)
#include "../NodePrefs.h"
// Layout constants shared by all keyboard users.
// Two pages: letters (page 0) and symbols (page 1), toggled by the "#@"/"abc"
// special key. Space lives only on the ⎵ special key now, so the freed grid
// slot on page 0 holds the comma; punctuation is grouped as . , ! ?
static const int KB_PAGES = 2;
static const char KB_CHARS[KB_PAGES][4][10] = {
{ // page 0 — letters + digits
{'a','b','c','d','e','f','g','h','i','j'},
{'k','l','m','n','o','p','q','r','s','t'},
{'u','v','w','x','y','z','.',',','!','?'},
{'1','2','3','4','5','6','7','8','9','0'},
},
{ // page 1 — symbols + digits (ASCII only — one byte per key)
{'@','#','&','*','(',')','-','_','+','='},
{'/','\\',':',';','\'','"','<','>','[',']'},
{'{','}','|','~','^','$','%','`',',','.'},
{'1','2','3','4','5','6','7','8','9','0'},
},
};
static const int KB_ROWS_CHAR = 4;
static const int KB_COLS_CHAR = 10;
static const int KB_SPECIAL = 6; // ⇧ ⎵ ⌫ {} #@/abc ✓
// T9 multi-tap layout (Settings Keyboard). A classic phone keypad: 9 cells (keys
// 1-9) laid out 3x3, each holding a handful of letters/symbols. Repeated Enter
// presses on the same cell within KB_T9_TIMEOUT_MS cycle through the group, ending
// on the cell's own digit (computed as '1'+cell, not stored here) before wrapping.
// Keys 0/*/# aren't part of the grid — space/backspace/etc. already live on the
// special row below, shared with the ABC layout.
static const int KB_T9_ROWS = 3;
static const int KB_T9_COLS = 3;
static const uint32_t KB_T9_TIMEOUT_MS = 800;
static const char* const KB_T9_GROUPS[KB_PAGES][9] = {
{ ".,!?'-", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" }, // page 0 — letters
{ "@#&", "*()", "-_+", "=/\\", ":;'\"", "<>[]", "{}|~", "^$%`", ",." }, // page 1 — symbols
};
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
// Non-Latin keyboard scripts. NodePrefs::keyboard_main_alphabet/
// keyboard_alt_alphabet (Settings > Keyboard's Main/Additional rows) pick
// which script occupies page 0 (the keyboard's default/opening page) and
// which joins it as page 1 -- either can be Latin, Cyrillic, or Greek; the
// same value in both collapses to a single-script + Symbols cycle (see
// hasAltAlphabet()). Every alphabet here must fall inside the misc-fixed
// font's U+0020-04FF range (src/helpers/ui/MiscFixedFont.h) since that's what
// actually draws these glyphs on-screen.
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
//
// Unlike KB_CHARS (single ASCII byte per cell), these hold UTF-8 strings —
// Cyrillic is 2 bytes/codepoint — so cells are `const char*`, not `char`.
// KeyboardWidget's insertion/backspace/T9-cycle logic works in codepoints via
// the kbUtf8*() helpers below, not raw bytes, to stay correct for either.
// Cyrillic ABC grid: rows 0-2 hold the 30 letters that fit alphabetically
// (а-э); row 3 holds the remaining 3 (ё, ю, я) plus basic punctuation. No
// digit row on this page (digits are already reachable via the Symbols page,
// same as the Latin page's own row 3 duplicates them there).
static const char* const KB_CYRILLIC_CHARS[4][10] = {
{ "а","б","в","г","д","е","ж","з","и","й" },
{ "к","л","м","н","о","п","р","с","т","у" },
{ "ф","х","ц","ч","ш","щ","ъ","ы","ь","э" },
{ "ю","я","ё",".",",","!","?","-","'","\"" },
};
// Cyrillic T9 groups: the classic Russian phone-keypad distribution. Cell 0
// (digit '1') is punctuation, matching KB_T9_GROUPS' page-0 convention;
// cells 1-8 (digits '2'-'9') hold the 33 letters, 3-5 per key.
static const char* const KB_T9_GROUPS_CYRILLIC[9] = {
".,!?'-", "абвг", "деёжз", "ийкл", "мноп", "рсту", "фхцч", "шщъыь", "эюя"
};
// Greek ABC grid: the 24-letter modern alphabet plus final sigma (ς, used
// only at the end of a word — σ is the regular form) = 25 letters, fitting
// rows 0-2 with 5 basic punctuation marks to spare; row 3 keeps the digit row
feat(ui): on-device channel management, remote admin tool, per-language keyboards Messages: - Add/edit/delete channels on-device (new ChannelsView, owned by the renamed MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope). Channel secret entry supports a typed passphrase (SHA-256'd, same primitive the library already uses for the routing hash) or a raw 32-hex-char key. - MyMesh::setChannelLocal() factors out the setChannel/saveChannels/ onChannelRemoved sequence previously duplicated across the two CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths. Tools > Admin (new): - Log into a repeater/room server's admin account and send CLI commands, the on-device equivalent of the app's repeater-admin feature. - Commands are organised into category tabs (System/Radio/Routing/Actions) with common get/set fields (name, radio profile, tx power, repeat, advert intervals, ...) plus a free-text "Custom command..." fallback for anything else. A field row fetches the current value, opens it pre-filled for editing, and sends the change — falling back to a blank editor if the fetch fails or times out. - The admin password persists and self-heals exactly like room logins in Messages: saved on a confirmed admin-level login, forgotten on a failed one, left alone if merely under-privileged. - New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so a reply reaches the UI without touching the existing BLE/app CLI-terminal path (queueMessage's should_display gate is untouched). Shared TabBar.h extracted from NearbyScreen/BotScreen's independently duplicated tab-carousel rendering (now a third consumer via Admin) — also fixes neighbouring tabs vanishing outright when they didn't fully fit; they now truncate with an ellipsis instead. Keyboard: the combined "Ext.Latin" alphabet split into 8 separate, linguistically complete per-language keyboards (Polish, Czech, Slovak, German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug where tall accented glyphs overlapped the keyboard's separator line (SH1106's Lemon-font ascent constant was 2-3px short for them). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
// (unlike Cyrillic or most of the Latin-diacritic alphabets below, Greek has
// room left over in its own letter rows).
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
// NOTE: monotonic Modern Greek normally marks stress with a tonos accent
// (ά έ ή ί ό ύ ώ) — omitted here to keep this a single, simple page. Fine for
// informal/transliteration-style typing; flag if proper accented Greek
// composition turns out to matter and we can add a second Greek page for them.
static const char* const KB_GREEK_CHARS[4][10] = {
{ "α","β","γ","δ","ε","ζ","η","θ","ι","κ" },
{ "λ","μ","ν","ξ","ο","π","ρ","σ","ς","τ" },
{ "υ","φ","χ","ψ","ω",".",",","!","?","'" },
{ "1","2","3","4","5","6","7","8","9","0" },
};
// Greek T9 groups: cell 0 (digit '1') is punctuation; cells 1-8 (digits
// '2'-'9') split the 25 letters roughly evenly, final sigma grouped with the
// regular sigma it's a variant of.
static const char* const KB_T9_GROUPS_GREEK[9] = {
".,!?'-", "αβγ", "δεζ", "ηθι", "κλμ", "νξο", "πρσς", "τυφ", "χψω"
};
// Buffer cap for typed text, in bytes. Matches MeshCore's MAX_TEXT_LEN
// (10*CIPHER_BLOCK_SIZE = 160) so a full-length message can be composed; each
// field passes its own smaller max to begin() where its store is smaller.
static const int KB_MAX_LEN = 160;
// Longest preview line we render per row. Caps the per-line stack buffers so a
// very wide display (small font → many chars per line) can't overrun them.
static const int KB_PREVIEW_CAP = 46;
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
// ── UTF-8 helpers for the alt-alphabet pages ─────────────────────────────────
// KB_CHARS/KB_T9_GROUPS content is ASCII (1 byte/char); KB_CYRILLIC_CHARS and
// KB_T9_GROUPS_CYRILLIC are UTF-8 (2 bytes/char in this range). These helpers
// let insertion, backspace and T9 cycling work in codepoints for either,
// instead of assuming 1 byte == 1 character.
// Apply Shift/caps to every codepoint in a UTF-8 string, writing the result
// (same codepoint count, each independently shifted) into `out`. Every script
// here pairs lower/uppercase differently, so each gets its own rule:
// - ASCII a-z and Cyrillic а-я: flat -0x20 codepoint offset. ё/Ё (U+0451/
// U+0401) break the Cyrillic pattern by 0x50 and are special-cased.
feat(ui): on-device channel management, remote admin tool, per-language keyboards Messages: - Add/edit/delete channels on-device (new ChannelsView, owned by the renamed MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope). Channel secret entry supports a typed passphrase (SHA-256'd, same primitive the library already uses for the routing hash) or a raw 32-hex-char key. - MyMesh::setChannelLocal() factors out the setChannel/saveChannels/ onChannelRemoved sequence previously duplicated across the two CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths. Tools > Admin (new): - Log into a repeater/room server's admin account and send CLI commands, the on-device equivalent of the app's repeater-admin feature. - Commands are organised into category tabs (System/Radio/Routing/Actions) with common get/set fields (name, radio profile, tx power, repeat, advert intervals, ...) plus a free-text "Custom command..." fallback for anything else. A field row fetches the current value, opens it pre-filled for editing, and sends the change — falling back to a blank editor if the fetch fails or times out. - The admin password persists and self-heals exactly like room logins in Messages: saved on a confirmed admin-level login, forgotten on a failed one, left alone if merely under-privileged. - New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so a reply reaches the UI without touching the existing BLE/app CLI-terminal path (queueMessage's should_display gate is untouched). Shared TabBar.h extracted from NearbyScreen/BotScreen's independently duplicated tab-carousel rendering (now a third consumer via Admin) — also fixes neighbouring tabs vanishing outright when they didn't fully fit; they now truncate with an ellipsis instead. Keyboard: the combined "Ext.Latin" alphabet split into 8 separate, linguistically complete per-language keyboards (Polish, Czech, Slovak, German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug where tall accented glyphs overlapped the keyboard's separator line (SH1106's Lemon-font ascent constant was 2-3px short for them). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
// - Latin-1 Supplement à-þ (U+00E0-00FE, used by every Latin-diacritic
// alphabet below — ö/ü/é/á/etc.): also a flat -0x20 offset, same as ASCII —
// the block was designed as parallel case pairs. U+00F7 (÷, division sign)
// sits in that numeric range but isn't a letter; excluded. ß (U+00DF) has
// no simple uppercase in this range (its uppercase ẞ is U+1E9E, outside
// Lemon's U+0020-04FF) — left as-is. ÿ (U+00FF, French) is the one letter
// in this block whose uppercase Ÿ (U+0178) falls outside it entirely —
// special-cased before the range rule.
// - Latin Extended-A (ą/č/ĺ/œ/etc., U+0100-017F): NOT a flat offset like
// Latin-1 — this block alternates even=uppercase/odd=lowercase in adjacent
// pairs, so lowercase - 1 = uppercase. Verified true (against Python's own
// str.upper()) for every character actually used across the Polish/Czech/
// Slovak/French keyboards below; NOT a universal rule for the whole block
// (it has a handful of unpaired/irregular codepoints elsewhere) — recheck
// before adding more from it.
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
// - Greek α-ω (U+03B1-03C9): flat -0x20 offset, same shape as Cyrillic/ASCII.
// Final sigma ς (U+03C2) is the one exception — it has no uppercase of its
// own; -0x20 would land on U+03A2, which is unassigned. It capitalizes to
// regular Σ (U+03A3) instead, same as σ, and is special-cased before the
// general range rule (03C2 falls inside 03B1-03C9, so order matters here).
// Used both for a single cell (one codepoint) and a whole T9 group label/string.
static void kbApplyCapsUtf8(const char* in, bool caps, char* out, size_t out_size) {
size_t o = 0;
const uint8_t* p = (const uint8_t*)in;
while (*p && o + 2 < out_size) {
uint32_t cp = DisplayDriver::decodeCodepoint(p);
if (caps) {
if (cp == 0x0451) cp = 0x0401; // ё -> Ё
else if (cp == 0x03C2) cp = 0x03A3; // ς -> Σ
feat(ui): on-device channel management, remote admin tool, per-language keyboards Messages: - Add/edit/delete channels on-device (new ChannelsView, owned by the renamed MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope). Channel secret entry supports a typed passphrase (SHA-256'd, same primitive the library already uses for the routing hash) or a raw 32-hex-char key. - MyMesh::setChannelLocal() factors out the setChannel/saveChannels/ onChannelRemoved sequence previously duplicated across the two CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths. Tools > Admin (new): - Log into a repeater/room server's admin account and send CLI commands, the on-device equivalent of the app's repeater-admin feature. - Commands are organised into category tabs (System/Radio/Routing/Actions) with common get/set fields (name, radio profile, tx power, repeat, advert intervals, ...) plus a free-text "Custom command..." fallback for anything else. A field row fetches the current value, opens it pre-filled for editing, and sends the change — falling back to a blank editor if the fetch fails or times out. - The admin password persists and self-heals exactly like room logins in Messages: saved on a confirmed admin-level login, forgotten on a failed one, left alone if merely under-privileged. - New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so a reply reaches the UI without touching the existing BLE/app CLI-terminal path (queueMessage's should_display gate is untouched). Shared TabBar.h extracted from NearbyScreen/BotScreen's independently duplicated tab-carousel rendering (now a third consumer via Admin) — also fixes neighbouring tabs vanishing outright when they didn't fully fit; they now truncate with an ellipsis instead. Keyboard: the combined "Ext.Latin" alphabet split into 8 separate, linguistically complete per-language keyboards (Polish, Czech, Slovak, German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug where tall accented glyphs overlapped the keyboard's separator line (SH1106's Lemon-font ascent constant was 2-3px short for them). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
else if (cp == 0x00FF) cp = 0x0178; // ÿ -> Ÿ (French; breaks the à-þ flat -0x20 rule below — Ÿ sits outside Latin-1 Supplement entirely)
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
else if (cp >= 0x0430 && cp <= 0x044F) cp -= 0x20; // а-я -> А
else if (cp >= 0x03B1 && cp <= 0x03C9) cp -= 0x20; // α-ω -> Α
else if (cp >= 0x00E0 && cp <= 0x00FE && cp != 0x00F7) cp -= 0x20; // à-þ -> À-Þ
else if (cp >= 0x0100 && cp < 0x0180 && (cp & 1) == 1) cp -= 1; // ą-ż (odd=lower) -> Ą-Ż
else if (cp >= 'a' && cp <= 'z') cp -= 0x20; // a-z -> A-Z
}
if (cp < 0x80) {
out[o++] = (char)cp;
} else {
out[o++] = (char)(0xC0 | (cp >> 6));
out[o++] = (char)(0x80 | (cp & 0x3F));
}
}
out[o] = '\0';
}
// Codepoint count of a UTF-8 string (byte length overcounts once a 2-byte
// alphabet is involved — this is what T9 cycling needs instead of strlen()).
static int kbUtf8Len(const char* s) {
int n = 0;
const uint8_t* p = (const uint8_t*)s;
while (*p) { DisplayDriver::decodeCodepoint(p); n++; }
return n;
}
// Extract the idx-th codepoint of a UTF-8 string as its own NUL-terminated
// UTF-8 bytes in `out` (>= 5 bytes). Empty string if idx is out of range.
static void kbUtf8CharAt(const char* s, int idx, char* out) {
const uint8_t* p = (const uint8_t*)s;
for (int i = 0; *p; i++) {
const uint8_t* start = p;
DisplayDriver::decodeCodepoint(p);
if (i == idx) {
int n = (int)(p - start);
memcpy(out, start, n);
out[n] = '\0';
return;
}
}
out[0] = '\0';
}
// Byte width of the LAST codepoint in buf[0..len) — for backspace and the T9
// in-place replace, which must remove/overwrite a whole codepoint, not one
// byte (a lone trailing continuation byte would otherwise corrupt a 2-byte
// character). Capped at 4 (UTF-8's max), though this codebase's alphabets are
// all <= 2 bytes today.
static int kbUtf8LastCharBytes(const char* buf, int len) {
if (len <= 0) return 0;
int n = 1;
while (n < len && n < 4 && ((uint8_t)buf[len - n] & 0xC0) == 0x80) n++;
return n;
}
// Byte width of the codepoint STARTING at buf[pos] (pos in [0,len)) — the
// forward counterpart to kbUtf8LastCharBytes, for moving the edit cursor
// right by one whole codepoint instead of one byte.
static int kbUtf8CharBytesAt(const char* buf, int pos, int len) {
if (pos >= len) return 0;
uint8_t c = (uint8_t)buf[pos];
int n = 1;
if ((c & 0xE0) == 0xC0) n = 2;
else if ((c & 0xF0) == 0xE0) n = 3;
else if ((c & 0xF8) == 0xF0) n = 4;
if (pos + n > len) n = len - pos; // truncated/corrupt sequence: don't overrun
return n;
}
2026-07-17 20:28:11 +02:00
// Accented variants of a Latin base letter, reachable by Hold-Enter on that
// letter's cell on the plain Latin ABC page (see handleInput's KEY_CONTEXT_MENU
// block and the accent_active state machine). Replaces the old per-language
// full alt-alphabet pages (Polish/Czech/Slovak/German/French/Spanish/
// Portuguese/Nordic) with one popup covering the union of all their accented
// letters -- closer to a phone keyboard's long-press accent picker than a
// second full page per language. Each entry is one UTF-8 string of
// concatenated variants (like KB_T9_GROUPS_* group strings), read with the
// same kbUtf8Len()/kbUtf8CharAt() helpers used there.
// Ligature/non-diacritic letters are filed under their conventional key, same
// as a phone keyboard's long-press: ß (German) -> s, œ (French) -> o. ĺ/ŕ
// (Slovak) are l/r with an acute, not i/e variants, so they're filed there.
static const char KB_ACCENT_BASES[] = "acdeilnorstuyz";
static const char* const KB_ACCENT_VARIANTS[] = {
"áàâãäåą", // a
"çćč", // c
"ď", // d
"éèêëěę", // e
"íîï", // i
"łĺľ", // l
"ñńň", // n
"óòôõöøœ", // o
"řŕ", // r
"śšß", // s
"ť", // t
"úùûüů", // u
"ýÿ", // y
"źżž", // z
};
static const int KB_ACCENT_COUNT = sizeof(KB_ACCENT_BASES) - 1; // exclude the trailing NUL
static int findAccentGroup(char base) {
for (int i = 0; i < KB_ACCENT_COUNT; i++) if (KB_ACCENT_BASES[i] == base) return i;
return -1;
}
static const int KB_PH_MAX = 20; // max placeholders in list (PopupMenu::PM_MAX_ITEMS=24 is the hard ceiling)
static const int KB_PH_LEN = 30; // max placeholder string length incl. null -- sized for the longest
// CLI-command candidate (AdminScreen), not just the short {x} tokens
static const int KB_PH_VISIBLE = 3; // items shown at once in overlay
struct KeyboardWidget;
// Optional hook: if set, called right before the placeholder picker opens so
// the caller can repopulate the list contextually (e.g. AdminScreen's CLI
// command autocomplete, filtered by what's already typed). When set, picking
// an entry also replaces the in-progress word (the text since the last
// space) instead of appending it -- true completion, not insertion. Fields
// that don't set this (the common case -- {loc}/{time} etc.) keep the
// original static-list, append-only behaviour untouched.
typedef void (*PlaceholderRefreshFn)(KeyboardWidget& kb, void* ctx);
struct KeyboardWidget {
char buf[KB_MAX_LEN + 1];
int len;
int max_len;
int cursor_pos; // insertion point into buf, in bytes; defaults to len (append)
2026-07-17 20:28:11 +02:00
bool cursor_mode; // true while UP-from-row-0 has parked the grid to reposition cursor_pos
bool accent_active = false; // true while the Hold-Enter accent popup is open
int accent_group = -1; // index into KB_ACCENT_VARIANTS for the held cell's base letter
int accent_sel = 0; // selected variant within that group
int row, col;
int page; // see totalPages()/scriptAt()/pageIsSymbols() below
bool caps;
// Shift is one-shot by default (like a phone keyboard: capitalises just the
// next letter, then reverts) — Hold-Enter on Shift toggles caps_lock, which
// keeps it on for a whole run of capitals instead.
bool caps_lock = false;
char _ph_buf[KB_PH_MAX][KB_PH_LEN];
int _ph_count;
PopupMenu _ph_menu;
PlaceholderRefreshFn _ph_refresh = nullptr;
void* _ph_refresh_ctx = nullptr;
const char* _ph_title = "Placeholder:"; // popup title -- overridable so e.g. AdminScreen can say "Commands:"
void setPlaceholderRefresh(PlaceholderRefreshFn fn, void* ctx, const char* title = "Placeholder:") {
_ph_refresh = fn; _ph_refresh_ctx = ctx; _ph_title = title;
}
// Live setting lookup — set once by UITask::begin(). NULL only in tests/tools
// that construct a KeyboardWidget standalone, in which case isT9() defaults
// to ABC and mainScript()/altScript() default to Latin-only.
NodePrefs* prefs = nullptr;
bool isT9() const { return prefs && prefs->keyboard_type == 1; }
// Which script occupies page 0 (the keyboard's default/opening page) and
// which occupies page 1 (reached by the #@/abc cycle key) -- Settings >
// Keyboard's Main/Additional rows. Additional equal to Main collapses to no
// second page at all (see hasAltAlphabet), same as the old Latin-hardcoded
// design's "alt == Latin means no alt".
uint8_t mainScript() const { return prefs ? prefs->keyboard_main_alphabet : NodePrefs::KB_ALPHABET_LATIN_ONLY; }
uint8_t altScript() const { return prefs ? prefs->keyboard_alt_alphabet : NodePrefs::KB_ALPHABET_LATIN_ONLY; }
bool hasAltAlphabet() const { return altScript() != mainScript(); }
// T9 multi-tap state: which grid cell is mid-cycle (-1 = none), its cycle
// position, and when the last Enter landed on it (for the timeout).
int t9_cell = -1;
int t9_cycle = 0;
uint32_t t9_last_ms = 0;
feat(admin): typed Radio/Routing field editors; pre-release audit fixes Tools > Admin: split the Radio tab's single "f,bw,sf,cr" comma-string field (free-text keyboard) into 4 independent, type-appropriate rows -- Frequency (same digit-cursor DigitEditor Settings/Repeater use locally), Bandwidth/Spreading factor/Coding rate (discrete-set LEFT/RIGHT stepping), plus TX power and the 3 Routing numeric fields as number steppers and Repeat as an ON/OFF toggle. Edits happen locally (zero mesh traffic per keystroke); only Enter sends one combined `set`, Cancel sends nothing. Full pre-release audit of all 16 commits since v1.22 (5 parallel focus areas: display/font, keyboard/messages, Nodes/Admin, Bot/channels, UITask+NodePrefs schema) turned up and fixed: - Admin: fetched FK_NUMBER values weren't clamped to the field's range, so a value already out-of-range could get stuck unreachable; fixed commit-time float format noise (%.6f -> %.3f); Cancel/failed-login always returned to the Nodes picker even when Admin was opened directly from a node's Hold-Enter action -- now returns to wherever it was actually opened from (AdminScreen::_from_picker). - Keyboard: one-shot Shift was consumed after the *first* T9 multi-tap, so cycling to the 2nd/3rd candidate always came out lowercase -- fixed by caching the cycle's caps state (t9_caps). - Messages: history is numbered newest-first, so a message arriving while scrolled up to an older one silently relabeled the view onto a different message -- selection now shifts with the insert. - Channels: onChannelRemoved() didn't clear ch_notif_override/ ch_notif_muted/ch_fav_bitmask despite its own contract comment requiring it (now far more reachable via the on-device Delete); an all-zero hex secret silently self-deleted the channel it was just saved into (collides with the empty-slot sentinel) -- rejected. - Room login: isRoomLoggedIn() indexed the login-tracking ring directly instead of via its head offset, silently wrong once the ring wraps (8+ rooms/session) -- the new on-device Logout depends on this being right. - Font/display: removed the now fully-inert Settings > Display > Font toggle and the dead LemonFont.h (retired by the earlier misc-fixed font unification, zero remaining includes); fixed a copy-pasted "5x7" comment (font is 6x9), two meaningless dead ternaries, and an OLED/e-ink inconsistency in the undefined-glyph fallback box offset. - AdminField's `kind`/bounds fields no longer rely on default member initializers inside aggregate-init: this toolchain's actual nRF52 build (unlike env:native) has no explicit -std= override, so it predates C++14's aggregate-with-default-member-initializer rule. Given an explicit constructor instead -- portable regardless of standard, all existing field-table literals unchanged. Docs updated to match: tools_screen.md (Diagnostics as a 3-tab carousel, was documented as one flat screen), message_screen.md (chat bubbles, newest-at-bottom, cursor mode, secret validation), settings_screen.md (dropped the dead Font row), solo_ui_framework.md (header menu_hint signatures, icon priority-drop, KeyboardWidget's T9/alphabets/cursor-mode). release-notes.md gains the v1.23 section covering all of the above plus the other 15 commits since v1.22. Build-verified on WioTrackerL1_companion_solo_dual. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 19:38:51 +02:00
// Caps state the *first* tap of the current T9 cycle applied -- reused by every
// later cycling tap on the same cell, since one-shot Shift is consumed (see
// below) right after that first tap, before the user has settled on a letter.
// Without this, cycling to the 2nd/3rd/... candidate would always render
// lowercase regardless of Shift.
bool t9_caps = false;
int gridRows() const { return isT9() ? KB_T9_ROWS : KB_ROWS_CHAR; }
int gridCols() const { return isT9() ? KB_T9_COLS : KB_COLS_CHAR; }
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
// ── Page model ────────────────────────────────────────────────────────────
// Logical page order: 0 = mainScript(), [1 = altScript(), if it differs],
// last = symbols. Without a distinct additional script this is exactly the
// original 2-page cycle; a distinct one inserts its page in the middle, so
// the #@/abc key's existing cycle (case 4 below) reaches it for free.
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
int totalPages() const { return hasAltAlphabet() ? 3 : 2; }
bool pageIsSymbols(int pg) const { return pg == totalPages() - 1; }
// Which script (Latin/Cyrillic/Greek) the given non-symbols page shows.
uint8_t scriptAt(int pg) const { return pg == 0 ? mainScript() : altScript(); }
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
// One script's ABC-grid cell content as a NUL-terminated UTF-8 string (1
// codepoint). Latin comes back through a small scratch buffer since
// KB_CHARS stores single ASCII bytes, not strings; Cyrillic/Greek cells are
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
// literal string-table entries, returned directly.
const char* scriptCellStr(uint8_t script, int r, int c) const {
switch (script) {
case NodePrefs::KB_ALPHABET_CYRILLIC: return KB_CYRILLIC_CHARS[r][c];
case NodePrefs::KB_ALPHABET_GREEK: return KB_GREEK_CHARS[r][c];
default: {
static char single[2];
single[0] = KB_CHARS[0][r][c];
single[1] = '\0';
return single;
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
}
}
}
const char* cellStr(int r, int c) const {
if (pageIsSymbols(page)) {
static char single[2];
single[0] = KB_CHARS[1][r][c];
single[1] = '\0';
return single;
}
return scriptCellStr(scriptAt(page), r, c);
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
}
// One script's T9 group string (UTF-8) for the given cell (0-8).
const char* scriptT9GroupStr(uint8_t script, int cell) const {
switch (script) {
case NodePrefs::KB_ALPHABET_CYRILLIC: return KB_T9_GROUPS_CYRILLIC[cell];
case NodePrefs::KB_ALPHABET_GREEK: return KB_T9_GROUPS_GREEK[cell];
default: return KB_T9_GROUPS[0][cell];
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
}
}
const char* t9GroupStr(int cell) const {
if (pageIsSymbols(page)) return KB_T9_GROUPS[1][cell];
return scriptT9GroupStr(scriptAt(page), cell);
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
}
// Compact ASCII hint for the #@/abc key when it's about to switch to
// `script`'s page. Deliberately ASCII (not the script's own glyphs) so it
// renders correctly even when Lemon isn't the user's current font choice —
// unlike a script's own grid, this hint is visible from every page,
// where render() doesn't force Lemon on (see render()).
static const char* scriptHint(uint8_t script) {
switch (script) {
case NodePrefs::KB_ALPHABET_CYRILLIC: return "CY";
case NodePrefs::KB_ALPHABET_GREEK: return "GR";
default: return "abc";
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
}
}
enum Result { NONE, DONE, CANCELLED };
void begin(const char* initial = "", int max = KB_MAX_LEN) {
max_len = (max > KB_MAX_LEN) ? KB_MAX_LEN : max;
strncpy(buf, initial, max_len);
buf[max_len] = '\0';
len = strlen(buf);
cursor_pos = len;
cursor_mode = false;
2026-07-17 20:28:11 +02:00
accent_active = false;
accent_group = -1;
accent_sel = 0;
row = col = 0;
page = 0;
caps = false;
caps_lock = false;
t9_cell = -1;
t9_cycle = 0;
_ph_menu.active = false;
_ph_refresh = nullptr; // opt-in per session -- the owning screen re-sets it if it wants
_ph_refresh_ctx = nullptr; // contextual autocomplete right after this begin()
_ph_title = "Placeholder:";
// default placeholders — always available
_ph_count = 0;
addPlaceholder("{loc}");
addPlaceholder("{time}");
}
2026-07-17 20:28:11 +02:00
// Insert one UTF-8 codepoint (a grid cell's own glyph, or a picked accent
// variant) at cursor_pos, applying Shift/caps-lock the same way every cell
// commit does. Shared by the plain-Latin-cell commit below and the accent
// popup's Enter commit, so the two paths can't drift apart.
void insertGlyph(const char* one, bool use_caps) {
char shown[5];
kbApplyCapsUtf8(one, use_caps, shown, sizeof(shown));
int n = (int)strlen(shown);
int tail_len = len - cursor_pos;
if (len + n <= max_len) {
memmove(buf + cursor_pos + n, buf + cursor_pos, tail_len);
memcpy(buf + cursor_pos, shown, n);
len += n;
cursor_pos += n;
buf[len] = '\0';
}
}
void clearPlaceholders() { _ph_count = 0; }
void addPlaceholder(const char* ph) {
if (_ph_count < KB_PH_MAX) {
strncpy(_ph_buf[_ph_count], ph, KB_PH_LEN - 1);
_ph_buf[_ph_count][KB_PH_LEN - 1] = '\0';
_ph_count++;
}
}
int render(DisplayDriver& display) {
// A stale mid-cycle T9 press (no further input since) finalizes on its own —
// the character is already committed to buf, this just stops a later Enter
// on the same cell from being treated as a continued cycle.
if (t9_cell >= 0 && millis() - t9_last_ms > KB_T9_TIMEOUT_MS) t9_cell = -1;
// Single UI font (misc-fixed 5x7) covers Latin/Greek/Cyrillic — the keyboard
// renders in it directly, no per-render font switching or headroom padding.
display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT);
const int rows = gridRows();
const int cols = gridCols();
const int lh = display.getLineHeight();
const int cw = display.getCharWidth();
const int cell_w = display.width() / cols;
// compact: don't stretch cells beyond lh; freed vertical space goes to preview lines
const int kb_h = (rows + 1) * lh;
const int preview_h = display.height() - kb_h - display.sepH();
const int prev_lines = (preview_h / lh) > 1 ? (preview_h / lh) : 1;
const int sep_y = prev_lines * lh;
const int chars_y = sep_y + display.sepH();
const int cell_h = (display.height() - chars_y) / (rows + 1);
const int spec_y = chars_y + rows * cell_h;
const int spec_w = display.width() / KB_SPECIAL;
// Multi-line text preview: the view follows cursor_pos (normally == len,
// i.e. the end — so this is identical to the old "always the last line"
// behaviour until cursor mode moves cursor_pos elsewhere, at which point
// the preview scrolls to keep the repositioned cursor in view).
int cpl = display.width() / cw; // chars per preview line
if (cpl < 1) cpl = 1;
if (cpl > KB_PREVIEW_CAP) cpl = KB_PREVIEW_CAP; // never overrun linebuf below
int cursor_line = cursor_pos / cpl;
int first_line = (cursor_line >= prev_lines) ? (cursor_line - prev_lines + 1) : 0;
int start = first_line * cpl;
for (int pl = 0; pl < prev_lines; pl++) {
int ps = start + pl * cpl;
int pe = ps + cpl;
bool cursor_here = (ps <= cursor_pos && (cursor_pos < pe || pl == prev_lines - 1));
char linebuf[KB_PREVIEW_CAP + 2]; // cpl chars + cursor '_' + NUL
if (cursor_here) {
// Cursor drawn as an inserted '_' between whatever text precedes and
// follows it on this line -- reduces to the old "text + trailing _"
// when cursor_pos == len (after_n is always 0 in that case).
int before_n = cursor_pos - ps; if (before_n < 0) before_n = 0; if (before_n > cpl) before_n = cpl;
int line_text_end = (len < pe) ? len : pe;
int after_n = line_text_end - cursor_pos; if (after_n < 0) after_n = 0;
if (before_n + after_n > cpl) after_n = cpl - before_n;
snprintf(linebuf, sizeof(linebuf), "%.*s_%.*s", before_n, buf + ps, after_n, buf + ps + before_n);
} else if (len > ps) {
int nc = (len < pe) ? (len - ps) : cpl;
snprintf(linebuf, sizeof(linebuf), "%.*s", nc, buf + ps);
} else {
linebuf[0] = '\0';
}
char linebuf_t[KB_PREVIEW_CAP + 2];
display.translateUTF8ToBlocks(linebuf_t, linebuf, sizeof(linebuf_t));
display.setCursor(0, pl * lh);
display.print(linebuf_t);
}
display.fillRect(0, sep_y, display.width(), display.sepH());
// Cursor-positioning mode: LEFT/RIGHT/UP/DOWN now drive the text cursor
// instead of the grid (see handleInput), so the grid would otherwise just
// sit there frozen with no sign anything's different. Replace it with an
// explicit hint instead.
if (cursor_mode) {
const int hh = lh + 2;
display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, chars_y, display.width(), hh);
display.setColor(DisplayDriver::DARK);
display.drawTextCentered(display.width() / 2, chars_y + 1, "CURSOR MODE");
display.setColor(DisplayDriver::LIGHT);
display.drawTextCentered(display.width() / 2, chars_y + hh + 2, "L/R move");
display.drawTextCentered(display.width() / 2, chars_y + hh + 2 + lh, "U/D start/end");
return 50;
}
// character grid
if (isT9()) {
for (int r = 0; r < rows; r++) {
int y = chars_y + r * cell_h;
for (int c = 0; c < cols; c++) {
bool sel = (row == r && col == c);
int cell = r * cols + c;
// Label the cell "<digit><group>" so it reads like a phone keypad. The
// digit is what the multi-tap cycle lands on after the letters (see
// handleInput: '1'+cell). No separator space — the widest group
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
// (Cyrillic "деёжз"/"шщъыь", 5 letters x up to 2 UTF-8 bytes) + digit
// still fits with room to spare.
char group_shown[12];
kbApplyCapsUtf8(t9GroupStr(cell), caps, group_shown, sizeof(group_shown));
char label[14];
snprintf(label, sizeof(label), "%c%s", (char)('1' + cell), group_shown);
int cx = c * cell_w;
display.drawSelectionRow(cx, y - 1, cell_w - 1, cell_h, sel);
int tw = display.getTextWidth(label);
display.setCursor(cx + (cell_w - tw) / 2, y);
display.print(label);
}
}
} else {
for (int r = 0; r < rows; r++) {
int y = chars_y + r * cell_h;
for (int c = 0; c < cols; c++) {
bool sel = (row == r && col == c);
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
char ch_buf[3];
kbApplyCapsUtf8(cellStr(r, c), caps, ch_buf, sizeof(ch_buf));
if (ch_buf[0] == ' ' && ch_buf[1] == '\0') ch_buf[0] = '_';
int cx = c * cell_w;
display.drawSelectionRow(cx, y - 1, cell_w - 1, cell_h, sel);
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
int tw = display.getTextWidth(ch_buf);
display.setCursor(cx + (cell_w - tw) / 2, y);
display.print(ch_buf);
}
}
}
// special row: caps ⇧ · space ⎵ · delete ⌫ · placeholders {} (text) · OK ✓
const int s = miniIconScale(display);
const int icy = spec_y + (cell_h - lh) / 2; // centre icons within the cell
for (int i = 0; i < KB_SPECIAL; i++) {
bool sel = (row == rows && col == i);
bool active = (i == 0 && caps);
int sx = i * spec_w;
display.drawSelectionRow(sx, spec_y - 1, spec_w - 1, cell_h, sel || active);
if (i == 3 || i == 4) { // text keys: {} picker, page toggle
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
// Shows what pressing it lands on next, same "reads as the
// destination" convention as the original 2-page abc<->#@ toggle,
// generalized to however many pages are in the cycle right now.
const char* lbl;
if (i == 3) {
lbl = "{}";
} else {
int next = (page + 1) % totalPages();
lbl = pageIsSymbols(next) ? "#@" : scriptHint(scriptAt(next));
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
}
int tw = display.getTextWidth(lbl);
display.setCursor(sx + (spec_w - tw) / 2, spec_y);
display.print(lbl);
} else if (i == 1) { // space ⎵ — two halves side by side
int icw = (ICON_SPACE_L.w + ICON_SPACE_R.w) * s;
int ix = sx + (spec_w - icw) / 2;
miniIconDraw(display, ix, icy, ICON_SPACE_L);
miniIconDraw(display, ix + ICON_SPACE_L.w * s, icy, ICON_SPACE_R);
} else {
const MiniIcon& ic = (i == 0) ? ICON_SHIFT
: (i == 2) ? ICON_BACKSPACE
: ICON_CHECK; // i == 5 → OK
int ix = sx + (spec_w - ic.w * s) / 2;
miniIconDraw(display, ix, icy, ic);
}
display.setColor(DisplayDriver::LIGHT);
}
2026-07-17 20:28:11 +02:00
// Accent popup: floats over the still-visible grid (same idea as the
// placeholder overlay just below), anchored on the held letter's own row
// so it reads as "popping out of" that key instead of taking over the
// whole keyboard. LEFT/RIGHT picks, Enter commits, Cancel dismisses.
if (accent_active) {
const char* group = KB_ACCENT_VARIANTS[accent_group];
int n = kbUtf8Len(group);
const int pad = 3;
int seg_w = cw * 2 + pad;
int bw = n * seg_w;
int max_bw = display.width() - 4;
if (bw > max_bw) { bw = max_bw; seg_w = bw / n; }
int bx = (display.width() - bw) / 2;
int by = chars_y + row * cell_h - 1;
int bh = cell_h + 1;
display.setColor(DisplayDriver::DARK);
display.fillRect(bx, by, bw, bh);
display.setColor(DisplayDriver::LIGHT);
display.drawRect(bx, by, bw, bh);
for (int i = 0; i < n; i++) {
char one[5]; kbUtf8CharAt(group, i, one);
char shown[5]; kbApplyCapsUtf8(one, caps, shown, sizeof(shown));
int x = bx + i * seg_w;
if (i == accent_sel) {
display.setColor(DisplayDriver::LIGHT);
display.fillRect(x + 1, by + 1, seg_w - 1, bh - 2);
display.setColor(DisplayDriver::DARK);
} else {
display.setColor(DisplayDriver::LIGHT);
}
int tw = display.getTextWidth(shown);
display.setCursor(x + (seg_w - tw) / 2, by + 1);
display.print(shown);
}
display.setColor(DisplayDriver::LIGHT);
}
// placeholder picker overlay (drawn on top of keyboard)
if (_ph_menu.active) _ph_menu.render(display);
return 50;
}
Result handleInput(char c) {
// placeholder overlay consumes all input
if (_ph_menu.active) {
auto res = _ph_menu.handleInput(c);
if (res == PopupMenu::SELECTED) {
int idx = _ph_menu.selectedIndex();
const char* ph = _ph_buf[idx];
int ph_len = strlen(ph);
// Contextual (refresh-hook) fields complete the in-progress word --
// the text since the last space, up to the cursor -- instead of
// appending after it, so picking a match doesn't duplicate what's
// already been typed. Anything after the cursor (if it's not at the
// end) shifts along with the insertion, same as a normal keystroke.
int base_len = cursor_pos;
if (_ph_refresh) {
while (base_len > 0 && buf[base_len - 1] != ' ') base_len--;
}
int tail_len = len - cursor_pos;
if (base_len + ph_len + tail_len <= max_len) {
memmove(buf + base_len + ph_len, buf + cursor_pos, tail_len);
memcpy(buf + base_len, ph, ph_len);
len = base_len + ph_len + tail_len;
cursor_pos = base_len + ph_len;
buf[len] = '\0';
}
}
return NONE;
}
2026-07-17 20:28:11 +02:00
// Cursor-positioning sub-mode (see the KEY_UP block below): the grid
// selection is parked while LEFT/RIGHT walk cursor_pos one codepoint at a
2026-07-17 20:28:11 +02:00
// time. UP/DOWN jump to the very start/end -- Home/End, in effect -- and,
// once already at that boundary, continue the wrap the entry trigger
// interrupted: UP again lands on the special row, DOWN again back on the
// letter grid's row 0, same destinations the plain grid wrap used to reach
// directly (see the entry/exit comment below). Enter/Cancel just leave the
// mode from anywhere; the actual edit (insert/backspace) happens back in
// normal typing, now targeting the repositioned cursor.
if (cursor_mode) {
if (c == KEY_LEFT) { if (cursor_pos > 0) cursor_pos -= kbUtf8LastCharBytes(buf, cursor_pos); return NONE; }
if (c == KEY_RIGHT) { if (cursor_pos < len) cursor_pos += kbUtf8CharBytesAt(buf, cursor_pos, len); return NONE; }
2026-07-17 20:28:11 +02:00
if (c == KEY_UP) {
if (cursor_pos > 0) { cursor_pos = 0; return NONE; }
cursor_mode = false;
row = gridRows();
col = col * KB_SPECIAL / gridCols();
return NONE;
}
if (c == KEY_DOWN) {
if (cursor_pos < len) { cursor_pos = len; return NONE; }
cursor_mode = false;
row = 0;
return NONE;
}
if (c == KEY_ENTER || c == KEY_CANCEL) { cursor_mode = false; return NONE; }
return NONE;
}
2026-07-17 20:28:11 +02:00
if (accent_active) {
const char* group = KB_ACCENT_VARIANTS[accent_group];
int n = kbUtf8Len(group);
if (keyIsPrev(c)) { accent_sel = (accent_sel > 0) ? accent_sel - 1 : n - 1; return NONE; }
if (keyIsNext(c)) { accent_sel = (accent_sel < n - 1) ? accent_sel + 1 : 0; return NONE; }
if (c == KEY_ENTER) {
char one[5]; kbUtf8CharAt(group, accent_sel, one);
insertGlyph(one, caps);
if (caps && !caps_lock) caps = false;
accent_active = false;
return NONE;
}
if (c == KEY_CANCEL) { accent_active = false; return NONE; }
return NONE;
}
if (c == KEY_CANCEL) return CANCELLED;
const int rows = gridRows();
const int cols = gridCols();
// Hold-Enter is normally "cancel", but three places give it a more useful
// meaning instead: Shift -> toggle a persistent caps-lock (a plain tap is
// one-shot -- see the commit sites below); Backspace -> clear the whole
2026-07-17 20:28:11 +02:00
// field in one action instead of holding it down; a Latin-page letter cell
// with accented variants -> open the accent popup (see accent_active
// above). Every other special-row cell keeps hold-to-cancel; any other
// letter/symbol cell (a plain letter with no accents, or any T9/alt-
// alphabet/symbols cell) is a silent no-op instead, so it can't
// accidentally close the keyboard. Cursor mode itself moved off Hold-Enter
// entirely -- see the KEY_UP block below, where UP from row 0 now enters
// it instead.
if (c == KEY_CONTEXT_MENU) {
if (row == rows && col == 0) { // Shift
caps_lock = !caps_lock;
caps = caps_lock;
return NONE;
}
if (row == rows && col == 2) { // Backspace
len = 0; buf[0] = '\0';
cursor_pos = 0;
t9_cell = -1;
return NONE;
}
2026-07-17 20:28:11 +02:00
if (row < rows) {
if (!isT9() && !pageIsSymbols(page) && scriptAt(page) == NodePrefs::KB_ALPHABET_LATIN_ONLY) {
2026-07-17 20:28:11 +02:00
int gi = findAccentGroup(cellStr(row, col)[0]);
if (gi >= 0) { accent_active = true; accent_group = gi; accent_sel = 0; t9_cell = -1; return NONE; }
}
return NONE; // no variants for this cell, or T9/non-Latin/symbols page
}
return CANCELLED;
}
if (c == KEY_UP) {
if (row > 0) {
row--;
if (row == rows - 1) // leaving special row upward
col = col * cols / KB_SPECIAL;
} else {
2026-07-17 20:28:11 +02:00
// row 0: enter cursor mode instead of wrapping to the special row --
// row/col are deliberately left as-is so the cursor-mode UP/DOWN
// continuation above can still reach the special row proportionally.
cursor_mode = true;
t9_cell = -1;
return NONE;
}
t9_cell = -1; // navigating away finalizes any pending multi-tap cycle
return NONE;
}
if (c == KEY_DOWN) {
if (row < rows) {
row++;
if (row == rows) // entering special row
col = col * KB_SPECIAL / cols;
} else {
row = 0; // wrap down onto the first char row
col = col * cols / KB_SPECIAL;
}
t9_cell = -1;
return NONE;
}
if (c == KEY_LEFT) {
int max_col = (row == rows) ? KB_SPECIAL - 1 : cols - 1;
col = (col > 0) ? col - 1 : max_col;
t9_cell = -1;
return NONE;
}
if (c == KEY_RIGHT) {
int max_col = (row == rows) ? KB_SPECIAL - 1 : cols - 1;
col = (col < max_col) ? col + 1 : 0;
t9_cell = -1;
return NONE;
}
if (c == KEY_ENTER) {
if (row < rows && isT9()) {
int cell = row * cols + col;
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
const char* group = t9GroupStr(cell);
int glen = kbUtf8Len(group); // codepoint count, not byte length
int total = glen + 1; // + the cell's own digit, at the end of the cycle
bool cycling = (t9_cell == cell) && (millis() - t9_last_ms < KB_T9_TIMEOUT_MS);
if (cycling) {
t9_cycle = (t9_cycle + 1) % total;
if (cursor_pos > 0) {
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
char one[5];
if (t9_cycle < glen) kbUtf8CharAt(group, t9_cycle, one);
else { one[0] = (char)('1' + cell); one[1] = '\0'; }
char shown[5];
feat(admin): typed Radio/Routing field editors; pre-release audit fixes Tools > Admin: split the Radio tab's single "f,bw,sf,cr" comma-string field (free-text keyboard) into 4 independent, type-appropriate rows -- Frequency (same digit-cursor DigitEditor Settings/Repeater use locally), Bandwidth/Spreading factor/Coding rate (discrete-set LEFT/RIGHT stepping), plus TX power and the 3 Routing numeric fields as number steppers and Repeat as an ON/OFF toggle. Edits happen locally (zero mesh traffic per keystroke); only Enter sends one combined `set`, Cancel sends nothing. Full pre-release audit of all 16 commits since v1.22 (5 parallel focus areas: display/font, keyboard/messages, Nodes/Admin, Bot/channels, UITask+NodePrefs schema) turned up and fixed: - Admin: fetched FK_NUMBER values weren't clamped to the field's range, so a value already out-of-range could get stuck unreachable; fixed commit-time float format noise (%.6f -> %.3f); Cancel/failed-login always returned to the Nodes picker even when Admin was opened directly from a node's Hold-Enter action -- now returns to wherever it was actually opened from (AdminScreen::_from_picker). - Keyboard: one-shot Shift was consumed after the *first* T9 multi-tap, so cycling to the 2nd/3rd candidate always came out lowercase -- fixed by caching the cycle's caps state (t9_caps). - Messages: history is numbered newest-first, so a message arriving while scrolled up to an older one silently relabeled the view onto a different message -- selection now shifts with the insert. - Channels: onChannelRemoved() didn't clear ch_notif_override/ ch_notif_muted/ch_fav_bitmask despite its own contract comment requiring it (now far more reachable via the on-device Delete); an all-zero hex secret silently self-deleted the channel it was just saved into (collides with the empty-slot sentinel) -- rejected. - Room login: isRoomLoggedIn() indexed the login-tracking ring directly instead of via its head offset, silently wrong once the ring wraps (8+ rooms/session) -- the new on-device Logout depends on this being right. - Font/display: removed the now fully-inert Settings > Display > Font toggle and the dead LemonFont.h (retired by the earlier misc-fixed font unification, zero remaining includes); fixed a copy-pasted "5x7" comment (font is 6x9), two meaningless dead ternaries, and an OLED/e-ink inconsistency in the undefined-glyph fallback box offset. - AdminField's `kind`/bounds fields no longer rely on default member initializers inside aggregate-init: this toolchain's actual nRF52 build (unlike env:native) has no explicit -std= override, so it predates C++14's aggregate-with-default-member-initializer rule. Given an explicit constructor instead -- portable regardless of standard, all existing field-table literals unchanged. Docs updated to match: tools_screen.md (Diagnostics as a 3-tab carousel, was documented as one flat screen), message_screen.md (chat bubbles, newest-at-bottom, cursor mode, secret validation), settings_screen.md (dropped the dead Font row), solo_ui_framework.md (header menu_hint signatures, icon priority-drop, KeyboardWidget's T9/alphabets/cursor-mode). release-notes.md gains the v1.23 section covering all of the above plus the other 15 commits since v1.22. Build-verified on WioTrackerL1_companion_solo_dual. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 19:38:51 +02:00
kbApplyCapsUtf8(one, t9_caps, shown, sizeof(shown));
// Replace the codepoint just before the cursor (what the previous
// tap inserted), preserving anything after the cursor too.
int old_n = kbUtf8LastCharBytes(buf, cursor_pos);
int new_pos = cursor_pos - old_n;
int tail_len = len - cursor_pos;
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
int n = (int)strlen(shown);
if (new_pos + n + tail_len <= max_len) {
memmove(buf + new_pos + n, buf + cursor_pos, tail_len);
memcpy(buf + new_pos, shown, n);
len = new_pos + n + tail_len;
cursor_pos = new_pos + n;
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
buf[len] = '\0';
}
}
} else if (len < max_len) {
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
char one[5]; kbUtf8CharAt(group, 0, one);
char shown[5]; kbApplyCapsUtf8(one, caps, shown, sizeof(shown));
int n = (int)strlen(shown);
int tail_len = len - cursor_pos;
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
if (len + n <= max_len) {
memmove(buf + cursor_pos + n, buf + cursor_pos, tail_len);
memcpy(buf + cursor_pos, shown, n);
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
len += n;
cursor_pos += n;
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
buf[len] = '\0';
t9_cell = cell;
t9_cycle = 0;
feat(admin): typed Radio/Routing field editors; pre-release audit fixes Tools > Admin: split the Radio tab's single "f,bw,sf,cr" comma-string field (free-text keyboard) into 4 independent, type-appropriate rows -- Frequency (same digit-cursor DigitEditor Settings/Repeater use locally), Bandwidth/Spreading factor/Coding rate (discrete-set LEFT/RIGHT stepping), plus TX power and the 3 Routing numeric fields as number steppers and Repeat as an ON/OFF toggle. Edits happen locally (zero mesh traffic per keystroke); only Enter sends one combined `set`, Cancel sends nothing. Full pre-release audit of all 16 commits since v1.22 (5 parallel focus areas: display/font, keyboard/messages, Nodes/Admin, Bot/channels, UITask+NodePrefs schema) turned up and fixed: - Admin: fetched FK_NUMBER values weren't clamped to the field's range, so a value already out-of-range could get stuck unreachable; fixed commit-time float format noise (%.6f -> %.3f); Cancel/failed-login always returned to the Nodes picker even when Admin was opened directly from a node's Hold-Enter action -- now returns to wherever it was actually opened from (AdminScreen::_from_picker). - Keyboard: one-shot Shift was consumed after the *first* T9 multi-tap, so cycling to the 2nd/3rd candidate always came out lowercase -- fixed by caching the cycle's caps state (t9_caps). - Messages: history is numbered newest-first, so a message arriving while scrolled up to an older one silently relabeled the view onto a different message -- selection now shifts with the insert. - Channels: onChannelRemoved() didn't clear ch_notif_override/ ch_notif_muted/ch_fav_bitmask despite its own contract comment requiring it (now far more reachable via the on-device Delete); an all-zero hex secret silently self-deleted the channel it was just saved into (collides with the empty-slot sentinel) -- rejected. - Room login: isRoomLoggedIn() indexed the login-tracking ring directly instead of via its head offset, silently wrong once the ring wraps (8+ rooms/session) -- the new on-device Logout depends on this being right. - Font/display: removed the now fully-inert Settings > Display > Font toggle and the dead LemonFont.h (retired by the earlier misc-fixed font unification, zero remaining includes); fixed a copy-pasted "5x7" comment (font is 6x9), two meaningless dead ternaries, and an OLED/e-ink inconsistency in the undefined-glyph fallback box offset. - AdminField's `kind`/bounds fields no longer rely on default member initializers inside aggregate-init: this toolchain's actual nRF52 build (unlike env:native) has no explicit -std= override, so it predates C++14's aggregate-with-default-member-initializer rule. Given an explicit constructor instead -- portable regardless of standard, all existing field-table literals unchanged. Docs updated to match: tools_screen.md (Diagnostics as a 3-tab carousel, was documented as one flat screen), message_screen.md (chat bubbles, newest-at-bottom, cursor mode, secret validation), settings_screen.md (dropped the dead Font row), solo_ui_framework.md (header menu_hint signatures, icon priority-drop, KeyboardWidget's T9/alphabets/cursor-mode). release-notes.md gains the v1.23 section covering all of the above plus the other 15 commits since v1.22. Build-verified on WioTrackerL1_companion_solo_dual. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 19:38:51 +02:00
t9_caps = caps; // remember it for every later cycling tap on this cell
if (caps && !caps_lock) caps = false; // one-shot: only this first tap gets capitalised
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
}
}
t9_last_ms = millis();
} else if (row < rows) {
2026-07-17 20:28:11 +02:00
insertGlyph(cellStr(row, col), caps);
if (caps && !caps_lock) caps = false; // one-shot: revert after the letter it capitalised
} else {
t9_cell = -1; // any special-row action finalizes a pending multi-tap cycle
switch (col) {
// Tap toggles one-shot caps on/off; while caps_lock is held (Hold-Enter
// on this key, see handleInput's top), a tap cancels the lock instead.
case 0: if (caps_lock) { caps = false; caps_lock = false; } else { caps = !caps; } break;
case 1:
if (len < max_len) {
memmove(buf + cursor_pos + 1, buf + cursor_pos, len - cursor_pos);
buf[cursor_pos] = ' ';
len++; cursor_pos++;
buf[len] = '\0';
}
break;
case 2:
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';
}
break;
case 3:
if (_ph_refresh) _ph_refresh(*this, _ph_refresh_ctx); // contextual repopulate, if wired up
_ph_menu.begin(_ph_title, KB_PH_VISIBLE);
for (int i = 0; i < _ph_count; i++) _ph_menu.addItem(_ph_buf[i]);
break;
case 4:
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets Alarm repeat (Clock Tools): - NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends. computeAlarmNextFire() scans the next 7 days for a matching weekday when set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot behaviour, so existing users see no change. On-screen keyboard alphabets (Settings > Keyboard > Alphabet): - KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/ kbUtf8CharAt/kbUtf8LastCharBytes. - Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/ French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed. NodePrefs::keyboard_alt_alphabet picks which one, if any, is active. - Lemon font is forced on transiently inside KeyboardWidget::render() (saved and restored every call) whenever the alt-alphabet page is showing or already-typed text has non-ASCII bytes, so composing is visible even if Font is set to Default. - Each script's caps-shift rule is distinct and documented in kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for Latin Extended-A (verified against every character actually used, not a blanket rule for that whole Unicode block). NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask, keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D, sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile). Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the Rooms keyboard note (no longer ASCII-only once an alphabet is enabled). Not build-verified — no PlatformIO toolchain available this session. Caps mappings cross-checked against Python's Unicode case tables; UTF-8 literal bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
page = (page + 1) % totalPages(); // cycle letters -> [alt alphabet] -> symbols
break;
case 5:
return DONE;
}
}
}
return NONE;
}
};