From 1cc6d0bbcd561bf51b24eeb9b553bb410d93b2ce Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Fri, 3 Jul 2026 11:07:47 -0400 Subject: [PATCH] pmacs GPU: don't misroute AltGr/international text as command chords (F-004) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit F-004. The generalized command-chord forwarding (Char/Enter/Tab with Ctrl/Alt -> daemon keymap) misclassifies AltGr-produced characters: on Windows and some layouts AltGr is reported as Ctrl+Alt, so typing `@ [ ] { } \ | €` etc. would be routed to the keymap instead of inserted. Fix: use winit's `KeyEvent.text` (the text a keypress produces). When a keypress yields printable text while a command modifier is held, it's layout text input (AltGr), not a command chord — strip the Ctrl/Alt (keeping Shift) so it inserts via the plain-text path (or the daemon's SelfInsert while a prompt is open). Genuine command chords produce no text (or a control char) and still route to the keymap; plain text has no command modifier and is unaffected. `is_layout_text` gates it, with unit coverage (AltGr text vs C-a vs Ctrl+A control char vs plain/Shift). Platform note: on layouts/platforms where AltGr isn't Ctrl+Alt (typical X11/Wayland), the chord was already plain, so this is a no-op there; the fix primarily protects Windows and non-US layouts. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U --- pmacs-gpu/src/main.rs | 54 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/pmacs-gpu/src/main.rs b/pmacs-gpu/src/main.rs index 4770d79..d5716c6 100644 --- a/pmacs-gpu/src/main.rs +++ b/pmacs-gpu/src/main.rs @@ -791,10 +791,30 @@ impl ApplicationHandler for App { return; } - let Some((pkey, pmods)) = translate_key(&key.logical_key, self.modifiers) else { + let Some((pkey, mut pmods)) = translate_key(&key.logical_key, self.modifiers) + else { return; }; + // AltGr / international text (audit F-004). winit reports + // the text a keypress produces; when a keypress yields + // printable text *while* Ctrl/Alt is held — AltGr is + // Ctrl+Alt on Windows and some layouts — it's text input, + // not a command chord. Strip the Ctrl/Alt (keep Shift) so + // it inserts (through the plain-text path, or the daemon's + // SelfInsert while a prompt is open) instead of being + // routed to the keymap. On platforms where AltGr isn't + // Ctrl/Alt this is a no-op (the chord was already plain). + if matches!(pkey, ProtocolKey::Char(_)) + && is_layout_text(key.text.as_deref(), pmods) + { + pmods = if pmods.contains(Modifiers::SHIFT) { + Modifiers::SHIFT + } else { + Modifiers::NONE + }; + } + // Ctrl-V — OS paste (Q#CM6). Read the system clipboard // locally via arboard and ship it as a `Paste` event; the // daemon inserts it. Handled before binding `client` so @@ -4782,6 +4802,19 @@ fn is_command_chord(key: ProtocolKey, mods: Modifiers) -> bool { ) && (mods.contains(Modifiers::CTRL) || mods.contains(Modifiers::ALT)) } +/// Whether a keypress is **layout text input carried under a command +/// modifier** — the `AltGr` case (audit F-004): winit produced printable +/// `text` while `Ctrl`/`Alt` is held. On layouts/platforms where `AltGr` +/// is `Ctrl+Alt` (Windows), the produced character would otherwise be +/// misclassified as a command chord; when this is true the caller strips +/// the command modifiers so it inserts. Returns `false` for genuine +/// command chords (which produce no text, or a control char) and for +/// plain text (no command modifier — already handled). +fn is_layout_text(text: Option<&str>, mods: Modifiers) -> bool { + !is_plain_text_modifiers(mods) + && text.is_some_and(|t| !t.is_empty() && t.chars().all(|c| !c.is_control())) +} + fn is_plain_text_modifiers(mods: Modifiers) -> bool { !mods.contains(Modifiers::CTRL) && !mods.contains(Modifiers::ALT) @@ -5855,6 +5888,25 @@ mod tests { assert!(!is_command_chord(ProtocolKey::Left, Modifiers::CTRL)); } + #[test] + fn is_layout_text_distinguishes_altgr_from_command_chords() { + // Audit F-004 — AltGr (Ctrl+Alt on Windows) produces printable + // text; that's text input, so the caller strips the modifiers. + let ctrl_alt = Modifiers::CTRL | Modifiers::ALT; + assert!(is_layout_text(Some("@"), ctrl_alt)); + assert!(is_layout_text(Some("€"), Modifiers::ALT)); + assert!(is_layout_text(Some("{"), ctrl_alt)); + // Genuine command chords produce no text (or a control char) — + // not layout text, so they still route to the keymap. + assert!(!is_layout_text(None, Modifiers::CTRL)); // C-a etc. + assert!(!is_layout_text(Some("\u{1}"), Modifiers::CTRL)); // Ctrl+A control char + assert!(!is_layout_text(Some(""), ctrl_alt)); // no text + // Plain text has no command modifier, so it's already handled and + // needs no stripping. + assert!(!is_layout_text(Some("a"), Modifiers::NONE)); + assert!(!is_layout_text(Some("A"), Modifiers::SHIFT)); + } + #[test] fn translate_key_carries_modifiers() { use winit::keyboard::{Key as WKey, ModifiersState, NamedKey};