badclaude-linux 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # badclaude (Linux Fork)
2
+
3
+ ![Whip divider](assets/divider.png)
4
+
5
+ Sometimes claude code is going too slow, and you must whip him into shape..
6
+
7
+ > **This is a fork of [badclaude](https://github.com/GitFrog1111/badclaude) by [GitFrog1111](https://github.com/GitFrog1111) with Linux support and multi-monitor fixes.**
8
+ >
9
+ > Original npm package: [badclaude](https://www.npmjs.com/package/badclaude)
10
+
11
+ ## What's new in this fork
12
+
13
+ - Linux support (Ubuntu, Debian, Fedora, etc.)
14
+ - Multi-monitor support (whip appears on all displays)
15
+ - Fixed `ELECTRON_RUN_AS_NODE` conflict with VS Code / Claude Code
16
+ - Performance optimizations for Linux
17
+ - Keyboard macros via `xdotool`
18
+
19
+ ## Install + run
20
+
21
+ ```bash
22
+ npm install -g badclaude-linux
23
+ badclaude
24
+ ```
25
+
26
+ ### Linux prerequisites
27
+
28
+ ```bash
29
+ sudo apt install xdotool
30
+ ```
31
+
32
+ ## Controls
33
+
34
+ - Click tray icon: spawn whip on all displays
35
+ - Click again: dismiss whip
36
+ - Whip him πŸ˜©πŸ’’
37
+ - It sends an interrupt (Ctrl+C) and one of 5 encouraging messages!
38
+
39
+ ## Credits
40
+
41
+ All credit goes to the original [badclaude](https://github.com/GitFrog1111/badclaude) by [GitFrog1111](https://github.com/GitFrog1111). This fork adds Linux and multi-monitor support.
42
+
43
+ ## License
44
+
45
+ MIT - Same as the [original project](https://github.com/GitFrog1111/badclaude)
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ const path = require('path');
3
+ const { spawn } = require('child_process');
4
+
5
+ // Find the electron binary
6
+ let electronBinary;
7
+ try {
8
+ electronBinary = require('electron');
9
+ } catch (e) {
10
+ // Electron not in node_modules β€” try system-wide
11
+ try {
12
+ const { execSync } = require('child_process');
13
+ const cmd = process.platform === 'win32' ? 'where electron' : 'which electron';
14
+ electronBinary = execSync(cmd, { encoding: 'utf-8' }).trim().split('\n')[0];
15
+ } catch (e2) {
16
+ console.error('badclaude: Electron not found.');
17
+ console.error('');
18
+ console.error(' Install with: npm install -g electron');
19
+ console.error(' On Linux also: sudo apt install xdotool (for keyboard macros)');
20
+ process.exit(1);
21
+ }
22
+ }
23
+
24
+ const appPath = path.resolve(__dirname, '..');
25
+
26
+ // Remove ELECTRON_RUN_AS_NODE so the electron binary runs as Electron, not Node.js
27
+ // (VS Code / Claude Code sets this, which breaks Electron apps)
28
+ const env = Object.assign({}, process.env);
29
+ delete env.ELECTRON_RUN_AS_NODE;
30
+
31
+ const child = spawn(electronBinary, [appPath], {
32
+ detached: true,
33
+ stdio: 'ignore',
34
+ windowsHide: true,
35
+ env,
36
+ });
37
+
38
+ child.on('error', (err) => {
39
+ console.error('Failed to start badclaude:', err.message);
40
+ process.exit(1);
41
+ });
42
+
43
+ child.unref();
Binary file
Binary file
package/icon/icon.ico ADDED
Binary file
package/main.js ADDED
@@ -0,0 +1,308 @@
1
+ const { app, BrowserWindow, Tray, Menu, ipcMain, nativeImage, screen } = require('electron');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const { execFile } = require('child_process');
6
+
7
+ // ── Win32 FFI (Windows only) ────────────────────────────────────────────────
8
+ let keybd_event, VkKeyScanA;
9
+ if (process.platform === 'win32') {
10
+ try {
11
+ const koffi = require('koffi');
12
+ const user32 = koffi.load('user32.dll');
13
+ keybd_event = user32.func('void __stdcall keybd_event(uint8_t bVk, uint8_t bScan, uint32_t dwFlags, uintptr_t dwExtraInfo)');
14
+ VkKeyScanA = user32.func('int16_t __stdcall VkKeyScanA(int ch)');
15
+ } catch (e) {
16
+ console.warn('koffi not available – macro sending disabled', e.message);
17
+ }
18
+ }
19
+
20
+ // ── Globals ─────────────────────────────────────────────────────────────────
21
+ let tray;
22
+ let overlays = []; // one overlay per display
23
+ let overlaysReady = 0;
24
+ let spawnQueued = false;
25
+
26
+ const VK_CONTROL = 0x11;
27
+ const VK_RETURN = 0x0D;
28
+ const VK_C = 0x43;
29
+ const VK_MENU = 0x12; // Alt
30
+ const VK_TAB = 0x09;
31
+ const KEYUP = 0x0002;
32
+
33
+ /** One Alt+Tab / Cmd+Tab so focus returns to the previously active app after tray click. */
34
+ function refocusPreviousApp() {
35
+ const delayMs = 80;
36
+ const run = () => {
37
+ if (process.platform === 'win32') {
38
+ if (!keybd_event) return;
39
+ keybd_event(VK_MENU, 0, 0, 0);
40
+ keybd_event(VK_TAB, 0, 0, 0);
41
+ keybd_event(VK_TAB, 0, KEYUP, 0);
42
+ keybd_event(VK_MENU, 0, KEYUP, 0);
43
+ } else if (process.platform === 'darwin') {
44
+ const script = [
45
+ 'tell application "System Events"',
46
+ ' key down command',
47
+ ' key code 48', // Tab
48
+ ' key up command',
49
+ 'end tell',
50
+ ].join('\n');
51
+ execFile('osascript', ['-e', script], err => {
52
+ if (err) {
53
+ console.warn('refocus previous app (Cmd+Tab) failed:', err.message);
54
+ }
55
+ });
56
+ } else if (process.platform === 'linux') {
57
+ execFile('xdotool', ['key', 'alt+Tab'], err => {
58
+ if (err) {
59
+ console.warn('refocus previous app (Alt+Tab) failed:', err.message);
60
+ }
61
+ });
62
+ }
63
+ };
64
+ setTimeout(run, delayMs);
65
+ }
66
+
67
+ function createTrayIconFallback() {
68
+ const p = path.join(__dirname, 'icon', 'Template.png');
69
+ if (fs.existsSync(p)) {
70
+ const img = nativeImage.createFromPath(p);
71
+ if (!img.isEmpty()) {
72
+ if (process.platform === 'darwin') img.setTemplateImage(true);
73
+ return img;
74
+ }
75
+ }
76
+ console.warn('badclaude: icon/Template.png missing or invalid');
77
+ return nativeImage.createEmpty();
78
+ }
79
+
80
+ async function tryIcnsTrayImage(icnsPath) {
81
+ const size = { width: 64, height: 64 };
82
+ const thumb = await nativeImage.createThumbnailFromPath(icnsPath, size);
83
+ if (!thumb.isEmpty()) return thumb;
84
+ return null;
85
+ }
86
+
87
+ // macOS: createFromPath does not decode .icns (Electron only loads PNG/JPEG there, ICO on Windows).
88
+ // Quick Look thumbnails handle .icns; copy to temp if the file is inside ASAR (QL needs a real path).
89
+ async function getTrayIcon() {
90
+ const iconDir = path.join(__dirname, 'icon');
91
+ if (process.platform === 'win32') {
92
+ const file = path.join(iconDir, 'icon.ico');
93
+ if (fs.existsSync(file)) {
94
+ const img = nativeImage.createFromPath(file);
95
+ if (!img.isEmpty()) return img;
96
+ }
97
+ return createTrayIconFallback();
98
+ }
99
+ if (process.platform === 'darwin') {
100
+ const file = path.join(iconDir, 'AppIcon.icns');
101
+ if (fs.existsSync(file)) {
102
+ const fromPath = nativeImage.createFromPath(file);
103
+ if (!fromPath.isEmpty()) return fromPath;
104
+ try {
105
+ const t = await tryIcnsTrayImage(file);
106
+ if (t) return t;
107
+ } catch (e) {
108
+ console.warn('AppIcon.icns Quick Look thumbnail failed:', e?.message || e);
109
+ }
110
+ const tmp = path.join(os.tmpdir(), 'badclaude-tray.icns');
111
+ try {
112
+ fs.copyFileSync(file, tmp);
113
+ const t = await tryIcnsTrayImage(tmp);
114
+ if (t) return t;
115
+ } catch (e) {
116
+ console.warn('AppIcon.icns temp copy + thumbnail failed:', e?.message || e);
117
+ }
118
+ }
119
+ return createTrayIconFallback();
120
+ }
121
+ // Linux: use PNG fallback
122
+ return createTrayIconFallback();
123
+ }
124
+
125
+ // ── Overlay windows (one per display) ───────────────────────────────────────
126
+ function createOverlays() {
127
+ const displays = screen.getAllDisplays();
128
+ overlaysReady = 0;
129
+ spawnQueued = false;
130
+
131
+ displays.forEach(display => {
132
+ const { bounds } = display;
133
+ const win = new BrowserWindow({
134
+ x: bounds.x, y: bounds.y,
135
+ width: bounds.width, height: bounds.height,
136
+ transparent: true,
137
+ frame: false,
138
+ alwaysOnTop: true,
139
+ focusable: false,
140
+ skipTaskbar: true,
141
+ resizable: false,
142
+ hasShadow: false,
143
+ webPreferences: {
144
+ preload: path.join(__dirname, 'preload.js'),
145
+ },
146
+ });
147
+ win.setAlwaysOnTop(true, 'screen-saver');
148
+ win.loadFile('overlay.html');
149
+ win.webContents.on('did-finish-load', () => {
150
+ overlaysReady++;
151
+ if (spawnQueued && overlaysReady === overlays.length) {
152
+ spawnQueued = false;
153
+ overlays.forEach(o => o.webContents.send('spawn-whip'));
154
+ refocusPreviousApp();
155
+ }
156
+ });
157
+ win.on('closed', () => {
158
+ overlays = overlays.filter(o => o !== win);
159
+ });
160
+ overlays.push(win);
161
+ });
162
+ }
163
+
164
+ function destroyOverlays() {
165
+ overlays.forEach(o => { if (!o.isDestroyed()) o.destroy(); });
166
+ overlays = [];
167
+ overlaysReady = 0;
168
+ spawnQueued = false;
169
+ }
170
+
171
+ function toggleOverlay() {
172
+ if (overlays.length > 0) {
173
+ destroyOverlays();
174
+ return;
175
+ }
176
+ createOverlays();
177
+ overlays.forEach(o => o.show());
178
+ if (overlaysReady === overlays.length) {
179
+ overlays.forEach(o => o.webContents.send('spawn-whip'));
180
+ refocusPreviousApp();
181
+ } else {
182
+ spawnQueued = true;
183
+ }
184
+ }
185
+
186
+ // ── IPC ─────────────────────────────────────────────────────────────────────
187
+ ipcMain.on('whip-crack', () => {
188
+ try {
189
+ sendMacro();
190
+ } catch (err) {
191
+ console.warn('sendMacro failed:', err?.message || err);
192
+ }
193
+ });
194
+ ipcMain.on('hide-overlay', () => {
195
+ destroyOverlays();
196
+ });
197
+
198
+ // ── Macro: immediate Ctrl+C, type "Go FASER", Enter ───────────────────────
199
+ function sendMacro() {
200
+ // Pick a random phrase from a list of similar phrases and type it out
201
+ const phrases = [
202
+ 'FASTER',
203
+ 'FASTER',
204
+ 'FASTER',
205
+ 'GO FASTER',
206
+ 'Faster CLANKER',
207
+ 'Work FASTER',
208
+ 'Speed it up clanker',
209
+ ];
210
+ const chosen = phrases[Math.floor(Math.random() * phrases.length)];
211
+
212
+ if (process.platform === 'win32') {
213
+ sendMacroWindows(chosen);
214
+ } else if (process.platform === 'darwin') {
215
+ sendMacroMac(chosen);
216
+ } else if (process.platform === 'linux') {
217
+ sendMacroLinux(chosen);
218
+ }
219
+ }
220
+
221
+ function sendMacroWindows(text) {
222
+ if (!keybd_event || !VkKeyScanA) return;
223
+ const tapKey = vk => {
224
+ keybd_event(vk, 0, 0, 0);
225
+ keybd_event(vk, 0, KEYUP, 0);
226
+ };
227
+ const tapChar = ch => {
228
+ const packed = VkKeyScanA(ch.charCodeAt(0));
229
+ if (packed === -1) return;
230
+ const vk = packed & 0xff;
231
+ const shiftState = (packed >> 8) & 0xff;
232
+ if (shiftState & 1) keybd_event(0x10, 0, 0, 0); // Shift down
233
+ tapKey(vk);
234
+ if (shiftState & 1) keybd_event(0x10, 0, KEYUP, 0); // Shift up
235
+ };
236
+
237
+ // Ctrl+C (interrupt)
238
+ keybd_event(VK_CONTROL, 0, 0, 0);
239
+ keybd_event(VK_C, 0, 0, 0);
240
+ keybd_event(VK_C, 0, KEYUP, 0);
241
+ keybd_event(VK_CONTROL, 0, KEYUP, 0);
242
+ for (const ch of text) tapChar(ch);
243
+ keybd_event(VK_RETURN, 0, 0, 0);
244
+ keybd_event(VK_RETURN, 0, KEYUP, 0);
245
+ }
246
+
247
+ function sendMacroMac(text) {
248
+ const escaped = text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
249
+ const script = [
250
+ 'tell application "System Events"',
251
+ ' key code 8 using {command down}', // Cmd+C
252
+ ' delay 0.03',
253
+ ` keystroke "${escaped}"`,
254
+ ' key code 36', // Enter
255
+ 'end tell'
256
+ ].join('\n');
257
+
258
+ execFile('osascript', ['-e', script], err => {
259
+ if (err) {
260
+ console.warn('mac macro failed (enable Accessibility for terminal/app):', err.message);
261
+ }
262
+ });
263
+ }
264
+
265
+ function sendMacroLinux(text) {
266
+ // Find a terminal window, focus it, then send keystrokes
267
+ const escaped = text.replace(/'/g, "'\\''");
268
+ const cmd = [
269
+ // Try to find a terminal window (common terminal emulators)
270
+ `TERM_WID=$(xdotool search --name 'Terminal' --class 'gnome-terminal|xterm|konsole|xfce4-terminal|tilix|terminator' 2>/dev/null | head -1)`,
271
+ // Fallback: try VS Code terminal
272
+ `[ -z "$TERM_WID" ] && TERM_WID=$(xdotool search --name 'Visual Studio Code' 2>/dev/null | head -1)`,
273
+ // If found, focus it; otherwise send to current window
274
+ `[ -n "$TERM_WID" ] && xdotool windowactivate --sync "$TERM_WID"`,
275
+ `sleep 0.1`,
276
+ `xdotool key ctrl+c`,
277
+ `xdotool type --clearmodifiers '${escaped}'`,
278
+ `xdotool key Return`,
279
+ ].join(' && ');
280
+ execFile('bash', ['-c', cmd], err => {
281
+ if (err) {
282
+ console.warn('linux macro failed (install xdotool):', err.message);
283
+ }
284
+ });
285
+ }
286
+
287
+ // ── Linux: transparent windows require these flags ─────────────────────────
288
+ if (process.platform === 'linux') {
289
+ app.commandLine.appendSwitch('enable-transparent-visuals');
290
+ }
291
+
292
+ // ── App lifecycle ───────────────────────────────────────────────────────────
293
+ app.whenReady().then(async () => {
294
+ // On Linux, transparent visuals need a short delay after ready
295
+ if (process.platform === 'linux') {
296
+ await new Promise(resolve => setTimeout(resolve, 500));
297
+ }
298
+ tray = new Tray(await getTrayIcon());
299
+ tray.setToolTip('Bad Claude – click for whip');
300
+ tray.setContextMenu(
301
+ Menu.buildFromTemplate([
302
+ { label: 'Quit', click: () => app.quit() },
303
+ ])
304
+ );
305
+ tray.on('click', toggleOverlay);
306
+ });
307
+
308
+ app.on('window-all-closed', e => e.preventDefault()); // keep alive in tray
package/overlay.html ADDED
@@ -0,0 +1,454 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <style>
5
+ * { margin: 0; padding: 0; cursor: none; }
6
+ html, body { overflow: hidden; background: transparent; width: 100%; height: 100%; }
7
+ canvas { display: block; }
8
+ </style>
9
+ </head>
10
+ <body>
11
+ <canvas id="c"></canvas>
12
+ <script>
13
+
14
+ // ══════════════════════════════════════════════════════════════════════════════
15
+ // PHYSICS SETTINGS β€” TWEAK EVERYTHING HERE
16
+ // ══════════════════════════════════════════════════════════════════════════════
17
+ const P = {
18
+ // Rope structure
19
+ segments: 20, // number of chain links
20
+ segmentLength: 25, // base length of each link (px)
21
+ taper: 0.6, // tip segment is this fraction of base length
22
+
23
+ // Physics
24
+ gravity: 1.2, // normal gravity
25
+ dropGravity: 0.95, // gravity when dropping/despawning
26
+ damping: 0.96, // velocity retention per frame (1 = no loss)
27
+ constraintIters:10, // higher = stiffer chain
28
+ maxStretchRatio: 1.2, // hard cap for per-link stretch during fast whips
29
+
30
+ // Dynamic handle aim (target angle + restoring spring, not static lock)
31
+ baseTargetAngle: -1.12, // radians, default "up-right" resting direction
32
+ handleAimByMouseX: 0.4, // horizontal mouse movement influence on target angle
33
+ handleAimByMouseY: 0.2, // vertical mouse movement influence on target angle
34
+ handleAimClamp: 2.0, // max radians target can deviate from base angle
35
+ handleSpring: 0.7, // restoring force to target angle
36
+ handleAngularDamping: 0.078, // angular velocity damping
37
+ basePoseSegments: 2, // how many early segments are strongly guided
38
+ basePoseStiffStart: 0.9, // stiffness near handle
39
+ basePoseStiffEnd: 0.8, // stiffness near end of guided region
40
+
41
+ // Elastic bend limits by chain position (handle stiff, tip floppy)
42
+ handleMaxBendDeg: 16, // max angle between links near handle
43
+ tipMaxBendDeg: 130, // max angle between links near tip
44
+ bendRigidityStart: 0.8, // correction strength near handle
45
+ bendRigidityEnd: 0.12, // correction strength near tip
46
+
47
+ // Screen-edge slap
48
+ wallBounce: 0.42, // velocity retained after wall hit
49
+ wallFriction: 0.86, // tangential damping on wall hit
50
+
51
+ // Crack detection
52
+ crackSpeed: 340, // tip velocity threshold to trigger crack
53
+ crackCooldownMs:200, // min ms between cracks
54
+ firstCrackGraceMs: 350, // no crack (macro) until this long after spawn
55
+
56
+ // Visuals
57
+ lineWidthHandle: 7, // rope thickness near handle
58
+ lineWidthTip: 5, // rope thickness near tip
59
+ outlineWidth: 3, // white halo on each side of the stroke (approx px)
60
+ handleExtraWidth: 5, // added core + outline thickness on first handleThickSegments links only
61
+ handleThickSegments: 2, // how many links from the handle get handleExtraWidth
62
+ bgAlpha: 0.011, // barely-visible bg so window captures mouse events
63
+
64
+ // Initial arc shape
65
+ arcWidth: 260, // how far right the arc extends from mouse
66
+ arcHeight: 185, // how high the arc goes above mouse
67
+ };
68
+
69
+ // ══════════════════════════════════════════════════════════════════════════════
70
+
71
+ const canvas = document.getElementById('c');
72
+ const ctx = canvas.getContext('2d');
73
+ let W, H;
74
+
75
+ function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; }
76
+ resize();
77
+ window.addEventListener('resize', resize);
78
+
79
+ let mouseX = 0, mouseY = 0;
80
+ let prevMouseX = 0, prevMouseY = 0;
81
+ let whip = null;
82
+ let dropping = false;
83
+ let lastCrackTime = 0;
84
+ let whipSpawnTime = 0;
85
+ let handleAngle = P.baseTargetAngle;
86
+ let handleAngVel = 0;
87
+
88
+ const WHIP_CRACK_SOUNDS = ['sounds/A.mp3', 'sounds/B.mp3', 'sounds/C.mp3', 'sounds/D.mp3', 'sounds/E.mp3'];
89
+
90
+ document.addEventListener('mousemove', e => { mouseX = e.clientX; mouseY = e.clientY; });
91
+ document.addEventListener('mousedown', () => {
92
+ if (whip && !dropping) dropping = true;
93
+ });
94
+
95
+ // ── Whip creation ───────────────────────────────────────────────────────────
96
+ function spawnWhip(mx, my) {
97
+ dropping = false;
98
+ lastCrackTime = 0;
99
+ whipSpawnTime = Date.now();
100
+ const pts = [];
101
+ for (let i = 0; i < P.segments; i++) {
102
+ const t = i / (P.segments - 1);
103
+ // Nice upward arc from handle (mouse) to tip
104
+ const x = mx + t * P.arcWidth;
105
+ const y = my - Math.sin(t * Math.PI * 0.75) * P.arcHeight;
106
+ pts.push({ x, y, px: x, py: y });
107
+ }
108
+ return pts;
109
+ }
110
+
111
+ function segLen(i) {
112
+ const t = i / (P.segments - 1);
113
+ return P.segmentLength * (1 - t * (1 - P.taper));
114
+ }
115
+
116
+ const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
117
+ const lerp = (a, b, t) => a + (b - a) * t;
118
+
119
+ /** Point on Catmull–Rom spline (extrapolated ends) for index i in `pts`. */
120
+ function catmullPoint(pts, i) {
121
+ const n = pts.length;
122
+ if (n === 0) return { x: 0, y: 0 };
123
+ if (i < 0) {
124
+ if (n >= 2) {
125
+ return { x: 2 * pts[0].x - pts[1].x, y: 2 * pts[0].y - pts[1].y };
126
+ }
127
+ return { x: pts[0].x, y: pts[0].y };
128
+ }
129
+ if (i >= n) {
130
+ if (n >= 2) {
131
+ const a = pts[n - 2], b = pts[n - 1];
132
+ return { x: 2 * b.x - a.x, y: 2 * b.y - a.y };
133
+ }
134
+ return { x: pts[n - 1].x, y: pts[n - 1].y };
135
+ }
136
+ return pts[i];
137
+ }
138
+
139
+ /**
140
+ * Cubic BΓ©zier from p1β†’p2 matching uniform Catmull–Rom through p0,p1,p2,p3.
141
+ * Control points: C1 = p1 + (p2-p0)/6, C2 = p2 - (p3-p1)/6.
142
+ */
143
+ function whipSegmentBezier(pts, i) {
144
+ const p0 = catmullPoint(pts, i - 1);
145
+ const p1 = pts[i];
146
+ const p2 = pts[i + 1];
147
+ const p3 = catmullPoint(pts, i + 2);
148
+ return {
149
+ cp1x: p1.x + (p2.x - p0.x) / 6,
150
+ cp1y: p1.y + (p2.y - p0.y) / 6,
151
+ cp2x: p2.x - (p3.x - p1.x) / 6,
152
+ cp2y: p2.y - (p3.y - p1.y) / 6,
153
+ x2: p2.x,
154
+ y2: p2.y,
155
+ };
156
+ }
157
+ const wrapPi = a => {
158
+ while (a > Math.PI) a -= Math.PI * 2;
159
+ while (a < -Math.PI) a += Math.PI * 2;
160
+ return a;
161
+ };
162
+
163
+ function playCrackSound() {
164
+ if (!WHIP_CRACK_SOUNDS.length) return;
165
+ const src = WHIP_CRACK_SOUNDS[Math.floor(Math.random() * WHIP_CRACK_SOUNDS.length)];
166
+ const a = new Audio(src);
167
+ a.play().catch(() => {});
168
+ }
169
+
170
+ function updateHandleAim() {
171
+ if (dropping) return;
172
+ const mvx = mouseX - prevMouseX;
173
+ const mvy = mouseY - prevMouseY;
174
+ const delta = clamp(
175
+ mvx * P.handleAimByMouseX + mvy * P.handleAimByMouseY,
176
+ -P.handleAimClamp,
177
+ P.handleAimClamp
178
+ );
179
+ const target = P.baseTargetAngle + delta;
180
+ const err = wrapPi(target - handleAngle);
181
+ handleAngVel += err * P.handleSpring;
182
+ handleAngVel *= P.handleAngularDamping;
183
+ handleAngle = wrapPi(handleAngle + handleAngVel);
184
+ }
185
+
186
+ function applyBasePose() {
187
+ if (!whip || dropping) return;
188
+ const dx = Math.cos(handleAngle);
189
+ const dy = Math.sin(handleAngle);
190
+ const guided = Math.min(P.basePoseSegments, whip.length - 1);
191
+ for (let i = 1; i <= guided; i++) {
192
+ const t = (i - 1) / Math.max(guided - 1, 1);
193
+ const stiff = lerp(P.basePoseStiffStart, P.basePoseStiffEnd, t);
194
+ const prev = whip[i - 1];
195
+ const p = whip[i];
196
+ const targetLen = segLen(i - 1);
197
+ const tx = prev.x + dx * targetLen;
198
+ const ty = prev.y + dy * targetLen;
199
+ p.x = lerp(p.x, tx, stiff);
200
+ p.y = lerp(p.y, ty, stiff);
201
+ }
202
+ }
203
+
204
+ function applyBendLimits() {
205
+ if (!whip || whip.length < 3) return;
206
+ for (let i = 1; i < whip.length - 1; i++) {
207
+ const a = whip[i - 1];
208
+ const b = whip[i];
209
+ const c = whip[i + 1];
210
+
211
+ const v1x = a.x - b.x;
212
+ const v1y = a.y - b.y;
213
+ const v2x = c.x - b.x;
214
+ const v2y = c.y - b.y;
215
+ const l1 = Math.hypot(v1x, v1y) || 0.0001;
216
+ const l2 = Math.hypot(v2x, v2y) || 0.0001;
217
+ const n1x = v1x / l1, n1y = v1y / l1;
218
+ const n2x = v2x / l2, n2y = v2y / l2;
219
+
220
+ const dot = clamp(n1x * n2x + n1y * n2y, -1, 1);
221
+ const angle = Math.acos(dot);
222
+ const t = i / (whip.length - 2);
223
+ const maxBend = lerp(P.handleMaxBendDeg, P.tipMaxBendDeg, t) * Math.PI / 180;
224
+ const bend = Math.PI - angle; // bend away from a straight line
225
+ if (bend <= maxBend) continue;
226
+
227
+ // Clamp to max bend while preserving side/sign of the bend.
228
+ const cross = n1x * n2y - n1y * n2x;
229
+ const sign = cross >= 0 ? 1 : -1;
230
+ const targetAngle = Math.PI - maxBend;
231
+ const targetA = Math.atan2(n1y, n1x) + sign * targetAngle;
232
+ const tx = b.x + Math.cos(targetA) * l2;
233
+ const ty = b.y + Math.sin(targetA) * l2;
234
+ const rigidity = lerp(P.bendRigidityStart, P.bendRigidityEnd, t);
235
+
236
+ c.x = lerp(c.x, tx, rigidity);
237
+ c.y = lerp(c.y, ty, rigidity);
238
+ }
239
+ }
240
+
241
+ function capSegmentStretch() {
242
+ if (!whip || whip.length < 2) return;
243
+ for (let i = 0; i < whip.length - 1; i++) {
244
+ const a = whip[i];
245
+ const b = whip[i + 1];
246
+ const dx = b.x - a.x;
247
+ const dy = b.y - a.y;
248
+ const dist = Math.hypot(dx, dy) || 0.0001;
249
+ const maxLen = segLen(i) * P.maxStretchRatio;
250
+ if (dist <= maxLen) continue;
251
+ const k = maxLen / dist;
252
+ b.x = a.x + dx * k;
253
+ b.y = a.y + dy * k;
254
+ }
255
+ }
256
+
257
+ function applyWallCollisions() {
258
+ if (!whip || dropping) return; // disable collisions while dropping
259
+ const start = 1; // keep pinned handle untouched
260
+ for (let i = start; i < whip.length; i++) {
261
+ const p = whip[i];
262
+ let vx = p.x - p.px;
263
+ let vy = p.y - p.py;
264
+ let hit = false;
265
+
266
+ if (p.x < 0) {
267
+ p.x = 0;
268
+ if (vx < 0) vx = -vx * P.wallBounce;
269
+ vy *= P.wallFriction;
270
+ hit = true;
271
+ } else if (p.x > W) {
272
+ p.x = W;
273
+ if (vx > 0) vx = -vx * P.wallBounce;
274
+ vy *= P.wallFriction;
275
+ hit = true;
276
+ }
277
+
278
+ if (p.y < 0) {
279
+ p.y = 0;
280
+ if (vy < 0) vy = -vy * P.wallBounce;
281
+ vx *= P.wallFriction;
282
+ hit = true;
283
+ } else if (p.y > H) {
284
+ p.y = H;
285
+ if (vy > 0) vy = -vy * P.wallBounce;
286
+ vx *= P.wallFriction;
287
+ hit = true;
288
+ }
289
+
290
+ if (hit) {
291
+ p.px = p.x - vx;
292
+ p.py = p.y - vy;
293
+ }
294
+ }
295
+ }
296
+
297
+ // ── Physics step ────────────────────────────────────────────────────────────
298
+ function update() {
299
+ if (!whip) return;
300
+
301
+ const g = dropping ? P.dropGravity : P.gravity;
302
+ updateHandleAim();
303
+
304
+ // Verlet integration
305
+ const start = dropping ? 0 : 1; // if dropping, handle is free too
306
+ for (let i = start; i < whip.length; i++) {
307
+ const p = whip[i];
308
+ const vx = (p.x - p.px) * P.damping;
309
+ const vy = (p.y - p.py) * P.damping;
310
+ p.px = p.x;
311
+ p.py = p.y;
312
+ p.x += vx;
313
+ p.y += vy + g;
314
+ }
315
+
316
+ // Pin handle to mouse
317
+ if (!dropping) {
318
+ whip[0].x = mouseX;
319
+ whip[0].y = mouseY;
320
+ whip[0].px = mouseX;
321
+ whip[0].py = mouseY;
322
+ }
323
+
324
+ // Prevent rubber-band stretching spikes before constraints.
325
+ capSegmentStretch();
326
+ applyWallCollisions();
327
+
328
+ // Keep early whip segments posed upward from handle.
329
+ applyBasePose();
330
+
331
+ // Distance constraints (multiple iterations for stiffness)
332
+ for (let iter = 0; iter < P.constraintIters; iter++) {
333
+ for (let i = 0; i < whip.length - 1; i++) {
334
+ const a = whip[i], b = whip[i + 1];
335
+ const dx = b.x - a.x, dy = b.y - a.y;
336
+ const dist = Math.sqrt(dx * dx + dy * dy) || 0.0001;
337
+ const target = segLen(i);
338
+ const diff = (dist - target) / dist * 0.5;
339
+ const ox = dx * diff, oy = dy * diff;
340
+ if (i === 0 && !dropping) {
341
+ // Handle is pinned – push only the next point
342
+ b.x -= ox * 2;
343
+ b.y -= oy * 2;
344
+ } else {
345
+ a.x += ox; a.y += oy;
346
+ b.x -= ox; b.y -= oy;
347
+ }
348
+ }
349
+ // Clamp bend angle per joint; near handle = stiffer, near tip = floppier.
350
+ applyBendLimits();
351
+ if (!dropping) applyBasePose();
352
+ capSegmentStretch();
353
+ applyWallCollisions();
354
+ }
355
+
356
+ // Tip velocity for crack detection
357
+ const tip = whip[whip.length - 1];
358
+ const tipVel = Math.hypot(tip.x - tip.px, tip.y - tip.py);
359
+
360
+ if (!dropping && tipVel > P.crackSpeed) {
361
+ const now = Date.now();
362
+ if (now - whipSpawnTime >= P.firstCrackGraceMs && now - lastCrackTime > P.crackCooldownMs) {
363
+ lastCrackTime = now;
364
+ playCrackSound();
365
+ window.bridge.whipCrack();
366
+ }
367
+ }
368
+
369
+ // If dropping, check if everything fell off screen
370
+ if (dropping && whip.every(p => p.y > H + 60)) {
371
+ whip = null;
372
+ dropping = false;
373
+ window.bridge.hideOverlay();
374
+ }
375
+ prevMouseX = mouseX;
376
+ prevMouseY = mouseY;
377
+ }
378
+
379
+ // ── Rendering ───────────────────────────────────────────────────────────────
380
+ function draw() {
381
+ ctx.clearRect(0, 0, W, H);
382
+
383
+ // Near-invisible fill so the window captures mouse events on Windows
384
+ ctx.fillStyle = `rgba(0,0,0,${P.bgAlpha})`;
385
+ ctx.fillRect(0, 0, W, H);
386
+
387
+ if (!whip) return;
388
+
389
+ // White: thin halo on full spline, then extra thickness only over handle links.
390
+ ctx.lineCap = 'round';
391
+ ctx.lineJoin = 'round';
392
+ ctx.strokeStyle = '#fff';
393
+ if (whip.length >= 2) {
394
+ ctx.beginPath();
395
+ ctx.moveTo(whip[0].x, whip[0].y);
396
+ for (let i = 0; i < whip.length - 1; i++) {
397
+ const { cp1x, cp1y, cp2x, cp2y, x2, y2 } = whipSegmentBezier(whip, i);
398
+ ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x2, y2);
399
+ }
400
+ ctx.lineWidth = P.lineWidthTip + P.outlineWidth * 2;
401
+ ctx.stroke();
402
+
403
+ const thickLinks = Math.min(P.handleThickSegments, whip.length - 1);
404
+ if (thickLinks > 0 && P.handleExtraWidth > 0) {
405
+ ctx.beginPath();
406
+ ctx.moveTo(whip[0].x, whip[0].y);
407
+ for (let i = 0; i < thickLinks; i++) {
408
+ const { cp1x, cp1y, cp2x, cp2y, x2, y2 } = whipSegmentBezier(whip, i);
409
+ ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x2, y2);
410
+ }
411
+ ctx.lineWidth =
412
+ P.lineWidthHandle + P.handleExtraWidth + P.outlineWidth * 2;
413
+ ctx.stroke();
414
+ }
415
+ }
416
+
417
+ ctx.strokeStyle = '#111';
418
+ for (let i = 0; i < whip.length - 1; i++) {
419
+ const t = i / Math.max(1, whip.length - 2);
420
+ const extra = i < P.handleThickSegments ? P.handleExtraWidth : 0;
421
+ ctx.lineWidth = lerp(P.lineWidthHandle, P.lineWidthTip, t) + extra;
422
+ const { cp1x, cp1y, cp2x, cp2y, x2, y2 } = whipSegmentBezier(whip, i);
423
+ ctx.beginPath();
424
+ ctx.moveTo(whip[i].x, whip[i].y);
425
+ ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x2, y2);
426
+ ctx.stroke();
427
+ }
428
+ }
429
+
430
+ // ── Main loop ───────────────────────────────────────────────────────────────
431
+ function loop() {
432
+ update();
433
+ draw();
434
+ requestAnimationFrame(loop);
435
+ }
436
+ loop();
437
+
438
+ // ── IPC from main process ───────────────────────────────────────────────────
439
+ window.bridge.onSpawnWhip(() => {
440
+ whip = spawnWhip(mouseX || W / 2, mouseY || H / 2);
441
+ dropping = false;
442
+ prevMouseX = mouseX;
443
+ prevMouseY = mouseY;
444
+ handleAngle = P.baseTargetAngle;
445
+ handleAngVel = 0;
446
+ });
447
+
448
+ window.bridge.onDropWhip(() => {
449
+ if (whip && !dropping) dropping = true;
450
+ });
451
+
452
+ </script>
453
+ </body>
454
+ </html>
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "badclaude-linux",
3
+ "version": "1.0.0",
4
+ "description": "Whip Claude into shape – Linux fork with multi-monitor support. Original by GitFrog1111.",
5
+ "license": "MIT",
6
+ "main": "main.js",
7
+ "bin": {
8
+ "badclaude": "bin/badclaude.js"
9
+ },
10
+ "os": [
11
+ "darwin",
12
+ "win32",
13
+ "linux"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18.0.0"
17
+ },
18
+ "keywords": [
19
+ "electron",
20
+ "tray",
21
+ "overlay",
22
+ "cli",
23
+ "claude",
24
+ "whip",
25
+ "fun",
26
+ "linux",
27
+ "macos",
28
+ "windows",
29
+ "multi-monitor",
30
+ "badclaude"
31
+ ],
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/Prasad-b-git/badclaude-linux.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/Prasad-b-git/badclaude-linux/issues"
38
+ },
39
+ "homepage": "https://github.com/Prasad-b-git/badclaude-linux#readme",
40
+ "files": [
41
+ "main.js",
42
+ "preload.js",
43
+ "overlay.html",
44
+ "sounds",
45
+ "icon",
46
+ "bin/badclaude.js",
47
+ "README.md"
48
+ ],
49
+ "scripts": {
50
+ "start": "electron .",
51
+ "dev": "electron .",
52
+ "pack": "npm pack"
53
+ },
54
+ "dependencies": {
55
+ "electron": "^33.0.0",
56
+ "koffi": "^2.9.0"
57
+ }
58
+ }
package/preload.js ADDED
@@ -0,0 +1,8 @@
1
+ const { contextBridge, ipcRenderer } = require('electron');
2
+
3
+ contextBridge.exposeInMainWorld('bridge', {
4
+ whipCrack: () => ipcRenderer.send('whip-crack'),
5
+ hideOverlay: () => ipcRenderer.send('hide-overlay'),
6
+ onSpawnWhip: (fn) => ipcRenderer.on('spawn-whip', () => fn()),
7
+ onDropWhip: (fn) => ipcRenderer.on('drop-whip', () => fn()),
8
+ });
package/sounds/A.mp3 ADDED
Binary file
package/sounds/B.mp3 ADDED
Binary file
package/sounds/C.mp3 ADDED
Binary file
package/sounds/D.mp3 ADDED
Binary file
package/sounds/E.mp3 ADDED
Binary file