mirror of
https://github.com/GitFrog1111/badclaude.git
synced 2026-04-21 19:46:24 +02:00
291 lines
9.2 KiB
JavaScript
291 lines
9.2 KiB
JavaScript
const { app, BrowserWindow, Tray, Menu, ipcMain, nativeImage, screen } = require('electron');
|
||
const path = require('path');
|
||
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 VK_MENU = 0x12; // Alt
|
||
const VK_TAB = 0x09;
|
||
const KEYUP = 0x0002;
|
||
|
||
/** One Alt+Tab / Cmd+Tab so focus returns to the previously active app after tray click. */
|
||
function refocusPreviousApp() {
|
||
const delayMs = 80;
|
||
const run = () => {
|
||
if (process.platform === 'win32') {
|
||
if (!keybd_event) return;
|
||
keybd_event(VK_MENU, 0, 0, 0);
|
||
keybd_event(VK_TAB, 0, 0, 0);
|
||
keybd_event(VK_TAB, 0, KEYUP, 0);
|
||
keybd_event(VK_MENU, 0, KEYUP, 0);
|
||
} else if (process.platform === 'darwin') {
|
||
const script = [
|
||
'tell application "System Events"',
|
||
' key down command',
|
||
' key code 48', // Tab
|
||
' key up command',
|
||
'end tell',
|
||
].join('\n');
|
||
execFile('osascript', ['-e', script], err => {
|
||
if (err) {
|
||
console.warn('refocus previous app (Cmd+Tab) failed:', err.message);
|
||
}
|
||
});
|
||
} else if (process.platform === 'linux') {
|
||
execFile('xdotool', ['key', '--clearmodifiers', 'alt+Tab'], err => {
|
||
if (err) {
|
||
console.warn('refocus previous app (Alt+Tab) failed. Install xdotool:', err.message);
|
||
}
|
||
});
|
||
}
|
||
};
|
||
setTimeout(run, delayMs);
|
||
}
|
||
|
||
function createTrayIconFallback() {
|
||
const p = path.join(__dirname, 'icon', 'Template.png');
|
||
if (fs.existsSync(p)) {
|
||
const img = nativeImage.createFromPath(p);
|
||
if (!img.isEmpty()) {
|
||
if (process.platform === 'darwin') img.setTemplateImage(true);
|
||
return img;
|
||
}
|
||
}
|
||
console.warn('openwhip: icon/Template.png missing or invalid');
|
||
return nativeImage.createEmpty();
|
||
}
|
||
|
||
async function tryIcnsTrayImage(icnsPath) {
|
||
const size = { width: 64, height: 64 };
|
||
const thumb = await nativeImage.createThumbnailFromPath(icnsPath, size);
|
||
if (!thumb.isEmpty()) return thumb;
|
||
return null;
|
||
}
|
||
|
||
// macOS: createFromPath does not decode .icns (Electron only loads PNG/JPEG there, ICO on Windows).
|
||
// Quick Look thumbnails handle .icns; copy to temp if the file is inside ASAR (QL needs a real path).
|
||
async function getTrayIcon() {
|
||
const iconDir = path.join(__dirname, 'icon');
|
||
if (process.platform === 'win32') {
|
||
const file = path.join(iconDir, 'icon.ico');
|
||
if (fs.existsSync(file)) {
|
||
const img = nativeImage.createFromPath(file);
|
||
if (!img.isEmpty()) return img;
|
||
}
|
||
return createTrayIconFallback();
|
||
}
|
||
if (process.platform === 'darwin') {
|
||
const file = path.join(iconDir, 'AppIcon.icns');
|
||
if (fs.existsSync(file)) {
|
||
const fromPath = nativeImage.createFromPath(file);
|
||
if (!fromPath.isEmpty()) return fromPath;
|
||
try {
|
||
const t = await tryIcnsTrayImage(file);
|
||
if (t) return t;
|
||
} catch (e) {
|
||
console.warn('AppIcon.icns Quick Look thumbnail failed:', e?.message || e);
|
||
}
|
||
const tmp = path.join(os.tmpdir(), 'openwhip-tray.icns');
|
||
try {
|
||
fs.copyFileSync(file, tmp);
|
||
const t = await tryIcnsTrayImage(tmp);
|
||
if (t) return t;
|
||
} catch (e) {
|
||
console.warn('AppIcon.icns temp copy + thumbnail failed:', e?.message || e);
|
||
}
|
||
}
|
||
return createTrayIconFallback();
|
||
}
|
||
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');
|
||
refocusPreviousApp();
|
||
}
|
||
});
|
||
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');
|
||
refocusPreviousApp();
|
||
} 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);
|
||
} else if (process.platform === 'linux') {
|
||
sendMacroLinux(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 interruptScript = [
|
||
'tell application "System Events"',
|
||
' key code 8 using {control down}', // Ctrl+C interrupt
|
||
'end tell'
|
||
].join('\n');
|
||
const typeAndEnterScript = [
|
||
'tell application "System Events"',
|
||
` keystroke "${escaped}"`,
|
||
' key code 36', // Enter
|
||
'end tell'
|
||
].join('\n');
|
||
|
||
execFile('osascript', ['-e', interruptScript], err => {
|
||
if (err) {
|
||
console.warn('mac macro failed (enable Accessibility for terminal/app):', err.message);
|
||
return;
|
||
}
|
||
|
||
setTimeout(() => {
|
||
execFile('osascript', ['-e', typeAndEnterScript], err2 => {
|
||
if (err2) {
|
||
console.warn('mac macro failed (enable Accessibility for terminal/app):', err2.message);
|
||
}
|
||
});
|
||
}, 300);
|
||
});
|
||
}
|
||
|
||
function sendMacroLinux(text) {
|
||
execFile(
|
||
'xdotool',
|
||
[
|
||
'key', '--clearmodifiers', 'ctrl+c',
|
||
'type', '--delay', '1', '--clearmodifiers', '--', text,
|
||
'key', 'Return',
|
||
],
|
||
err => {
|
||
if (err) {
|
||
console.warn('linux macro failed. Install xdotool:', err.message);
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
// ── App lifecycle ───────────────────────────────────────────────────────────
|
||
app.whenReady().then(async () => {
|
||
tray = new Tray(await getTrayIcon());
|
||
tray.setToolTip('OpenWhip - 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
|