Hmbown/CodeWhale · error · ExecError
wtype exited
Error message
wtype exited ${result.code}: ${result.stderr.trim().slice(0, 200)} What it means
Thrown by the Wayland key path when the wtype subprocess exits non-zero. wtype synthesizes a temporary virtual Wayland keyboard and the code keeps the whole gesture (press, hold, release) in one process; the error surfaces wtype's own failure with its stderr (first 200 chars). Abort is checked before and after the run, so this is purely a wtype-level failure.
Solutions
- Read wtype's stderr in the message; 'compositor does not support' means you must switch input backends (ydotool + ydotoold).
- Keep holdMs modest or raise the timeout path; the runtime is max(10000, holdMs+8000) ms.
- Test wtype directly: `wtype -P ctrl -p ctrl` to confirm compositor support.
- On GNOME, install ydotool and start ydotoold instead of relying on wtype.
- Ensure the session probe identified Wayland correctly; under XWayland-only apps wtype gestures may not land.
Example fix
// before
await waylandKey('ctrl+c', { holdMs: 60000 }); // exceeds timeout
// after
await waylandKey('ctrl+c', { holdMs: 500 }); Defensive patterns
Strategy: try-catch
Validate before calling
async function wtypeWorks() {
try { await run('wtype', ['-P', 'shift', '-p', 'shift'], { timeoutMs: 5000 }); return true; }
catch { return false; }
}
const holdOk = (holdMs) => !holdMs || holdMs <= 60_000; Try / catch
try {
await waylandKey(combo, { holdMs });
} catch (e) {
if (e instanceof ExecError && e.message.includes('wtype exited')) {
// fall back to ydotool (with ydotoold) — wtype unsupported on this compositor
await ydotoolKey(combo);
} else throw e;
} Prevention
- Preflight `wtype` with a harmless press to confirm compositor support (fails on GNOME).
- Keep holdMs small; runtime timeout is max(10s, holdMs+8s).
- Have ydotool+ydotoold configured as the fallback input path.
- Run inside the real Wayland session, not an XWayland-only context.
- Match wtype version to compositor protocol support.
When it happens
Trigger: wtype present and selected on Wayland, but the compositor rejects the virtual keyboard protocol (e.g. GNOME does not support zwp_virtual_keyboard_v1 for wtype), the gesture exceeds the timeout (max(10s, holdMs+8s)), or invalid key args were passed through.
Common situations: Running wtype on GNOME/Wayland compositors lacking the virtual-keyboard protocol; extremely long holdMs causing the computed timeout to fire and the process to be killed (non-zero exit); wtype built without protocol support.
Related errors
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/b0613e66bc1a33d8.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/src/backends/linux.mjs:201
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);
}
function assertNum(v, name) {
const n = Number(v);
if (!Number.isFinite(n)) throw new ExecError(`${name} must be a finite number`);
return n;
}
// ---------- AT-SPI tree ----------
const PYATSPI_APP = `def resolve_app(desktop, app_name):
apps = [desktop.getChildAtIndex(i) for i in range(desktop.childCount)]
if app_name:
matches = [app for app in apps if app and app_name.casefold() == (app.name or "").casefold()]
return matches[0] if len(matches) == 1 else None
return next((app for app in apps if app and app.childCount), None)
`;
const PYATSPI_WALK = `import json, sys, pyatspi
${PYATSPI_APP}View on GitHub (pinned to 73e0f67d83)