Hmbown/CodeWhale · error · ExecError

list_apps needs wmctrl (X11) or swaymsg/hyprctl (Wayland)

Error message

list_apps needs wmctrl (X11) or swaymsg/hyprctl (Wayland)

What it means

Thrown by list_apps when no supported application-enumeration tool exists for the session: wmctrl on X11, or swaymsg/hyprctl on Wayland. Unlike the AT-SPI path, the code here requires one of these window-manager tools; without them it throws this directive message instead of returning results.

Solutions

  1. On X11 install wmctrl (`sudo apt install wmctrl`) and verify with `wmctrl -lx`.
  2. On sway/hyprland ensure swaymsg/hyprctl are in PATH (they ship with the compositor).
  3. On other Wayland compositors, this backend cannot list apps via this path — switch to the AT-SPI-backed flow or run an X11/XWayland session with wmctrl.
  4. Check that probeSession detected the expected session; a wrong session guess makes it look for the wrong tool.
  5. Confirm the binary is on PATH for the same user running the plugin.

Example fix

// before (X11)
# wmctrl: command not found
// after
sudo apt install wmctrl
Defensive patterns

Strategy: fallback

Validate before calling

import { execFileSync } from 'child_process';
const has = (cmd) => { try { execFileSync('sh', ['-c', `command -v ${cmd}`], { stdio: 'ignore' }); return true; } catch { return false; } };
function appsAvailable(session) {
  return session === 'x11' ? has('wmctrl') : (has('swaymsg') || has('hyprctl'));
}

Try / catch

try {
  const { apps } = await listApps();
} catch (e) {
  if (e instanceof ExecError && e.message.startsWith('list_apps needs')) {
    // fallback: empty app list with a capability note, or route to AT-SPI flow
    return { apps: [], note: 'install wmctrl/swaymsg/hyprctl for app enumeration' };
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling list_apps when: X11 session without wmctrl installed; Wayland session where probeSession succeeded but neither swaymsg nor hyprctl is available (or the sway/hypr branches were skipped because the tools are missing).

Common situations: Fresh desktop installs without wmctrl; Wayland compositors other than sway/hyprland (GNOME, KDE) where none of the three tools apply; containerized sessions with no window-manager tooling.

Understand the failure class

Background: "not installed", "pip install", "required for": how missing-dependency errors surface across open-source libraries — this error's family across 34 libraries.

Related errors


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

Appendix: source

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

        const r = await runOk("wmctrl", ["-lx"], { timeoutMs: 15_000 });
        const seen = new Map();
        for (const line of r.stdout.split("\n")) {
          const parts = line.split(/\s+/);
          const wmClass = parts[2];
          if (wmClass) seen.set(wmClass, { name: wmClass.split(".")[0], wm_class: wmClass });
        }
        return { apps: [...seen.values()] };
      }
      if (session === "wayland" && (tools.swaymsg || tools.hyprctl)) {
        const w = await this.list_windows();
        const seen = new Map();
        for (const win of w.windows) {
          const cls = win.wm_class || win.app_id;
          if (cls) seen.set(cls, { name: cls, wm_class: cls });
        }
        return { apps: [...seen.values()] };
      }
      throw new ExecError("list_apps needs wmctrl (X11) or swaymsg/hyprctl (Wayland)");
    },
    list_windows: async (args = {}) => {
      rejectWindowSelectors(args);
      await probeSession();
      if (session === "x11" && tools.wmctrl) {
        const r = await runOk("wmctrl", ["-lGx"], { timeoutMs: 15_000 });
        const windows = [];
        for (const line of r.stdout.split("\n")) {
          const m = /^(\S+)\s+(-?\d+)\s+(-?\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(line);
          if (m) windows.push({ id: m[1], desktop: m[2], position: { x: Number(m[3]), y: Number(m[4]) }, size: { w: Number(m[5]), h: Number(m[6]) }, wm_class: m[7], title: m[8] });
        }
        return { windows };
      }
      if (session === "wayland" && tools.swaymsg) {
        const r = await run("swaymsg", ["-t", "get_tree", "-r"], { timeoutMs: 15_000 });
        const windows = [];
        const walk = (n) => {
          if (n.type === "con" && n.name) windows.push({ id: String(n.id), title: n.name, wm_class: n.app_id ?? null, position: { x: n.rect?.x, y: n.rect?.y }, size: { w: n.rect?.width, h: n.rect?.height }, focused: !!n.focused });

View on GitHub (pinned to 73e0f67d83)