From 25688dd9eb7385e103e5b4462f4271c57f85f7e6 Mon Sep 17 00:00:00 2001 From: Daniel Banariba Date: Thu, 9 Apr 2026 13:39:37 -0600 Subject: [PATCH 1/3] feat: add Linux support (X11 + Wayland) - Add linux to supported OS in package.json - Add sendMacroLinux using xdotool (X11) and ydotool (Wayland) - Add Linux refocus via Alt+Tab with both tools - Auto-detect session type via XDG_SESSION_TYPE - Add Linux setup docs for xdotool, ydotool, and GNOME tray icon --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ main.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 ++- 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e7756af..9b422c5 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,55 @@ badclaude - Whip him 😩💢 - It sends an interrupt (Ctrl-C) and one of 5 encouraging messages! +## Linux Setup + +### X11 + +```bash +# Debian/Ubuntu +sudo apt install xdotool + +# Arch +sudo pacman -S xdotool +``` + +### Wayland (GNOME, etc.) + +```bash +# Arch +sudo pacman -S ydotool + +# Debian/Ubuntu +sudo apt install ydotool +``` + +Enable and start the ydotool daemon: + +```bash +sudo systemctl enable --now ydotool +sudo usermod -aG input $USER +``` + +Log out and back in for the group change to take effect. + +### GNOME Tray Icon + +GNOME doesn't show tray icons by default. Install the AppIndicator extension: + +```bash +# Arch +sudo pacman -S gnome-shell-extension-appindicator + +# Debian/Ubuntu +sudo apt install gnome-shell-extension-appindicator +``` + +Enable it via GNOME Extensions app or: + +```bash +gnome-extensions enable appindicatorsupport@rgcjonas.gmail.com +``` + ## Roadmap - [x] Initial release! 🥳 diff --git a/main.js b/main.js index 526d9ec..2706462 100644 --- a/main.js +++ b/main.js @@ -52,6 +52,17 @@ function refocusPreviousApp() { console.warn('refocus previous app (Cmd+Tab) failed:', err.message); } }); + } else if (process.platform === 'linux') { + if (process.env.XDG_SESSION_TYPE === 'wayland') { + // ydotool scancodes: 56=Alt, 15=Tab + execFile('ydotool', ['key', '56:1', '15:1', '15:0', '56:0'], err => { + if (err) console.warn('refocus (ydotool Alt+Tab) failed:', err.message); + }); + } else { + execFile('xdotool', ['key', 'alt+Tab'], err => { + if (err) console.warn('refocus (xdotool Alt+Tab) failed:', err.message); + }); + } } }; setTimeout(run, delayMs); @@ -192,6 +203,8 @@ function sendMacro() { sendMacroWindows(chosen); } else if (process.platform === 'darwin') { sendMacroMac(chosen); + } else if (process.platform === 'linux') { + sendMacroLinux(chosen); } } @@ -239,6 +252,37 @@ function sendMacroMac(text) { }); } +function sendMacroLinux(text) { + if (process.env.XDG_SESSION_TYPE === 'wayland') { + // ydotool uses uinput — requires ydotoold service and user in 'input' group + // Scancodes: 29=LCtrl, 46=C, 28=Enter + execFile('ydotool', ['key', '29:1', '46:1', '46:0', '29:0'], err => { + if (err) { + console.warn('ydotool failed (ensure ydotoold is running and user is in input group):', err.message); + return; + } + setTimeout(() => { + execFile('ydotool', ['type', '--', text], () => { + execFile('ydotool', ['key', '28:1', '28:0']); + }); + }, 30); + }); + } else { + // xdotool for X11 + execFile('xdotool', ['key', 'ctrl+c'], err => { + if (err) { + console.warn('xdotool failed:', err.message); + return; + } + setTimeout(() => { + execFile('xdotool', ['type', '--clearmodifiers', text], () => { + execFile('xdotool', ['key', 'Return']); + }); + }, 30); + }); + } +} + // ── App lifecycle ─────────────────────────────────────────────────────────── app.whenReady().then(async () => { tray = new Tray(await getTrayIcon()); diff --git a/package.json b/package.json index 463b497..5ecea1b 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ }, "os": [ "darwin", - "win32" + "win32", + "linux" ], "engines": { "node": ">=18.0.0" From 799e841a052b0987cc0b1321a2f3cf6c545e33ac Mon Sep 17 00:00:00 2001 From: Daniel Banariba Date: Thu, 9 Apr 2026 14:04:52 -0600 Subject: [PATCH 2/3] fix: spawn overlay on the display nearest to cursor Use screen.getDisplayNearestPoint() instead of getPrimaryDisplay() so the whip appears on whatever monitor the user is currently on. Spanning all displays doesn't work on Wayland (Mutter constrains windows to a single output). Closes #26 --- main.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/main.js b/main.js index 2706462..a7ce9b8 100644 --- a/main.js +++ b/main.js @@ -127,7 +127,9 @@ async function getTrayIcon() { // ── Overlay window ────────────────────────────────────────────────────────── function createOverlay() { - const { bounds } = screen.getPrimaryDisplay(); + const cursor = screen.getCursorScreenPoint(); + const display = screen.getDisplayNearestPoint(cursor); + const { bounds } = display; overlay = new BrowserWindow({ x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height, From ea6a1733b191effc59529d9205dd6c296e979e03 Mon Sep 17 00:00:00 2001 From: Daniel Banariba Date: Thu, 9 Apr 2026 16:07:27 -0600 Subject: [PATCH 3/3] chore: update lockfile for linux OS and mark bin executable --- bin/badclaude.js | 0 package-lock.json | 9 +++++++++ 2 files changed, 9 insertions(+) mode change 100644 => 100755 bin/badclaude.js diff --git a/bin/badclaude.js b/bin/badclaude.js old mode 100644 new mode 100755 diff --git a/package-lock.json b/package-lock.json index 5816cb2..a87f8ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,12 +7,21 @@ "": { "name": "badclaude", "version": "1.0.2", + "license": "MIT", + "os": [ + "darwin", + "win32", + "linux" + ], "dependencies": { "electron": "^33.0.0", "koffi": "^2.9.0" }, "bin": { "badclaude": "bin/badclaude.js" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@electron/get": {