#!/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 """ 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()