mirror of
https://github.com/tiennm99/ccstatusline.git
synced 2026-10-13 18:15:21 +00:00
Initial commit
This commit is contained in:
commit
d5c193c209
13 files changed
+2222
No files matched your search
+34
@@ -0,0 +1,34 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# Source files
|
||||
src/
|
||||
*.ts
|
||||
*.tsx
|
||||
|
||||
# Development files
|
||||
bun.lock
|
||||
CLAUDE.md
|
||||
.claude/
|
||||
test-settings.json
|
||||
|
||||
# Build files
|
||||
tsconfig.json
|
||||
|
||||
# Test and example files
|
||||
*.test.*
|
||||
*.spec.*
|
||||
|
||||
# Git files
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Node modules (in case someone uses npm instead of bun)
|
||||
node_modules/
|
||||
@@ -0,0 +1,90 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
ccstatusline is a customizable status line formatter for Claude Code CLI that displays model info, git branch, token usage, and other metrics. It functions as both:
|
||||
1. A piped command processor for Claude Code status lines
|
||||
2. An interactive TUI configuration tool when run without input
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
bun install
|
||||
|
||||
# Run locally (TUI mode)
|
||||
bun run src/ccstatusline.ts
|
||||
|
||||
# Test with piped input
|
||||
echo '{"model":{"display_name":"Claude 3.5 Sonnet"},"transcript_path":"test.jsonl"}' | bun run src/ccstatusline.ts
|
||||
|
||||
# Build for npm distribution
|
||||
bun run build # Creates dist/ccstatusline.js
|
||||
|
||||
# Test npm package locally
|
||||
npm link
|
||||
echo '{"model":{"display_name":"Claude 3.5 Sonnet"},"transcript_path":"test.jsonl"}' | ccstatusline
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The project has dual runtime compatibility - it works with both Bun and Node.js:
|
||||
|
||||
- **src/ccstatusline.ts**: Main entry point that detects piped vs interactive mode
|
||||
- Piped mode: Parses JSON from stdin and renders formatted status line
|
||||
- Interactive mode: Launches React/Ink TUI for configuration
|
||||
|
||||
- **src/tui.tsx**: React-based terminal UI using Ink for configuration interface
|
||||
- Item management (add/remove/reorder status items)
|
||||
- Color customization for different elements
|
||||
|
||||
- **src/config.ts**: Settings management
|
||||
- Loads from `~/.config/ccstatusline/settings.json`
|
||||
- Handles migration from old settings format
|
||||
- Default configuration if no settings exist
|
||||
|
||||
- **npm publishing**: Uses `--packages=external` to keep dependencies external for cross-platform compatibility
|
||||
|
||||
## Key Implementation Details
|
||||
|
||||
- **Cross-platform stdin reading**: Code detects Bun vs Node.js environment and uses appropriate stdin API
|
||||
- **Token metrics**: Parses Claude Code transcript files (JSONL format) to calculate token usage
|
||||
- **Git integration**: Uses child_process.execSync to get current branch
|
||||
- **80-char limit**: Status line always formats to exactly 80 characters for Claude Code compatibility
|
||||
- **Flex separators**: Special separator type that expands to fill available space
|
||||
|
||||
## Bun Usage Preferences
|
||||
|
||||
Default to using Bun instead of Node.js.
|
||||
|
||||
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
||||
- Use `bun test` instead of `jest` or `vitest`
|
||||
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
||||
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
||||
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
||||
- Bun automatically loads .env, so don't use dotenv.
|
||||
|
||||
## APIs
|
||||
|
||||
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
||||
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
||||
- `Bun.redis` for Redis. Don't use `ioredis`.
|
||||
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
||||
- `WebSocket` is built-in. Don't use `ws`.
|
||||
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
||||
- Bun.$`ls` instead of execa.
|
||||
|
||||
## Testing
|
||||
|
||||
Use `bun test` to run tests.
|
||||
|
||||
```ts#index.test.ts
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
test("hello world", () => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
```
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "ccstatusline",
|
||||
"dependencies": {
|
||||
"@types/react": "^19.1.9",
|
||||
"chalk": "^5.5.0",
|
||||
"ink": "^6.1.0",
|
||||
"ink-select-input": "^6.2.0",
|
||||
"ink-text-input": "^6.0.0",
|
||||
"react": "^19.1.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.1.3", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
|
||||
|
||||
"@types/node": ["@types/node@24.2.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="],
|
||||
|
||||
"ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||
|
||||
"auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
|
||||
|
||||
"chalk": ["chalk@5.5.0", "", {}, "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg=="],
|
||||
|
||||
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
|
||||
|
||||
"cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
|
||||
|
||||
"cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="],
|
||||
|
||||
"code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
|
||||
|
||||
"convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
|
||||
|
||||
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
|
||||
|
||||
"es-toolkit": ["es-toolkit@1.39.8", "", {}, "sha512-A8QO9TfF+rltS8BXpdu8OS+rpGgEdnRhqIVxO/ZmNvnXBYgOdSsxukT55ELyP94gZIntWJ+Li9QRrT2u1Kitpg=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
|
||||
|
||||
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
|
||||
|
||||
"get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="],
|
||||
|
||||
"indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
|
||||
|
||||
"ink": ["ink@6.1.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.1.3", "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.22.0", "indent-string": "^5.0.0", "is-in-ci": "^1.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.32.0", "scheduler": "^0.23.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^7.2.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^4.19.1" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-YQ+lbMD79y3FBAJXXZnuRajLEgaMFp102361eY5NrBIEVCi9oFo7gNZU4z2LBWlcjZFiTt7jetlkIbKCCH4KJA=="],
|
||||
|
||||
"ink-select-input": ["ink-select-input@6.2.0", "", { "dependencies": { "figures": "^6.1.0", "to-rotated": "^1.0.0" }, "peerDependencies": { "ink": ">=5.0.0", "react": ">=18.0.0" } }, "sha512-304fZXxkpYxJ9si5lxRCaX01GNlmPBgOZumXXRnPYbHW/iI31cgQynqk2tRypGLOF1cMIwPUzL2LSm6q4I5rQQ=="],
|
||||
|
||||
"ink-text-input": ["ink-text-input@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "type-fest": "^4.18.2" }, "peerDependencies": { "ink": ">=5", "react": ">=18" } }, "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
|
||||
|
||||
"is-in-ci": ["is-in-ci@1.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg=="],
|
||||
|
||||
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
|
||||
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||
|
||||
"patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
|
||||
|
||||
"react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="],
|
||||
|
||||
"react-devtools-core": ["react-devtools-core@6.1.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA=="],
|
||||
|
||||
"react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="],
|
||||
|
||||
"restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
|
||||
|
||||
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
||||
|
||||
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
|
||||
|
||||
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"slice-ansi": ["slice-ansi@7.1.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg=="],
|
||||
|
||||
"stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
|
||||
|
||||
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
|
||||
"to-rotated": ["to-rotated@1.0.0", "", {}, "sha512-KsEID8AfgUy+pxVRLsWp0VzCa69wxzUDZnzGbyIST/bcgcrMvTYoFBX/QORH4YApoD89EDuUovx4BTdpOn319Q=="],
|
||||
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||
|
||||
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
|
||||
|
||||
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="],
|
||||
|
||||
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||
|
||||
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
|
||||
|
||||
"cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="],
|
||||
|
||||
"react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
|
||||
|
||||
"react-reconciler/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
||||
|
||||
"slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.0.0", "", { "dependencies": { "get-east-asian-width": "^1.0.0" } }, "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA=="],
|
||||
}
|
||||
}
|
||||
Generated
+619
@@ -0,0 +1,619 @@
|
||||
{
|
||||
"name": "ccstatusline",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ccstatusline",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "^19.1.9",
|
||||
"chalk": "^5.5.0",
|
||||
"ink": "^6.1.0",
|
||||
"ink-select-input": "^6.2.0",
|
||||
"ink-text-input": "^6.0.0",
|
||||
"react": "^19.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"ccstatusline": "dist/ccstatusline.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
}
|
||||
},
|
||||
"node_modules/@alcalzone/ansi-tokenize": {
|
||||
"version": "0.1.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.2.1",
|
||||
"is-fullwidth-code-point": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bun": {
|
||||
"version": "1.2.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.2.19.tgz",
|
||||
"integrity": "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bun-types": "1.2.19"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.2.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.9",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-escapes": {
|
||||
"version": "7.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"environment": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "6.2.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/auto-bind": {
|
||||
"version": "5.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/bun-types": {
|
||||
"version": "1.2.19",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "5.5.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-boxes": {
|
||||
"version": "3.0.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "4.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"restore-cursor": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-truncate": {
|
||||
"version": "4.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"slice-ansi": "^5.0.0",
|
||||
"string-width": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-truncate/node_modules/slice-ansi": {
|
||||
"version": "5.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.0.0",
|
||||
"is-fullwidth-code-point": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/code-excerpt": {
|
||||
"version": "4.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"convert-to-spaces": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-to-spaces": {
|
||||
"version": "2.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "10.4.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/environment": {
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/es-toolkit": {
|
||||
"version": "1.39.8",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"docs",
|
||||
"benchmarks"
|
||||
]
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/figures": {
|
||||
"version": "6.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-unicode-supported": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-east-asian-width": {
|
||||
"version": "1.3.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/indent-string": {
|
||||
"version": "5.0.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ink": {
|
||||
"version": "6.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alcalzone/ansi-tokenize": "^0.1.3",
|
||||
"ansi-escapes": "^7.0.0",
|
||||
"ansi-styles": "^6.2.1",
|
||||
"auto-bind": "^5.0.1",
|
||||
"chalk": "^5.3.0",
|
||||
"cli-boxes": "^3.0.0",
|
||||
"cli-cursor": "^4.0.0",
|
||||
"cli-truncate": "^4.0.0",
|
||||
"code-excerpt": "^4.0.0",
|
||||
"es-toolkit": "^1.22.0",
|
||||
"indent-string": "^5.0.0",
|
||||
"is-in-ci": "^1.0.0",
|
||||
"patch-console": "^2.0.0",
|
||||
"react-reconciler": "^0.32.0",
|
||||
"scheduler": "^0.23.0",
|
||||
"signal-exit": "^3.0.7",
|
||||
"slice-ansi": "^7.1.0",
|
||||
"stack-utils": "^2.0.6",
|
||||
"string-width": "^7.2.0",
|
||||
"type-fest": "^4.27.0",
|
||||
"widest-line": "^5.0.0",
|
||||
"wrap-ansi": "^9.0.0",
|
||||
"ws": "^8.18.0",
|
||||
"yoga-layout": "~3.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=19.0.0",
|
||||
"react": ">=19.0.0",
|
||||
"react-devtools-core": "^4.19.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-devtools-core": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ink-select-input": {
|
||||
"version": "6.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"figures": "^6.1.0",
|
||||
"to-rotated": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ink": ">=5.0.0",
|
||||
"react": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ink-text-input": {
|
||||
"version": "6.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
"type-fest": "^4.18.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ink": ">=5",
|
||||
"react": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "4.0.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-in-ci": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"is-in-ci": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-unicode-supported": {
|
||||
"version": "2.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-fn": {
|
||||
"version": "2.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/onetime": {
|
||||
"version": "5.1.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-fn": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/patch-console": {
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.1.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-reconciler": {
|
||||
"version": "0.32.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-reconciler/node_modules/scheduler": {
|
||||
"version": "0.26.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/restore-cursor": {
|
||||
"version": "4.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"onetime": "^5.1.0",
|
||||
"signal-exit": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "3.0.7",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/slice-ansi": {
|
||||
"version": "7.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.2.1",
|
||||
"is-fullwidth-code-point": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
|
||||
"version": "5.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-east-asian-width": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-utils": {
|
||||
"version": "2.0.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "7.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^10.3.0",
|
||||
"get-east-asian-width": "^1.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "7.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/to-rotated": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "4.41.0",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.2",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.10.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/widest-line": {
|
||||
"version": "5.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"string-width": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "9.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.2.1",
|
||||
"string-width": "^7.0.0",
|
||||
"strip-ansi": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/yoga-layout": {
|
||||
"version": "3.2.1",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "ccstatusline",
|
||||
"version": "1.0.0",
|
||||
"description": "A customizable status line formatter for Claude Code CLI",
|
||||
"module": "src/ccstatusline.ts",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"ccstatusline": "dist/ccstatusline.js"
|
||||
},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "bun run src/ccstatusline.ts",
|
||||
"build": "bun build src/ccstatusline.ts --target=node --outfile=dist/ccstatusline.js --packages=external --target-version=14",
|
||||
"prepublishOnly": "bun run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/react": "^19.1.9",
|
||||
"chalk": "^5.5.0",
|
||||
"ink": "^6.1.0",
|
||||
"ink-select-input": "^6.2.0",
|
||||
"ink-text-input": "^6.0.0",
|
||||
"react": "^19.1.1"
|
||||
},
|
||||
"keywords": [
|
||||
"claude",
|
||||
"claude-code",
|
||||
"cli",
|
||||
"status-line",
|
||||
"terminal"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/sirmalloc/ccstatusline.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
#!/usr/bin/env node
|
||||
import chalk from 'chalk';
|
||||
import { execSync } from 'child_process';
|
||||
import { runTUI } from './tui.tsx';
|
||||
import { loadSettings } from './config';
|
||||
import * as fs from 'fs';
|
||||
import { promisify } from 'util';
|
||||
|
||||
// Ensure fs.promises compatibility for older Node versions
|
||||
const readFile = fs.promises?.readFile || promisify(fs.readFile);
|
||||
|
||||
// Force chalk to use colors even when piped
|
||||
chalk.level = 3;
|
||||
|
||||
interface StatusJSON {
|
||||
session_id: string;
|
||||
transcript_path: string;
|
||||
cwd: string;
|
||||
model: {
|
||||
id: string;
|
||||
display_name: string;
|
||||
};
|
||||
workspace: {
|
||||
current_dir: string;
|
||||
project_dir: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface TokenUsage {
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
cache_creation_input_tokens?: number;
|
||||
cache_read_input_tokens?: number;
|
||||
}
|
||||
|
||||
interface TranscriptLine {
|
||||
message?: {
|
||||
usage?: TokenUsage;
|
||||
};
|
||||
}
|
||||
|
||||
async function readStdin(): Promise<string | null> {
|
||||
// Check if stdin is a TTY (terminal) - if it is, there's no piped data
|
||||
if (process.stdin.isTTY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const chunks: string[] = [];
|
||||
|
||||
try {
|
||||
// Use Node.js compatible approach
|
||||
if (typeof Bun !== 'undefined' && Bun.stdin) {
|
||||
// Bun environment
|
||||
const decoder = new TextDecoder();
|
||||
for await (const chunk of Bun.stdin.stream()) {
|
||||
chunks.push(decoder.decode(chunk));
|
||||
}
|
||||
} else {
|
||||
// Node.js environment
|
||||
process.stdin.setEncoding('utf8');
|
||||
for await (const chunk of process.stdin) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
}
|
||||
return chunks.join('');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getGitBranch(): string | null {
|
||||
try {
|
||||
const branch = execSync('git branch --show-current 2>/dev/null', {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore']
|
||||
}).trim();
|
||||
return branch || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getTokenMetrics(transcriptPath: string): Promise<{
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
cachedTokens: number;
|
||||
totalTokens: number;
|
||||
contextLength: number;
|
||||
}> {
|
||||
try {
|
||||
// Use Node.js-compatible file reading
|
||||
if (!fs.existsSync(transcriptPath)) {
|
||||
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
|
||||
}
|
||||
|
||||
const content = await readFile(transcriptPath, 'utf-8');
|
||||
const lines = content.trim().split('\n');
|
||||
|
||||
let inputTokens = 0;
|
||||
let outputTokens = 0;
|
||||
let cachedTokens = 0;
|
||||
|
||||
// Parse each line and sum up token usage
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const data: TranscriptLine = JSON.parse(line);
|
||||
if (data.message?.usage) {
|
||||
inputTokens += data.message.usage.input_tokens || 0;
|
||||
outputTokens += data.message.usage.output_tokens || 0;
|
||||
cachedTokens += data.message.usage.cache_read_input_tokens || 0;
|
||||
cachedTokens += data.message.usage.cache_creation_input_tokens || 0;
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid JSON lines
|
||||
}
|
||||
}
|
||||
|
||||
const totalTokens = inputTokens + outputTokens + cachedTokens;
|
||||
const contextLength = inputTokens + outputTokens; // Current context size
|
||||
|
||||
return { inputTokens, outputTokens, cachedTokens, totalTokens, contextLength };
|
||||
} catch {
|
||||
return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
async function renderStatusLine(data: StatusJSON) {
|
||||
const settings = await loadSettings();
|
||||
const terminalWidth = 80; // Always use 80 chars for Claude Code status line
|
||||
const elements: { content: string, type: string }[] = [];
|
||||
let hasFlexSeparator = false;
|
||||
|
||||
// Get token metrics if needed
|
||||
const hasTokenItems = settings.items.some(item =>
|
||||
['tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage'].includes(item.type)
|
||||
);
|
||||
|
||||
let tokenMetrics: any = null;
|
||||
if (hasTokenItems && data.transcript_path) {
|
||||
tokenMetrics = await getTokenMetrics(data.transcript_path);
|
||||
}
|
||||
|
||||
// Helper function to format token counts
|
||||
const formatTokens = (count: number): string => {
|
||||
if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
|
||||
if (count >= 1000) return `${(count / 1000).toFixed(1)}k`;
|
||||
return count.toString();
|
||||
};
|
||||
|
||||
// Build elements based on configured items
|
||||
for (const item of settings.items) {
|
||||
switch (item.type) {
|
||||
case 'model':
|
||||
if (data.model) {
|
||||
const color = (chalk as any)[item.color || settings.colors.model] || chalk.cyan;
|
||||
elements.push({ content: color(`Model: ${data.model.display_name}`), type: 'model' });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'git-branch':
|
||||
const branch = getGitBranch();
|
||||
if (branch) {
|
||||
const color = (chalk as any)[item.color || settings.colors.gitBranch] || chalk.magenta;
|
||||
elements.push({ content: color(`⎇ ${branch}`), type: 'git-branch' });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tokens-input':
|
||||
if (tokenMetrics) {
|
||||
const color = (chalk as any)[item.color || 'yellow'] || chalk.yellow;
|
||||
elements.push({ content: color(`In: ${formatTokens(tokenMetrics.inputTokens)}`), type: 'tokens-input' });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tokens-output':
|
||||
if (tokenMetrics) {
|
||||
const color = (chalk as any)[item.color || 'green'] || chalk.green;
|
||||
elements.push({ content: color(`Out: ${formatTokens(tokenMetrics.outputTokens)}`), type: 'tokens-output' });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tokens-cached':
|
||||
if (tokenMetrics) {
|
||||
const color = (chalk as any)[item.color || 'blue'] || chalk.blue;
|
||||
elements.push({ content: color(`Cached: ${formatTokens(tokenMetrics.cachedTokens)}`), type: 'tokens-cached' });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tokens-total':
|
||||
if (tokenMetrics) {
|
||||
const color = (chalk as any)[item.color || 'white'] || chalk.white;
|
||||
elements.push({ content: color(`Total: ${formatTokens(tokenMetrics.totalTokens)}`), type: 'tokens-total' });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'context-length':
|
||||
if (tokenMetrics) {
|
||||
const color = (chalk as any)[item.color || 'cyan'] || chalk.cyan;
|
||||
elements.push({ content: color(`Ctx: ${formatTokens(tokenMetrics.contextLength)}`), type: 'context-length' });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'context-percentage':
|
||||
if (tokenMetrics) {
|
||||
const percentage = Math.min(100, (tokenMetrics.contextLength / 200000) * 100);
|
||||
const color = (chalk as any)[item.color || 'cyan'] || chalk.cyan;
|
||||
elements.push({ content: color(`Ctx: ${percentage.toFixed(1)}%`), type: 'context-percentage' });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'separator':
|
||||
// Only add separator if there are already elements and the last one isn't a separator
|
||||
if (elements.length > 0 && elements[elements.length - 1].type !== 'separator') {
|
||||
const sepColor = (chalk as any)[settings.colors.separator] || chalk.dim;
|
||||
elements.push({ content: sepColor(' | '), type: 'separator' });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'flex-separator':
|
||||
elements.push({ content: 'FLEX', type: 'flex-separator' });
|
||||
hasFlexSeparator = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (elements.length === 0) return;
|
||||
|
||||
// Build the final status line
|
||||
let statusLine = '';
|
||||
|
||||
if (hasFlexSeparator) {
|
||||
// Split elements by flex separators
|
||||
const parts: string[][] = [[]];
|
||||
let currentPart = 0;
|
||||
|
||||
for (const elem of elements) {
|
||||
if (elem.type === 'flex-separator') {
|
||||
currentPart++;
|
||||
parts[currentPart] = [];
|
||||
} else {
|
||||
parts[currentPart].push(elem.content);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total length of all non-flex content
|
||||
const partLengths = parts.map(part => {
|
||||
const joined = part.join('');
|
||||
return joined.replace(/\x1b\[[0-9;]*m/g, '').length;
|
||||
});
|
||||
const totalContentLength = partLengths.reduce((sum, len) => sum + len, 0);
|
||||
|
||||
// Calculate space to distribute among flex separators
|
||||
const flexCount = parts.length - 1; // Number of flex separators
|
||||
const totalSpace = Math.max(0, terminalWidth - totalContentLength);
|
||||
const spacePerFlex = flexCount > 0 ? Math.floor(totalSpace / flexCount) : 0;
|
||||
const extraSpace = flexCount > 0 ? totalSpace % flexCount : 0;
|
||||
|
||||
// Build the status line with distributed spacing
|
||||
statusLine = '';
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
statusLine += parts[i].join('');
|
||||
if (i < parts.length - 1) {
|
||||
// Add flex spacing
|
||||
const spaces = spacePerFlex + (i < extraSpace ? 1 : 0);
|
||||
statusLine += ' '.repeat(spaces);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No flex separator, just join all elements
|
||||
statusLine = elements.map(e => e.content).join('');
|
||||
|
||||
// Pad to full width with spaces
|
||||
const contentLength = statusLine.replace(/\x1b\[[0-9;]*m/g, '').length;
|
||||
const remainingSpace = terminalWidth - contentLength;
|
||||
if (remainingSpace > 0) {
|
||||
statusLine = statusLine + ' '.repeat(remainingSpace);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure we never exceed 80 chars
|
||||
const plainLength = statusLine.replace(/\x1b\[[0-9;]*m/g, '').length;
|
||||
if (plainLength > 80) {
|
||||
// Truncate with ellipsis if too long
|
||||
const visibleText = statusLine.replace(/\x1b\[[0-9;]*m/g, '');
|
||||
const truncated = visibleText.substring(0, 77) + '...';
|
||||
console.log(truncated);
|
||||
} else {
|
||||
console.log(statusLine);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Check if we're in a piped/non-TTY environment first
|
||||
if (!process.stdin.isTTY) {
|
||||
// We're receiving piped input
|
||||
const input = await readStdin();
|
||||
if (input && input.trim() !== '') {
|
||||
try {
|
||||
const data: StatusJSON = JSON.parse(input);
|
||||
await renderStatusLine(data);
|
||||
} catch (error) {
|
||||
console.error('Error parsing JSON:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.error('No input received');
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
// Interactive mode - run TUI
|
||||
await runTUI();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,74 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { promisify } from 'util';
|
||||
|
||||
// Ensure fs.promises compatibility
|
||||
const readFile = fs.promises?.readFile || promisify(fs.readFile);
|
||||
const writeFile = fs.promises?.writeFile || promisify(fs.writeFile);
|
||||
const mkdir = fs.promises?.mkdir || promisify(fs.mkdir);
|
||||
|
||||
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
|
||||
|
||||
interface ClaudeSettings {
|
||||
permissions?: {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
};
|
||||
statusLine?: {
|
||||
type: string;
|
||||
command: string;
|
||||
padding?: number;
|
||||
};
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export async function loadClaudeSettings(): Promise<ClaudeSettings> {
|
||||
try {
|
||||
if (!fs.existsSync(CLAUDE_SETTINGS_PATH)) {
|
||||
return {};
|
||||
}
|
||||
const content = await readFile(CLAUDE_SETTINGS_PATH, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveClaudeSettings(settings: ClaudeSettings): Promise<void> {
|
||||
const dir = path.dirname(CLAUDE_SETTINGS_PATH);
|
||||
await mkdir(dir, { recursive: true });
|
||||
await writeFile(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
export async function isInstalled(): Promise<boolean> {
|
||||
const settings = await loadClaudeSettings();
|
||||
return settings.statusLine?.command === 'npx ccstatusline';
|
||||
}
|
||||
|
||||
export async function installStatusLine(): Promise<void> {
|
||||
const settings = await loadClaudeSettings();
|
||||
|
||||
// Update settings with our status line (confirmation already handled in TUI)
|
||||
settings.statusLine = {
|
||||
type: 'command',
|
||||
command: 'npx ccstatusline',
|
||||
padding: 1
|
||||
};
|
||||
|
||||
await saveClaudeSettings(settings);
|
||||
}
|
||||
|
||||
export async function uninstallStatusLine(): Promise<void> {
|
||||
const settings = await loadClaudeSettings();
|
||||
|
||||
if (settings.statusLine) {
|
||||
delete settings.statusLine;
|
||||
await saveClaudeSettings(settings);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getExistingStatusLine(): Promise<string | null> {
|
||||
const settings = await loadClaudeSettings();
|
||||
return settings.statusLine?.command || null;
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { promisify } from 'util';
|
||||
|
||||
// Ensure fs.promises compatibility
|
||||
const readFile = fs.promises?.readFile || promisify(fs.readFile);
|
||||
const writeFile = fs.promises?.writeFile || promisify(fs.writeFile);
|
||||
const mkdir = fs.promises?.mkdir || promisify(fs.mkdir);
|
||||
|
||||
export type StatusItemType = 'model' | 'git-branch' | 'separator' | 'flex-separator' |
|
||||
'tokens-input' | 'tokens-output' | 'tokens-cached' | 'tokens-total' | 'context-length' | 'context-percentage';
|
||||
|
||||
export interface StatusItem {
|
||||
id: string;
|
||||
type: StatusItemType;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
items: StatusItem[];
|
||||
colors: {
|
||||
model: string;
|
||||
gitBranch: string;
|
||||
separator: string;
|
||||
};
|
||||
}
|
||||
|
||||
const CONFIG_DIR = path.join(os.homedir(), '.config', 'ccstatusline');
|
||||
const SETTINGS_PATH = path.join(CONFIG_DIR, 'settings.json');
|
||||
|
||||
export const DEFAULT_SETTINGS: Settings = {
|
||||
items: [
|
||||
{ id: '1', type: 'model', color: 'cyan' },
|
||||
{ id: '2', type: 'separator' },
|
||||
{ id: '3', type: 'git-branch', color: 'magenta' },
|
||||
],
|
||||
colors: {
|
||||
model: 'cyan',
|
||||
gitBranch: 'magenta',
|
||||
separator: 'dim',
|
||||
},
|
||||
};
|
||||
|
||||
export async function loadSettings(): Promise<Settings> {
|
||||
try {
|
||||
// Use Node.js-compatible file reading
|
||||
if (!fs.existsSync(SETTINGS_PATH)) {
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
|
||||
const content = await readFile(SETTINGS_PATH, 'utf-8');
|
||||
const loaded = JSON.parse(content);
|
||||
|
||||
// Migrate old format if needed
|
||||
if (loaded.elements || loaded.layout) {
|
||||
return migrateOldSettings(loaded);
|
||||
}
|
||||
|
||||
return { ...DEFAULT_SETTINGS, ...loaded };
|
||||
} catch {
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
}
|
||||
|
||||
function migrateOldSettings(old: any): Settings {
|
||||
const items: StatusItem[] = [];
|
||||
let id = 1;
|
||||
|
||||
if (old.elements?.model) {
|
||||
items.push({ id: String(id++), type: 'model', color: old.colors?.model });
|
||||
}
|
||||
|
||||
if (items.length > 0 && old.elements?.gitBranch) {
|
||||
items.push({ id: String(id++), type: 'separator' });
|
||||
}
|
||||
|
||||
if (old.elements?.gitBranch) {
|
||||
items.push({ id: String(id++), type: 'git-branch', color: old.colors?.gitBranch });
|
||||
}
|
||||
|
||||
if (old.layout?.expandingSeparators) {
|
||||
// Replace regular separators with flex separators
|
||||
items.forEach(item => {
|
||||
if (item.type === 'separator') {
|
||||
item.type = 'flex-separator';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
colors: old.colors || DEFAULT_SETTINGS.colors,
|
||||
};
|
||||
}
|
||||
|
||||
export async function saveSettings(settings: Settings): Promise<void> {
|
||||
// Ensure config directory exists
|
||||
await mkdir(CONFIG_DIR, { recursive: true });
|
||||
|
||||
// Write settings using Node.js-compatible API
|
||||
await writeFile(SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf-8');
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
console.log("Hello via Bun!");
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
import * as p from '@clack/prompts';
|
||||
import chalk from 'chalk';
|
||||
import { loadSettings, saveSettings, type Settings } from './config';
|
||||
|
||||
const COLOR_OPTIONS = [
|
||||
{ value: 'black', label: chalk.black('black') },
|
||||
{ value: 'red', label: chalk.red('red') },
|
||||
{ value: 'green', label: chalk.green('green') },
|
||||
{ value: 'yellow', label: chalk.yellow('yellow') },
|
||||
{ value: 'blue', label: chalk.blue('blue') },
|
||||
{ value: 'magenta', label: chalk.magenta('magenta') },
|
||||
{ value: 'cyan', label: chalk.cyan('cyan') },
|
||||
{ value: 'white', label: chalk.white('white') },
|
||||
{ value: 'gray', label: chalk.gray('gray') },
|
||||
{ value: 'redBright', label: chalk.redBright('redBright') },
|
||||
{ value: 'greenBright', label: chalk.greenBright('greenBright') },
|
||||
{ value: 'yellowBright', label: chalk.yellowBright('yellowBright') },
|
||||
{ value: 'blueBright', label: chalk.blueBright('blueBright') },
|
||||
{ value: 'magentaBright', label: chalk.magentaBright('magentaBright') },
|
||||
{ value: 'cyanBright', label: chalk.cyanBright('cyanBright') },
|
||||
{ value: 'whiteBright', label: chalk.whiteBright('whiteBright') },
|
||||
];
|
||||
|
||||
function previewStatusLine(settings: Settings): string {
|
||||
const elements: string[] = [];
|
||||
|
||||
if (settings.elements.model) {
|
||||
const modelColor = (chalk as any)[settings.colors.model] || chalk.white;
|
||||
elements.push(modelColor('Model: Claude'));
|
||||
}
|
||||
|
||||
if (settings.elements.gitBranch) {
|
||||
const branchColor = (chalk as any)[settings.colors.gitBranch] || chalk.white;
|
||||
elements.push(branchColor('Branch: main'));
|
||||
}
|
||||
|
||||
const separatorColor = (chalk as any)[settings.colors.separator] || chalk.dim;
|
||||
return elements.join(separatorColor(' | '));
|
||||
}
|
||||
|
||||
export async function runTUI() {
|
||||
let settings = await loadSettings();
|
||||
|
||||
console.clear();
|
||||
p.intro(chalk.bold.cyan('🎨 CCStatusline Configuration'));
|
||||
|
||||
let continueLoop = true;
|
||||
|
||||
while (continueLoop) {
|
||||
console.log('\n' + chalk.dim('Current preview:'));
|
||||
console.log(' ' + previewStatusLine(settings) + '\n');
|
||||
|
||||
const action = await p.select({
|
||||
message: 'What would you like to configure?',
|
||||
options: [
|
||||
{ value: 'elements', label: '🔧 Toggle Elements' },
|
||||
{ value: 'colors', label: '🎨 Configure Colors' },
|
||||
{ value: 'save', label: '💾 Save & Exit' },
|
||||
{ value: 'exit', label: '❌ Exit without saving' },
|
||||
],
|
||||
});
|
||||
|
||||
if (p.isCancel(action)) {
|
||||
continueLoop = false;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case 'elements':
|
||||
console.clear();
|
||||
p.intro(chalk.bold.cyan('🎨 CCStatusline Configuration'));
|
||||
console.log('\n' + chalk.dim('Current preview:'));
|
||||
console.log(' ' + previewStatusLine(settings) + '\n');
|
||||
const selectedElements = await p.multiselect({
|
||||
message: 'Select which elements to display:',
|
||||
options: [
|
||||
{
|
||||
value: 'model',
|
||||
label: 'Model',
|
||||
hint: settings.elements.model ? 'currently enabled' : 'currently disabled'
|
||||
},
|
||||
{
|
||||
value: 'gitBranch',
|
||||
label: 'Git Branch',
|
||||
hint: settings.elements.gitBranch ? 'currently enabled' : 'currently disabled'
|
||||
},
|
||||
],
|
||||
initialValues: Object.entries(settings.elements)
|
||||
.filter(([_, enabled]) => enabled)
|
||||
.map(([key, _]) => key),
|
||||
required: false,
|
||||
});
|
||||
|
||||
if (!p.isCancel(selectedElements)) {
|
||||
settings.elements.model = selectedElements.includes('model');
|
||||
settings.elements.gitBranch = selectedElements.includes('gitBranch');
|
||||
}
|
||||
console.clear();
|
||||
p.intro(chalk.bold.cyan('🎨 CCStatusline Configuration'));
|
||||
break;
|
||||
|
||||
case 'colors':
|
||||
console.clear();
|
||||
p.intro(chalk.bold.cyan('🎨 CCStatusline Configuration'));
|
||||
console.log('\n' + chalk.dim('Current preview:'));
|
||||
console.log(' ' + previewStatusLine(settings) + '\n');
|
||||
const colorElement = await p.select({
|
||||
message: 'Which element color to configure?',
|
||||
options: [
|
||||
{ value: 'model', label: 'Model' },
|
||||
{ value: 'gitBranch', label: 'Git Branch' },
|
||||
{ value: 'separator', label: 'Separator' },
|
||||
{ value: 'back', label: '← Back' },
|
||||
],
|
||||
});
|
||||
|
||||
if (!p.isCancel(colorElement) && colorElement !== 'back') {
|
||||
console.clear();
|
||||
p.intro(chalk.bold.cyan('🎨 CCStatusline Configuration'));
|
||||
console.log('\n' + chalk.dim('Current preview:'));
|
||||
console.log(' ' + previewStatusLine(settings) + '\n');
|
||||
const color = await p.select({
|
||||
message: `Select color for ${colorElement}:`,
|
||||
options: COLOR_OPTIONS,
|
||||
initialValue: settings.colors[colorElement as keyof typeof settings.colors],
|
||||
});
|
||||
|
||||
if (!p.isCancel(color)) {
|
||||
(settings.colors as any)[colorElement] = color;
|
||||
}
|
||||
}
|
||||
console.clear();
|
||||
p.intro(chalk.bold.cyan('🎨 CCStatusline Configuration'));
|
||||
break;
|
||||
|
||||
case 'save':
|
||||
await saveSettings(settings);
|
||||
p.outro(chalk.green('✅ Settings saved successfully!'));
|
||||
continueLoop = false;
|
||||
break;
|
||||
|
||||
case 'exit':
|
||||
p.outro(chalk.yellow('Exited without saving'));
|
||||
continueLoop = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
+589
@@ -0,0 +1,589 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { render, Box, Text, useInput, useApp } from 'ink';
|
||||
import SelectInput from 'ink-select-input';
|
||||
import chalk from 'chalk';
|
||||
import { loadSettings, saveSettings, type Settings, type StatusItem, type StatusItemType } from './config';
|
||||
import { isInstalled, installStatusLine, uninstallStatusLine, getExistingStatusLine } from './claude-settings';
|
||||
|
||||
interface StatusLinePreviewProps {
|
||||
items: StatusItem[];
|
||||
terminalWidth: number;
|
||||
}
|
||||
|
||||
const StatusLinePreview: React.FC<StatusLinePreviewProps> = ({ items, terminalWidth }) => {
|
||||
const width = 80; // Status line is always 80 chars max
|
||||
const elements: string[] = [];
|
||||
let hasFlexSeparator = false;
|
||||
|
||||
items.forEach(item => {
|
||||
switch (item.type) {
|
||||
case 'model':
|
||||
const modelColor = (chalk as any)[item.color || 'cyan'] || chalk.cyan;
|
||||
elements.push(modelColor('Model: Claude'));
|
||||
break;
|
||||
case 'git-branch':
|
||||
const branchColor = (chalk as any)[item.color || 'magenta'] || chalk.magenta;
|
||||
elements.push(branchColor('⎇ main'));
|
||||
break;
|
||||
case 'tokens-input':
|
||||
const inputColor = (chalk as any)[item.color || 'yellow'] || chalk.yellow;
|
||||
elements.push(inputColor('In: 15.2k'));
|
||||
break;
|
||||
case 'tokens-output':
|
||||
const outputColor = (chalk as any)[item.color || 'green'] || chalk.green;
|
||||
elements.push(outputColor('Out: 3.4k'));
|
||||
break;
|
||||
case 'tokens-cached':
|
||||
const cachedColor = (chalk as any)[item.color || 'blue'] || chalk.blue;
|
||||
elements.push(cachedColor('Cached: 12k'));
|
||||
break;
|
||||
case 'tokens-total':
|
||||
const totalColor = (chalk as any)[item.color || 'white'] || chalk.white;
|
||||
elements.push(totalColor('Total: 30.6k'));
|
||||
break;
|
||||
case 'context-length':
|
||||
const ctxColor = (chalk as any)[item.color || 'cyan'] || chalk.cyan;
|
||||
elements.push(ctxColor('Ctx: 18.6k'));
|
||||
break;
|
||||
case 'context-percentage':
|
||||
const ctxPctColor = (chalk as any)[item.color || 'cyan'] || chalk.cyan;
|
||||
elements.push(ctxPctColor('Ctx: 9.3%'));
|
||||
break;
|
||||
case 'separator':
|
||||
elements.push(chalk.dim(' | '));
|
||||
break;
|
||||
case 'flex-separator':
|
||||
elements.push('FLEX');
|
||||
hasFlexSeparator = true;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Build the status line with flex separator support
|
||||
let statusLine = '';
|
||||
if (hasFlexSeparator) {
|
||||
const parts: string[][] = [[]];
|
||||
let currentPart = 0;
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].type === 'flex-separator') {
|
||||
currentPart++;
|
||||
parts[currentPart] = [];
|
||||
} else {
|
||||
const element = elements[i];
|
||||
if (element !== 'FLEX') {
|
||||
parts[currentPart].push(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total length of all non-flex content
|
||||
const partLengths = parts.map(part => {
|
||||
const joined = part.join('');
|
||||
return joined.replace(/\x1b\[[0-9;]*m/g, '').length;
|
||||
});
|
||||
const totalContentLength = partLengths.reduce((sum, len) => sum + len, 0);
|
||||
|
||||
// Calculate space to distribute among flex separators
|
||||
const flexCount = parts.length - 1; // Number of flex separators
|
||||
const totalSpace = Math.max(0, width - totalContentLength);
|
||||
const spacePerFlex = flexCount > 0 ? Math.floor(totalSpace / flexCount) : 0;
|
||||
const extraSpace = flexCount > 0 ? totalSpace % flexCount : 0;
|
||||
|
||||
// Build the status line with distributed spacing
|
||||
statusLine = '';
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
statusLine += parts[i].join('');
|
||||
if (i < parts.length - 1) {
|
||||
// Add flex spacing
|
||||
const spaces = spacePerFlex + (i < extraSpace ? 1 : 0);
|
||||
statusLine += ' '.repeat(spaces);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
statusLine = elements.filter(e => e !== 'FLEX').join('');
|
||||
}
|
||||
|
||||
// Build the Claude Code input box - account for ink's padding
|
||||
const boxWidth = Math.min(terminalWidth - 4, process.stdout.columns - 4 || 76);
|
||||
const topLine = chalk.dim('╭' + '─'.repeat(Math.max(0, boxWidth - 2)) + '╮');
|
||||
const middleLine = chalk.dim('│') + ' > ' + ' '.repeat(Math.max(0, boxWidth - 5)) + chalk.dim('│');
|
||||
const bottomLine = chalk.dim('╰' + '─'.repeat(Math.max(0, boxWidth - 2)) + '╯');
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text>{topLine}</Text>
|
||||
<Text>{middleLine}</Text>
|
||||
<Text>{bottomLine}</Text>
|
||||
<Text>{statusLine}</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
message: string;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ message, onConfirm, onCancel }) => {
|
||||
const items = [
|
||||
{ label: '✅ Yes', value: 'yes' },
|
||||
{ label: '❌ No', value: 'no' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text>{message}</Text>
|
||||
<Box marginTop={1}>
|
||||
<SelectInput
|
||||
items={items}
|
||||
onSelect={(item) => item.value === 'yes' ? onConfirm() : onCancel()}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface MainMenuProps {
|
||||
onSelect: (value: string) => void;
|
||||
isClaudeInstalled: boolean;
|
||||
hasChanges: boolean;
|
||||
}
|
||||
|
||||
const MainMenu: React.FC<MainMenuProps> = ({ onSelect, isClaudeInstalled, hasChanges }) => {
|
||||
const items = [
|
||||
{ label: '📝 Edit Status Line Items', value: 'items' },
|
||||
{ label: '🎨 Configure Colors', value: 'colors' },
|
||||
{ label: isClaudeInstalled ? '🗑️ Uninstall from Claude Code' : '📦 Install to Claude Code', value: 'install' },
|
||||
];
|
||||
|
||||
if (hasChanges) {
|
||||
items.push(
|
||||
{ label: '💾 Save & Exit', value: 'save' },
|
||||
{ label: '❌ Exit without saving', value: 'exit' }
|
||||
);
|
||||
} else {
|
||||
items.push({ label: '🚪 Exit', value: 'exit' });
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text bold>Main Menu</Text>
|
||||
<Box marginTop={1}>
|
||||
<SelectInput items={items} onSelect={(item) => onSelect(item.value)} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ItemsEditorProps {
|
||||
items: StatusItem[];
|
||||
onUpdate: (items: StatusItem[]) => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const ItemsEditor: React.FC<ItemsEditorProps> = ({ items, onUpdate, onBack }) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [moveMode, setMoveMode] = useState(false);
|
||||
|
||||
useInput((input, key) => {
|
||||
if (moveMode) {
|
||||
// In move mode, use up/down to move the selected item
|
||||
if (key.upArrow && selectedIndex > 0) {
|
||||
const newItems = [...items];
|
||||
[newItems[selectedIndex], newItems[selectedIndex - 1]] =
|
||||
[newItems[selectedIndex - 1], newItems[selectedIndex]];
|
||||
onUpdate(newItems);
|
||||
setSelectedIndex(selectedIndex - 1);
|
||||
} else if (key.downArrow && selectedIndex < items.length - 1) {
|
||||
const newItems = [...items];
|
||||
[newItems[selectedIndex], newItems[selectedIndex + 1]] =
|
||||
[newItems[selectedIndex + 1], newItems[selectedIndex]];
|
||||
onUpdate(newItems);
|
||||
setSelectedIndex(selectedIndex + 1);
|
||||
} else if (key.escape || key.return) {
|
||||
// Exit move mode
|
||||
setMoveMode(false);
|
||||
}
|
||||
} else {
|
||||
// Normal mode
|
||||
if (key.upArrow) {
|
||||
setSelectedIndex(Math.max(0, selectedIndex - 1));
|
||||
} else if (key.downArrow) {
|
||||
setSelectedIndex(Math.min(items.length - 1, selectedIndex + 1));
|
||||
} else if (key.leftArrow && items.length > 0) {
|
||||
// Toggle item type backwards
|
||||
const types: StatusItemType[] = ['model', 'git-branch', 'separator', 'flex-separator',
|
||||
'tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage'];
|
||||
const currentType = items[selectedIndex].type;
|
||||
const currentIndex = types.indexOf(currentType);
|
||||
const prevIndex = currentIndex === 0 ? types.length - 1 : currentIndex - 1;
|
||||
const newItems = [...items];
|
||||
newItems[selectedIndex] = { ...newItems[selectedIndex], type: types[prevIndex] };
|
||||
onUpdate(newItems);
|
||||
} else if (key.rightArrow && items.length > 0) {
|
||||
// Toggle item type forwards
|
||||
const types: StatusItemType[] = ['model', 'git-branch', 'separator', 'flex-separator',
|
||||
'tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage'];
|
||||
const currentType = items[selectedIndex].type;
|
||||
const currentIndex = types.indexOf(currentType);
|
||||
const nextIndex = (currentIndex + 1) % types.length;
|
||||
const newItems = [...items];
|
||||
newItems[selectedIndex] = { ...newItems[selectedIndex], type: types[nextIndex] };
|
||||
onUpdate(newItems);
|
||||
} else if (key.return && items.length > 0) {
|
||||
// Enter move mode
|
||||
setMoveMode(true);
|
||||
} else if (input === 'a') {
|
||||
// Add item at end
|
||||
const newItem: StatusItem = {
|
||||
id: Date.now().toString(),
|
||||
type: 'separator',
|
||||
};
|
||||
onUpdate([...items, newItem]);
|
||||
} else if (input === 'i') {
|
||||
// Insert item after selected
|
||||
const newItem: StatusItem = {
|
||||
id: Date.now().toString(),
|
||||
type: 'separator',
|
||||
};
|
||||
const newItems = [...items];
|
||||
newItems.splice(selectedIndex + 1, 0, newItem);
|
||||
onUpdate(newItems);
|
||||
setSelectedIndex(selectedIndex + 1);
|
||||
} else if (input === 'd' && items.length > 0) {
|
||||
// Delete selected item
|
||||
const newItems = items.filter((_, i) => i !== selectedIndex);
|
||||
onUpdate(newItems);
|
||||
if (selectedIndex >= newItems.length && selectedIndex > 0) {
|
||||
setSelectedIndex(selectedIndex - 1);
|
||||
}
|
||||
} else if (key.escape) {
|
||||
onBack();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const getItemDisplay = (item: StatusItem) => {
|
||||
switch (item.type) {
|
||||
case 'model':
|
||||
return chalk.cyan('Model');
|
||||
case 'git-branch':
|
||||
return chalk.magenta('Git Branch');
|
||||
case 'separator':
|
||||
return chalk.dim('Separator |');
|
||||
case 'flex-separator':
|
||||
return chalk.yellow('Flex Separator ─────');
|
||||
case 'tokens-input':
|
||||
return chalk.yellow('Tokens Input');
|
||||
case 'tokens-output':
|
||||
return chalk.green('Tokens Output');
|
||||
case 'tokens-cached':
|
||||
return chalk.blue('Tokens Cached');
|
||||
case 'tokens-total':
|
||||
return chalk.white('Tokens Total');
|
||||
case 'context-length':
|
||||
return chalk.cyan('Context Length');
|
||||
case 'context-percentage':
|
||||
return chalk.cyan('Context %');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text bold>Edit Status Line Items {moveMode && <Text color="yellow">[MOVE MODE]</Text>}</Text>
|
||||
{moveMode ? (
|
||||
<Text dim>↑↓ to move item, ESC or Enter to exit move mode</Text>
|
||||
) : (
|
||||
<Text dim>↑↓ select, ←→ change type, Enter to move, (a)dd, (i)nsert, (d)elete, ESC back</Text>
|
||||
)}
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
{items.length === 0 ? (
|
||||
<Text dim>No items. Press 'a' to add one.</Text>
|
||||
) : (
|
||||
items.map((item, index) => (
|
||||
<Box key={item.id}>
|
||||
<Text color={index === selectedIndex ? (moveMode ? 'yellow' : 'green') : undefined}>
|
||||
{index === selectedIndex ? (moveMode ? '◆ ' : '▶ ') : ' '}
|
||||
{index + 1}. {getItemDisplay(item)}
|
||||
</Text>
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface ColorMenuProps {
|
||||
items: StatusItem[];
|
||||
onUpdate: (items: StatusItem[]) => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const ColorMenu: React.FC<ColorMenuProps> = ({ items, onUpdate, onBack }) => {
|
||||
const colorableItems = items.filter(item =>
|
||||
['model', 'git-branch', 'tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage'].includes(item.type)
|
||||
);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
// Handle ESC key
|
||||
useInput((input, key) => {
|
||||
if (key.escape) {
|
||||
onBack();
|
||||
}
|
||||
});
|
||||
|
||||
if (colorableItems.length === 0) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text bold>Configure Colors</Text>
|
||||
<Text dim marginTop={1}>No colorable items in the status line.</Text>
|
||||
<Text dim>Add a Model or Git Branch item first.</Text>
|
||||
<Text marginTop={1}>Press any key to go back...</Text>
|
||||
{useInput(() => onBack())}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const getItemLabel = (item: StatusItem) => {
|
||||
switch (item.type) {
|
||||
case 'model': return 'Model';
|
||||
case 'git-branch': return 'Git Branch';
|
||||
case 'tokens-input': return 'Tokens Input';
|
||||
case 'tokens-output': return 'Tokens Output';
|
||||
case 'tokens-cached': return 'Tokens Cached';
|
||||
case 'tokens-total': return 'Tokens Total';
|
||||
case 'context-length': return 'Context Length';
|
||||
case 'context-percentage': return 'Context Percentage';
|
||||
default: return item.type;
|
||||
}
|
||||
};
|
||||
|
||||
// Create menu items with colored labels
|
||||
const menuItems = colorableItems.map((item, index) => {
|
||||
const color = item.color || 'white';
|
||||
const colorFunc = (chalk as any)[color] || chalk.white;
|
||||
return {
|
||||
label: colorFunc(`${getItemLabel(item)} #${index + 1}`),
|
||||
value: item.id,
|
||||
};
|
||||
});
|
||||
menuItems.push({ label: '← Back', value: 'back' });
|
||||
|
||||
const handleSelect = (selected: { value: string }) => {
|
||||
if (selected.value === 'back') {
|
||||
onBack();
|
||||
} else {
|
||||
// Cycle through colors
|
||||
const newItems = items.map(item => {
|
||||
if (item.id === selected.value) {
|
||||
const currentColorIndex = colors.indexOf(item.color || 'white');
|
||||
const nextColor = colors[(currentColorIndex + 1) % colors.length];
|
||||
return { ...item, color: nextColor };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
onUpdate(newItems);
|
||||
}
|
||||
};
|
||||
|
||||
const handleHighlight = (item: { value: string }) => {
|
||||
if (item.value !== 'back') {
|
||||
const itemIndex = colorableItems.findIndex(i => i.id === item.value);
|
||||
if (itemIndex !== -1) {
|
||||
setSelectedIndex(itemIndex);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Color list for cycling
|
||||
const colors = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white',
|
||||
'gray', 'redBright', 'greenBright', 'yellowBright', 'blueBright',
|
||||
'magentaBright', 'cyanBright', 'whiteBright'];
|
||||
|
||||
// Get current color for selected item (if a valid colorable item is selected)
|
||||
const selectedItem = selectedIndex < colorableItems.length ? colorableItems[selectedIndex] : null;
|
||||
const currentColor = selectedItem ? (selectedItem.color || 'white') : 'white';
|
||||
const colorIndex = colors.indexOf(currentColor);
|
||||
const colorNumber = colorIndex === -1 ? 8 : colorIndex + 1; // Default to white (8) if not found
|
||||
const colorDisplay = (chalk as any)[currentColor] ? (chalk as any)[currentColor](currentColor) : chalk.white(currentColor);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text bold>Configure Colors</Text>
|
||||
<Text dim>↑↓ to select item, Enter to cycle color, ESC to go back</Text>
|
||||
{selectedItem && (
|
||||
<Text marginTop={1}>
|
||||
Current color ({colorNumber}/{colors.length}): {colorDisplay}
|
||||
</Text>
|
||||
)}
|
||||
<Box marginTop={1}>
|
||||
<SelectInput
|
||||
items={menuItems}
|
||||
onSelect={handleSelect}
|
||||
onHighlight={handleHighlight}
|
||||
initialIndex={selectedIndex}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const App: React.FC = () => {
|
||||
const { exit } = useApp();
|
||||
const [settings, setSettings] = useState<Settings | null>(null);
|
||||
const [originalSettings, setOriginalSettings] = useState<Settings | null>(null);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [screen, setScreen] = useState<'main' | 'items' | 'colors' | 'confirm'>('main');
|
||||
const [confirmDialog, setConfirmDialog] = useState<{ message: string; action: () => Promise<void> } | null>(null);
|
||||
const [isClaudeInstalled, setIsClaudeInstalled] = useState(false);
|
||||
const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80);
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings().then(loadedSettings => {
|
||||
setSettings(loadedSettings);
|
||||
setOriginalSettings(JSON.parse(JSON.stringify(loadedSettings))); // Deep copy
|
||||
});
|
||||
isInstalled().then(setIsClaudeInstalled);
|
||||
|
||||
const handleResize = () => {
|
||||
setTerminalWidth(process.stdout.columns || 80);
|
||||
};
|
||||
|
||||
process.stdout.on('resize', handleResize);
|
||||
return () => {
|
||||
process.stdout.off('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Check for changes whenever settings update
|
||||
useEffect(() => {
|
||||
if (settings && originalSettings) {
|
||||
const hasAnyChanges = JSON.stringify(settings) !== JSON.stringify(originalSettings);
|
||||
setHasChanges(hasAnyChanges);
|
||||
}
|
||||
}, [settings, originalSettings]);
|
||||
|
||||
useInput((input, key) => {
|
||||
if (key.ctrl && input === 'c') {
|
||||
exit();
|
||||
}
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
return <Text>Loading settings...</Text>;
|
||||
}
|
||||
|
||||
const handleInstallUninstall = async () => {
|
||||
if (isClaudeInstalled) {
|
||||
// Uninstall
|
||||
setConfirmDialog({
|
||||
message: 'This will remove ccstatusline from ~/.claude/settings.json. Continue?',
|
||||
action: async () => {
|
||||
await uninstallStatusLine();
|
||||
setIsClaudeInstalled(false);
|
||||
setScreen('main');
|
||||
setConfirmDialog(null);
|
||||
}
|
||||
});
|
||||
setScreen('confirm');
|
||||
} else {
|
||||
// Always ask for consent before modifying Claude settings
|
||||
const existing = await getExistingStatusLine();
|
||||
let message: string;
|
||||
|
||||
if (existing && existing !== 'npx ccstatusline') {
|
||||
message = `This will modify ~/.claude/settings.json\n\nA status line is already configured: "${existing}"\nReplace it with ccstatusline?`;
|
||||
} else if (existing === 'npx ccstatusline') {
|
||||
message = 'ccstatusline is already installed in ~/.claude/settings.json\nReinstall it?';
|
||||
} else {
|
||||
message = 'This will modify ~/.claude/settings.json to add ccstatusline.\nContinue?';
|
||||
}
|
||||
|
||||
setConfirmDialog({
|
||||
message,
|
||||
action: async () => {
|
||||
await installStatusLine();
|
||||
setIsClaudeInstalled(true);
|
||||
setScreen('main');
|
||||
setConfirmDialog(null);
|
||||
}
|
||||
});
|
||||
setScreen('confirm');
|
||||
}
|
||||
};
|
||||
|
||||
const handleMainMenuSelect = async (value: string) => {
|
||||
switch (value) {
|
||||
case 'items':
|
||||
setScreen('items');
|
||||
break;
|
||||
case 'colors':
|
||||
setScreen('colors');
|
||||
break;
|
||||
case 'install':
|
||||
await handleInstallUninstall();
|
||||
break;
|
||||
case 'save':
|
||||
await saveSettings(settings);
|
||||
setOriginalSettings(JSON.parse(JSON.stringify(settings))); // Update original after save
|
||||
setHasChanges(false);
|
||||
exit();
|
||||
break;
|
||||
case 'exit':
|
||||
exit();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const updateItems = (items: StatusItem[]) => {
|
||||
setSettings({ ...settings, items });
|
||||
};
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" padding={1}>
|
||||
<Box marginBottom={1}>
|
||||
<Text bold color="cyan">🎨 CCStatusline Configuration</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginBottom={1}>
|
||||
<Text dim>Preview:</Text>
|
||||
</Box>
|
||||
<StatusLinePreview items={settings.items} terminalWidth={terminalWidth} />
|
||||
|
||||
<Box marginTop={2}>
|
||||
{screen === 'main' && <MainMenu onSelect={handleMainMenuSelect} isClaudeInstalled={isClaudeInstalled} hasChanges={hasChanges} />}
|
||||
{screen === 'items' && (
|
||||
<ItemsEditor
|
||||
items={settings.items}
|
||||
onUpdate={updateItems}
|
||||
onBack={() => setScreen('main')}
|
||||
/>
|
||||
)}
|
||||
{screen === 'colors' && (
|
||||
<ColorMenu
|
||||
items={settings.items}
|
||||
onUpdate={updateItems}
|
||||
onBack={() => setScreen('main')}
|
||||
/>
|
||||
)}
|
||||
{screen === 'confirm' && confirmDialog && (
|
||||
<ConfirmDialog
|
||||
message={confirmDialog.message}
|
||||
onConfirm={confirmDialog.action}
|
||||
onCancel={() => {
|
||||
setScreen('main');
|
||||
setConfirmDialog(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export function runTUI() {
|
||||
render(<App />);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user