pmacs GPU: don't misroute AltGr/international text as command chords (F-004)

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
This commit is contained in:
Levi Neuwirth 2026-07-03 11:07:47 -04:00
parent 45735f4232
commit 1cc6d0bbcd
1 changed files with 53 additions and 1 deletions

View File

@ -791,10 +791,30 @@ impl ApplicationHandler<AppEvent> 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};