The alphabet by ear - Says each letter by its name (keyboard/letter_names.json), never the bare key a voice reads as a word, and the app's own words in the language. The names are data with a review: drafted, checked by an independent back-reading and a register check, disputes decided and checked again (rounds 2-3), then a voice check with espeak-ng (tools/keyboard/check_letter_names.py). 43 of 45 languages agreed; Amharic (ኸ) and Tagalog (z, v, e) are not, and there it says it is not ready yet. The readers were AI models, not native speakers, and the table says so. - Its own speech path (tts::say_cue): the device voice of the language, no text_for_speech, and never over or cancelling the person's words; after Speak the next cue waits until the message has been said. It does not start without a voice that speaks the language, and says why. - Offers a letter's marks, a kana's forms and an Ethiopic consonant's orders after it; reads a lone suggestion as the word; Stop is in the last group; a key reaches it from every keyboard. Each choice records whether a tap relayed it; "only my own switch or look" is a setting; a message spelled with relayed choices is learned only when the person keeps it (Item::Learn) with their own yes. A panel open takes the switch first. Keyboards - National layouts from published standards, both layers, read from xkeyboard-config (tools/keyboard/build_layouts.py): InScript (IS 13194) for the nine Indic languages, Kedmanee (TIS 820-2538) for Thai, CRULP phonetic for Urdu; Shift's layer is a page at the same positions. - Defaults follow the access method (kbd::suggested): most used letters first for a switch, colour groups (or alphabetical, large keys) for gaze; a key size setting. A Board key leaves text mode. Amharic syllable pages have Back. Malayalam signs shown bare (WebKit drew two circles). - Pinyin: each candidate covers its own letters, ranked by the likeliest split (不 + 舒服, not 部署 + 副); ▸▸ pages through them; capitals read as the letters; no conversion during a device IME's composition; a failed download is said once and retried with back-off; the note says exactly what it does. Access and fit - With the on-screen keys, the box never takes a switch's Space or Enter, and a chosen suggestion no longer moves the focus into it. - A look must leave a key before it can choose again. - Keys never shrink below 24 x 32 px: on a phone on its side the box goes beside the keys, and the keyboard scrolls with the scan's highlight kept in view; the colour frame's text keeps its end in view; the notes fold to a line; the colour marks sit on badges with 4.5:1 ink; toggles show in dark and high-contrast; dir=auto on the box and what by-ear shows. - Phrases page: Add to bar and Recent. Unreadable keyboard settings are not overwritten at start. First letters keeps where it reads from when text mode comes back. Credits: Settings -> Keyboard lists every word list's source and licence; the pinyin licence file carries pypinyin's, pinyin-data's, phrase-pinyin-data's and the Unicode notices in full (the generator pins pypinyin 0.55.0); the compose table names the Unicode data it comes from. SPEC §12 and spec/keyboard.md updated. Co-Authored-By: Claude Opus 5.5 <[email protected]>
128 lines
4.8 KiB
Python
128 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Write aac-core's keyboard/standard_layouts.rs: the national keyboards that
|
|
follow a published standard, read from xkeyboard-config's symbol files (the
|
|
X.Org keyboard database, MIT licence), with both layers — the keys as typed,
|
|
and with Shift:
|
|
|
|
InScript (IS 13194:1991, the Indian standard) for Hindi, Marathi, Bengali,
|
|
Punjabi, Gujarati, Tamil, Telugu, Kannada and Malayalam;
|
|
Kedmanee (TIS 820-2538) for Thai;
|
|
CRULP's phonetic layout (v1.1) for Urdu.
|
|
|
|
Rows are the keyboard's four rows (the number row, then Q, A and Z); the key
|
|
right of the A row's last (\\) is put at that row's end, so no row is more than
|
|
twelve keys. A row that has no letter in a layer is left out there.
|
|
|
|
usage: python3 tools/keyboard/build_layouts.py <xkeyboard-config/symbols> <xorgproto keysymdef.h>
|
|
"""
|
|
import re
|
|
import sys
|
|
import unicodedata
|
|
from pathlib import Path
|
|
|
|
OUT = Path(__file__).resolve().parents[2] / "crates/aac-core/src/keyboard/standard_layouts.rs"
|
|
# code, xkb file, xkb variant, the standard's name
|
|
LAYOUTS = [
|
|
("hi", "in", "deva", "InScript"),
|
|
("mr", "in", "marathi", "InScript"),
|
|
("bn", "in", "ben_inscript", "InScript"),
|
|
("pa", "in", "guru", "InScript"),
|
|
("gu", "in", "guj", "InScript"),
|
|
("ta", "in", "tam", "InScript"),
|
|
("te", "in", "tel", "InScript"),
|
|
("kn", "in", "kan", "InScript"),
|
|
("ml", "in", "mal", "InScript"),
|
|
("th", "th", "basic", "Kedmanee"),
|
|
("ur", "pk", "urd-crulp", "CRULP phonetic"),
|
|
]
|
|
ROWS = [
|
|
[f"AE{i:02d}" for i in range(1, 13)],
|
|
[f"AD{i:02d}" for i in range(1, 13)],
|
|
[f"AC{i:02d}" for i in range(1, 12)] + ["BKSL"],
|
|
[f"AB{i:02d}" for i in range(1, 11)],
|
|
]
|
|
|
|
|
|
def keysyms(path):
|
|
names = {}
|
|
for line in Path(path).read_text(encoding="latin-1").splitlines():
|
|
m = re.match(r"#define XK_(\w+)\s+0x([0-9a-fA-F]+)\s*/\*[ (]*U\+([0-9A-F]{4,6})", line)
|
|
if m:
|
|
names[m.group(1)] = chr(int(m.group(3), 16))
|
|
return names
|
|
|
|
|
|
def block(text, variant):
|
|
m = re.search(r'xkb_symbols\s+"' + re.escape(variant) + r'"\s*\{(.*?)\n\};', text, re.S)
|
|
if not m:
|
|
sys.exit(f"no variant {variant}")
|
|
return m.group(1)
|
|
|
|
|
|
def char_of(sym, names):
|
|
sym = sym.strip()
|
|
if re.fullmatch(r"U[0-9A-Fa-f]{4,6}", sym):
|
|
return chr(int(sym[1:], 16))
|
|
if re.fullmatch(r"0x1[0-9A-Fa-f]{6}", sym):
|
|
return chr(int(sym, 16) - 0x1000000)
|
|
if len(sym) == 1:
|
|
return sym
|
|
if sym in ("NoSymbol", "VoidSymbol"):
|
|
return ""
|
|
if sym in names:
|
|
return names[sym]
|
|
sys.exit(f"unknown keysym {sym}")
|
|
|
|
|
|
def layers(symbols_dir, names, file, variant):
|
|
text = (Path(symbols_dir) / file).read_text(encoding="utf-8")
|
|
body = block(text, variant)
|
|
keys = {}
|
|
for m in re.finditer(r"key\s+<(\w+)>\s*\{[^\[]*\[([^\]]*)\]", body):
|
|
syms = [s for s in m.group(2).split(",")]
|
|
keys[m.group(1)] = [char_of(s, names) for s in syms[:2]] + [""] * (2 - len(syms[:2]))
|
|
if not keys:
|
|
sys.exit(f"{file}({variant}): no keys (an include-only variant?)")
|
|
# Keys type NFC (a nukta letter Unicode excludes from composition, य़,
|
|
# is its letter and the nukta).
|
|
nfc = lambda c: unicodedata.normalize("NFC", c)
|
|
return [[[nfc(keys.get(k, ["", ""])[level]) for k in row] for row in ROWS] for level in (0, 1)]
|
|
|
|
|
|
def rust_str(s):
|
|
return '"' + "".join(c if c.isprintable() and c not in '"\\' else f"\\u{{{ord(c):04X}}}" for c in s) + '"'
|
|
|
|
|
|
def main():
|
|
symbols_dir, keysymdef = sys.argv[1], sys.argv[2]
|
|
names = keysyms(keysymdef)
|
|
out = [
|
|
"//! Generated by `tools/keyboard/build_layouts.py`; do not edit by hand.",
|
|
"//!",
|
|
"//! National keyboards that follow a published standard, both layers (as",
|
|
"//! typed, and with Shift), row by row: InScript (IS 13194:1991) for the",
|
|
"//! Indic scripts, Kedmanee (TIS 820-2538) for Thai, CRULP's phonetic layout",
|
|
"//! for Urdu. Read from xkeyboard-config (the X.Org keyboard database,",
|
|
"//! https://gitlab.freedesktop.org/xkeyboard-config/xkeyboard-config, MIT",
|
|
"//! licence). An empty string is a key with nothing in that layer.",
|
|
"",
|
|
"/// `(language, standard, [layer as typed, layer with Shift])`.",
|
|
"#[allow(clippy::type_complexity)]",
|
|
"pub(super) const STANDARD_LAYOUTS: &[(&str, &str, [&[&[&str]]; 2])] = &[",
|
|
]
|
|
for code, file, variant, standard in LAYOUTS:
|
|
ls = layers(symbols_dir, names, file, variant)
|
|
out.append(f" (\"{code}\", \"{standard}\", [")
|
|
for layer in ls:
|
|
out.append(" &[")
|
|
for row in layer:
|
|
out.append(" &[" + ", ".join(rust_str(c) for c in row) + "],")
|
|
out.append(" ],")
|
|
out.append(" ]),")
|
|
out.append("];")
|
|
OUT.write_text("\n".join(out) + "\n", encoding="utf-8")
|
|
print(f"{len(LAYOUTS)} layouts -> {OUT}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|