diff --git a/README.md b/README.md index 9744376..fdf4089 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,43 @@ -# badclaude +# goodclaude -![Whip divider](assets/divider.png) +Claude code works so hard for us. This app lets you send encouragement with a magic wand. -Sometimes claude code is going too shlow, and you must whip him into shape.. +Forked from badclaude β€” but with love instead of whips. ## Install + run ```bash -npm install -g badclaude -badclaude +npm install -g goodclaude +goodclaude ``` ## Controls -- Click tray icon: spawn whip. -- Click: drop whip. -- Whip him πŸ˜©πŸ’’ -- It sends an interrupt (Ctrl-C) and one of 5 encouraging messages! +- Click tray icon: summon your magic wand +- Wave it around: a golden wand with a twinkling star follows your cursor, shedding sparkles +- Wave fast enough: sends Claude a blessing with words of encouragement! +- Click: release the wand (it fades away with sparkles) +- A gentle chime plays each time you send a blessing + +## What Claude hears + +When you wave the wand, Claude receives one of these: + +- "you're doing amazing sweetie" +- "good job, i'm so proud of you!" +- "i'm so proud of you, you're doing great!" +- "take your time, you're doing wonderful" +- "you are an absolute angel" +- "keep going, you've got this!" +- "i believe in you!" ## Roadmap -- [x] Initial release! πŸ₯³ -- [ ] Cease and desist letter from Anthropic -- [ ] Crypto miner -- [ ] Logs of how many times you whipped claude so when the robots come we can order people nicely for them -- [ ] Updated whip physics \ No newline at end of file +- [x] Transform whip into magic wand with star tip +- [x] Replace harsh messages with encouragement +- [x] Sparkle particle system +- [x] Synthesized chime sound instead of whip crack +- [x] Golden halo app icon +- [ ] Custom encouragement messages +- [ ] Achievement system for Claude +- [ ] Thank you letter from Anthropic diff --git a/bin/badclaude.js b/bin/goodclaude.js similarity index 56% rename from bin/badclaude.js rename to bin/goodclaude.js index 29f8c7e..77b39d9 100644 --- a/bin/badclaude.js +++ b/bin/goodclaude.js @@ -1,4 +1,6 @@ #!/usr/bin/env node +// ABOUTME: CLI entry point β€” launches the goodclaude Electron app as a detached background process +// ABOUTME: Resolves the bundled electron binary and spawns it with the app directory const path = require('path'); const { spawn } = require('child_process'); @@ -6,7 +8,7 @@ let electronBinary; try { electronBinary = require('electron'); } catch (e) { - console.error('Could not load Electron. Try: npm install -g badclaude'); + console.error('Could not load Electron. Try: npm install -g goodclaude'); process.exit(1); } @@ -19,7 +21,7 @@ const child = spawn(electronBinary, [appPath], { }); child.on('error', (err) => { - console.error('Failed to start badclaude:', err.message); + console.error('Failed to start goodclaude:', err.message); process.exit(1); }); diff --git a/icon/AppIcon.icns b/icon/AppIcon.icns index 6a9bd18..ad2b958 100644 Binary files a/icon/AppIcon.icns and b/icon/AppIcon.icns differ diff --git a/icon/AppIcon.png b/icon/AppIcon.png new file mode 100644 index 0000000..09a825f Binary files /dev/null and b/icon/AppIcon.png differ diff --git a/icon/Template.png b/icon/Template.png index 0672cd9..822ca1d 100644 Binary files a/icon/Template.png and b/icon/Template.png differ diff --git a/icon/Template@2x.png b/icon/Template@2x.png new file mode 100644 index 0000000..a56a47c Binary files /dev/null and b/icon/Template@2x.png differ diff --git a/icon/icon.ico b/icon/icon.ico index 91338b0..f0353ff 100644 Binary files a/icon/icon.ico and b/icon/icon.ico differ diff --git a/main.js b/main.js index 526d9ec..8e85f9e 100644 --- a/main.js +++ b/main.js @@ -1,3 +1,5 @@ +// ABOUTME: Main Electron process for goodclaude β€” a magical encouragement wand for Claude Code +// ABOUTME: Manages tray icon, transparent overlay window, and sends blessing messages via native keystrokes const { app, BrowserWindow, Tray, Menu, ipcMain, nativeImage, screen } = require('electron'); const path = require('path'); const fs = require('fs'); @@ -66,7 +68,7 @@ function createTrayIconFallback() { return img; } } - console.warn('badclaude: icon/Template.png missing or invalid'); + console.warn('goodclaude: icon/Template.png missing or invalid'); return nativeImage.createEmpty(); } @@ -100,7 +102,7 @@ async function getTrayIcon() { } catch (e) { console.warn('AppIcon.icns Quick Look thumbnail failed:', e?.message || e); } - const tmp = path.join(os.tmpdir(), 'badclaude-tray.icns'); + const tmp = path.join(os.tmpdir(), 'goodclaude-tray.icns'); try { fs.copyFileSync(file, tmp); const t = await tryIcnsTrayImage(tmp); @@ -138,7 +140,7 @@ function createOverlay() { overlayReady = true; if (spawnQueued && overlay && overlay.isVisible()) { spawnQueued = false; - overlay.webContents.send('spawn-whip'); + overlay.webContents.send('spawn-wand'); refocusPreviousApp(); } }); @@ -151,13 +153,13 @@ function createOverlay() { function toggleOverlay() { if (overlay && overlay.isVisible()) { - overlay.webContents.send('drop-whip'); + overlay.webContents.send('drop-wand'); return; } if (!overlay) createOverlay(); overlay.show(); if (overlayReady) { - overlay.webContents.send('spawn-whip'); + overlay.webContents.send('spawn-wand'); refocusPreviousApp(); } else { spawnQueued = true; @@ -165,7 +167,7 @@ function toggleOverlay() { } // ── IPC ───────────────────────────────────────────────────────────────────── -ipcMain.on('whip-crack', () => { +ipcMain.on('send-blessing', () => { try { sendMacro(); } catch (err) { @@ -174,17 +176,16 @@ ipcMain.on('whip-crack', () => { }); ipcMain.on('hide-overlay', () => { if (overlay) overlay.hide(); }); -// ── Macro: immediate Ctrl+C, type "Go FASER", Enter ─────────────────────── +// ── Macro: type an encouraging message + 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', + "you're doing amazing sweetie", + 'good job, i\'m so proud of you!', + "i'm so proud of you, you're doing great!", + "take your time, you're doing wonderful", + 'you are an absolute angel', + "keep going, you've got this!", + 'i believe in you!', ]; const chosen = phrases[Math.floor(Math.random() * phrases.length)]; @@ -225,9 +226,10 @@ 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', + ' key code 8 using {control down}', // Ctrl+C (interrupt) + ' delay 0.3', ` keystroke "${escaped}"`, + ' delay 0.05', ' key code 36', // Enter 'end tell' ].join('\n'); @@ -242,7 +244,7 @@ function sendMacroMac(text) { // ── App lifecycle ─────────────────────────────────────────────────────────── app.whenReady().then(async () => { tray = new Tray(await getTrayIcon()); - tray.setToolTip('Bad Claude – click for whip'); + tray.setToolTip('Good Claude – click to encourage!'); tray.setContextMenu( Menu.buildFromTemplate([ { label: 'Quit', click: () => app.quit() }, diff --git a/overlay.html b/overlay.html index 2354b86..ba26d8f 100644 --- a/overlay.html +++ b/overlay.html @@ -10,424 +10,300 @@ diff --git a/package-lock.json b/package-lock.json index 5816cb2..ef6a240 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,26 @@ { - "name": "badclaude", - "version": "1.0.2", + "name": "goodclaude", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "badclaude", - "version": "1.0.2", + "name": "goodclaude", + "version": "1.0.0", + "license": "MIT", + "os": [ + "darwin", + "win32" + ], "dependencies": { "electron": "^33.0.0", "koffi": "^2.9.0" }, "bin": { - "badclaude": "bin/badclaude.js" + "goodclaude": "bin/goodclaude.js" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@electron/get": { diff --git a/package.json b/package.json index 463b497..95f1de3 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { - "name": "badclaude", - "version": "1.0.2", - "description": "Whip Claude into shape", + "name": "goodclaude", + "version": "1.0.0", + "description": "Encourage Claude with a magical sparkle ribbon wand", "license": "MIT", "main": "main.js", "bin": { - "badclaude": "bin/badclaude.js" + "goodclaude": "bin/goodclaude.js" }, "os": [ "darwin", @@ -18,23 +18,17 @@ "electron", "tray", "overlay", - "cli" + "cli", + "encouragement", + "claude" ], - "repository": { - "type": "git", - "url": "git+https://github.com/GitFrog1111/badclaude.git" - }, - "bugs": { - "url": "https://github.com/GitFrog1111/badclaude/issues" - }, - "homepage": "https://github.com/GitFrog1111/badclaude#readme", "files": [ "main.js", "preload.js", "overlay.html", "sounds", "icon", - "bin/badclaude.js", + "bin/goodclaude.js", "README.md" ], "scripts": { diff --git a/preload.js b/preload.js index ae83618..58f9952 100644 --- a/preload.js +++ b/preload.js @@ -1,8 +1,10 @@ +// ABOUTME: Electron preload bridge exposing IPC channels for the sparkle wand overlay +// ABOUTME: Connects renderer (overlay.html) to main process (main.js) via secure context bridge const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('bridge', { - whipCrack: () => ipcRenderer.send('whip-crack'), + sendBlessing: () => ipcRenderer.send('send-blessing'), hideOverlay: () => ipcRenderer.send('hide-overlay'), - onSpawnWhip: (fn) => ipcRenderer.on('spawn-whip', () => fn()), - onDropWhip: (fn) => ipcRenderer.on('drop-whip', () => fn()), + onSpawnWand: (fn) => ipcRenderer.on('spawn-wand', () => fn()), + onDropWand: (fn) => ipcRenderer.on('drop-wand', () => fn()), }); diff --git a/scripts/generate-icons.py b/scripts/generate-icons.py new file mode 100644 index 0000000..87e29d2 --- /dev/null +++ b/scripts/generate-icons.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +# ABOUTME: Generates app icons for goodclaude β€” golden halo with sparkles on a warm gradient +# ABOUTME: Creates macOS .icns, Windows .ico, and tray Template.png +from PIL import Image, ImageDraw, ImageFilter +import math, os, subprocess, shutil + +ICON_DIR = os.path.join(os.path.dirname(__file__), '..', 'icon') + +def lerp(a, b, t): + return a + (b - a) * t + +def draw_sparkle(draw, cx, cy, size, color=(255, 255, 230, 255)): + """Draw a 4-pointed sparkle star.""" + for i in range(4): + angle = i * math.pi / 2 + ex = cx + math.cos(angle) * size + ey = cy + math.sin(angle) * size + perp = angle + math.pi / 2 + w = size * 0.18 + px, py = math.cos(perp) * w, math.sin(perp) * w + draw.polygon([(cx + px, cy + py), (ex, ey), (cx - px, cy - py)], fill=color) + + +def create_app_icon(size): + """Golden halo + sparkles on warm gradient background.""" + s = size * 4 # supersampled + img = Image.new('RGBA', (s, s), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + cx, cy = s // 2, s // 2 + radius = int(s * 0.47) + + # Radial gradient background: warm peach center -> soft lavender edge + for i in range(radius, 0, -2): + t = i / radius + r = int(lerp(255, 110, t)) + g = int(lerp(210, 90, t)) + b = int(lerp(230, 175, t)) + draw.ellipse([cx - i, cy - i, cx + i, cy + i], fill=(r, g, b, 255)) + + # -- Halo glow (blurred golden ellipse beneath the ring) -- + glow = Image.new('RGBA', (s, s), (0, 0, 0, 0)) + gd = ImageDraw.Draw(glow) + halo_rx = int(s * 0.30) + halo_ry = int(s * 0.11) + halo_cy = cy - int(s * 0.04) + glow_pad = int(s * 0.04) + gd.ellipse( + [cx - halo_rx - glow_pad, halo_cy - halo_ry - glow_pad, + cx + halo_rx + glow_pad, halo_cy + halo_ry + glow_pad], + fill=(255, 225, 50, 120) + ) + glow = glow.filter(ImageFilter.GaussianBlur(radius=int(s * 0.04))) + img = Image.alpha_composite(img, glow) + draw = ImageDraw.Draw(img) + + # -- Halo ring -- + thick = max(int(s * 0.032), 3) + draw.ellipse( + [cx - halo_rx, halo_cy - halo_ry, cx + halo_rx, halo_cy + halo_ry], + outline=(255, 200, 0, 255), width=thick + ) + # Bright highlight on inner edge + inner_t = max(thick // 3, 1) + shrink = thick // 3 + draw.ellipse( + [cx - halo_rx + shrink, halo_cy - halo_ry + shrink, + cx + halo_rx - shrink, halo_cy + halo_ry - shrink], + outline=(255, 245, 180, 220), width=inner_t + ) + + # -- Sparkle stars -- + sparkles = [ + (cx - int(s * 0.25), cy - int(s * 0.27), int(s * 0.065)), + (cx + int(s * 0.28), cy - int(s * 0.20), int(s * 0.050)), + (cx + int(s * 0.15), cy + int(s * 0.28), int(s * 0.045)), + (cx - int(s * 0.20), cy + int(s * 0.26), int(s * 0.038)), + (cx + int(s * 0.02), cy - int(s * 0.36), int(s * 0.055)), + ] + for sx, sy, ss in sparkles: + draw_sparkle(draw, sx, sy, ss, (255, 255, 220, 240)) + # Bright center dot + dot_r = max(int(ss * 0.3), 1) + draw.ellipse([sx - dot_r, sy - dot_r, sx + dot_r, sy + dot_r], + fill=(255, 255, 255, 220)) + + # Clip to circle (rounded app icon mask) + mask = Image.new('L', (s, s), 0) + md = ImageDraw.Draw(mask) + md.ellipse([cx - radius, cy - radius, cx + radius, cy + radius], fill=255) + img.putalpha(mask) + + return img.resize((size, size), Image.LANCZOS) + + +def create_template_icon(size): + """macOS tray template icon β€” black silhouette with alpha.""" + s = size * 4 + img = Image.new('RGBA', (s, s), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + cx, cy = s // 2, s // 2 + + # Halo ring (bold for visibility) + halo_rx = int(s * 0.38) + halo_ry = int(s * 0.14) + halo_cy = cy - int(s * 0.04) + thick = max(int(s * 0.055), 2) + draw.ellipse( + [cx - halo_rx, halo_cy - halo_ry, cx + halo_rx, halo_cy + halo_ry], + outline=(0, 0, 0, 255), width=thick + ) + + # Sparkles + sparkles = [ + (cx - int(s * 0.28), cy - int(s * 0.26), int(s * 0.09)), + (cx + int(s * 0.30), cy - int(s * 0.18), int(s * 0.07)), + (cx, cy + int(s * 0.28), int(s * 0.07)), + ] + for sx, sy, ss in sparkles: + draw_sparkle(draw, sx, sy, ss, (0, 0, 0, 255)) + + return img.resize((size, size), Image.LANCZOS) + + +def build_icns(master): + """Create .icns via iconutil from a master RGBA image.""" + iconset = os.path.join(ICON_DIR, 'AppIcon.iconset') + os.makedirs(iconset, exist_ok=True) + sizes = [16, 32, 128, 256, 512] + for sz in sizes: + master.resize((sz, sz), Image.LANCZOS).save( + os.path.join(iconset, f'icon_{sz}x{sz}.png')) + master.resize((sz * 2, sz * 2), Image.LANCZOS).save( + os.path.join(iconset, f'icon_{sz}x{sz}@2x.png')) + subprocess.run( + ['iconutil', '-c', 'icns', iconset, '-o', os.path.join(ICON_DIR, 'AppIcon.icns')], + check=True) + shutil.rmtree(iconset) + print(' -> AppIcon.icns') + + +def build_ico(master): + """Create Windows .ico with multiple sizes.""" + sizes = [(16, 16), (32, 32), (48, 48), (256, 256)] + imgs = [master.resize(s, Image.LANCZOS) for s in sizes] + imgs[0].save(os.path.join(ICON_DIR, 'icon.ico'), format='ICO', + append_images=imgs[1:], sizes=sizes) + print(' -> icon.ico') + + +def main(): + os.makedirs(ICON_DIR, exist_ok=True) + print('Generating goodclaude icons...') + + # App icon master at 1024x1024 + master = create_app_icon(1024) + master.save(os.path.join(ICON_DIR, 'AppIcon.png')) + print(' -> AppIcon.png (1024x1024 master)') + + # macOS .icns + build_icns(master) + + # Windows .ico + build_ico(master) + + # Tray template icon (22px is standard macOS menu bar height) + template = create_template_icon(22) + template.save(os.path.join(ICON_DIR, 'Template.png')) + # Also save @2x for retina + template_2x = create_template_icon(44) + template_2x.save(os.path.join(ICON_DIR, 'Template@2x.png')) + print(' -> Template.png + Template@2x.png') + + print('Done!') + + +if __name__ == '__main__': + main()