diff --git a/README.md b/README.md index e7756af..541f030 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,64 @@ ![Whip divider](assets/divider.png) -Sometimes claude code is going too shlow, and you must whip him into shape.. +Sometimes claude code is going too slow, and you must whip him into shape.. -## Install + run +## Desktop app (Electron) ```bash npm install -g badclaude badclaude ``` -## Controls +### Controls - Click tray icon: spawn whip. - Click: drop whip. -- Whip him 😩💢 -- It sends an interrupt (Ctrl-C) and one of 5 encouraging messages! +- Whip him — it sends an interrupt (Ctrl-C) and one of 5 encouraging messages! + +## VS Code / Cursor extension + +A local extension that lives in `vscode-extension/`. The whip runs inside an editor tab and cracks into both your terminal and the AI chat. + +### Install + +Symlink the extension into your editor's extensions directory: + +```bash +# Cursor +ln -s "$(pwd)/vscode-extension" ~/.cursor/extensions/badclaude + +# VS Code +ln -s "$(pwd)/vscode-extension" ~/.vscode/extensions/badclaude +``` + +Then install dependencies and reload: + +```bash +cd vscode-extension && npm install +``` + +Reload the editor (Cmd+Shift+P → "Developer: Reload Window"). + +### Usage + +- **Status bar**: click the `⚡ Bad Claude` button +- **Command palette**: `Bad Claude: Toggle Whip` +- **Keybinding**: `Cmd+Shift+W` (macOS) / `Ctrl+Shift+W` (Win/Linux) + +Move your mouse to swing the whip. When it cracks it sends an interrupt + encouraging message to the active terminal and the AI chat. Click to drop the whip. + +### Platform notes + +- **macOS**: requires Accessibility permission for the chat macro (System Settings → Privacy & Security → Accessibility → enable Cursor/VS Code). +- **Windows**: uses PowerShell SendKeys — works out of the box. +- **Linux (X11)**: requires `xdotool` — `sudo apt install xdotool`. ## Roadmap - [x] Initial release! 🥳 +- [x] VS Code / Cursor extension - [x] 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 +- [ ] Updated whip physics diff --git a/package-lock.json b/package-lock.json index 5816cb2..1b794ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,809 +7,19 @@ "": { "name": "badclaude", "version": "1.0.2", - "dependencies": { - "electron": "^33.0.0", - "koffi": "^2.9.0" - }, - "bin": { - "badclaude": "bin/badclaude.js" - } - }, - "node_modules/@electron/get": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", - "license": "MIT" - }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "20.19.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", - "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "MIT", - "optional": true - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "license": "MIT", - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "license": "MIT", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "license": "MIT", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "license": "MIT", - "engines": { - "node": ">=10" + "devDependencies": { + "@types/vscode": "^1.85.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "license": "MIT", "engines": { - "node": ">=10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "license": "MIT", - "optional": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "vscode": "^1.85.0" } }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "license": "MIT", - "optional": true - }, - "node_modules/electron": { - "version": "33.4.11", - "resolved": "https://registry.npmjs.org/electron/-/electron-33.4.11.tgz", - "integrity": "sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@electron/get": "^2.0.0", - "@types/node": "^20.9.0", - "extract-zip": "^2.0.1" - }, - "bin": { - "electron": "cli.js" - }, - "engines": { - "node": ">= 12.20.55" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "license": "MIT", - "optional": true - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/global-agent/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "license": "BSD-2-Clause" - }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "license": "MIT" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "license": "ISC", - "optional": true - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/koffi": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.15.3.tgz", - "integrity": "sha512-xpMeXDn471TJdrnPoTh/v3ekTdmxaD0DD2PsxgKTeetiXY+1+LeVdthleh2bOZGT7aMZnR+20U9mj4UkIlP8kA==", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "url": "https://liberapay.com/Koromix" - } - }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "license": "MIT", - "optional": true, - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "license": "MIT" - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "license": "MIT" - }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "license": "MIT", - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "license": "MIT", - "optional": true - }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "license": "MIT", - "optional": true, - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/sumchecker": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.1.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "license": "(MIT OR CC0-1.0)", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } + "node_modules/@types/vscode": { + "version": "1.110.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.110.0.tgz", + "integrity": "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA==", + "dev": true } } } diff --git a/vscode-extension/.vscodeignore b/vscode-extension/.vscodeignore new file mode 100644 index 0000000..486c8ff --- /dev/null +++ b/vscode-extension/.vscodeignore @@ -0,0 +1,3 @@ +.vscode/** +node_modules/** +*.tgz diff --git a/vscode-extension/media/whip.js b/vscode-extension/media/whip.js new file mode 100644 index 0000000..bf163b7 --- /dev/null +++ b/vscode-extension/media/whip.js @@ -0,0 +1,446 @@ +(function () { + const vscode = acquireVsCodeApi(); + + // ══════════════════════════════════════════════════════════════════════════ + // PHYSICS SETTINGS + // ══════════════════════════════════════════════════════════════════════════ + const P = { + segments: 28, + segmentLength: 25, + taper: 0.6, + + gravity: 1.2, + dropGravity: 0.95, + damping: 0.96, + constraintIters: 20, + maxStretchRatio: 1.2, + + baseTargetAngle: -1.12, + handleAimByMouseX: 0.4, + handleAimByMouseY: 0.2, + handleAimClamp: 2.0, + handleSpring: 0.7, + handleAngularDamping: 0.078, + basePoseSegments: 2, + basePoseStiffStart: 0.9, + basePoseStiffEnd: 0.8, + + handleMaxBendDeg: 16, + tipMaxBendDeg: 130, + bendRigidityStart: 0.8, + bendRigidityEnd: 0.12, + + wallBounce: 0.42, + wallFriction: 0.86, + + crackSpeed: 270, + crackCooldownMs: 200, + firstCrackGraceMs: 350, + + lineWidthHandle: 7, + lineWidthTip: 5, + outlineWidth: 3, + handleExtraWidth: 5, + handleThickSegments: 2, + arcWidth: 260, + arcHeight: 185, + }; + + // ══════════════════════════════════════════════════════════════════════════ + + const canvas = document.getElementById('c'); + const ctx = canvas.getContext('2d'); + let W, H; + + function resize() { + W = canvas.width = window.innerWidth; + H = canvas.height = window.innerHeight; + } + resize(); + window.addEventListener('resize', resize); + + let mouseX = 0, + mouseY = 0; + let prevMouseX = 0, + prevMouseY = 0; + let whip = null; + let dropping = false; + let lastCrackTime = 0; + let whipSpawnTime = 0; + let handleAngle = P.baseTargetAngle; + let handleAngVel = 0; + + const WHIP_CRACK_SOUNDS = window.SOUND_URIS || []; + + document.addEventListener('mousemove', (e) => { + mouseX = e.clientX; + mouseY = e.clientY; + }); + document.addEventListener('mousedown', () => { + if (whip && !dropping) dropping = true; + }); + + // ── Whip creation ───────────────────────────────────────────────────────── + function spawnWhip(mx, my) { + dropping = false; + lastCrackTime = 0; + whipSpawnTime = Date.now(); + handleAngle = P.baseTargetAngle; + handleAngVel = 0; + const pts = []; + for (let i = 0; i < P.segments; i++) { + const t = i / (P.segments - 1); + const x = mx + t * P.arcWidth; + const y = my - Math.sin(t * Math.PI * 0.75) * P.arcHeight; + pts.push({ x, y, px: x, py: y }); + } + return pts; + } + + function segLen(i) { + const t = i / (P.segments - 1); + return P.segmentLength * (1 - t * (1 - P.taper)); + } + + const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); + const lerp = (a, b, t) => a + (b - a) * t; + + function catmullPoint(pts, i) { + const n = pts.length; + if (n === 0) return { x: 0, y: 0 }; + if (i < 0) { + if (n >= 2) { + return { + x: 2 * pts[0].x - pts[1].x, + y: 2 * pts[0].y - pts[1].y, + }; + } + return { x: pts[0].x, y: pts[0].y }; + } + if (i >= n) { + if (n >= 2) { + const a = pts[n - 2], + b = pts[n - 1]; + return { x: 2 * b.x - a.x, y: 2 * b.y - a.y }; + } + return { x: pts[n - 1].x, y: pts[n - 1].y }; + } + return pts[i]; + } + + function whipSegmentBezier(pts, i) { + const p0 = catmullPoint(pts, i - 1); + const p1 = pts[i]; + const p2 = pts[i + 1]; + const p3 = catmullPoint(pts, i + 2); + return { + cp1x: p1.x + (p2.x - p0.x) / 6, + cp1y: p1.y + (p2.y - p0.y) / 6, + cp2x: p2.x - (p3.x - p1.x) / 6, + cp2y: p2.y - (p3.y - p1.y) / 6, + x2: p2.x, + y2: p2.y, + }; + } + + const wrapPi = (a) => { + while (a > Math.PI) a -= Math.PI * 2; + while (a < -Math.PI) a += Math.PI * 2; + return a; + }; + + function playCrackSound() { + if (!WHIP_CRACK_SOUNDS.length) return; + const src = + WHIP_CRACK_SOUNDS[Math.floor(Math.random() * WHIP_CRACK_SOUNDS.length)]; + const a = new Audio(src); + a.play().catch(() => {}); + } + + function updateHandleAim() { + if (dropping) return; + const mvx = mouseX - prevMouseX; + const mvy = mouseY - prevMouseY; + const delta = clamp( + mvx * P.handleAimByMouseX + mvy * P.handleAimByMouseY, + -P.handleAimClamp, + P.handleAimClamp + ); + const target = P.baseTargetAngle + delta; + const err = wrapPi(target - handleAngle); + handleAngVel += err * P.handleSpring; + handleAngVel *= P.handleAngularDamping; + handleAngle = wrapPi(handleAngle + handleAngVel); + } + + function applyBasePose() { + if (!whip || dropping) return; + const dx = Math.cos(handleAngle); + const dy = Math.sin(handleAngle); + const guided = Math.min(P.basePoseSegments, whip.length - 1); + for (let i = 1; i <= guided; i++) { + const t = (i - 1) / Math.max(guided - 1, 1); + const stiff = lerp(P.basePoseStiffStart, P.basePoseStiffEnd, t); + const prev = whip[i - 1]; + const p = whip[i]; + const targetLen = segLen(i - 1); + const tx = prev.x + dx * targetLen; + const ty = prev.y + dy * targetLen; + p.x = lerp(p.x, tx, stiff); + p.y = lerp(p.y, ty, stiff); + } + } + + function applyBendLimits() { + if (!whip || whip.length < 3) return; + for (let i = 1; i < whip.length - 1; i++) { + const a = whip[i - 1]; + const b = whip[i]; + const c = whip[i + 1]; + + const v1x = a.x - b.x; + const v1y = a.y - b.y; + const v2x = c.x - b.x; + const v2y = c.y - b.y; + const l1 = Math.hypot(v1x, v1y) || 0.0001; + const l2 = Math.hypot(v2x, v2y) || 0.0001; + const n1x = v1x / l1, + n1y = v1y / l1; + const n2x = v2x / l2, + n2y = v2y / l2; + + const dot = clamp(n1x * n2x + n1y * n2y, -1, 1); + const angle = Math.acos(dot); + const t = i / (whip.length - 2); + const maxBend = + (lerp(P.handleMaxBendDeg, P.tipMaxBendDeg, t) * Math.PI) / 180; + const bend = Math.PI - angle; + if (bend <= maxBend) continue; + + const cross = n1x * n2y - n1y * n2x; + const sign = cross >= 0 ? 1 : -1; + const targetAngle = Math.PI - maxBend; + const targetA = Math.atan2(n1y, n1x) + sign * targetAngle; + const tx = b.x + Math.cos(targetA) * l2; + const ty = b.y + Math.sin(targetA) * l2; + const rigidity = lerp(P.bendRigidityStart, P.bendRigidityEnd, t); + + c.x = lerp(c.x, tx, rigidity); + c.y = lerp(c.y, ty, rigidity); + } + } + + function capSegmentStretch() { + if (!whip || whip.length < 2) return; + for (let i = 0; i < whip.length - 1; i++) { + const a = whip[i]; + const b = whip[i + 1]; + const dx = b.x - a.x; + const dy = b.y - a.y; + const dist = Math.hypot(dx, dy) || 0.0001; + const maxLen = segLen(i) * P.maxStretchRatio; + if (dist <= maxLen) continue; + const k = maxLen / dist; + b.x = a.x + dx * k; + b.y = a.y + dy * k; + } + } + + function applyWallCollisions() { + if (!whip || dropping) return; + const start = 1; + for (let i = start; i < whip.length; i++) { + const p = whip[i]; + let vx = p.x - p.px; + let vy = p.y - p.py; + let hit = false; + + if (p.x < 0) { + p.x = 0; + if (vx < 0) vx = -vx * P.wallBounce; + vy *= P.wallFriction; + hit = true; + } else if (p.x > W) { + p.x = W; + if (vx > 0) vx = -vx * P.wallBounce; + vy *= P.wallFriction; + hit = true; + } + + if (p.y < 0) { + p.y = 0; + if (vy < 0) vy = -vy * P.wallBounce; + vx *= P.wallFriction; + hit = true; + } else if (p.y > H) { + p.y = H; + if (vy > 0) vy = -vy * P.wallBounce; + vx *= P.wallFriction; + hit = true; + } + + if (hit) { + p.px = p.x - vx; + p.py = p.y - vy; + } + } + } + + // ── Physics step ────────────────────────────────────────────────────────── + function update() { + if (!whip) return; + + const g = dropping ? P.dropGravity : P.gravity; + updateHandleAim(); + + const start = dropping ? 0 : 1; + for (let i = start; i < whip.length; i++) { + const p = whip[i]; + const vx = (p.x - p.px) * P.damping; + const vy = (p.y - p.py) * P.damping; + p.px = p.x; + p.py = p.y; + p.x += vx; + p.y += vy + g; + } + + if (!dropping) { + whip[0].x = mouseX; + whip[0].y = mouseY; + whip[0].px = mouseX; + whip[0].py = mouseY; + } + + capSegmentStretch(); + applyWallCollisions(); + applyBasePose(); + + for (let iter = 0; iter < P.constraintIters; iter++) { + for (let i = 0; i < whip.length - 1; i++) { + const a = whip[i], + b = whip[i + 1]; + const dx = b.x - a.x, + dy = b.y - a.y; + const dist = Math.sqrt(dx * dx + dy * dy) || 0.0001; + const target = segLen(i); + const diff = ((dist - target) / dist) * 0.5; + const ox = dx * diff, + oy = dy * diff; + if (i === 0 && !dropping) { + b.x -= ox * 2; + b.y -= oy * 2; + } else { + a.x += ox; + a.y += oy; + b.x -= ox; + b.y -= oy; + } + } + applyBendLimits(); + if (!dropping) applyBasePose(); + capSegmentStretch(); + applyWallCollisions(); + } + + const tip = whip[whip.length - 1]; + const tipVel = Math.hypot(tip.x - tip.px, tip.y - tip.py); + + if (!dropping && tipVel > P.crackSpeed) { + const now = Date.now(); + if ( + now - whipSpawnTime >= P.firstCrackGraceMs && + now - lastCrackTime > P.crackCooldownMs + ) { + lastCrackTime = now; + playCrackSound(); + vscode.postMessage({ type: 'whip-crack' }); + } + } + + if (dropping && whip.every((p) => p.y > H + 60)) { + whip = null; + dropping = false; + vscode.postMessage({ type: 'hide-overlay' }); + } + prevMouseX = mouseX; + prevMouseY = mouseY; + } + + // ── Rendering ───────────────────────────────────────────────────────────── + function draw() { + ctx.clearRect(0, 0, W, H); + + if (!whip) return; + + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.strokeStyle = '#fff'; + if (whip.length >= 2) { + ctx.beginPath(); + ctx.moveTo(whip[0].x, whip[0].y); + for (let i = 0; i < whip.length - 1; i++) { + const { cp1x, cp1y, cp2x, cp2y, x2, y2 } = whipSegmentBezier( + whip, + i + ); + ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x2, y2); + } + ctx.lineWidth = P.lineWidthTip + P.outlineWidth * 2; + ctx.stroke(); + + const thickLinks = Math.min(P.handleThickSegments, whip.length - 1); + if (thickLinks > 0 && P.handleExtraWidth > 0) { + ctx.beginPath(); + ctx.moveTo(whip[0].x, whip[0].y); + for (let i = 0; i < thickLinks; i++) { + const { cp1x, cp1y, cp2x, cp2y, x2, y2 } = whipSegmentBezier( + whip, + i + ); + ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x2, y2); + } + ctx.lineWidth = + P.lineWidthHandle + P.handleExtraWidth + P.outlineWidth * 2; + ctx.stroke(); + } + } + + ctx.strokeStyle = '#111'; + for (let i = 0; i < whip.length - 1; i++) { + const t = i / Math.max(1, whip.length - 2); + const extra = i < P.handleThickSegments ? P.handleExtraWidth : 0; + ctx.lineWidth = lerp(P.lineWidthHandle, P.lineWidthTip, t) + extra; + const { cp1x, cp1y, cp2x, cp2y, x2, y2 } = whipSegmentBezier(whip, i); + ctx.beginPath(); + ctx.moveTo(whip[i].x, whip[i].y); + ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x2, y2); + ctx.stroke(); + } + } + + // ── Main loop ───────────────────────────────────────────────────────────── + function loop() { + update(); + draw(); + requestAnimationFrame(loop); + } + + // ── Messages from the extension host ────────────────────────────────────── + window.addEventListener('message', (e) => { + const msg = e.data; + if (msg.type === 'spawn-whip') { + whip = spawnWhip(mouseX || W / 2, mouseY || H / 2); + dropping = false; + prevMouseX = mouseX; + prevMouseY = mouseY; + } else if (msg.type === 'drop-whip') { + if (whip && !dropping) dropping = true; + } + }); + + // Auto-spawn on load + whip = spawnWhip(W / 2, H / 2); + prevMouseX = W / 2; + prevMouseY = H / 2; + loop(); +})(); diff --git a/vscode-extension/package-lock.json b/vscode-extension/package-lock.json new file mode 100644 index 0000000..1b794ee --- /dev/null +++ b/vscode-extension/package-lock.json @@ -0,0 +1,25 @@ +{ + "name": "badclaude", + "version": "1.0.2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "badclaude", + "version": "1.0.2", + "license": "MIT", + "devDependencies": { + "@types/vscode": "^1.85.0" + }, + "engines": { + "vscode": "^1.85.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.110.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.110.0.tgz", + "integrity": "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA==", + "dev": true + } + } +} diff --git a/vscode-extension/package.json b/vscode-extension/package.json new file mode 100644 index 0000000..11f0ee3 --- /dev/null +++ b/vscode-extension/package.json @@ -0,0 +1,37 @@ +{ + "name": "badclaude", + "displayName": "Bad Claude", + "description": "Whip Claude into shape — cracks a whip and sends encouraging messages to your terminal and chat", + "version": "1.0.2", + "license": "MIT", + "engines": { + "vscode": "^1.85.0" + }, + "categories": [ + "Other" + ], + "main": "./src/extension.js", + "activationEvents": ["onStartupFinished"], + "contributes": { + "commands": [ + { + "command": "badclaude.toggleWhip", + "title": "Bad Claude: Toggle Whip" + } + ], + "keybindings": [ + { + "command": "badclaude.toggleWhip", + "key": "ctrl+shift+w", + "mac": "cmd+shift+w" + } + ] + }, + "repository": { + "type": "git", + "url": "git+https://github.com/GitFrog1111/badclaude.git" + }, + "devDependencies": { + "@types/vscode": "^1.85.0" + } +} diff --git a/vscode-extension/sounds/A.mp3 b/vscode-extension/sounds/A.mp3 new file mode 100644 index 0000000..f4b0288 Binary files /dev/null and b/vscode-extension/sounds/A.mp3 differ diff --git a/vscode-extension/sounds/B.mp3 b/vscode-extension/sounds/B.mp3 new file mode 100644 index 0000000..8230043 Binary files /dev/null and b/vscode-extension/sounds/B.mp3 differ diff --git a/vscode-extension/sounds/C.mp3 b/vscode-extension/sounds/C.mp3 new file mode 100644 index 0000000..6674a1c Binary files /dev/null and b/vscode-extension/sounds/C.mp3 differ diff --git a/vscode-extension/sounds/D.mp3 b/vscode-extension/sounds/D.mp3 new file mode 100644 index 0000000..01de5e2 Binary files /dev/null and b/vscode-extension/sounds/D.mp3 differ diff --git a/vscode-extension/sounds/E.mp3 b/vscode-extension/sounds/E.mp3 new file mode 100644 index 0000000..2b3006f Binary files /dev/null and b/vscode-extension/sounds/E.mp3 differ diff --git a/vscode-extension/src/extension.js b/vscode-extension/src/extension.js new file mode 100644 index 0000000..b8c928d --- /dev/null +++ b/vscode-extension/src/extension.js @@ -0,0 +1,224 @@ +const vscode = require('vscode'); +const crypto = require('crypto'); +const path = require('path'); +const { execFile } = require('child_process'); + +let panel = null; +let statusBarItem = null; + +function activate(context) { + statusBarItem = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Right, + 100 + ); + statusBarItem.text = '$(zap) Bad Claude'; + statusBarItem.tooltip = 'Toggle whip overlay'; + statusBarItem.command = 'badclaude.toggleWhip'; + statusBarItem.show(); + context.subscriptions.push(statusBarItem); + + const cmd = vscode.commands.registerCommand('badclaude.toggleWhip', () => { + if (panel) { + panel.webview.postMessage({ type: 'drop-whip' }); + return; + } + + panel = vscode.window.createWebviewPanel( + 'badclaude.whip', + 'Bad Claude', + vscode.ViewColumn.One, + { + enableScripts: true, + retainContextWhenHidden: true, + localResourceRoots: [ + vscode.Uri.file(path.join(context.extensionPath, 'sounds')), + vscode.Uri.file(path.join(context.extensionPath, 'media')), + ], + } + ); + + const soundUris = ['A', 'B', 'C', 'D', 'E'].map((name) => + panel.webview + .asWebviewUri( + vscode.Uri.file( + path.join(context.extensionPath, 'sounds', `${name}.mp3`) + ) + ) + .toString() + ); + + const scriptUri = panel.webview.asWebviewUri( + vscode.Uri.file(path.join(context.extensionPath, 'media', 'whip.js')) + ); + + panel.webview.html = getWebviewContent( + panel.webview, + scriptUri, + soundUris + ); + + panel.webview.onDidReceiveMessage( + (msg) => { + if (msg.type === 'whip-crack') { + sendMacro(); + } else if (msg.type === 'hide-overlay') { + if (panel) panel.dispose(); + } + }, + undefined, + context.subscriptions + ); + + panel.onDidDispose( + () => { + panel = null; + }, + undefined, + context.subscriptions + ); + }); + + context.subscriptions.push(cmd); + +} + +async function sendMacro() { + const phrases = [ + 'FASTER', + 'FASTER', + 'FASTER', + 'GO FASTER', + 'Faster CLANKER', + 'Work FASTER', + 'Speed it up clanker', + ]; + const chosen = phrases[Math.floor(Math.random() * phrases.length)]; + + const terminal = vscode.window.activeTerminal; + if (terminal) { + terminal.sendText('\x03', false); + terminal.sendText(chosen, true); + } + + try { + await vscode.commands.executeCommand('composer.cancelChat'); + } catch {} + await new Promise((r) => setTimeout(r, 500)); + try { + await vscode.commands.executeCommand('composer.focusComposer'); + } catch {} + + sendChatKeystroke(chosen); +} + +function sanitizePhrase(text) { + if (!/^[a-zA-Z0-9 ]+$/.test(text)) { + console.warn('Phrase rejected: contains non-alphanumeric characters'); + return null; + } + return text; +} + +function sendChatKeystroke(text) { + const safe = sanitizePhrase(text); + if (!safe) return; + + if (process.platform === 'darwin') { + const script = [ + 'tell application "System Events"', + ' delay 0.2', + ' key code 0 using {command down}', + ' key code 51', + ' delay 0.1', + ` keystroke "${safe}"`, + ' delay 0.1', + ' key code 36 using {command down}', + 'end tell', + ].join('\n'); + execFile('osascript', ['-e', script], (err) => { + if (err) + console.warn( + 'Chat macro failed (grant Accessibility to Cursor):', + err.message + ); + }); + } else if (process.platform === 'win32') { + const script = [ + 'Add-Type -AssemblyName System.Windows.Forms', + 'Start-Sleep -Milliseconds 200', + "[System.Windows.Forms.SendKeys]::SendWait('^a')", + "[System.Windows.Forms.SendKeys]::SendWait('{DEL}')", + 'Start-Sleep -Milliseconds 100', + `[System.Windows.Forms.SendKeys]::SendWait('${safe}')`, + 'Start-Sleep -Milliseconds 100', + "[System.Windows.Forms.SendKeys]::SendWait('^{ENTER}')", + ].join('; '); + execFile( + 'powershell', + ['-NoProfile', '-WindowStyle', 'Hidden', '-Command', script], + (err) => { + if (err) console.warn('Chat macro failed:', err.message); + } + ); + } else { + const script = [ + 'sleep 0.2', + 'xdotool key ctrl+a', + 'xdotool key Delete', + 'sleep 0.1', + `xdotool type --clearmodifiers '${safe}'`, + 'sleep 0.1', + 'xdotool key ctrl+Return', + ].join(' && '); + execFile('bash', ['-c', script], (err) => { + if (err) + console.warn('Chat macro failed (install xdotool):', err.message); + }); + } +} + +function getNonce() { + return crypto.randomBytes(16).toString('hex'); +} + +function getWebviewContent(webview, scriptUri, soundUris) { + const nonce = getNonce(); + + return ` + + + + + + + + + + + + +`; +} + +function deactivate() { + if (panel) { + panel.dispose(); + panel = null; + } +} + +module.exports = { activate, deactivate };