From afdfca6f9bcfa2325be17b275148b30d6287f35d Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Fri, 17 Jul 2026 20:50:41 +0200 Subject: [PATCH] feat(keyboard): selectable main/additional script for the on-screen keyboard Page 0 was hardcoded to Latin -- Cyrillic/Greek could only ever be the second, cycled-to page. Settings > Keyboard's Alphabet row splits into Main (which script the keyboard opens on by default) and Additional (the second one reached via #@/abc), so a Cyrillic/Greek typist can make their own script the default instead of always landing on Latin first. Setting Additional equal to Main collapses back to a 2-page cycle (that script + Symbols), same rule the old Latin-hardcoded design already used implicitly. KeyboardWidget.h: cellStr()/t9GroupStr() now dispatch through scriptCellStr()/scriptT9GroupStr(), treating Latin as an ordinary peer of Cyrillic/Greek instead of a special case; scriptHint() replaces altAlphabetHint() so the #@/abc key's "next page" hint is correct regardless of which script that lands on; the accent popup's gating checks the current page's actual script instead of assuming page 0 is always Latin. Removed the now-dead pageIsAltAlphabet(). NodePrefs gains keyboard_main_alphabet (schema sentinel 0xC0DE001F -> 0xC0DE0020, same append-at-tail/clamp-on-load pattern as every prior schema growth this file uses). Verified via a real build that the new field lands in existing tail padding -- sizeof(NodePrefs) is unchanged at 2712. Co-Authored-By: Claude Sonnet 5 --- docs/design/solo_ui_framework.md | 19 ++- .../settings_screen/settings_screen.md | 7 +- examples/companion_radio/DataStore.cpp | 7 + examples/companion_radio/NodePrefs.h | 14 +- .../companion_radio/ui-new/KeyboardWidget.h | 120 ++++++++++-------- .../companion_radio/ui-new/SettingsScreen.h | 15 ++- release-notes.md | 2 +- 7 files changed, 119 insertions(+), 65 deletions(-) diff --git a/docs/design/solo_ui_framework.md b/docs/design/solo_ui_framework.md index aca809ae..5b250297 100644 --- a/docs/design/solo_ui_framework.md +++ b/docs/design/solo_ui_framework.md @@ -175,13 +175,18 @@ the board's sensors actually provide. Expand them with `expandMsg()` at send tim Two layouts share every grid: **ABC** (one key per letter) and **T9** (phone-keypad multi-tap — repeated Enter within `KB_T9_TIMEOUT_MS` cycles a -cell's letter group, then its digit). `NodePrefs::keyboard_alt_alphabet` adds a -non-Latin script's own page to the cycle (Latin → alphabet → Symbols → Latin); -Cyrillic/Greek each define both an ABC grid (`KB_CYRILLIC_CHARS`/ -`KB_GREEK_CHARS`) and a T9 group table (`KB_T9_GROUPS_CYRILLIC`/`_GREEK`) so -the two layouts always offer the same letters. Latin-diacritic letters (Polish, -Czech, German, etc.) aren't alt-alphabet pages — they're reached by Hold-Enter -on a plain Latin cell instead (see `KB_ACCENT_VARIANTS` below). +cell's letter group, then its digit). Page 0 is no longer hardcoded to Latin: +`NodePrefs::keyboard_main_alphabet`/`keyboard_alt_alphabet` (Settings > +Keyboard's Main/Additional rows) each pick a script — Latin, Cyrillic, or +Greek — for page 0 and page 1 respectively (`KeyboardWidget::mainScript()`/ +`altScript()`); equal values collapse to a single script + Symbols (2 pages +instead of 3, see `hasAltAlphabet()`). `scriptCellStr()`/`scriptT9GroupStr()` +dispatch each script to its own ABC grid (`KB_CHARS`/`KB_CYRILLIC_CHARS`/ +`KB_GREEK_CHARS`) and T9 group table (`KB_T9_GROUPS`/`KB_T9_GROUPS_CYRILLIC`/ +`_GREEK`) so the two layouts always offer the same letters regardless of which +page they're on. Latin-diacritic letters (Polish, Czech, German, etc.) aren't +alt-alphabet pages — they're reached by Hold-Enter on whichever page currently +shows Latin instead (see `KB_ACCENT_VARIANTS` below). Shift is one-shot by default (capitalises the next letter, including whichever candidate a T9 multi-tap cycle settles on) or Hold-Enter to toggle caps-lock; Hold-Clear erases the whole field. **UP from the top letter row** enters diff --git a/docs/solo_features/settings_screen/settings_screen.md b/docs/solo_features/settings_screen/settings_screen.md index eddf8b54..dc862d3a 100644 --- a/docs/solo_features/settings_screen/settings_screen.md +++ b/docs/solo_features/settings_screen/settings_screen.md @@ -99,12 +99,13 @@ The **repeater** mode and its flood filters live on their own screen — see **T | Setting | Options | Notes | | -------- | ---------- | -------------------------------------------------------------------------------------------------- | -| Layout | ABC / T9 | On-screen keyboard style. **ABC**: an a-b-c…z grid, one key per letter (the original layout). **T9**: phone-keypad multi-tap — each key is labelled with its **digit** and a letter group (e.g. `2abc`); repeated **Enter** presses cycle through the letters and then the digit itself. Applies to whichever alphabet page is active (see Alphabet below), not just Latin. | -| Alphabet | Latin / Cyrillic / Greek | Which extra (non-Latin) script, if any, joins the keyboard's page cycle. **Latin** (default): only the Latin letters and Symbols pages, as before. **Cyrillic**/**Greek** add that script's own page to the same **#@/abc** key's cycle (Latin → alphabet → Symbols → Latin) — no separate key to switch scripts. **Greek** covers the 24-letter alphabet plus final sigma (`ς`) but not the tonos stress accents used in proper Modern Greek spelling. Every alphabet's letters render natively — the display font (a single unified Unicode font used everywhere on-screen) covers all of them, no separate toggle needed. | +| Layout | ABC / T9 | On-screen keyboard style. **ABC**: an a-b-c…z grid, one key per letter (the original layout). **T9**: phone-keypad multi-tap — each key is labelled with its **digit** and a letter group (e.g. `2abc`); repeated **Enter** presses cycle through the letters and then the digit itself. Applies to whichever script page is active (see Main/Additional below), not just Latin. | +| Main | Latin / Cyrillic / Greek | Which script the keyboard **opens on by default**. **Latin** (default) matches earlier releases; pick **Cyrillic** or **Greek** here instead to make that script the one you land on every time, with Latin becoming the one reached via cycling (see Additional below) instead of the other way round. | +| Additional | Latin / Cyrillic / Greek | The second script added to the same **#@/abc** key's cycle (Main → Additional → Symbols → Main) — no separate key to switch scripts. Setting Additional to the **same** script as Main drops the cycle back to just that script plus Symbols (no second script page at all). **Greek** covers the 24-letter alphabet plus final sigma (`ς`) but not the tonos stress accents used in proper Modern Greek spelling. Every script's letters render natively — the display font (a single unified Unicode font used everywhere on-screen) covers all of them, no separate toggle needed. | Applies to every on-screen text field (messages, waypoint labels, room passwords, preset names). Earlier releases labelled the grid *QWERTY*; the layout has always been alphabetical, so it is now named **ABC**. -European Latin-diacritic letters (Polish, Czech, Slovak, German, French, Spanish, Portuguese, Nordic, etc.) aren't separate alphabet pages — instead, **Hold Enter** on a plain Latin letter that has accented variants (`a c d e i l n o r s t u y z`) opens a one-row popup of its accents (e.g. holding `a` offers `á à â ã ä å ą`); **LEFT/RIGHT** picks, **Enter** inserts it, **Cancel** dismisses with no change. Holding a letter with no accented variants (e.g. `b`) does nothing. +European Latin-diacritic letters (Polish, Czech, Slovak, German, French, Spanish, Portuguese, Nordic, etc.) aren't separate alphabet pages — instead, **Hold Enter** on a plain Latin letter that has accented variants (`a c d e i l n o r s t u y z`) opens a one-row popup of its accents (e.g. holding `a` offers `á à â ã ä å ą`); **LEFT/RIGHT** picks, **Enter** inserts it, **Cancel** dismisses with no change. Holding a letter with no accented variants (e.g. `b`) does nothing. Works on whichever page is currently showing Latin, whether that's Main or Additional. --- diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 72960aa7..c0958ff9 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -520,6 +520,12 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no rd(&_prefs.bot_commands_room, sizeof(_prefs.bot_commands_room)); if (_prefs.bot_commands_room > 1) _prefs.bot_commands_room = 0; + // → 0xC0DE0020: append keyboard_main_alphabet at the tail. A pre-0x20 file + // has the old sentinel bytes / EOF here; clamp anything out of range to 0 + // (Latin), matching the keyboard's original always-Latin-main behaviour. + rd(&_prefs.keyboard_main_alphabet, sizeof(_prefs.keyboard_main_alphabet)); + if (_prefs.keyboard_main_alphabet >= NodePrefs::KB_ALPHABET_COUNT) _prefs.keyboard_main_alphabet = 0; + // Schema sentinel: bumped on layout changes. Mismatch means an older file // (or a different schema); rd() already zero-inits any fields not present, // so we just log it — next savePrefs writes the current sentinel. @@ -723,6 +729,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)_prefs.bot_reply_room, sizeof(_prefs.bot_reply_room)); file.write((uint8_t *)&_prefs.bot_commands_ch, sizeof(_prefs.bot_commands_ch)); file.write((uint8_t *)&_prefs.bot_commands_room, sizeof(_prefs.bot_commands_room)); + file.write((uint8_t *)&_prefs.keyboard_main_alphabet, sizeof(_prefs.keyboard_main_alphabet)); // Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL. Its write is // the one we check: once the flash fills, writes return 0, so a good diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 15412f65..8ee93e30 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -390,6 +390,15 @@ struct NodePrefs { // persisted to file uint8_t bot_commands_ch; uint8_t bot_commands_room; + // Which script (KB_ALPHABET_LATIN_ONLY/CYRILLIC/GREEK) occupies the on-screen + // keyboard's page 0 -- its default/opening page -- vs. keyboard_alt_alphabet + // above, which occupies page 1. Settings > Keyboard's Main/Additional rows. + // Equal to keyboard_alt_alphabet means no second page (see KeyboardWidget's + // hasAltAlphabet()). Appended at the tail (see the serialization tripwire + // below); default 0 (Latin) matches the keyboard's original always-Latin- + // main behaviour for upgraders. + uint8_t keyboard_main_alphabet; + // Single source of truth for the live-share option tables (shared by the Map // UI labels and the auto-send engine in UITask). static const uint8_t LOC_SHARE_MOVE_COUNT = 4; @@ -452,7 +461,7 @@ struct NodePrefs { // persisted to file // adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so // older saves are detected on load and skipped (zero-init defaults kept). // High 24 bits identify the file format; low byte is the schema revision. - static const uint32_t SCHEMA_SENTINEL = 0xC0DE001F; + static const uint32_t SCHEMA_SENTINEL = 0xC0DE0020; // Bit-index for each home page. Used by page_order (entries store bit+1) and // by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage @@ -549,6 +558,9 @@ struct NodePrefs { // persisted to file // 3. clamp it on load (an upgrader's file lacks it → stray bytes) // 4. bump SCHEMA_SENTINEL's low byte // (Padding can also shift sizeof; a "false" trip just means re-check + rebump.) +// keyboard_main_alphabet (added alongside this bump) landed in existing tail +// padding -- confirmed via a real build's sizeof() -- so the size is +// unchanged from before that field existed. static_assert(sizeof(NodePrefs) == 2712, "NodePrefs layout changed — sync DataStore save/load + clamp, bump " "SCHEMA_SENTINEL, then update this size (see steps above)."); diff --git a/examples/companion_radio/ui-new/KeyboardWidget.h b/examples/companion_radio/ui-new/KeyboardWidget.h index 56f5631d..3cded1e1 100644 --- a/examples/companion_radio/ui-new/KeyboardWidget.h +++ b/examples/companion_radio/ui-new/KeyboardWidget.h @@ -43,11 +43,14 @@ static const char* const KB_T9_GROUPS[KB_PAGES][9] = { { "@#&", "*()", "-_+", "=/\\", ":;'\"", "<>[]", "{}|~", "^$%`", ",." }, // page 1 — symbols }; -// Additional (non-Latin) keyboard alphabets — NodePrefs::keyboard_alt_alphabet -// picks which one (if any) joins the Latin/symbols page cycle. 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. +// 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. // // 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`. @@ -271,7 +274,7 @@ struct KeyboardWidget { 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()/pageIsAltAlphabet()/pageIsSymbols() below + 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 @@ -289,12 +292,17 @@ struct KeyboardWidget { // 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 hasAltAlphabet() defaults to Latin-only. + // to ABC and mainScript()/altScript() default to Latin-only. NodePrefs* prefs = nullptr; bool isT9() const { return prefs && prefs->keyboard_type == 1; } - bool hasAltAlphabet() const { - return prefs && prefs->keyboard_alt_alphabet != NodePrefs::KB_ALPHABET_LATIN_ONLY; - } + // 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). @@ -312,54 +320,64 @@ struct KeyboardWidget { int gridCols() const { return isT9() ? KB_T9_COLS : KB_COLS_CHAR; } // ── Page model ──────────────────────────────────────────────────────────── - // Logical page order: 0 = Latin letters, [1 = the enabled alt alphabet], - // last = symbols. Without an alt alphabet this is exactly the original - // 2-page Latin/symbols cycle; enabling one inserts its page in the middle, - // so the #@/abc key's existing cycle (case 4 below) reaches it for free. + // 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. int totalPages() const { return hasAltAlphabet() ? 3 : 2; } - bool pageIsAltAlphabet(int pg) const { return hasAltAlphabet() && pg == 1; } 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(); } - // ABC-grid cell content as a NUL-terminated UTF-8 string (1 codepoint). - // Latin/symbols cells come back through a small scratch buffer since - // KB_CHARS stores single ASCII bytes, not strings; alt-alphabet cells are + // 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 // 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; + } + } + } const char* cellStr(int r, int c) const { - if (pageIsAltAlphabet(page)) { - switch (prefs->keyboard_alt_alphabet) { - case NodePrefs::KB_ALPHABET_CYRILLIC: return KB_CYRILLIC_CHARS[r][c]; - case NodePrefs::KB_ALPHABET_GREEK: return KB_GREEK_CHARS[r][c]; - default: return "?"; - } + if (pageIsSymbols(page)) { + static char single[2]; + single[0] = KB_CHARS[1][r][c]; + single[1] = '\0'; + return single; } - static char single[2]; - single[0] = KB_CHARS[pageIsSymbols(page) ? 1 : 0][r][c]; - single[1] = '\0'; - return single; + return scriptCellStr(scriptAt(page), r, c); } - // T9 group string (UTF-8) for the given cell (0-8), page-aware like cellStr. + // 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]; + } + } const char* t9GroupStr(int cell) const { - if (pageIsAltAlphabet(page)) { - switch (prefs->keyboard_alt_alphabet) { - case NodePrefs::KB_ALPHABET_CYRILLIC: return KB_T9_GROUPS_CYRILLIC[cell]; - case NodePrefs::KB_ALPHABET_GREEK: return KB_T9_GROUPS_GREEK[cell]; - default: return "?"; - } - } - return KB_T9_GROUPS[pageIsSymbols(page) ? 1 : 0][cell]; + if (pageIsSymbols(page)) return KB_T9_GROUPS[1][cell]; + return scriptT9GroupStr(scriptAt(page), cell); } - // Compact ASCII hint for the #@/abc key when it's about to switch to the - // alt-alphabet page. Deliberately ASCII (not the alphabet's own script) so - // it renders correctly even when Lemon isn't the user's current font choice - // — unlike the alphabet's own grid, this hint is visible from the Latin/ - // symbols pages too, where render() doesn't force Lemon on (see render()). - static const char* altAlphabetHint(uint8_t alt) { - switch (alt) { - case NodePrefs::KB_ALPHABET_CYRILLIC: return "CY"; - case NodePrefs::KB_ALPHABET_GREEK: return "GR"; - default: return "?"; + // 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"; } } @@ -555,9 +573,7 @@ struct KeyboardWidget { lbl = "{}"; } else { int next = (page + 1) % totalPages(); - lbl = pageIsSymbols(next) ? "#@" - : pageIsAltAlphabet(next) ? altAlphabetHint(prefs->keyboard_alt_alphabet) - : "abc"; + lbl = pageIsSymbols(next) ? "#@" : scriptHint(scriptAt(next)); } int tw = display.getTextWidth(lbl); display.setCursor(sx + (spec_w - tw) / 2, spec_y); @@ -722,11 +738,11 @@ struct KeyboardWidget { return NONE; } if (row < rows) { - if (!isT9() && page == 0) { + if (!isT9() && !pageIsSymbols(page) && scriptAt(page) == NodePrefs::KB_ALPHABET_LATIN_ONLY) { 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/alt-alphabet/symbols page + return NONE; // no variants for this cell, or T9/non-Latin/symbols page } return CANCELLED; } diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index ad739233..0e63f3f7 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -71,6 +71,7 @@ class SettingsScreen : public UIScreen { // Keyboard section SECTION_KEYBOARD, KEYBOARD_TYPE, + KEYBOARD_MAIN_ALPHABET, KEYBOARD_ALPHABET, // Contacts section SECTION_CONTACTS, DM_FILTER, CH_FILTER, ROOM_FILTER, @@ -565,8 +566,12 @@ class SettingsScreen : public UIScreen { display.print("Type"); display.setCursor(valCol(display), y); display.print((p && p->keyboard_type) ? "T9" : "ABC"); + } else if (item == KEYBOARD_MAIN_ALPHABET) { + display.print("Main"); + display.setCursor(valCol(display), y); + display.print(NodePrefs::keyboardAlphabetLabel(p ? p->keyboard_main_alphabet : 0)); } else if (item == KEYBOARD_ALPHABET) { - display.print("Alphabet"); + display.print("Additional"); display.setCursor(valCol(display), y); display.print(NodePrefs::keyboardAlphabetLabel(p ? p->keyboard_alt_alphabet : 0)); } else if (item == BATT_DISPLAY) { @@ -924,6 +929,14 @@ public: _dirty = true; return true; } + if (_selected == KEYBOARD_MAIN_ALPHABET && p && (left || right || enter)) { + int idx = p->keyboard_main_alphabet; + if (right || enter) idx = (idx + 1) % NodePrefs::KB_ALPHABET_COUNT; + else if (left) idx = (idx + NodePrefs::KB_ALPHABET_COUNT - 1) % NodePrefs::KB_ALPHABET_COUNT; + p->keyboard_main_alphabet = (uint8_t)idx; + _dirty = true; + return true; + } if (_selected == KEYBOARD_ALPHABET && p && (left || right || enter)) { int idx = p->keyboard_alt_alphabet; if (right || enter) idx = (idx + 1) % NodePrefs::KB_ALPHABET_COUNT; diff --git a/release-notes.md b/release-notes.md index 1f7f0871..5d5d3257 100644 --- a/release-notes.md +++ b/release-notes.md @@ -6,7 +6,7 @@ - **On-device channel management.** Messages › Channels gets a **"+ Add channel"** row and per-channel **Edit**/**Delete** — create or join a channel by typing either a passphrase (hashed to the channel secret, same idea as a room password) or the exact 32-hex-character key from a channel QR code. No phone app required. - **Tools › Admin — administer a remote repeater/room from the device.** Log into a node you have admin rights on (same self-healing saved-password handshake as room logins) and browse its settings in category tabs — **System** (name, owner info), **Radio** (Frequency/Bandwidth/Spreading factor/Coding rate/TX power), **Routing** (Repeat/Advert interval/Flood advert interval/Max hops), **Actions** (reboot, send advert, sync clock, …) — plus a free-text **Custom command…** escape hatch (with **{}**-key command-name completion) for anything not covered. Radio and Routing fields use a type-appropriate editor instead of raw text — a digit-cursor for Frequency (the same widget Settings' own Radio screen uses locally), discrete-set stepping for Bandwidth/Spreading factor/Coding rate, plain number steppers for the rest, and an ON/OFF toggle for Repeat — adjusting a value costs no mesh traffic until you actually confirm it with **Enter** (**Cancel** sends nothing). Reachable either from **Tools › Admin** (opens the same Nodes list to pick a target) or directly via a repeater/room's own **Hold Enter › Admin** action. - **Auto-Reply Bot gains a third target: room servers**, alongside DM and Channel, with its own trigger/reply/commands — the screen is redesigned as a circular tab carousel (**Direct/Channel/Room/Other**, **LEFT/RIGHT** to switch, **UP/DOWN** within a tab) instead of one long list. Each target's **Enable** and **Commands** toggle is now fully independent (they used to secretly share the DM tab's setting for Channel/Room too). New `{name}`/`{hops}` reply placeholders, a DM allow-list (all chat contacts or favourites only), and a dedicated stepper for Quiet Hours. -- **Keyboard: accent popup, better editing.** Holding **Enter** on a Latin letter with accented variants (`a c d e i l n o r s t u y z`) now opens a one-row popup of that letter's accents — e.g. holding `a` offers `á à â ã ä å ą` — covering every European Latin-diacritic letter (Polish, Czech, Slovak, German, French, Spanish, Portuguese, Nordic, …) from one popup instead of a separate page per language; **Cyrillic**/**Greek** remain full alt-alphabet pages under **Settings › Keyboard › Alphabet**. Shift is now **one-shot** by default (capitalises just the next letter; **Hold Enter** on Shift toggles the old sticky caps-lock back on), **Hold Enter** on Backspace clears the whole field, and **UP** from the top letter row enters a cursor-positioning mode (**LEFT/RIGHT** move; **UP/DOWN** jump to start/end, then continue on to the special row / letter grid if pressed again once already there) so you can edit or insert anywhere in what you've typed, not just at the end. +- **Keyboard: accent popup, selectable main script, better editing.** Holding **Enter** on a Latin letter with accented variants (`a c d e i l n o r s t u y z`) now opens a one-row popup of that letter's accents — e.g. holding `a` offers `á à â ã ä å ą` — covering every European Latin-diacritic letter (Polish, Czech, Slovak, German, French, Spanish, Portuguese, Nordic, …) from one popup instead of a separate page per language. **Settings › Keyboard** splits **Alphabet** into **Main** and **Additional**: Main picks which script (Latin/Cyrillic/Greek) the keyboard opens on by default, Additional picks the second one reached via the **#@/abc** cycle key — so a Cyrillic or Greek typist can make their own script the default instead of always landing on Latin first. Shift is now **one-shot** by default (capitalises just the next letter; **Hold Enter** on Shift toggles the old sticky caps-lock back on), **Hold Enter** on Backspace clears the whole field, and **UP** from the top letter row enters a cursor-positioning mode (**LEFT/RIGHT** move; **UP/DOWN** jump to start/end, then continue on to the special row / letter grid if pressed again once already there) so you can edit or insert anywhere in what you've typed, not just at the end. - **Messages get a messenger-style history.** History bubbles are now sized to their content and anchored **right** (outgoing) / **left** (incoming); the list runs **newest at the bottom**, growing upward as you scroll into the past; the **[+ send]** compose button moved to the right edge to match. - **Clock Tools: alarm repeat.** The Alarm screen gains a **Repeat** row (Off/Daily/Weekdays/Weekends) — Hour/Minute merge into one **Time** row edited with an HH:MM digit cursor, and **Repeat**/**Armed** now respond to **LEFT/RIGHT** like every other Settings field instead of Enter-only. - **Diagnostics becomes a tab carousel** (Live/System/Font): **System** adds firmware version + build date, device model and the active radio parameters; **Font** is a rendering test card with one sample line per script the on-device font covers, so coverage can be eyeballed directly.