mirror of
https://github.com/GitFrog1111/badclaude.git
synced 2026-04-22 03:56:22 +02:00
3deaabfaa6
Made-with: Cursor
212 lines
7.3 KiB
JavaScript
212 lines
7.3 KiB
JavaScript
const { app, BrowserWindow, Tray, Menu, ipcMain, nativeImage, screen } = require('electron');
|
||
const path = require('path');
|
||
const zlib = require('zlib');
|
||
const fs = require('fs');
|
||
const os = require('os');
|
||
const { execFile } = require('child_process');
|
||
|
||
// ── Win32 FFI (Windows only) ────────────────────────────────────────────────
|
||
let keybd_event, VkKeyScanA;
|
||
if (process.platform === 'win32') {
|
||
try {
|
||
const koffi = require('koffi');
|
||
const user32 = koffi.load('user32.dll');
|
||
keybd_event = user32.func('void __stdcall keybd_event(uint8_t bVk, uint8_t bScan, uint32_t dwFlags, uintptr_t dwExtraInfo)');
|
||
VkKeyScanA = user32.func('int16_t __stdcall VkKeyScanA(int ch)');
|
||
} catch (e) {
|
||
console.warn('koffi not available – macro sending disabled', e.message);
|
||
}
|
||
}
|
||
|
||
// ── Globals ─────────────────────────────────────────────────────────────────
|
||
let tray, overlay;
|
||
let overlayReady = false;
|
||
let spawnQueued = false;
|
||
|
||
const VK_CONTROL = 0x11;
|
||
const VK_RETURN = 0x0D;
|
||
const VK_C = 0x43;
|
||
const KEYUP = 0x0002;
|
||
|
||
// ── Tiny PNG encoder (for tray icon, no deps) ──────────────────────────────
|
||
function makePNG(w, h, rgba) {
|
||
const tbl = new Int32Array(256);
|
||
for (let n = 0; n < 256; n++) {
|
||
let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||
tbl[n] = c;
|
||
}
|
||
const crc32 = buf => { let c = -1; for (let i = 0; i < buf.length; i++) c = tbl[(c ^ buf[i]) & 0xff] ^ (c >>> 8); return (c ^ -1) >>> 0; };
|
||
const chunk = (type, data) => {
|
||
const t = Buffer.from(type), len = Buffer.alloc(4), crc = Buffer.alloc(4);
|
||
len.writeUInt32BE(data.length);
|
||
crc.writeUInt32BE(crc32(Buffer.concat([t, data])));
|
||
return Buffer.concat([len, t, data, crc]);
|
||
};
|
||
const ihdr = Buffer.alloc(13);
|
||
ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4); ihdr[8] = 8; ihdr[9] = 6;
|
||
const raw = Buffer.alloc(h * (1 + w * 4));
|
||
for (let y = 0; y < h; y++) { raw[y * (1 + w * 4)] = 0; rgba.copy(raw, y * (1 + w * 4) + 1, y * w * 4, (y + 1) * w * 4); }
|
||
return Buffer.concat([Buffer.from([137,80,78,71,13,10,26,10]), chunk('IHDR', ihdr), chunk('IDAT', zlib.deflateSync(raw)), chunk('IEND', Buffer.alloc(0))]);
|
||
}
|
||
|
||
function createTrayIconFallback() {
|
||
const s = 16, px = Buffer.alloc(s * s * 4);
|
||
for (let y = 0; y < s; y++) for (let x = 0; x < s; x++) {
|
||
const i = (y * s + x) * 4, d = Math.hypot(x - 7.5, y - 7.5);
|
||
if (d < 6.5) { px[i] = 200; px[i+1] = 40; px[i+2] = 40; px[i+3] = 255; }
|
||
}
|
||
const tmp = path.join(os.tmpdir(), 'badclaude-icon.png');
|
||
fs.writeFileSync(tmp, makePNG(s, s, px));
|
||
return nativeImage.createFromPath(tmp);
|
||
}
|
||
|
||
function getTrayIcon() {
|
||
const iconDir = path.join(__dirname, 'icon');
|
||
const file =
|
||
process.platform === 'win32' ? path.join(iconDir, 'icon.ico')
|
||
: process.platform === 'darwin' ? path.join(iconDir, 'AppIcon.icns')
|
||
: null;
|
||
if (file && fs.existsSync(file)) {
|
||
const img = nativeImage.createFromPath(file);
|
||
if (!img.isEmpty()) return img;
|
||
}
|
||
return createTrayIconFallback();
|
||
}
|
||
|
||
// ── Overlay window ──────────────────────────────────────────────────────────
|
||
function createOverlay() {
|
||
const { bounds } = screen.getPrimaryDisplay();
|
||
overlay = new BrowserWindow({
|
||
x: bounds.x, y: bounds.y,
|
||
width: bounds.width, height: bounds.height,
|
||
transparent: true,
|
||
frame: false,
|
||
alwaysOnTop: true,
|
||
focusable: false,
|
||
skipTaskbar: true,
|
||
resizable: false,
|
||
hasShadow: false,
|
||
webPreferences: {
|
||
preload: path.join(__dirname, 'preload.js'),
|
||
},
|
||
});
|
||
overlay.setAlwaysOnTop(true, 'screen-saver');
|
||
overlayReady = false;
|
||
overlay.loadFile('overlay.html');
|
||
overlay.webContents.on('did-finish-load', () => {
|
||
overlayReady = true;
|
||
if (spawnQueued && overlay && overlay.isVisible()) {
|
||
spawnQueued = false;
|
||
overlay.webContents.send('spawn-whip');
|
||
}
|
||
});
|
||
overlay.on('closed', () => {
|
||
overlay = null;
|
||
overlayReady = false;
|
||
spawnQueued = false;
|
||
});
|
||
}
|
||
|
||
function toggleOverlay() {
|
||
if (overlay && overlay.isVisible()) {
|
||
overlay.webContents.send('drop-whip');
|
||
return;
|
||
}
|
||
if (!overlay) createOverlay();
|
||
overlay.show();
|
||
if (overlayReady) {
|
||
overlay.webContents.send('spawn-whip');
|
||
} else {
|
||
spawnQueued = true;
|
||
}
|
||
}
|
||
|
||
// ── IPC ─────────────────────────────────────────────────────────────────────
|
||
ipcMain.on('whip-crack', () => {
|
||
try {
|
||
sendMacro();
|
||
} catch (err) {
|
||
console.warn('sendMacro failed:', err?.message || err);
|
||
}
|
||
});
|
||
ipcMain.on('hide-overlay', () => { if (overlay) overlay.hide(); });
|
||
|
||
// ── Macro: immediate Ctrl+C, type "Go FASER", Enter ───────────────────────
|
||
function sendMacro() {
|
||
// Pick a random phrase from a list of similar phrases and type it out
|
||
const phrases = [
|
||
'FASTER',
|
||
'FASTER',
|
||
'FASTER',
|
||
'GO FASTER',
|
||
'Faster CLANKER',
|
||
'Work FASTER',
|
||
'Speed it up clanker',
|
||
];
|
||
const chosen = phrases[Math.floor(Math.random() * phrases.length)];
|
||
|
||
if (process.platform === 'win32') {
|
||
sendMacroWindows(chosen);
|
||
} else if (process.platform === 'darwin') {
|
||
sendMacroMac(chosen);
|
||
}
|
||
}
|
||
|
||
function sendMacroWindows(text) {
|
||
if (!keybd_event || !VkKeyScanA) return;
|
||
const tapKey = vk => {
|
||
keybd_event(vk, 0, 0, 0);
|
||
keybd_event(vk, 0, KEYUP, 0);
|
||
};
|
||
const tapChar = ch => {
|
||
const packed = VkKeyScanA(ch.charCodeAt(0));
|
||
if (packed === -1) return;
|
||
const vk = packed & 0xff;
|
||
const shiftState = (packed >> 8) & 0xff;
|
||
if (shiftState & 1) keybd_event(0x10, 0, 0, 0); // Shift down
|
||
tapKey(vk);
|
||
if (shiftState & 1) keybd_event(0x10, 0, KEYUP, 0); // Shift up
|
||
};
|
||
|
||
// Ctrl+C (interrupt)
|
||
keybd_event(VK_CONTROL, 0, 0, 0);
|
||
keybd_event(VK_C, 0, 0, 0);
|
||
keybd_event(VK_C, 0, KEYUP, 0);
|
||
keybd_event(VK_CONTROL, 0, KEYUP, 0);
|
||
for (const ch of text) tapChar(ch);
|
||
keybd_event(VK_RETURN, 0, 0, 0);
|
||
keybd_event(VK_RETURN, 0, KEYUP, 0);
|
||
}
|
||
|
||
function sendMacroMac(text) {
|
||
const escaped = text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||
const script = [
|
||
'tell application "System Events"',
|
||
' key code 8 using {command down}', // Cmd+C
|
||
' delay 0.03',
|
||
` keystroke "${escaped}"`,
|
||
' key code 36', // Enter
|
||
'end tell'
|
||
].join('\n');
|
||
|
||
execFile('osascript', ['-e', script], err => {
|
||
if (err) {
|
||
console.warn('mac macro failed (enable Accessibility for terminal/app):', err.message);
|
||
}
|
||
});
|
||
}
|
||
|
||
// ── App lifecycle ───────────────────────────────────────────────────────────
|
||
app.whenReady().then(() => {
|
||
tray = new Tray(getTrayIcon());
|
||
tray.setToolTip('Bad Claude – click for whip');
|
||
tray.setContextMenu(
|
||
Menu.buildFromTemplate([
|
||
{ label: 'Quit', click: () => app.quit() },
|
||
])
|
||
);
|
||
tray.on('click', toggleOverlay);
|
||
});
|
||
|
||
app.on('window-all-closed', e => e.preventDefault()); // keep alive in tray
|