Hmbown/CodeWhale · error · ExecError
unknown key combination
Error message
unknown key combination "${text}" What it means
Thrown by waylandKey when the requested key combination string cannot be mapped to a ydotool/wtype gesture: either the final key fails xdotoolKey() translation (returns falsy) or a modifier is not in the known set {ctrl, alt, shift, logo, win, altgr, capslock}. The library validates the combination up front instead of letting the subprocess fail opaquely.
Solutions
- Use only supported modifiers: ctrl, alt, shift, logo/win/super/cmd, altgr, capslock.
- Use standard xdotool-style key names for the final key (e.g. 'a', 'Return', 'F5', 'space').
- Check the alias table in waylandKey (control->ctrl, meta/cmd/super->logo) and map your app's names to those before calling.
- Trim/sanitize the combination string; stray '+' or whitespace yields an empty part.
- If you need an unmapped modifier, extend the alias/modifiers sets in the backend or pre-emit the raw key instead.
Example fix
// before
await waylandKey('option+c');
// after
await waylandKey('alt+c'); Defensive patterns
Strategy: validation
Validate before calling
const MODIFIERS = new Set(['ctrl','alt','shift','logo','win','altgr','capslock']);
const ALIASES = { control: 'ctrl', meta: 'logo', cmd: 'logo', super: 'logo' };
function isValidCombo(text) {
const parts = String(text).split('+').map((p) => p.trim().toLowerCase());
if (parts.some((p) => !p)) return false;
const rawKey = parts.pop();
const mods = parts.map((p) => ALIASES[p] ?? p);
return mods.every((m) => MODIFIERS.has(m)) && /^[a-z0-9]+$|^f([1-9]|1[0-2])$|^return$|^space$|^tab$|^escape$/.test(rawKey);
} Type guard
const isKeyCombo = (v) => typeof v === 'string' && v.trim().length > 0 && isValidCombo(v);
Try / catch
if (!isKeyCombo(combo)) {
throw new Error(`unsupported combination: ${combo}`);
}
await waylandKey(combo); // or wrap in try/catch for ExecError fallback Prevention
- Normalize shortcut names through an alias map (cmd/meta/super -> logo, option -> alt) before calling.
- Reject empty or double-'+' strings at your boundary.
- Keep to xdotool-style key names for the final key.
- Validate modifiers against the backend's supported set, not your app's vocabulary.
- Unit-test your shortcut table against the backend's alias/modifier sets.
When it happens
Trigger: Calling a key-press action on Wayland with a combination string like 'foo+c', an unrecognized modifier ('command+c' is fine via alias cmd->logo, but 'hyper+c' is not), or an unmappable raw key (empty string, unknown symbol).
Common situations: Porting macOS-style shortcut names ('cmd', 'meta') that are aliased — but using names outside the alias table like 'hyper', 'option' (not aliased to alt here); passing an empty key string after splitting on '+'; locale-specific key names.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- empty key text
- multiple non-modifier keys in
- no non-modifier key in
- unknown key " " (supported: + modifiers…
- 1
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/c54a10cb47fd5fd1.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/src/backends/linux.mjs:185
function xdotoolKey(text) {
return String(text).split("+").map((p) => {
const k = p.trim().toLowerCase();
if (XKEYS[k]) return XKEYS[k];
if (/^f\d{1,2}$/.test(k)) return k.toUpperCase();
return p.trim(); // pass through names already in xdotool form
}).join("+");
}
async function waylandKey(text, { repeat = 1, holdMs = 0 } = {}) {
need("wtype", "key presses on Wayland");
const parts = String(text).split("+").map((part) => part.trim().toLowerCase());
const aliases = { control: "ctrl", meta: "logo", cmd: "logo", super: "logo" };
const modifiers = new Set(["ctrl", "alt", "shift", "logo", "win", "altgr", "capslock"]);
const rawKey = parts.pop();
const key = xdotoolKey(rawKey);
const mods = parts.map((part) => aliases[part] ?? part);
if (!key || mods.some((mod) => !modifiers.has(mod))) throw new ExecError(`unknown key combination "${text}"`);
const modKey = aliases[rawKey] ?? rawKey;
const onlyModifier = modifiers.has(modKey);
const args = mods.flatMap((mod) => ["-M", mod]);
for (let i = 0; i < repeat; i++) {
args.push(onlyModifier ? "-M" : "-P", onlyModifier ? modKey : key);
if (holdMs) args.push("-s", String(holdMs));
args.push(onlyModifier ? "-m" : "-p", onlyModifier ? modKey : key);
}
args.push(...mods.reverse().flatMap((mod) => ["-m", mod]));
// wtype owns a temporary Wayland keyboard; the compositor releases its
// keys on process exit, including cancellation. Keep the complete gesture
// in one process (https://github.com/atx/wtype#usage).
throwIfAborted();
const result = await run("wtype", args, { timeoutMs: Math.max(10_000, holdMs + 8_000) });
throwIfAborted();
if (result.code !== 0) throw new ExecError(`wtype exited ${result.code}: ${result.stderr.trim().slice(0, 200)}`, result);
}
View on GitHub (pinned to 73e0f67d83)