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

  1. Use only supported modifiers: ctrl, alt, shift, logo/win/super/cmd, altgr, capslock.
  2. Use standard xdotool-style key names for the final key (e.g. 'a', 'Return', 'F5', 'space').
  3. Check the alias table in waylandKey (control->ctrl, meta/cmd/super->logo) and map your app's names to those before calling.
  4. Trim/sanitize the combination string; stray '+' or whitespace yields an empty part.
  5. 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

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


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)