Hmbown/CodeWhale · error · ExecError

xdotool exited

Error message

xdotool ${args[0]} exited ${r.code}: ${r.stderr.trim().slice(0, 200)}

What it means

Thrown by the xdotool wrapper when the xdotool subprocess exits non-zero on X11. The message includes the first xdotool subcommand (args[0], e.g. key, mousemove, click) plus up to 200 chars of stderr. The wrapper first verifies xdotool exists via `need`, so this error means xdotool ran but rejected the request.

Solutions

  1. Read the stderr in the message; xdotool errors usually name the bad keysym or 'Can't open display'.
  2. Ensure DISPLAY and XAUTHORITY are correct for the target X server (`xdotool getactivewindow` as a smoke test).
  3. Normalize modifier/key names before calling: this backend maps meta->logo only in waylandKey, so pass xdotool-valid names like 'ctrl', 'alt', 'super'.
  4. Re-resolve the target window id if you passed one; the window may have closed.
  5. If on Wayland, ensure the session probe picked the right backend (ydotool/wtype) rather than xdotool.

Example fix

// before
await xdotool(['key', 'Meta_L']);
// after
await xdotool(['key', 'super']);
Defensive patterns

Strategy: try-catch

Validate before calling

async function xdotoolReady() {
  try {
    await run('xdotool', ['getdisplaygeometry'], { timeoutMs: 5000 });
    return true;
  } catch { return false; }
}

Try / catch

try {
  await xdotool(['key', name]);
} catch (e) {
  if (e instanceof ExecError && e.message.startsWith('xdotool')) {
    if (/Can't open display/.test(e.message)) fixDisplayEnv();
    else if (/window.*invalid|Unknown window/i.test(e.message)) await refreshWindowTarget();
    else console.error('xdotool rejected:', e.message);
  } else throw e;
}

Prevention

When it happens

Trigger: Any input action (key, type, click, move) routed through xdotool(args, opts) on X11 where xdotool exits non-zero: unknown keysym, no X display/authority, window target disappeared, or malformed coordinate arguments.

Common situations: SSH/headless X11 without DISPLAY set; X SECURITY blocking the connection; sending a key name xdotool doesn't recognize (e.g. 'meta' instead of mapping it); clicking a window that closed between lookup and action.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/93e937071e35ad0e. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/plugins/computer-use/src/backends/linux.mjs:155

      args.push(file);
    } else {
      if (region) args.push("-crop", `${Math.round(region[2])}x${Math.round(region[3])}+${Math.round(region[0])}+${Math.round(region[1])}`);
      args.push(file);
    }
    const r = await run(cmd, args, { timeoutMs: 10_000 });
    if (r.code !== 0) throw new ExecError(`${cmd} exited ${r.code}: ${r.stderr.trim().slice(0, 300)}`, r);
  }

  function recordingsDir() {
    return path.resolve(process.env.CODEWHALE_CU_RECORDINGS_DIR || path.join(os.homedir(), ".codewhale-cu", "recordings"));
  }

  async function xdotool(args, opts = {}) {
    need("xdotool", "input on X11");
    throwIfAborted();
    const r = await run("xdotool", args, opts);
    throwIfAborted();
    if (r.code !== 0) throw new ExecError(`xdotool ${args[0]} exited ${r.code}: ${r.stderr.trim().slice(0, 200)}`, r);
    return r.stdout.trim();
  }

  async function ydotool(args, opts = {}) {
    need("ydotool", "input on Wayland (ydotool needs its daemon running: sudo ydotoold)");
    throwIfAborted();
    const r = await run("ydotool", args, opts);
    throwIfAborted();
    if (r.code !== 0) throw new ExecError(`ydotool exited ${r.code}: ${r.stderr.trim().slice(0, 200)}`, r);
    return r.stdout.trim();
  }

  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

View on GitHub (pinned to 73e0f67d83)