Fail CI when a key is bound but not in the gesture book, or listed but not bound

The gesture book is generated from GESTURE tags, so it could not describe a
gesture nobody tagged, but nothing made anyone tag one. The arrow keys, Enter,
P, X, U, Delete, F1 and F2 all worked in the grid with no line in the help
sheet, and a tag could name a key whose handler had gone.

Key handlers now compare one canonical string, Keys.chord(event) == "Ctrl+Z",
instead of reading event.text and the modifiers themselves. keys.slint folds
the key and its modifiers into that spelling, so the literal in the handler is
the whole binding and the checker reads exactly what the handler dispatches
on. Each handler carries a KEYMAP comment naming the gesture-book section its
keys belong to, and a tag's keys field names its keys between backticks.
gestures-check now fails when a handler binds a key no tag in that section
names, when a tag names a key no handler there binds, when any .slint file
other than keys.slint reads event.text, when a compared literal is not
canonical, and when keys.slint's named keys drift from the Rust list.

Spellings are normalised in one place, chord.rs: Ctrl+z, Control+Z and
LeftArrow all mean what the handler's "Ctrl+Z" and "Left" mean. Shift and Alt
count only for letters and named keys, because on the French layout every
digit needs shift and a 6 has to be a 6 however it was typed.

A Rust keymap that both dispatched and was read by the generator was the
alternative. It would have moved the handlers' decisions away from the Slint
state they depend on, and a window that forgot to install it would have had
no working keys at all.

The keys that were already bound and undocumented are now tagged.
This commit is contained in:
2026-09-24 23:42:25 -04:00
parent 150e53e878
commit 9d1e31ffbb
15 changed files with 1696 additions and 258 deletions
+38 -1
View File
@@ -11,7 +11,9 @@
//! ```
//!
//! The gesture half scans a different tag out of the same files — see
//! [`traceability::gestures`] for what it is and why it lives here.
//! [`traceability::gestures`] for what it is and why it lives here — and holds
//! the keys the Slint handlers bind to the keys those tags name, in both
//! directions ([`traceability::keymap`]).
//!
//! The gate fails hard on a *misconfigured run* — zero requirements parsed, or
//! zero source files scanned — rather than reporting a plausible-looking 0%.
@@ -142,6 +144,8 @@ fn run_gestures(base: &Path, check: bool) -> Result<()> {
let mut found = Vec::new();
let mut problems = Vec::new();
let mut bindings = Vec::new();
let mut keys_file_seen = false;
for file in &files {
let text = std::fs::read_to_string(file).unwrap_or_default();
let rel = file
@@ -159,6 +163,18 @@ fn run_gestures(base: &Path, check: bool) -> Result<()> {
let (g, p) = gestures::extract_from_text(&text, &rel);
found.extend(g);
problems.extend(p);
if rel.ends_with(".slint") {
let (b, p) = keymap::extract_bindings(&text, &rel);
bindings.extend(b);
problems.extend(p);
if rel.ends_with(&format!("/{}", keymap::KEYS_FILE)) {
keys_file_seen = true;
problems.extend(keymap::check_vocabulary(
&keymap::slint_named_keys(&text),
&rel,
));
}
}
}
// A `manual:` must name a section the manual has, checked against the
@@ -173,6 +189,7 @@ fn run_gestures(base: &Path, check: bool) -> Result<()> {
println!("files scanned {}", files.len());
println!("gestures found {}", found.len());
println!("places {}", gestures::by_section(&found).len());
println!("keys bound {}", bindings.len());
if !problems.is_empty() {
for p in &problems {
@@ -181,6 +198,26 @@ fn run_gestures(base: &Path, check: bool) -> Result<()> {
bail!("{} malformed gesture tag(s)", problems.len());
}
// The key half, after the tags parse: a key cannot be held to a tag that
// did not. A run that found no bindings, or no `keys.slint`, read the
// wrong tree — the application has keys, so zero is a misconfiguration.
if !keys_file_seen || bindings.is_empty() {
bail!(
"no {} or no key bindings found — misconfigured, not an application without keys",
keymap::KEYS_FILE
);
}
let key_problems = keymap::cross_check(&found, &bindings);
if !key_problems.is_empty() {
for p in &key_problems {
println!(" {p}");
}
bail!(
"{} key(s) bound and undocumented, or documented and unbound",
key_problems.len()
);
}
let doc = gestures::render_markdown(&found);
let table = gestures::render_rust(&found);