Powerline capability, better color support, custom colors, multiple install options, Output Style widget, UI cleanup

This commit is contained in:
Matthew Breedlove committed 2025-08-17 04:12:34 -04:00
1 parent bd7010fcd2
commit 365b64092a
62 files changed
+7484 -2411

No files matched your search

+46 -46
View File
@@ -14,77 +14,77 @@ ccstatusline is a customizable status line formatter for Claude Code CLI that di
# Install dependencies
bun install
# Run locally (TUI mode)
bun run src/ccstatusline.ts
# Run with patch (TUI mode)
bun run start
# Run directly (TUI mode)
bun run statusline
# 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
bun run build # Creates dist/ccstatusline.js with Node.js 14+ compatibility
# Test npm package locally
npm link
echo '{"model":{"display_name":"Claude 3.5 Sonnet"},"transcript_path":"test.jsonl"}' | ccstatusline
# Lint and type check
bun run lint # Runs TypeScript type checking and ESLint with auto-fix
```
## Architecture
The project has dual runtime compatibility - it works with both Bun and Node.js:
The project has dual runtime compatibility - works with both Bun and Node.js:
### Core Structure
- **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
### TUI Components (src/tui/)
- **index.tsx**: Main TUI entry point that handles React/Ink initialization
- **App.tsx**: Root component managing navigation and state
- **components/**: Modular UI components for different configuration screens
- MainMenu, LineSelector, ItemsEditor, ColorMenu, GlobalOverridesMenu
- PowerlineSetup, TerminalOptionsMenu, StatusLinePreview
### Utilities (src/utils/)
- **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
- **renderer.ts**: Core rendering logic for status lines
- Handles terminal width detection and truncation
- Applies colors, padding, and separators
- Manages flex separator expansion
- **powerline.ts**: Powerline font detection and installation
- **claude-settings.ts**: Integration with Claude Code settings.json
- **colors.ts**: Color definitions and ANSI code mapping
## Key Implementation Details
- **Cross-platform stdin reading**: Code detects Bun vs Node.js environment and uses appropriate stdin API
- **Cross-platform stdin reading**: 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
- **Git integration**: Uses child_process.execSync to get current branch and changes
- **Terminal width management**: Three modes for handling width (full, full-minus-40, full-until-compact)
- **Flex separators**: Special separator type that expands to fill available space
- **Powerline mode**: Optional Powerline-style rendering with arrow separators
- **Custom commands**: Execute shell commands and display output in status line
- **Mergeable items**: Items can be merged together with or without padding
## Bun Usage Preferences
Default to using Bun instead of Node.js.
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.
- Use `bun install` instead of `npm install`
- Use `bun run <script>` instead of `npm run <script>`
- Use `bun build` with appropriate options for building
- 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);
});
```
## Important Notes
- **patch-package**: The project uses patch-package to fix ink-gradient compatibility. Always run `bun run patch` before starting development
- **ESLint configuration**: Uses flat config format (eslint.config.js) with TypeScript and React plugins
- **Build target**: When building for distribution, target Node.js 14+ for maximum compatibility
- **Dependencies**: All runtime dependencies are bundled using `--packages=external` for npm package
- **Type checking and linting**: Only run via `bun run lint` command, never using `npx eslint` or `eslint` directly. Never run `tsx`, `bun tsc` or any other variation
- **Lint rules**: Never disable a lint rule via a comment, no matter how benign the lint warning or error may seem
- **Testing**: No test framework is currently configured. Manual testing is done via piped input and TUI interaction
+45 -41
View File
@@ -42,10 +42,14 @@
## 🆕 Recent Updates
### v1.1.0 - Enhanced Customization
- **🎨 Background Colors** - Set background colors for individual status items in Color Configuration menu
- **🔤 Bold Text Styling** - Toggle bold formatting for individual items in Color Configuration menu
- **⚙️ Global Options Menu** - New menu for global formatting preferences including padding, separators, color inheritance, and overrides
### v2.0.0 - Powerline Support & Enhanced Themes
- **⚡ Powerline Mode** - Beautiful Powerline-style status lines with arrow separators and customizable caps
- **🎨 Built-in Themes** - Multiple pre-configured themes that you can copy and customize
- **🌈 Advanced Color Support** - Basic (16), 256-color (with custom ANSI codes), and truecolor (with hex codes) modes
- **🔗 Widget Merging** - Merge multiple widgets together with or without padding for seamless designs
- **📦 Easy Installation** - Install directly with `npx` or `bunx` - no global package needed
- **🔤 Custom Separators** - Add multiple Powerline separators with custom hex codes for font support
- **🚀 Auto Font Install** - Automatic Powerline font installation with user consent
---
@@ -53,6 +57,7 @@
- **📊 Real-time Metrics** - Display model name, git branch, token usage, session duration, and more
- **🎨 Fully Customizable** - Choose what to display and customize colors for each element
- **⚡ Powerline Support** - Beautiful Powerline-style rendering with arrow separators, caps, and custom fonts
- **📐 Multi-line Support** - Configure up to 3 independent status lines
- **🖥️ Interactive TUI** - Built-in configuration interface using React/Ink
- **⚙️ Global Options** - Apply consistent formatting across all items (padding, separators, bold, background)
@@ -64,11 +69,14 @@
## 🚀 Quick Start
### No installation needed! Use directly with npx:
### No installation needed! Use directly with npx or bunx:
```bash
# Run the configuration TUI
# Run the configuration TUI with npm
npx ccstatusline@latest
# Or with Bun (faster)
bunx ccstatusline@latest
```
### Configure ccstatusline
@@ -97,6 +105,7 @@ Once configured, ccstatusline automatically formats your Claude Code status line
- **Git Changes** - Shows uncommitted insertions/deletions (e.g., "+42,-10")
- **Session Clock** - Shows elapsed time since session start (e.g., "2hr 15m")
- **Version** - Shows Claude Code version
- **Output Style** - Shows the currently set output style in Claude Code
- **Tokens Input** - Shows input tokens used
- **Tokens Output** - Shows output tokens used
- **Tokens Cached** - Shows cached tokens used
@@ -112,36 +121,7 @@ Once configured, ccstatusline automatically formats your Claude Code status line
---
### ⌨️ TUI Controls
#### Main Menu
- **↑↓** - Navigate menu items
- **Enter** - Select item
- **Ctrl+C** - Exit
#### Line Editor
- **↑↓** - Select item
- **←→** - Change item type
- **Enter** - Enter move mode (reorder items)
- **a** - Add item at end
- **i** - Insert item before selected
- **d** - Delete selected item
- **c** - Clear entire line
- **r** - Toggle raw value mode (no labels)
- **b** - Toggle bold text for this item
- **e** - Edit value (for custom-text and custom-command items)
- **w** - Set max width (for custom-command items)
- **t** - Set timeout in milliseconds (for custom-command items)
- **p** - Toggle preserve colors (for custom-command items)
- **Space** - Change separator character (for separator items)
- **ESC** - Go back
#### Color Configuration
- **↑↓** - Select item
- **Enter** - Cycle through colors
- **ESC** - Go back
#### Terminal Width Options
### Terminal Width Options
These settings affect where long lines are truncated, and where right-alignment occurs when using flex separators:
- **Full width always** - Uses full terminal width (may wrap if auto-compact message appears or IDE integration adds text)
- **Full width minus 40** - Reserves 40 characters for auto-compact message to prevent wrapping (default)
@@ -271,11 +251,35 @@ bun run build
```
ccstatusline/
├── src/
│ ├── ccstatusline.ts # Main entry point
│ ├── tui.tsx # React/Ink configuration UI
│ ├── config.ts # Settings management
│ └── claude-settings.ts # Claude Code settings integration
├── dist/ # Built files (generated)
│ ├── ccstatusline.ts # Main entry point
│ ├── tui/ # React/Ink configuration UI
│ │ ├── App.tsx # Root TUI component
│ │ ├── index.tsx # TUI entry point
│ │ └── components/ # UI components
│ │ ├── MainMenu.tsx
│ │ ├── LineSelector.tsx
│ │ ├── ItemsEditor.tsx
│ │ ├── ColorMenu.tsx
│ │ ├── PowerlineSetup.tsx
│ │ └── ...
│ ├── widgets/ # Status line widget implementations
│ │ ├── Model.ts
│ │ ├── GitBranch.ts
│ │ ├── TokensTotal.ts
│ │ ├── OutputStyle.ts
│ │ └── ...
│ ├── utils/ # Utility functions
│ │ ├── config.ts # Settings management
│ │ ├── renderer.ts # Core rendering logic
│ │ ├── powerline.ts # Powerline font utilities
│ │ ├── colors.ts # Color definitions
│ │ └── claude-settings.ts # Claude Code integration
│ └── types/ # TypeScript type definitions
│ ├── Settings.ts
│ ├── Widget.ts
│ ├── PowerlineConfig.ts
│ └── ...
├── dist/ # Built files (generated)
├── package.json
├── tsconfig.json
└── README.md
+514 -20
View File
@@ -4,54 +4,204 @@
"": {
"name": "ccstatusline",
"dependencies": {
"chalk": "^5.5.0",
"ink": "^6.1.0",
"ink-gradient": "^3.0.0",
"ink-select-input": "^6.2.0",
"react": "^19.1.1",
"strip-ansi": "^7.1.0",
"zod": "^4.0.17",
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@stylistic/eslint-plugin": "^5.2.3",
"@types/bun": "latest",
"@types/react": "^19.1.9",
"@types/react": "^19.1.10",
"chalk": "^5.5.0",
"eslint": "^9.33.0",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-import-newlines": "^1.4.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"ink": "^6.2.0",
"ink-gradient": "^3.0.0",
"ink-select-input": "^6.2.0",
"patch-package": "^8.0.0",
},
"peerDependencies": {
"typescript": "^5",
"react": "^19.1.1",
"typescript": "^5.9.2",
"typescript-eslint": "^8.39.1",
},
},
},
"trustedDependencies": [
"unrs-resolver",
],
"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=="],
"@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" } }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
"@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
"@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.3.1", "", {}, "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA=="],
"@eslint/core": ["@eslint/core@0.15.2", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
"@eslint/js": ["@eslint/js@9.33.0", "", {}, "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
"@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.2.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/types": "^8.38.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-oY7GVkJGVMI5benlBDCaRrSC1qPasafyv5dOBLLv5MTilMGnErKhO6ziEfodDDIZbo5QxPUNW360VudJOFODMw=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
"@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/gradient-string": ["@types/gradient-string@1.1.6", "", { "dependencies": { "@types/tinycolor2": "*" } }, "sha512-LkaYxluY4G5wR1M4AKQUal2q61Di1yVVCw42ImFTuaIoQVgmV0WP1xUaLB8zwb47mp82vWTpePI9JmrjEnJ7nQ=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
"@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=="],
"@types/react": ["@types/react@19.1.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg=="],
"@types/tinycolor2": ["@types/tinycolor2@1.4.6", "", {}, "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.39.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.39.1", "@typescript-eslint/type-utils": "8.39.1", "@typescript-eslint/utils": "8.39.1", "@typescript-eslint/visitor-keys": "8.39.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.39.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.39.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.39.1", "@typescript-eslint/types": "8.39.1", "@typescript-eslint/typescript-estree": "8.39.1", "@typescript-eslint/visitor-keys": "8.39.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.39.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.39.1", "@typescript-eslint/types": "^8.39.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.39.1", "", { "dependencies": { "@typescript-eslint/types": "8.39.1", "@typescript-eslint/visitor-keys": "8.39.1" } }, "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.39.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.39.1", "", { "dependencies": { "@typescript-eslint/types": "8.39.1", "@typescript-eslint/typescript-estree": "8.39.1", "@typescript-eslint/utils": "8.39.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.39.1", "", {}, "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.39.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.39.1", "@typescript-eslint/tsconfig-utils": "8.39.1", "@typescript-eslint/types": "8.39.1", "@typescript-eslint/visitor-keys": "8.39.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.39.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.39.1", "@typescript-eslint/types": "8.39.1", "@typescript-eslint/typescript-estree": "8.39.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.39.1", "", { "dependencies": { "@typescript-eslint/types": "8.39.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A=="],
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="],
"@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="],
"@unrs/resolver-binding-darwin-arm64": ["@unrs/resolver-binding-darwin-arm64@1.11.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g=="],
"@unrs/resolver-binding-darwin-x64": ["@unrs/resolver-binding-darwin-x64@1.11.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ=="],
"@unrs/resolver-binding-freebsd-x64": ["@unrs/resolver-binding-freebsd-x64@1.11.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw=="],
"@unrs/resolver-binding-linux-arm-gnueabihf": ["@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw=="],
"@unrs/resolver-binding-linux-arm-musleabihf": ["@unrs/resolver-binding-linux-arm-musleabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw=="],
"@unrs/resolver-binding-linux-arm64-gnu": ["@unrs/resolver-binding-linux-arm64-gnu@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ=="],
"@unrs/resolver-binding-linux-arm64-musl": ["@unrs/resolver-binding-linux-arm64-musl@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w=="],
"@unrs/resolver-binding-linux-ppc64-gnu": ["@unrs/resolver-binding-linux-ppc64-gnu@1.11.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA=="],
"@unrs/resolver-binding-linux-riscv64-gnu": ["@unrs/resolver-binding-linux-riscv64-gnu@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ=="],
"@unrs/resolver-binding-linux-riscv64-musl": ["@unrs/resolver-binding-linux-riscv64-musl@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew=="],
"@unrs/resolver-binding-linux-s390x-gnu": ["@unrs/resolver-binding-linux-s390x-gnu@1.11.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg=="],
"@unrs/resolver-binding-linux-x64-gnu": ["@unrs/resolver-binding-linux-x64-gnu@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w=="],
"@unrs/resolver-binding-linux-x64-musl": ["@unrs/resolver-binding-linux-x64-musl@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA=="],
"@unrs/resolver-binding-wasm32-wasi": ["@unrs/resolver-binding-wasm32-wasi@1.11.1", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" }, "cpu": "none" }, "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ=="],
"@unrs/resolver-binding-win32-arm64-msvc": ["@unrs/resolver-binding-win32-arm64-msvc@1.11.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw=="],
"@unrs/resolver-binding-win32-ia32-msvc": ["@unrs/resolver-binding-win32-ia32-msvc@1.11.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ=="],
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
"@yarnpkg/lockfile": ["@yarnpkg/lockfile@1.1.0", "", {}, "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"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=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
"array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="],
"array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="],
"array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="],
"array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="],
"array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="],
"array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="],
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
"at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="],
"auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
"bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="],
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
@@ -59,6 +209,8 @@
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"chalk": ["chalk@5.5.0", "", {}, "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg=="],
"ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="],
@@ -83,100 +235,282 @@
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
"data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
"es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-iterator-helpers": ["es-iterator-helpers@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.6", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.4", "safe-array-concat": "^1.1.3" } }, "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="],
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
"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=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@9.33.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.33.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA=="],
"eslint-import-context": ["eslint-import-context@0.1.9", "", { "dependencies": { "get-tsconfig": "^4.10.1", "stable-hash-x": "^0.2.0" }, "peerDependencies": { "unrs-resolver": "^1.0.0" }, "optionalPeers": ["unrs-resolver"] }, "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg=="],
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="],
"eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@4.4.4", "", { "dependencies": { "debug": "^4.4.1", "eslint-import-context": "^0.1.8", "get-tsconfig": "^4.10.1", "is-bun-module": "^2.0.0", "stable-hash-x": "^0.2.0", "tinyglobby": "^0.2.14", "unrs-resolver": "^1.7.11" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw=="],
"eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="],
"eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="],
"eslint-plugin-import-newlines": ["eslint-plugin-import-newlines@1.4.0", "", { "peerDependencies": { "eslint": ">=6.0.0" }, "bin": { "import-linter": "lib/index.js" } }, "sha512-+Cz1x2xBLtI9gJbmuYEpvY7F8K75wskBmJ7rk4VRObIJo+jklUJaejFJgtnWeL0dCFWabGEkhausrikXaNbtoQ=="],
"eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="],
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="],
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"find-yarn-workspace-root": ["find-yarn-workspace-root@2.0.0", "", { "dependencies": { "micromatch": "^4.0.2" } }, "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ=="],
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
"fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="],
"functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
"get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"gradient-string": ["gradient-string@2.0.2", "", { "dependencies": { "chalk": "^4.1.2", "tinygradient": "^1.1.5" } }, "sha512-rEDCuqUQ4tbD78TpzsMtt5OIf0cBCSDWSJtUDaF6JsAh+k0v9r++NzxNEG87oDZx9ZwGhD8DaezR2L/yrw0Jdw=="],
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
"has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"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": ["ink@6.2.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-NQbNokT11cuxlIcCDfBMk1vEwaqc/cjTSqc4R4JugBO4BpWVe2B2A6ElC2koZQ9Vj91z0C40zid/jxOF2hJL9A=="],
"ink-gradient": ["ink-gradient@3.0.0", "", { "dependencies": { "@types/gradient-string": "^1.1.2", "gradient-string": "^2.0.2", "prop-types": "^15.8.1", "strip-ansi": "^7.1.0" }, "peerDependencies": { "ink": ">=4" } }, "sha512-OVyPBovBxE1tFcBhSamb+P1puqDP6pG3xFe2W9NiLgwUZd9RbcjBeR7twLbliUT9navrUstEf1ZcPKKvx71BsQ=="],
"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=="],
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
"is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
"is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="],
"is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="],
"is-bun-module": ["is-bun-module@2.0.0", "", { "dependencies": { "semver": "^7.7.1" } }, "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ=="],
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
"is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="],
"is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
"is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
"is-generator-function": ["is-generator-function@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-in-ci": ["is-in-ci@1.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg=="],
"is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
"is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
"is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
"is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="],
"is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="],
"is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="],
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
"is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="],
"is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="],
"is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="],
"is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
"isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stable-stringify": ["json-stable-stringify@1.3.0", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "isarray": "^2.0.5", "jsonify": "^0.0.1", "object-keys": "^1.1.1" } }, "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
"jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
"jsonify": ["jsonify@0.0.1", "", {}, "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg=="],
"jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"klaw-sync": ["klaw-sync@6.0.0", "", { "dependencies": { "graceful-fs": "^4.1.11" } }, "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"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=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
@@ -185,53 +519,121 @@
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"napi-postinstall": ["napi-postinstall@0.3.3", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
"object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
"object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="],
"object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="],
"object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="],
"object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="],
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
"patch-package": ["patch-package@8.0.0", "", { "dependencies": { "@yarnpkg/lockfile": "^1.1.0", "chalk": "^4.1.2", "ci-info": "^3.7.0", "cross-spawn": "^7.0.3", "find-yarn-workspace-root": "^2.0.0", "fs-extra": "^9.0.0", "json-stable-stringify": "^1.0.2", "klaw-sync": "^6.0.0", "minimist": "^1.2.6", "open": "^7.4.2", "rimraf": "^2.6.3", "semver": "^7.5.3", "slash": "^2.0.0", "tmp": "^0.0.33", "yaml": "^2.2.2" }, "bin": { "patch-package": "index.js" } }, "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
"react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"react-devtools-core": ["react-devtools-core@6.1.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="],
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="],
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
"resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
"safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
"set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
@@ -239,16 +641,38 @@
"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=="],
"stable-hash-x": ["stable-hash-x@0.2.0", "", {}, "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ=="],
"stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
"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=="],
"string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="],
"string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="],
"string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="],
"string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="],
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
"strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="],
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"tinygradient": ["tinygradient@1.1.5", "", { "dependencies": { "@types/tinycolor2": "^1.4.0", "tinycolor2": "^1.0.0" } }, "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw=="],
"tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="],
@@ -257,18 +681,52 @@
"to-rotated": ["to-rotated@1.0.0", "", {}, "sha512-KsEID8AfgUy+pxVRLsWp0VzCa69wxzUDZnzGbyIST/bcgcrMvTYoFBX/QORH4YApoD89EDuUovx4BTdpOn319Q=="],
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
"tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
"typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="],
"typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="],
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"typescript-eslint": ["typescript-eslint@8.39.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.39.1", "@typescript-eslint/parser": "8.39.1", "@typescript-eslint/typescript-estree": "8.39.1", "@typescript-eslint/utils": "8.39.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-GDUv6/NDYngUlNvwaHM1RamYftxf782IyEDbdj3SeaIHHv8fNQVRC++fITT7kUJV/5rIA/tkoRSSskt6osEfqg=="],
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
"which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="],
"which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="],
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"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=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
@@ -277,20 +735,56 @@
"yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
"zod": ["zod@4.0.17", "", {}, "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"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=="],
"eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"eslint-import-resolver-node/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
"eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"gradient-string/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"is-bun-module/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"patch-package/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"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=="],
"patch-package/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"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=="],
"stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"gradient-string/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"patch-package/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+159
View File
@@ -0,0 +1,159 @@
import js from '@eslint/js';
import ts from 'typescript-eslint';
import stylistic from '@stylistic/eslint-plugin';
import importPlugin from 'eslint-plugin-import';
import importNewlinesPlugin from 'eslint-plugin-import-newlines';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import globals from 'globals';
export default ts.config([
{
files: ['**/*.ts', '**/*.tsx'],
plugins: {
stylistic,
importPlugin,
'import-newlines': importNewlinesPlugin
},
extends: [
js.configs.recommended,
ts.configs.strictTypeChecked,
ts.configs.stylisticTypeChecked,
importPlugin.flatConfigs.recommended,
stylistic.configs.customize({
quotes: 'single',
semi: true,
indent: 4,
jsx: true
})
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: import.meta.dirname
},
globals: {
...globals.node,
...globals.es2021
}
},
settings: {
'import/resolver': {
typescript: {
project: ['./tsconfig.json'],
alwaysTryTypes: true,
noWarnOnMultipleProjects: true
},
parcel2: {},
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json']
}
},
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx']
},
'import/external-module-folders': ['node_modules', 'node_modules/@types']
},
rules: {
'no-control-regex': 'off', // We intentionally match ANSI escape sequences
'eqeqeq': 'error',
'import/order': ['error', {
alphabetize: {
'order': 'asc',
'orderImportKind': 'asc',
'caseInsensitive': false
},
named: {
'enabled': true,
'types': 'types-last'
},
pathGroupsExcludedImportTypes: ["builtin"],
groups: [
['builtin', 'external'],
'internal',
'parent',
'sibling',
'index',
'unknown'
],
'newlines-between': 'always'
}],
'import-newlines/enforce': ['error', {
items: 1,
semi: true
}],
'@typescript-eslint/no-unused-vars': ['error', { 'args': 'none' }],
'@typescript-eslint/no-empty-function': ['error', { 'allow': ['private-constructors'] }],
'@typescript-eslint/array-type': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/no-inferrable-types': ['error', { 'ignoreProperties': true }],
'@typescript-eslint/restrict-template-expressions': 'off',
'@stylistic/indent': ['error', 4, {
'ImportDeclaration': 1
}],
'@stylistic/key-spacing': 'off',
'@stylistic/comma-dangle': ['error', 'never'],
'@stylistic/no-multi-spaces': 'off',
'@stylistic/brace-style': ['error', '1tbs', { 'allowSingleLine': true }],
'@stylistic/max-statements-per-line': ['error', { 'max': 2 }],
'@stylistic/operator-linebreak': ['error', 'before'],
'@stylistic/padding-line-between-statements': 'error',
'@stylistic/implicit-arrow-linebreak': ['error', 'beside'],
'@stylistic/no-extra-semi': 'error',
'@stylistic/nonblock-statement-body-position': ['error', 'below'],
'@stylistic/object-curly-newline': ['error', { 'multiline': true }],
'@stylistic/switch-colon-spacing': 'error',
'@stylistic/eol-last': ['error', 'never'],
'@stylistic/jsx-quotes': ['error', 'prefer-single'],
'@stylistic/multiline-ternary': 'off',
'import/no-unresolved': ['error'],
'import/no-named-as-default': 'off',
'import/no-named-as-default-member': 'off',
'import/default': 'off'
}
},
{
files: ['**/*.tsx', '**/*.jsx'],
plugins: {
react: reactPlugin,
'react-hooks': reactHooksPlugin
},
settings: {
...{
'import/resolver': {
typescript: {
project: ['./tsconfig.json'],
alwaysTryTypes: true,
noWarnOnMultipleProjects: true
},
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json']
}
},
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx']
},
'import/external-module-folders': ['node_modules', 'node_modules/@types']
},
react: {
version: 'detect'
}
},
rules: {
'react/jsx-uses-react': 'error',
'react/jsx-uses-vars': 'error',
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn'
}
},
{
ignores: [
'**/dist/',
'**/node_modules/',
'**/*.js',
'!eslint.config.js'
]
}
]);
+24 -9
View File
@@ -1,12 +1,10 @@
{
"name": "ccstatusline",
"version": "1.2.0",
"version": "2.0.0",
"description": "A customizable status line formatter for Claude Code CLI",
"module": "src/ccstatusline.ts",
"type": "module",
"bin": {
"ccstatusline": "dist/ccstatusline.js"
},
"bin": "dist/ccstatusline.js",
"files": [
"dist/"
],
@@ -15,18 +13,28 @@
"statusline": "bun run src/ccstatusline.ts",
"patch": "patch-package",
"build": "bun run patch && rm -rf dist/* && bun build src/ccstatusline.ts --target=node --outfile=dist/ccstatusline.js --target-version=14",
"prepublishOnly": "bun run build"
"prepublishOnly": "bun run build",
"lint": "bun tsc --noEmit; eslint . --config eslint.config.js --max-warnings=999999 --fix"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@stylistic/eslint-plugin": "^5.2.3",
"@types/bun": "latest",
"@types/react": "^19.1.9",
"typescript": "^5",
"@types/react": "^19.1.10",
"chalk": "^5.5.0",
"ink": "^6.1.0",
"eslint": "^9.33.0",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-import-newlines": "^1.4.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"ink": "^6.2.0",
"ink-gradient": "^3.0.0",
"ink-select-input": "^6.2.0",
"patch-package": "^8.0.0",
"react": "^19.1.1"
"react": "^19.1.1",
"typescript": "^5.9.2",
"typescript-eslint": "^8.39.1"
},
"keywords": [
"claude",
@@ -43,5 +51,12 @@
},
"engines": {
"node": ">=14.0.0"
},
"trustedDependencies": [
"unrs-resolver"
],
"dependencies": {
"strip-ansi": "^7.1.0",
"zod": "^4.0.17"
}
}
File renamed without changes.
+93 -32
View File
@@ -1,12 +1,19 @@
#!/usr/bin/env node
import { runTUI } from './tui.tsx';
import { loadSettings } from './utils/config';
import chalk from 'chalk';
import { runTUI } from './tui';
import { StatusJSONSchema } from './types/StatusJSON';
import { updateColorMap } from './utils/colors';
import {
loadSettings,
saveSettings
} from './utils/config';
import {
type StatusJSON,
renderStatusLine,
getTokenMetrics,
getSessionDuration,
type RenderContext
getTokenMetrics,
renderStatusLine,
type RenderContext,
type StatusJSON
} from './utils/renderer';
async function readStdin(): Promise<string | null> {
@@ -19,7 +26,7 @@ async function readStdin(): Promise<string | null> {
try {
// Use Node.js compatible approach
if (typeof Bun !== 'undefined' && Bun.stdin) {
if (typeof Bun !== 'undefined') {
// Bun environment
const decoder = new TextDecoder();
for await (const chunk of Bun.stdin.stream()) {
@@ -29,7 +36,7 @@ async function readStdin(): Promise<string | null> {
// Node.js environment
process.stdin.setEncoding('utf8');
for await (const chunk of process.stdin) {
chunks.push(chunk);
chunks.push(chunk as string);
}
}
return chunks.join('');
@@ -41,30 +48,27 @@ async function readStdin(): Promise<string | null> {
async function renderMultipleLines(data: StatusJSON) {
const settings = await loadSettings();
// Get all lines to render (support both old items format and new lines format)
let lines = settings.lines || (settings.items ? [settings.items] : [[]]);
// Set global chalk level based on settings
chalk.level = settings.colorLevel;
// Update color map after setting chalk level
updateColorMap();
// Get all lines to render
const lines = settings.lines;
// Get token metrics if needed (check all lines)
const hasTokenItems = lines.some(line =>
line.some(item =>
['tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage', 'context-percentage-usable'].includes(item.type)
)
);
const hasTokenItems = lines.some(line => line.some(item => ['tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage', 'context-percentage-usable'].includes(item.type)));
// Check if session clock is needed
const hasSessionClock = lines.some(line =>
line.some(item => item.type === 'session-clock')
);
const hasSessionClock = lines.some(line => line.some(item => item.type === 'session-clock'));
let tokenMetrics = null;
if (hasTokenItems && data.transcript_path) {
if (hasTokenItems && data.transcript_path)
tokenMetrics = await getTokenMetrics(data.transcript_path);
}
let sessionDuration = null;
if (hasSessionClock && data.transcript_path) {
if (hasSessionClock && data.transcript_path)
sessionDuration = await getSessionDuration(data.transcript_path);
}
// Create render context
const context: RenderContext = {
@@ -75,14 +79,58 @@ async function renderMultipleLines(data: StatusJSON) {
};
// Render each line
for (const lineItems of lines) {
let globalSeparatorIndex = 0;
for (let i = 0; i < lines.length; i++) {
const lineItems = lines[i];
if (lineItems && lineItems.length > 0) {
const line = renderStatusLine(lineItems, settings, context);
// Replace all spaces with non-breaking spaces to prevent VSCode trimming
let outputLine = line.replace(/ /g, '\u00A0');
// Add reset code at the beginning to override Claude Code's dim setting
outputLine = '\x1b[0m' + outputLine;
console.log(outputLine);
const lineContext = { ...context, lineIndex: i, globalSeparatorIndex };
const line = renderStatusLine(lineItems, settings, lineContext);
// Only output the line if it has content (not just ANSI codes)
// Strip ANSI codes to check if there's actual text
const strippedLine = line.replace(/\x1b\[[0-9;]*m/g, '').trim();
if (strippedLine.length > 0) {
// Count separators used in this line (widgets - 1, excluding merged widgets)
const nonMergedWidgets = lineItems.filter((_, idx) => idx === lineItems.length - 1 || !lineItems[idx]?.merge);
if (nonMergedWidgets.length > 1)
globalSeparatorIndex += nonMergedWidgets.length - 1;
// Replace all spaces with non-breaking spaces to prevent VSCode trimming
let outputLine = line.replace(/ /g, '\u00A0');
// Add reset code at the beginning to override Claude Code's dim setting
outputLine = '\x1b[0m' + outputLine;
console.log(outputLine);
}
}
}
// Check if there's an update message to display
if (settings.updatemessage?.message
&& settings.updatemessage.message.trim() !== ''
&& settings.updatemessage.remaining
&& settings.updatemessage.remaining > 0) {
// Display the message
console.log(settings.updatemessage.message);
// Decrement the remaining count
const newRemaining = settings.updatemessage.remaining - 1;
// Update or remove the updatemessage
if (newRemaining <= 0) {
// Remove the entire updatemessage block
const { updatemessage, ...newSettings } = settings;
void updatemessage;
await saveSettings(newSettings);
} else {
// Update the remaining count
await saveSettings({
...settings,
updatemessage: {
...settings.updatemessage,
remaining: newRemaining
}
});
}
}
}
@@ -94,8 +142,14 @@ async function main() {
const input = await readStdin();
if (input && input.trim() !== '') {
try {
const data: StatusJSON = JSON.parse(input);
await renderMultipleLines(data);
// Parse and validate JSON in one step
const result = StatusJSONSchema.safeParse(JSON.parse(input));
if (!result.success) {
console.error('Invalid status JSON format:', result.error.message);
process.exit(1);
}
await renderMultipleLines(result.data);
} catch (error) {
console.error('Error parsing JSON:', error);
process.exit(1);
@@ -106,8 +160,15 @@ async function main() {
}
} else {
// Interactive mode - run TUI
// Remove updatemessage before running TUI
const settings = await loadSettings();
if (settings.updatemessage) {
const { updatemessage, ...newSettings } = settings;
void updatemessage;
await saveSettings(newSettings);
}
runTUI();
}
}
main();
void main();
-1603
View File
File diff suppressed because it is too large. Load diff
+467
View File
@@ -0,0 +1,467 @@
import chalk from 'chalk';
import {
Box,
Text,
render,
useApp,
useInput
} from 'ink';
import Gradient from 'ink-gradient';
import React, {
useEffect,
useState
} from 'react';
import type { Settings } from '../types/Settings';
import type { WidgetItem } from '../types/Widget';
import {
getExistingStatusLine,
installStatusLine,
isBunxAvailable,
isInstalled,
uninstallStatusLine
} from '../utils/claude-settings';
import {
loadSettings,
saveSettings
} from '../utils/config';
import {
checkPowerlineFonts,
checkPowerlineFontsAsync,
installPowerlineFonts,
type PowerlineFontStatus
} from '../utils/powerline';
import { getPackageVersion } from '../utils/terminal';
import {
ColorMenu,
ConfirmDialog,
GlobalOverridesMenu,
InstallMenu,
ItemsEditor,
LineSelector,
MainMenu,
PowerlineSetup,
StatusLinePreview,
TerminalOptionsMenu,
TerminalWidthMenu
} from './components';
export 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' | 'lines' | 'items' | 'colorLines' | 'colors' | 'terminalWidth' | 'terminalConfig' | 'globalOverrides' | 'confirm' | 'powerline' | 'install'>('main');
const [selectedLine, setSelectedLine] = useState(0);
const [menuSelections, setMenuSelections] = useState<Record<string, number>>({});
const [confirmDialog, setConfirmDialog] = useState<{ message: string; action: () => Promise<void> } | null>(null);
const [isClaudeInstalled, setIsClaudeInstalled] = useState(false);
const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80);
const [powerlineFontStatus, setPowerlineFontStatus] = useState<PowerlineFontStatus>({ installed: false });
const [installingFonts, setInstallingFonts] = useState(false);
const [fontInstallMessage, setFontInstallMessage] = useState<string | null>(null);
const [existingStatusLine, setExistingStatusLine] = useState<string | null>(null);
const [saveMessage, setSaveMessage] = useState<string | null>(null);
const [previewIsTruncated, setPreviewIsTruncated] = useState(false);
useEffect(() => {
// Load existing status line
void getExistingStatusLine().then(setExistingStatusLine);
void loadSettings().then((loadedSettings) => {
// Set global chalk level based on settings (default to 256 colors for compatibility)
chalk.level = loadedSettings.colorLevel;
// Ensure lines array has 3 slots
while (loadedSettings.lines.length < 3) {
loadedSettings.lines.push([]);
}
setSettings(loadedSettings);
setOriginalSettings(JSON.parse(JSON.stringify(loadedSettings)) as Settings); // Deep copy
});
void isInstalled().then(setIsClaudeInstalled);
// Check for Powerline fonts on startup (use sync version that doesn't call execSync)
const fontStatus = checkPowerlineFonts();
setPowerlineFontStatus(fontStatus);
// Optionally do the async check later (but not blocking React)
void checkPowerlineFontsAsync().then((asyncStatus) => {
setPowerlineFontStatus(asyncStatus);
});
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 (originalSettings) {
const hasAnyChanges = JSON.stringify(settings) !== JSON.stringify(originalSettings);
setHasChanges(hasAnyChanges);
}
}, [settings, originalSettings]);
// Clear save message after 2 seconds
useEffect(() => {
if (saveMessage) {
const timer = setTimeout(() => {
setSaveMessage(null);
}, 2000);
return () => { clearTimeout(timer); };
}
}, [saveMessage]);
useInput((input, key) => {
if (key.ctrl && input === 'c') {
exit();
}
// Global save shortcut
if (key.ctrl && input === 's' && settings) {
void (async () => {
await saveSettings(settings);
setOriginalSettings(JSON.parse(JSON.stringify(settings)) as Settings);
setHasChanges(false);
setSaveMessage('✓ Configuration saved');
})();
}
});
if (!settings) {
return <Text>Loading settings...</Text>;
}
const handleInstallUninstall = () => {
if (isClaudeInstalled) {
// Uninstall
setConfirmDialog({
message: 'This will remove ccstatusline from ~/.claude/settings.json. Continue?',
action: async () => {
await uninstallStatusLine();
setIsClaudeInstalled(false);
setExistingStatusLine(null);
setScreen('main');
setConfirmDialog(null);
}
});
setScreen('confirm');
} else {
// Show install menu to select npx or bunx
setScreen('install');
}
};
const handleMainMenuSelect = async (value: string) => {
switch (value) {
case 'lines':
setScreen('lines');
break;
case 'colors':
setScreen('colorLines');
break;
case 'terminalConfig':
setScreen('terminalConfig');
break;
case 'globalOverrides':
setScreen('globalOverrides');
break;
case 'powerline':
setScreen('powerline');
break;
case 'install':
handleInstallUninstall();
break;
case 'save':
await saveSettings(settings);
setOriginalSettings(JSON.parse(JSON.stringify(settings)) as Settings); // Update original after save
setHasChanges(false);
exit();
break;
case 'exit':
exit();
break;
}
};
const updateLine = (lineIndex: number, widgets: WidgetItem[]) => {
const newLines = [...settings.lines];
newLines[lineIndex] = widgets;
setSettings({ ...settings, lines: newLines });
};
const handleLineSelect = (lineIndex: number) => {
setSelectedLine(lineIndex);
setScreen('items');
};
return (
<Box flexDirection='column' padding={1}>
<Box marginBottom={1}>
<Text bold>
<Gradient name='retro'>
CCStatusline Configuration
</Gradient>
</Text>
<Text bold>
{` | ${getPackageVersion() && `v${getPackageVersion()}`}`}
</Text>
{saveMessage && (
<Text color='green' bold>
{` ${saveMessage}`}
</Text>
)}
</Box>
<StatusLinePreview
lines={settings.lines}
terminalWidth={terminalWidth}
settings={settings}
onTruncationChange={setPreviewIsTruncated}
/>
<Box marginTop={1}>
{screen === 'main' && (
<MainMenu
onSelect={(value) => {
// Only persist menu selection if not exiting
if (value !== 'save' && value !== 'exit') {
const menuMap: Record<string, number> = {
lines: 0,
colors: 1,
powerline: 2,
terminalConfig: 3,
globalOverrides: 4,
install: 5
};
setMenuSelections({ ...menuSelections, main: menuMap[value] ?? 0 });
}
void handleMainMenuSelect(value);
}}
isClaudeInstalled={isClaudeInstalled}
hasChanges={hasChanges}
initialSelection={menuSelections.main}
powerlineFontStatus={powerlineFontStatus}
settings={settings}
previewIsTruncated={previewIsTruncated}
/>
)}
{screen === 'lines' && (
<LineSelector
lines={settings.lines}
onSelect={(line) => {
setMenuSelections({ ...menuSelections, lines: line });
handleLineSelect(line);
}}
onBack={() => {
// Save that we came from 'lines' menu (index 0)
// Clear the line selection so it resets next time we enter
setMenuSelections({ ...menuSelections, main: 0 });
setScreen('main');
}}
initialSelection={menuSelections.lines}
title='Select Line to Edit Items'
/>
)}
{screen === 'items' && (
<ItemsEditor
widgets={settings.lines[selectedLine] ?? []}
onUpdate={(widgets) => { updateLine(selectedLine, widgets); }}
onBack={() => {
// When going back to lines menu, preserve which line was selected
setMenuSelections({ ...menuSelections, lines: selectedLine });
setScreen('lines');
}}
lineNumber={selectedLine + 1}
settings={settings}
/>
)}
{screen === 'colorLines' && (
<LineSelector
lines={settings.lines}
onSelect={(line) => {
setMenuSelections({ ...menuSelections, lines: line });
setSelectedLine(line);
setScreen('colors');
}}
onBack={() => {
// Save that we came from 'colors' menu (index 1)
setMenuSelections({ ...menuSelections, main: 1 });
setScreen('main');
}}
initialSelection={menuSelections.lines}
title='Select Line to Edit Colors'
blockIfPowerlineActive={true}
settings={settings}
/>
)}
{screen === 'colors' && (
<ColorMenu
widgets={settings.lines[selectedLine] ?? []}
lineIndex={selectedLine}
settings={settings}
onUpdate={(updatedWidgets) => {
// Update only the selected line
const newLines = [...settings.lines];
newLines[selectedLine] = updatedWidgets;
setSettings({ ...settings, lines: newLines });
}}
onBack={() => {
// Go back to line selection for colors
setScreen('colorLines');
}}
/>
)}
{screen === 'terminalConfig' && (
<TerminalOptionsMenu
settings={settings}
onUpdate={(updatedSettings) => {
setSettings(updatedSettings);
}}
onBack={(target?: string) => {
if (target === 'width') {
setScreen('terminalWidth');
} else {
// Save that we came from 'terminalConfig' menu (index 3)
setMenuSelections({ ...menuSelections, main: 3 });
setScreen('main');
}
}}
/>
)}
{screen === 'terminalWidth' && (
<TerminalWidthMenu
settings={settings}
onUpdate={(updatedSettings) => {
setSettings(updatedSettings);
}}
onBack={() => {
setScreen('terminalConfig');
}}
/>
)}
{screen === 'globalOverrides' && (
<GlobalOverridesMenu
settings={settings}
onUpdate={(updatedSettings) => {
setSettings(updatedSettings);
}}
onBack={() => {
// Save that we came from 'globalOverrides' menu (index 4)
setMenuSelections({ ...menuSelections, main: 4 });
setScreen('main');
}}
/>
)}
{screen === 'confirm' && confirmDialog && (
<ConfirmDialog
message={confirmDialog.message}
onConfirm={() => void confirmDialog.action()}
onCancel={() => {
setScreen('main');
setConfirmDialog(null);
}}
/>
)}
{screen === 'install' && (
<InstallMenu
bunxAvailable={isBunxAvailable()}
existingStatusLine={existingStatusLine}
onSelectNpx={() => {
void getExistingStatusLine().then((existing) => {
const isAlreadyInstalled = ['npx -y ccstatusline@latest', 'bunx -y ccstatusline@latest'].includes(existing ?? '');
let message: string;
if (existing && !isAlreadyInstalled) {
message = `This will modify ~/.claude/settings.json\n\nA status line is already configured: "${existing}"\nReplace it with npx -y ccstatusline@latest?`;
} else if (isAlreadyInstalled) {
message = 'ccstatusline is already installed in ~/.claude/settings.json\nUpdate it with npx -y ccstatusline@latest?';
} else {
message = 'This will modify ~/.claude/settings.json to add ccstatusline with npx.\nContinue?';
}
setConfirmDialog({
message,
action: async () => {
await installStatusLine(false);
setIsClaudeInstalled(true);
setExistingStatusLine('npx -y ccstatusline@latest');
setScreen('main');
setConfirmDialog(null);
}
});
setScreen('confirm');
});
}}
onSelectBunx={() => {
void getExistingStatusLine().then((existing) => {
const isAlreadyInstalled = ['npx -y ccstatusline@latest', 'bunx -y ccstatusline@latest'].includes(existing ?? '');
let message: string;
if (existing && !isAlreadyInstalled) {
message = `This will modify ~/.claude/settings.json\n\nA status line is already configured: "${existing}"\nReplace it with bunx -y ccstatusline@latest?`;
} else if (isAlreadyInstalled) {
message = 'ccstatusline is already installed in ~/.claude/settings.json\nUpdate it with bunx -y ccstatusline@latest?';
} else {
message = 'This will modify ~/.claude/settings.json to add ccstatusline with bunx.\nContinue?';
}
setConfirmDialog({
message,
action: async () => {
await installStatusLine(true);
setIsClaudeInstalled(true);
setExistingStatusLine('bunx -y ccstatusline@latest');
setScreen('main');
setConfirmDialog(null);
}
});
setScreen('confirm');
});
}}
onCancel={() => {
setScreen('main');
}}
/>
)}
{screen === 'powerline' && (
<PowerlineSetup
settings={settings}
powerlineFontStatus={powerlineFontStatus}
onUpdate={(updatedSettings) => {
setSettings(updatedSettings);
}}
onBack={() => {
setScreen('main');
}}
onInstallFonts={() => {
setInstallingFonts(true);
void installPowerlineFonts().then((result) => {
setInstallingFonts(false);
setFontInstallMessage(result.message);
// Refresh font status
void checkPowerlineFontsAsync().then((asyncStatus) => {
setPowerlineFontStatus(asyncStatus);
});
});
}}
installingFonts={installingFonts}
fontInstallMessage={fontInstallMessage}
onClearMessage={() => { setFontInstallMessage(null); }}
/>
)}
</Box>
</Box>
);
};
export function runTUI() {
// Clear the terminal before starting the TUI
process.stdout.write('\x1b[2J\x1b[H');
render(<App />);
}
+577
View File
@@ -0,0 +1,577 @@
import chalk from 'chalk';
import {
Box,
Text,
useInput
} from 'ink';
import SelectInput from 'ink-select-input';
import React, { useState } from 'react';
import { getColorLevelString } from '../../types/ColorLevel';
import type { Settings } from '../../types/Settings';
import type { WidgetItem } from '../../types/Widget';
import {
applyColors,
getAvailableBackgroundColorsForUI,
getAvailableColorsForUI
} from '../../utils/colors';
import { getWidget } from '../../utils/widgets';
import { ConfirmDialog } from './ConfirmDialog';
export interface ColorMenuProps {
widgets: WidgetItem[];
lineIndex?: number;
settings: Settings;
onUpdate: (widgets: WidgetItem[]) => void;
onBack: () => void;
}
export const ColorMenu: React.FC<ColorMenuProps> = ({ widgets, lineIndex, settings, onUpdate, onBack }) => {
const [showSeparators, setShowSeparators] = useState(false);
const [hexInputMode, setHexInputMode] = useState(false);
const [hexInput, setHexInput] = useState('');
const [ansi256InputMode, setAnsi256InputMode] = useState(false);
const [ansi256Input, setAnsi256Input] = useState('');
const [showClearConfirm, setShowClearConfirm] = useState(false);
const powerlineEnabled = settings.powerline.enabled;
const colorableWidgets = widgets.filter((widget) => {
// Include separators only if showSeparators is true
if (widget.type === 'separator') {
return showSeparators;
}
// Use the widget's supportsColors method
try {
const widgetInstance = getWidget(widget.type);
return widgetInstance.supportsColors(widget);
} catch {
// If widget type is not found, exclude it
return false;
}
});
const [highlightedItemId, setHighlightedItemId] = useState<string | null>(colorableWidgets[0]?.id ?? null);
const [editingBackground, setEditingBackground] = useState(false);
// Handle keyboard input
const hasNoItems = colorableWidgets.length === 0;
useInput((input, key) => {
// If no items, any key goes back
if (hasNoItems) {
onBack();
return;
}
// Skip input handling when confirmation is active - let ConfirmDialog handle it
if (showClearConfirm) {
return;
}
// Handle hex input mode
if (hexInputMode) {
// Disable arrow keys in input mode
if (key.upArrow || key.downArrow) {
return;
}
if (key.escape) {
setHexInputMode(false);
setHexInput('');
} else if (key.return) {
// Validate and apply the hex color
if (hexInput.length === 6) {
const hexColor = `hex:${hexInput}`;
const selectedWidget = colorableWidgets.find(widget => widget.id === highlightedItemId);
if (selectedWidget) {
// IMPORTANT: Update ALL items (not just colorableWidgets) to maintain proper indexing
const newItems = widgets.map((widget) => {
if (widget.id === highlightedItemId) {
if (editingBackground) {
return { ...widget, backgroundColor: hexColor };
} else {
return { ...widget, color: hexColor };
}
}
return widget;
});
onUpdate(newItems);
}
setHexInputMode(false);
setHexInput('');
}
} else if (key.backspace || key.delete) {
setHexInput(hexInput.slice(0, -1));
} else if (input && hexInput.length < 6) {
// Only accept hex characters (0-9, A-F, a-f)
const upperInput = input.toUpperCase();
if (/^[0-9A-F]$/.test(upperInput)) {
setHexInput(hexInput + upperInput);
}
}
return;
}
// Handle ansi256 input mode
if (ansi256InputMode) {
// Disable arrow keys in input mode
if (key.upArrow || key.downArrow) {
return;
}
if (key.escape) {
setAnsi256InputMode(false);
setAnsi256Input('');
} else if (key.return) {
// Validate and apply the ansi256 color
const code = parseInt(ansi256Input, 10);
if (!isNaN(code) && code >= 0 && code <= 255) {
const ansiColor = `ansi256:${code}`;
const selectedWidget = colorableWidgets.find(widget => widget.id === highlightedItemId);
if (selectedWidget) {
// IMPORTANT: Update ALL items (not just colorableWidgets) to maintain proper indexing
const newItems = widgets.map((widget) => {
if (widget.id === highlightedItemId) {
if (editingBackground) {
return { ...widget, backgroundColor: ansiColor };
} else {
return { ...widget, color: ansiColor };
}
}
return widget;
});
onUpdate(newItems);
setAnsi256InputMode(false);
setAnsi256Input('');
}
}
} else if (key.backspace || key.delete) {
setAnsi256Input(ansi256Input.slice(0, -1));
} else if (input && ansi256Input.length < 3) {
// Only accept numeric characters (0-9)
if (/^[0-9]$/.test(input)) {
const newInput = ansi256Input + input;
const code = parseInt(newInput, 10);
// Only allow if it won't exceed 255
if (code <= 255) {
setAnsi256Input(newInput);
}
}
}
return;
}
// Ignore number keys to prevent SelectInput numerical navigation
if (input && /^[0-9]$/.test(input)) {
return;
}
// Normal keyboard handling when there are items
if (key.escape) {
if (editingBackground) {
setEditingBackground(false);
} else {
onBack();
}
} else if (input === 'h' || input === 'H') {
// Enter hex input mode (only in truecolor mode)
if (highlightedItemId && highlightedItemId !== 'back' && settings.colorLevel === 3) {
setHexInputMode(true);
setHexInput('');
}
} else if (input === 'a' || input === 'A') {
// Enter ansi256 input mode (only in 256 color mode)
if (highlightedItemId && highlightedItemId !== 'back' && settings.colorLevel === 2) {
setAnsi256InputMode(true);
setAnsi256Input('');
}
} else if ((input === 's' || input === 'S') && !key.ctrl) {
// Toggle show separators (only if not in powerline mode and no default separator)
if (!settings.powerline.enabled && !settings.defaultSeparator) {
setShowSeparators(!showSeparators);
// The highlighted item ID will be maintained, and we'll recalculate
// the initial index when rendering the SelectInput
}
} else if (input === 'f' || input === 'F') {
if (colorableWidgets.length > 0) {
setEditingBackground(!editingBackground);
}
} else if (input === 'b' || input === 'B') {
if (highlightedItemId && highlightedItemId !== 'back') {
// Toggle bold for the highlighted item
const selectedWidget = colorableWidgets.find(widget => widget.id === highlightedItemId);
if (selectedWidget) {
const newItems = widgets.map((widget) => {
if (widget.id === selectedWidget.id) {
return { ...widget, bold: !widget.bold };
}
return widget;
});
onUpdate(newItems);
}
}
} else if (input === 'r' || input === 'R') {
if (highlightedItemId && highlightedItemId !== 'back') {
// Reset all styling (color, background, and bold) for the highlighted item
const selectedWidget = colorableWidgets.find(widget => widget.id === highlightedItemId);
if (selectedWidget) {
const newItems = widgets.map((widget) => {
if (widget.id === selectedWidget.id) {
// Remove color, backgroundColor, and bold properties
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { color, backgroundColor, bold, ...restWidget } = widget;
return restWidget;
}
return widget;
});
onUpdate(newItems);
}
}
} else if (input === 'c' || input === 'C') {
// Show clear all confirmation
setShowClearConfirm(true);
} else if (key.leftArrow || key.rightArrow) {
// Cycle through colors with arrow keys
if (highlightedItemId && highlightedItemId !== 'back') {
const selectedWidget = colorableWidgets.find(widget => widget.id === highlightedItemId);
if (selectedWidget) {
const newItems = widgets.map((widget) => {
if (widget.id === selectedWidget.id) {
if (editingBackground) {
const currentBgColor = widget.backgroundColor ?? ''; // Empty string for 'none'
let currentBgColorIndex = bgColors.indexOf(currentBgColor);
// If color not found, start from beginning
if (currentBgColorIndex === -1)
currentBgColorIndex = 0;
let nextBgColorIndex;
if (key.rightArrow) {
nextBgColorIndex = (currentBgColorIndex + 1) % bgColors.length;
} else {
nextBgColorIndex = currentBgColorIndex === 0 ? bgColors.length - 1 : currentBgColorIndex - 1;
}
const nextBgColor = bgColors[nextBgColorIndex];
return { ...widget, backgroundColor: nextBgColor === '' ? undefined : nextBgColor };
} else {
let defaultColor = 'white';
if (widget.type !== 'separator' && widget.type !== 'flex-separator') {
const widgetImpl = getWidget(widget.type);
defaultColor = widgetImpl.getDefaultColor();
}
let currentColor = widget.color ?? defaultColor;
// If color is 'dim', treat as if no color was set
if (currentColor === 'dim') {
currentColor = defaultColor;
}
let currentColorIndex = colors.indexOf(currentColor);
// If color not found, start from beginning
if (currentColorIndex === -1)
currentColorIndex = 0;
let nextColorIndex;
if (key.rightArrow) {
nextColorIndex = (currentColorIndex + 1) % colors.length;
} else {
nextColorIndex = currentColorIndex === 0 ? colors.length - 1 : currentColorIndex - 1;
}
const nextColor = colors[nextColorIndex];
return { ...widget, color: nextColor };
}
}
return widget;
});
onUpdate(newItems);
}
}
}
});
if (hasNoItems) {
return (
<Box flexDirection='column'>
<Text bold>
Configure Colors
{lineIndex !== undefined ? ` - Line ${lineIndex + 1}` : ''}
</Text>
<Box marginTop={1}><Text dimColor>No colorable items in the status line.</Text></Box>
<Text dimColor>Add a Model or Git Branch item first.</Text>
<Box marginTop={1}><Text>Press any key to go back...</Text></Box>
</Box>
);
}
const getItemLabel = (widget: WidgetItem) => {
if (widget.type === 'separator') {
const char = widget.character ?? '|';
return `Separator: ${char === ' ' ? 'space' : char}`;
}
if (widget.type === 'flex-separator') {
return 'Flex Separator';
}
const widgetImpl = getWidget(widget.type);
return widgetImpl.getDisplayName();
};
// Color list for cycling
// Get available colors from colors.ts
const colorOptions = getAvailableColorsForUI();
const colors = colorOptions.map(c => c.value || '');
// For background, get background colors
const bgColorOptions = getAvailableBackgroundColorsForUI();
const bgColors = bgColorOptions.map(c => c.value || '');
// Create menu items with colored labels
const menuItems = colorableWidgets.map((widget, index) => {
const label = `${index + 1}: ${getItemLabel(widget)}`;
// Apply both foreground and background colors
const level = getColorLevelString(settings.colorLevel);
let defaultColor = 'white';
if (widget.type !== 'separator' && widget.type !== 'flex-separator') {
const widgetImpl = getWidget(widget.type);
defaultColor = widgetImpl.getDefaultColor();
}
const styledLabel = applyColors(label, widget.color ?? defaultColor, widget.backgroundColor, widget.bold, level);
return {
label: styledLabel,
value: widget.id
};
});
menuItems.push({ label: '← Back', value: 'back' });
const handleSelect = (selected: { value: string }) => {
if (selected.value === 'back') {
onBack();
}
// Enter no longer cycles colors - use left/right arrow keys instead
};
const handleHighlight = (item: { value: string }) => {
setHighlightedItemId(item.value);
};
// Get current color for highlighted item
const selectedWidget = highlightedItemId && highlightedItemId !== 'back'
? colorableWidgets.find(widget => widget.id === highlightedItemId)
: null;
const currentColor = editingBackground
? (selectedWidget?.backgroundColor ?? '') // Empty string for 'none'
: (selectedWidget ? (selectedWidget.color ?? (() => {
if (selectedWidget.type !== 'separator' && selectedWidget.type !== 'flex-separator') {
const widgetImpl = getWidget(selectedWidget.type);
return widgetImpl.getDefaultColor();
}
return 'white';
})()) : 'white');
const colorList = editingBackground ? bgColors : colors;
const colorIndex = colorList.indexOf(currentColor);
const colorNumber = colorIndex === -1 ? 'custom' : colorIndex + 1;
let colorDisplay;
if (editingBackground) {
if (!currentColor || currentColor === '') {
colorDisplay = chalk.gray('(no background)');
} else {
// Determine display name based on format
let displayName;
if (currentColor.startsWith('ansi256:')) {
displayName = `ANSI ${currentColor.substring(8)}`;
} else if (currentColor.startsWith('hex:')) {
displayName = `#${currentColor.substring(4)}`;
} else {
const colorOption = bgColorOptions.find(c => c.value === currentColor);
displayName = colorOption ? colorOption.name : currentColor;
}
// Apply the color using our applyColors function with the current colorLevel
const level = getColorLevelString(settings.colorLevel);
colorDisplay = applyColors(` ${displayName} `, undefined, currentColor, false, level);
}
} else {
if (!currentColor || currentColor === '') {
colorDisplay = chalk.gray('(default)');
} else {
// Determine display name based on format
let displayName;
if (currentColor.startsWith('ansi256:')) {
displayName = `ANSI ${currentColor.substring(8)}`;
} else if (currentColor.startsWith('hex:')) {
displayName = `#${currentColor.substring(4)}`;
} else {
const colorOption = colorOptions.find(c => c.value === currentColor);
displayName = colorOption ? colorOption.name : currentColor;
}
// Apply the color using our applyColors function with the current colorLevel
const level = getColorLevelString(settings.colorLevel);
colorDisplay = applyColors(displayName, currentColor, undefined, false, level);
}
}
// Show confirmation dialog if clearing all colors
if (showClearConfirm) {
return (
<Box flexDirection='column'>
<Text bold color='yellow'>⚠ Confirm Clear All Colors</Text>
<Box marginTop={1} flexDirection='column'>
<Text>This will reset all colors for all widgets to their defaults.</Text>
<Text color='red'>This action cannot be undone!</Text>
</Box>
<Box marginTop={2}>
<Text>Continue?</Text>
</Box>
<Box marginTop={1}>
<ConfirmDialog
inline={true}
onConfirm={() => {
// Clear all colors from all widgets
const newItems = widgets.map((widget) => {
// Remove color, backgroundColor, and bold properties
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { color, backgroundColor, bold, ...restWidget } = widget;
return restWidget;
});
onUpdate(newItems);
setShowClearConfirm(false);
}}
onCancel={() => {
setShowClearConfirm(false);
}}
/>
</Box>
</Box>
);
}
// Check for global overrides
// Note: When powerline is enabled, background override doesn't affect the display
// since powerline uses item-specific backgrounds for segments
const hasGlobalFgOverride = !!settings.overrideForegroundColor;
const hasGlobalBgOverride = !!settings.overrideBackgroundColor && !powerlineEnabled;
const globalOverrideMessage = hasGlobalFgOverride && hasGlobalBgOverride
? '⚠ Global override for FG and BG active'
: hasGlobalFgOverride
? '⚠ Global override for FG active'
: hasGlobalBgOverride
? '⚠ Global override for BG active'
: null;
return (
<Box flexDirection='column'>
<Box>
<Text bold>
Configure Colors
{lineIndex !== undefined ? ` - Line ${lineIndex + 1}` : ''}
{editingBackground && chalk.yellow(' [Background Mode]')}
</Text>
{globalOverrideMessage && (
<Text color='yellow' dimColor>
{'. '}
{globalOverrideMessage}
</Text>
)}
</Box>
{hexInputMode ? (
<Box flexDirection='column'>
<Text>Enter 6-digit hex color code (without #):</Text>
<Text>
#
{hexInput}
<Text dimColor>{hexInput.length < 6 ? '_'.repeat(6 - hexInput.length) : ''}</Text>
</Text>
<Text> </Text>
<Text dimColor>Press Enter when done, ESC to cancel</Text>
</Box>
) : ansi256InputMode ? (
<Box flexDirection='column'>
<Text>Enter ANSI 256 color code (0-255):</Text>
<Text>
{ansi256Input}
<Text dimColor>{ansi256Input.length === 0 ? '___' : ansi256Input.length === 1 ? '__' : ansi256Input.length === 2 ? '_' : ''}</Text>
</Text>
<Text> </Text>
<Text dimColor>Press Enter when done, ESC to cancel</Text>
</Box>
) : (
<>
<Text dimColor>
↑↓ to select, ←→ to cycle
{' '}
{editingBackground ? 'background' : 'foreground'}
, (f) to toggle bg/fg, (b)old,
{settings.colorLevel === 3 ? ' (h)ex,' : settings.colorLevel === 2 ? ' (a)nsi256,' : ''}
{' '}
(r)eset, (c)lear all, ESC to go back
</Text>
{!settings.powerline.enabled && !settings.defaultSeparator && (
<Text dimColor>
(s)how separators:
{showSeparators ? chalk.green('ON') : chalk.gray('OFF')}
</Text>
)}
{selectedWidget ? (
<Box marginTop={1}>
<Text>
Current
{' '}
{editingBackground ? 'background' : 'foreground'}
{' '}
(
{colorNumber === 'custom' ? 'custom' : `${colorNumber}/${colorList.length}`}
):
{' '}
{colorDisplay}
{selectedWidget.bold && chalk.bold(' [BOLD]')}
</Text>
</Box>
) : (
<Box marginTop={1}>
<Text> </Text>
</Box>
)}
</>
)}
<Box marginTop={1}>
{(hexInputMode || ansi256InputMode) ? (
// Static list when in input mode - no keyboard interaction
<Box flexDirection='column'>
{menuItems.map(item => (
<Text
key={item.value}
color={item.value === highlightedItemId ? 'cyan' : 'white'}
bold={item.value === highlightedItemId}
>
{item.value === highlightedItemId ? '▶ ' : ' '}
{item.label}
</Text>
))}
</Box>
) : (
// Interactive SelectInput when not in input mode
<SelectInput
key={`${showSeparators}-${highlightedItemId}`}
items={menuItems}
onSelect={handleSelect}
onHighlight={handleHighlight}
initialIndex={Math.max(0, menuItems.findIndex(item => item.value === highlightedItemId))}
indicatorComponent={({ isSelected }) => (
<Text>{isSelected ? '▶' : ' '}</Text>
)}
itemComponent={({ isSelected, label }) => (
// The label already has ANSI codes applied via applyColors()
// We need to pass it directly as a single Text child to preserve the codes
<Text>{` ${label}`}</Text>
)}
/>
)}
</Box>
<Box marginTop={1} flexDirection='column'>
<Text color='yellow'>⚠ VSCode Users: </Text>
<Text dimColor wrap='wrap'>If colors appear incorrect in the VSCode integrated terminal, the "Terminal › Integrated: Minimum Contrast Ratio" (`terminal.integrated.minimumContrastRatio`) setting is forcing a minimum contrast between foreground and background colors. You can adjust this setting to 1 to disable the contrast enforcement, or use a standalone terminal for accurate colors.</Text>
</Box>
</Box>
);
};
+64
View File
@@ -0,0 +1,64 @@
import {
Box,
Text,
useInput
} from 'ink';
import React, { useState } from 'react';
export interface ConfirmDialogProps {
message?: string;
onConfirm: () => void;
onCancel: () => void;
inline?: boolean;
}
export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ message, onConfirm, onCancel, inline = false }) => {
const [selectedIndex, setSelectedIndex] = useState(0); // Default to "Yes"
useInput((input, key) => {
if (key.upArrow) {
setSelectedIndex(Math.max(0, selectedIndex - 1));
} else if (key.downArrow) {
setSelectedIndex(Math.min(1, selectedIndex + 1));
} else if (key.return) {
if (selectedIndex === 0) {
onConfirm();
} else {
onCancel();
}
} else if (key.escape) {
onCancel();
}
});
const renderOptions = () => {
const yesStyle = selectedIndex === 0 ? { color: 'cyan' } : {};
const noStyle = selectedIndex === 1 ? { color: 'cyan' } : {};
return (
<Box flexDirection='column'>
<Text {...yesStyle}>
{selectedIndex === 0 ? '▶ ' : ' '}
Yes
</Text>
<Text {...noStyle}>
{selectedIndex === 1 ? '▶ ' : ' '}
No
</Text>
</Box>
);
};
if (inline) {
return renderOptions();
}
return (
<Box flexDirection='column'>
<Text>{message}</Text>
<Box marginTop={1}>
{renderOptions()}
</Box>
</Box>
);
};
+314
View File
@@ -0,0 +1,314 @@
import {
Box,
Text,
useInput
} from 'ink';
import React, { useState } from 'react';
import type { Settings } from '../../types/Settings';
import {
COLOR_MAP,
getChalkColor,
getColorDisplayName
} from '../../utils/colors';
import { ConfirmDialog } from './ConfirmDialog';
export interface GlobalOverridesMenuProps {
settings: Settings;
onUpdate: (settings: Settings) => void;
onBack: () => void;
}
export const GlobalOverridesMenu: React.FC<GlobalOverridesMenuProps> = ({ settings, onUpdate, onBack }) => {
const [editingPadding, setEditingPadding] = useState(false);
const [editingSeparator, setEditingSeparator] = useState(false);
const [confirmingSeparator, setConfirmingSeparator] = useState(false);
const [paddingInput, setPaddingInput] = useState(settings.defaultPadding ?? '');
const [separatorInput, setSeparatorInput] = useState(settings.defaultSeparator ?? '');
const [inheritColors, setInheritColors] = useState(settings.inheritSeparatorColors);
const [globalBold, setGlobalBold] = useState(settings.globalBold);
const isPowerlineEnabled = settings.powerline.enabled;
// Check if there are any manual separators in the current configuration
const hasManualSeparators = settings.lines.some(line => line.some(item => item.type === 'separator')
);
// Get colors from COLOR_MAP
const bgColors = ['none', ...COLOR_MAP.filter(c => c.isBackground).map(c => c.name)];
const fgColors = ['none', ...COLOR_MAP.filter(c => !c.isBackground).map(c => c.name)];
const currentBgIndex = bgColors.indexOf(settings.overrideBackgroundColor ?? 'none');
const currentFgIndex = fgColors.indexOf(settings.overrideForegroundColor ?? 'none');
useInput((input, key) => {
if (editingPadding) {
if (key.return) {
const updatedSettings = {
...settings,
defaultPadding: paddingInput
};
onUpdate(updatedSettings);
setEditingPadding(false);
} else if (key.escape) {
setPaddingInput(settings.defaultPadding ?? '');
setEditingPadding(false);
} else if (key.backspace) {
setPaddingInput(paddingInput.slice(0, -1));
} else if (key.delete) {
// For simple text inputs without cursor, forward delete does nothing
} else if (input) {
setPaddingInput(paddingInput + input);
}
} else if (editingSeparator) {
if (key.return) {
// Only show confirmation if setting a non-empty separator AND there are manual separators
if (separatorInput && hasManualSeparators) {
setEditingSeparator(false);
setConfirmingSeparator(true);
} else {
// Apply directly without confirmation
const updatedSettings = {
...settings,
defaultSeparator: separatorInput || undefined,
// Only remove manual separators if we're setting a non-empty default
lines: separatorInput
? settings.lines.map(line => line.filter(item => item.type !== 'separator'))
: settings.lines
};
onUpdate(updatedSettings);
setEditingSeparator(false);
}
} else if (key.escape) {
setSeparatorInput(settings.defaultSeparator ?? '');
setEditingSeparator(false);
} else if (key.backspace) {
setSeparatorInput(separatorInput.slice(0, -1));
} else if (key.delete) {
// For simple text inputs without cursor, forward delete does nothing
} else if (input) {
setSeparatorInput(separatorInput + input);
}
} else if (confirmingSeparator) {
// Skip input handling when confirmation is active - let ConfirmDialog handle it
return;
} else {
if (key.escape) {
onBack();
} else if (input === 'p' || input === 'P') {
setEditingPadding(true);
} else if ((input === 's' || input === 'S') && !isPowerlineEnabled && !key.ctrl) {
setEditingSeparator(true);
} else if ((input === 'i' || input === 'I') && !isPowerlineEnabled) {
const newInheritColors = !inheritColors;
setInheritColors(newInheritColors);
const updatedSettings = {
...settings,
inheritSeparatorColors: newInheritColors
};
onUpdate(updatedSettings);
} else if ((input === 'b' || input === 'B') && !isPowerlineEnabled) {
// Cycle through background colors
const nextIndex = (currentBgIndex + 1) % bgColors.length;
const nextBgColor = bgColors[nextIndex];
const updatedSettings = {
...settings,
overrideBackgroundColor: nextBgColor === 'none' ? undefined : nextBgColor
};
onUpdate(updatedSettings);
} else if ((input === 'c' || input === 'C') && !isPowerlineEnabled) {
// Clear override background color
const updatedSettings = {
...settings,
overrideBackgroundColor: undefined
};
onUpdate(updatedSettings);
} else if (input === 'o' || input === 'O') {
// Toggle global bold
const newGlobalBold = !globalBold;
setGlobalBold(newGlobalBold);
const updatedSettings = {
...settings,
globalBold: newGlobalBold
};
onUpdate(updatedSettings);
} else if (input === 'f' || input === 'F') {
// Cycle through foreground colors
const nextIndex = (currentFgIndex + 1) % fgColors.length;
const nextFgColor = fgColors[nextIndex];
const updatedSettings = {
...settings,
overrideForegroundColor: nextFgColor === 'none' ? undefined : nextFgColor
};
onUpdate(updatedSettings);
} else if (input === 'g' || input === 'G') {
// Clear override foreground color
const updatedSettings = {
...settings,
overrideForegroundColor: undefined
};
onUpdate(updatedSettings);
}
}
});
return (
<Box flexDirection='column'>
<Text bold>Global Overrides</Text>
<Text dimColor>Configure automatic padding and separators between items</Text>
{isPowerlineEnabled && (
<Box marginTop={1}>
<Text color='yellow'>⚠ Some options are disabled while Powerline mode is active</Text>
</Box>
)}
<Box marginTop={1} />
{editingPadding ? (
<Box flexDirection='column'>
<Box>
<Text>Enter default padding (applied to left and right of each item): </Text>
<Text color='cyan'>{paddingInput ? `"${paddingInput}"` : '(empty)'}</Text>
</Box>
<Text dimColor>Press Enter to save, ESC to cancel</Text>
</Box>
) : editingSeparator ? (
<Box flexDirection='column'>
<Box>
<Text>Enter default separator (placed between items): </Text>
<Text color='cyan'>{separatorInput ? `"${separatorInput}"` : '(empty - no separator will be added)'}</Text>
</Box>
<Text dimColor>Press Enter to save, ESC to cancel</Text>
</Box>
) : confirmingSeparator ? (
<Box flexDirection='column'>
<Box marginBottom={1}>
<Text color='yellow'>⚠ Warning: Setting a default separator will remove all existing manual separators from your status lines.</Text>
</Box>
<Box>
<Text>New default separator: </Text>
<Text color='cyan'>{separatorInput ? `"${separatorInput}"` : '(empty)'}</Text>
</Box>
<Box marginTop={1}>
<Text>Do you want to continue? </Text>
</Box>
<Box marginTop={1}>
<ConfirmDialog
inline={true}
onConfirm={() => {
// Remove all manual separators from lines
const updatedSettings = {
...settings,
defaultSeparator: separatorInput,
lines: settings.lines.map(line => line.filter(item => item.type !== 'separator')
)
};
onUpdate(updatedSettings);
setConfirmingSeparator(false);
}}
onCancel={() => {
// Cancel without applying changes
setSeparatorInput(settings.defaultSeparator ?? '');
setConfirmingSeparator(false);
}}
/>
</Box>
</Box>
) : (
<>
<Box>
<Text> Global Bold: </Text>
<Text color={globalBold ? 'green' : 'red'}>{globalBold ? '✓ Enabled' : '✗ Disabled'}</Text>
<Text dimColor> - Press (o) to toggle</Text>
</Box>
<Box>
<Text> Default Padding: </Text>
<Text color='cyan'>{settings.defaultPadding ? `"${settings.defaultPadding}"` : '(none)'}</Text>
<Text dimColor> - Press (p) to edit</Text>
</Box>
<Box>
<Text>Override FG Color: </Text>
{(() => {
const fgColor = settings.overrideForegroundColor ?? 'none';
if (fgColor === 'none') {
return <Text color='gray'>(none)</Text>;
} else {
const displayName = getColorDisplayName(fgColor);
const fgChalk = getChalkColor(fgColor, 'ansi16', false);
const display = fgChalk ? fgChalk(displayName) : displayName;
return <Text>{display}</Text>;
}
})()}
<Text dimColor> - (f) cycle, (g) clear</Text>
</Box>
<Box>
<Text>Override BG Color: </Text>
{isPowerlineEnabled ? (
<Text dimColor>[disabled - Powerline active]</Text>
) : (
<>
{(() => {
const bgColor = settings.overrideBackgroundColor ?? 'none';
if (bgColor === 'none') {
return <Text color='gray'>(none)</Text>;
} else {
const displayName = getColorDisplayName(bgColor);
const bgChalk = getChalkColor(bgColor, 'ansi16', true);
const display = bgChalk ? bgChalk(` ${displayName} `) : displayName;
return <Text>{display}</Text>;
}
})()}
<Text dimColor> - (b) cycle, (c) clear</Text>
</>
)}
</Box>
<Box>
<Text> Inherit Colors: </Text>
{isPowerlineEnabled ? (
<Text dimColor>[disabled - Powerline active]</Text>
) : (
<>
<Text color={inheritColors ? 'green' : 'red'}>{inheritColors ? '✓ Enabled' : '✗ Disabled'}</Text>
<Text dimColor> - Press (i) to toggle</Text>
</>
)}
</Box>
<Box>
<Text>Default Separator: </Text>
{isPowerlineEnabled ? (
<Text dimColor>[disabled - Powerline active]</Text>
) : (
<>
<Text color='cyan'>{settings.defaultSeparator ? `"${settings.defaultSeparator}"` : '(none)'}</Text>
<Text dimColor> - Press (s) to edit</Text>
</>
)}
</Box>
<Box marginTop={2}>
<Text dimColor>Press ESC to go back</Text>
</Box>
<Box marginTop={1} flexDirection='column'>
<Text dimColor wrap='wrap'>
Note: These settings are applied during rendering and don't add items to your widget list.
</Text>
<Text dimColor wrap='wrap'>
• Inherit colors: Separators will use colors from the preceding widget
</Text>
<Text dimColor wrap='wrap'>
• Global Bold: Makes all text bold regardless of individual settings
</Text>
<Text dimColor wrap='wrap'>
• Override colors: All items will use these colors instead of their configured colors
</Text>
</Box>
</>
)}
</Box>
);
};
+107
View File
@@ -0,0 +1,107 @@
import {
Box,
Text,
useInput
} from 'ink';
import React, { useState } from 'react';
export interface InstallMenuProps {
bunxAvailable: boolean;
existingStatusLine: string | null;
onSelectNpx: () => void;
onSelectBunx: () => void;
onCancel: () => void;
}
export const InstallMenu: React.FC<InstallMenuProps> = ({
bunxAvailable,
existingStatusLine,
onSelectNpx,
onSelectBunx,
onCancel
}) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const maxIndex = 2; // npx, bunx (if available), and back
useInput((input, key) => {
if (key.escape) {
onCancel();
} else if (key.upArrow) {
if (selectedIndex === 2) {
setSelectedIndex(bunxAvailable ? 1 : 0); // Skip bunx if not available
} else {
setSelectedIndex(Math.max(0, selectedIndex - 1));
}
} else if (key.downArrow) {
if (selectedIndex === 0) {
setSelectedIndex(bunxAvailable ? 1 : 2); // Skip bunx if not available
} else if (selectedIndex === 1 && bunxAvailable) {
setSelectedIndex(2);
} else {
setSelectedIndex(Math.min(maxIndex, selectedIndex + 1));
}
} else if (key.return) {
if (selectedIndex === 0) {
onSelectNpx();
} else if (selectedIndex === 1 && bunxAvailable) {
onSelectBunx();
} else if (selectedIndex === 2) {
onCancel();
}
}
});
return (
<Box flexDirection='column'>
<Text bold>Install ccstatusline to Claude Code</Text>
{existingStatusLine && (
<Box marginBottom={1}>
<Text color='yellow'>
⚠ Current status line: "
{existingStatusLine}
"
</Text>
</Box>
)}
<Box>
<Text dimColor>Select package manager to use:</Text>
</Box>
<Box marginTop={1} flexDirection='column'>
<Box>
<Text color={selectedIndex === 0 ? 'blue' : undefined}>
{selectedIndex === 0 ? '▶ ' : ' '}
npx - Node Package Execute
</Text>
</Box>
<Box>
<Text color={selectedIndex === 1 && bunxAvailable ? 'blue' : undefined} dimColor={!bunxAvailable}>
{selectedIndex === 1 && bunxAvailable ? '▶ ' : ' '}
bunx - Bun Package Execute
{!bunxAvailable && ' (not installed)'}
</Text>
</Box>
<Box marginTop={1}>
<Text color={selectedIndex === 2 ? 'blue' : undefined}>
{selectedIndex === 2 ? '▶ ' : ' '}
← Back
</Text>
</Box>
</Box>
<Box marginTop={2}>
<Text dimColor>
The selected command will be written to ~/.claude/settings.json
</Text>
</Box>
<Box marginTop={1}>
<Text dimColor>Press Enter to select, ESC to cancel</Text>
</Box>
</Box>
);
};
+436
View File
@@ -0,0 +1,436 @@
import {
Box,
Text,
useInput
} from 'ink';
import React, { useState } from 'react';
import type { Settings } from '../../types/Settings';
import type {
Widget,
WidgetItem,
WidgetItemType
} from '../../types/Widget';
import { getBackgroundColorsForPowerline } from '../../utils/colors';
import { generateGuid } from '../../utils/guid';
import { canDetectTerminalWidth } from '../../utils/terminal';
import {
getAllWidgetTypes,
getWidget
} from '../../utils/widgets';
export interface ItemsEditorProps {
widgets: WidgetItem[];
onUpdate: (widgets: WidgetItem[]) => void;
onBack: () => void;
lineNumber: number;
settings: Settings;
}
export const ItemsEditor: React.FC<ItemsEditorProps> = ({ widgets, onUpdate, onBack, lineNumber, settings }) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const [moveMode, setMoveMode] = useState(false);
const [customEditorWidget, setCustomEditorWidget] = useState<{ widget: WidgetItem; impl: Widget } | null>(null);
const separatorChars = ['|', '-', ',', ' '];
// Determine which item types are allowed based on settings
const getAllowedTypes = (): WidgetItemType[] => {
let allowedTypes = getAllWidgetTypes(settings);
// Remove separator if default separator is set
if (settings.defaultSeparator) {
allowedTypes = allowedTypes.filter(t => t !== 'separator');
}
// Remove both separator and flex-separator if powerline mode is enabled
if (settings.powerline.enabled) {
allowedTypes = allowedTypes.filter(t => t !== 'separator' && t !== 'flex-separator');
}
return allowedTypes;
};
// Get the default type for new widgets (first non-separator type)
const getDefaultItemType = (): WidgetItemType => {
const allowedTypes = getAllowedTypes();
return allowedTypes.includes('model') ? 'model' : (allowedTypes[0] ?? 'model');
};
// Get a unique background color for powerline mode
const getUniqueBackgroundColor = (insertIndex: number): string | undefined => {
// Only apply background colors if powerline is enabled and NOT using custom theme
if (!settings.powerline.enabled || settings.powerline.theme === 'custom') {
return undefined;
}
// Get all available background colors (excluding black for better visibility)
const bgColors = getBackgroundColorsForPowerline();
// Get colors of adjacent items
const prevWidget = insertIndex > 0 ? widgets[insertIndex - 1] : null;
const nextWidget = insertIndex < widgets.length ? widgets[insertIndex] : null;
const prevBg = prevWidget?.backgroundColor;
const nextBg = nextWidget?.backgroundColor;
// Filter out colors that match neighbors
const availableColors = bgColors.filter(color => color !== prevBg && color !== nextBg);
// If we have available colors, pick one randomly
if (availableColors.length > 0) {
const randomIndex = Math.floor(Math.random() * availableColors.length);
return availableColors[randomIndex];
}
// Fallback: if somehow both neighbors use all 14 colors (impossible with 2 neighbors),
// just pick any color that's different from the previous
return bgColors.find(c => c !== prevBg) ?? bgColors[0];
};
const handleEditorComplete = (updatedWidget: WidgetItem) => {
const newWidgets = [...widgets];
newWidgets[selectedIndex] = updatedWidget;
onUpdate(newWidgets);
setCustomEditorWidget(null);
};
const handleEditorCancel = () => {
setCustomEditorWidget(null);
};
useInput((input, key) => {
// Skip input if custom editor is active
if (customEditorWidget) {
return;
}
if (moveMode) {
// In move mode, use up/down to move the selected item
if (key.upArrow && selectedIndex > 0) {
const newWidgets = [...widgets];
const temp = newWidgets[selectedIndex];
const prev = newWidgets[selectedIndex - 1];
if (temp && prev) {
[newWidgets[selectedIndex], newWidgets[selectedIndex - 1]] = [prev, temp];
}
onUpdate(newWidgets);
setSelectedIndex(selectedIndex - 1);
} else if (key.downArrow && selectedIndex < widgets.length - 1) {
const newWidgets = [...widgets];
const temp = newWidgets[selectedIndex];
const next = newWidgets[selectedIndex + 1];
if (temp && next) {
[newWidgets[selectedIndex], newWidgets[selectedIndex + 1]] = [next, temp];
}
onUpdate(newWidgets);
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(widgets.length - 1, selectedIndex + 1));
} else if (key.leftArrow && widgets.length > 0) {
// Toggle item type backwards
const types = getAllowedTypes();
const currentWidget = widgets[selectedIndex];
if (currentWidget) {
const currentType = currentWidget.type;
let currentIndex = types.indexOf(currentType);
// If current type is not in allowed types (e.g., separator when disabled), find a valid type
if (currentIndex === -1) {
currentIndex = 0;
}
const prevIndex = currentIndex === 0 ? types.length - 1 : currentIndex - 1;
const newWidgets = [...widgets];
const prevType = types[prevIndex];
if (prevType) {
newWidgets[selectedIndex] = { ...currentWidget, type: prevType };
onUpdate(newWidgets);
}
}
} else if (key.rightArrow && widgets.length > 0) {
// Toggle item type forwards
const types = getAllowedTypes();
const currentWidget = widgets[selectedIndex];
if (currentWidget) {
const currentType = currentWidget.type;
let currentIndex = types.indexOf(currentType);
// If current type is not in allowed types (e.g., separator when disabled), find a valid type
if (currentIndex === -1) {
currentIndex = 0;
}
const nextIndex = (currentIndex + 1) % types.length;
const newWidgets = [...widgets];
const nextType = types[nextIndex];
if (nextType) {
newWidgets[selectedIndex] = { ...currentWidget, type: nextType };
onUpdate(newWidgets);
}
}
} else if (key.return && widgets.length > 0) {
// Enter move mode
setMoveMode(true);
} else if (input === 'a') {
// Add widget after selected
const insertIndex = widgets.length > 0 ? selectedIndex + 1 : 0;
const backgroundColor = getUniqueBackgroundColor(insertIndex);
const newWidget: WidgetItem = {
id: generateGuid(),
type: getDefaultItemType(),
...(backgroundColor && { backgroundColor })
};
const newWidgets = [...widgets];
newWidgets.splice(insertIndex, 0, newWidget);
onUpdate(newWidgets);
setSelectedIndex(insertIndex); // Move selection to new widget
} else if (input === 'i') {
// Insert item before selected
const insertIndex = selectedIndex;
const backgroundColor = getUniqueBackgroundColor(insertIndex);
const newWidget: WidgetItem = {
id: generateGuid(),
type: getDefaultItemType(),
...(backgroundColor && { backgroundColor })
};
const newWidgets = [...widgets];
newWidgets.splice(insertIndex, 0, newWidget);
onUpdate(newWidgets);
// Keep selection on the new widget (which is now at selectedIndex)
} else if (input === 'd' && widgets.length > 0) {
// Delete selected item
const newWidgets = widgets.filter((_, i) => i !== selectedIndex);
onUpdate(newWidgets);
if (selectedIndex >= newWidgets.length && selectedIndex > 0) {
setSelectedIndex(selectedIndex - 1);
}
} else if (input === 'c') {
// Clear entire line
onUpdate([]);
setSelectedIndex(0);
} else if (input === ' ' && widgets.length > 0) {
// Space key - cycle separator character for separator types only (not flex)
const currentWidget = widgets[selectedIndex];
if (currentWidget && currentWidget.type === 'separator') {
const currentChar = currentWidget.character ?? '|';
const currentCharIndex = separatorChars.indexOf(currentChar);
const nextChar = separatorChars[(currentCharIndex + 1) % separatorChars.length];
const newWidgets = [...widgets];
newWidgets[selectedIndex] = { ...currentWidget, character: nextChar };
onUpdate(newWidgets);
}
} else if (input === 'r' && widgets.length > 0) {
// Toggle raw value for non-separator items
const currentWidget = widgets[selectedIndex];
if (currentWidget && currentWidget.type !== 'separator' && currentWidget.type !== 'flex-separator' && currentWidget.type !== 'custom-text') {
const newWidgets = [...widgets];
newWidgets[selectedIndex] = { ...currentWidget, rawValue: !currentWidget.rawValue };
onUpdate(newWidgets);
}
} else if (input === 'm' && widgets.length > 0) {
// Cycle through merge states: undefined -> true -> 'no-padding' -> undefined
const currentWidget = widgets[selectedIndex];
// Don't allow merge on the last item or on separators
if (currentWidget && selectedIndex < widgets.length - 1
&& currentWidget.type !== 'separator' && currentWidget.type !== 'flex-separator') {
const newWidgets = [...widgets];
let nextMergeState: boolean | 'no-padding' | undefined;
if (currentWidget.merge === undefined) {
nextMergeState = true;
} else if (currentWidget.merge === true) {
nextMergeState = 'no-padding';
} else {
nextMergeState = undefined;
}
if (nextMergeState === undefined) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { merge: _merge, ...rest } = currentWidget;
newWidgets[selectedIndex] = rest;
} else {
newWidgets[selectedIndex] = { ...currentWidget, merge: nextMergeState };
}
onUpdate(newWidgets);
}
} else if (key.escape) {
onBack();
} else if (widgets.length > 0) {
// Check for custom widget keybinds
const currentWidget = widgets[selectedIndex];
if (currentWidget && currentWidget.type !== 'separator' && currentWidget.type !== 'flex-separator') {
try {
const widgetImpl = getWidget(currentWidget.type);
if (widgetImpl.getCustomKeybinds) {
const customKeybinds = widgetImpl.getCustomKeybinds();
const matchedKeybind = customKeybinds.find(kb => kb.key === input);
if (matchedKeybind) {
// Handle toggle actions directly
if (matchedKeybind.action === 'toggle-preserve') {
const newWidgets = [...widgets];
newWidgets[selectedIndex] = { ...currentWidget, preserveColors: !currentWidget.preserveColors };
onUpdate(newWidgets);
} else if (widgetImpl.renderEditor) {
// Set the action on the widget if it supports it
if (widgetImpl.setEditorAction) {
widgetImpl.setEditorAction(matchedKeybind.action);
}
// Open the widget's custom editor
setCustomEditorWidget({ widget: currentWidget, impl: widgetImpl });
}
}
}
} catch {
// Widget not found or doesn't support custom keybinds
}
}
}
}
});
const getWidgetDisplay = (widget: WidgetItem) => {
// Special handling for separators (not widgets)
if (widget.type === 'separator') {
const char = widget.character ?? '|';
const charDisplay = char === ' ' ? '(space)' : char;
return `Separator ${charDisplay}`;
}
if (widget.type === 'flex-separator') {
return 'Flex Separator';
}
// Handle regular widgets - delegate to widget for display
const widgetImpl = getWidget(widget.type);
const { displayText, modifierText } = widgetImpl.getEditorDisplay(widget);
// Return plain text without colors
return displayText + (modifierText ? ` ${modifierText}` : '');
};
const hasFlexSeparator = widgets.some(widget => widget.type === 'flex-separator');
const widthDetectionAvailable = canDetectTerminalWidth();
// Build dynamic help text based on selected item
const currentWidget = widgets[selectedIndex];
const isSeparator = currentWidget?.type === 'separator';
const isFlexSeparator = currentWidget?.type === 'flex-separator';
// Check if widget supports raw value using registry
let canToggleRaw = false;
let customKeybinds: { key: string; label: string; action: string }[] = [];
if (currentWidget && !isSeparator && !isFlexSeparator) {
try {
const widgetImpl = getWidget(currentWidget.type);
canToggleRaw = widgetImpl.supportsRawValue();
// Get custom keybinds from the widget
if (widgetImpl.getCustomKeybinds) {
customKeybinds = widgetImpl.getCustomKeybinds();
}
} catch {
canToggleRaw = false;
}
}
const canMerge = currentWidget && selectedIndex < widgets.length - 1 && !isSeparator && !isFlexSeparator;
// Build main help text (without custom keybinds)
let helpText = '↑↓ select, ←→ change type';
if (isSeparator) {
helpText += ', Space edit separator';
}
helpText += ', Enter to move, (a)dd, (i)nsert, (d)elete, (c)lear line';
if (canToggleRaw) {
helpText += ', (r)aw value';
}
if (canMerge) {
helpText += ', (m)erge';
}
helpText += ', ESC back';
// Build custom keybinds text
const customKeybindsText = customKeybinds.map(kb => kb.label).join(', ');
// If custom editor is active, render it instead of the normal UI
if (customEditorWidget?.impl.renderEditor) {
return customEditorWidget.impl.renderEditor({
widget: customEditorWidget.widget,
onComplete: handleEditorComplete,
onCancel: handleEditorCancel
});
}
return (
<Box flexDirection='column'>
<Box>
<Text bold>
Edit Line
{' '}
{lineNumber}
{' '}
{moveMode && <Text color='yellow'>[MOVE MODE]</Text>}
</Text>
{(settings.powerline.enabled || Boolean(settings.defaultSeparator)) && (
<Box marginLeft={2}>
<Text color='yellow'>
⚠
{' '}
{settings.powerline.enabled
? 'Powerline mode active: separators controlled by powerline settings'
: 'Default separator active: manual separators disabled'}
</Text>
</Box>
)}
</Box>
{moveMode ? (
<Text dimColor>↑↓ to move item, ESC or Enter to exit move mode</Text>
) : (
<Box flexDirection='column'>
<Text dimColor>{helpText}</Text>
<Text dimColor>{customKeybindsText || ' '}</Text>
</Box>
)}
{hasFlexSeparator && !widthDetectionAvailable && (
<Box marginTop={1}>
<Text color='yellow'>⚠ Note: Terminal width detection is currently unavailable in your environment.</Text>
<Text dimColor> Flex separators will act as normal separators until width detection is available.</Text>
</Box>
)}
<Box marginTop={1} flexDirection='column'>
{widgets.length === 0 ? (
<Text dimColor>No items. Press 'a' to add one.</Text>
) : (
widgets.map((widget, index) => {
const isSelected = index === selectedIndex;
const widgetImpl = widget.type !== 'separator' && widget.type !== 'flex-separator' ? getWidget(widget.type) : null;
const { displayText, modifierText } = widgetImpl?.getEditorDisplay(widget) ?? { displayText: getWidgetDisplay(widget) };
return (
<Box key={widget.id}>
<Text>
<Text color={isSelected ? (moveMode ? 'yellow' : 'blue') : undefined}>
{isSelected ? (moveMode ? '◆ ' : '▶ ') : ' '}
{` ${index + 1}. ${displayText || getWidgetDisplay(widget)}`}
</Text>
{modifierText && (
<Text dimColor>
{' '}
{modifierText}
</Text>
)}
{widget.rawValue && <Text dimColor> (raw value)</Text>}
{widget.merge === true && <Text dimColor> (merged→)</Text>}
{widget.merge === 'no-padding' && <Text dimColor> (merged-no-pad→)</Text>}
</Text>
</Box>
);
})
)}
</Box>
</Box>
);
};
+118
View File
@@ -0,0 +1,118 @@
import {
Box,
Text,
useInput
} from 'ink';
import React, { useState } from 'react';
import type { Settings } from '../../types/Settings';
import type { WidgetItem } from '../../types/Widget';
interface LineSelectorProps {
lines: WidgetItem[][];
onSelect: (line: number) => void;
onBack: () => void;
initialSelection?: number;
title?: string;
blockIfPowerlineActive?: boolean;
settings?: Settings;
}
const LineSelector: React.FC<LineSelectorProps> = ({ lines, onSelect, onBack, initialSelection = 0, title, blockIfPowerlineActive = false, settings }) => {
const [selectedIndex, setSelectedIndex] = useState(initialSelection);
// Check if powerline theme is managing colors
const powerlineEnabled = settings ? settings.powerline.enabled : false;
const powerlineTheme = settings ? settings.powerline.theme : undefined;
const isThemeManaged = blockIfPowerlineActive && powerlineEnabled && powerlineTheme && powerlineTheme !== 'custom';
// Handle keyboard input
useInput((input, key) => {
// If theme-managed and blocking is enabled, any key goes back
if (isThemeManaged) {
onBack();
return;
}
if (key.escape) {
onBack();
} else if (key.upArrow) {
setSelectedIndex(Math.max(0, selectedIndex - 1));
} else if (key.downArrow) {
setSelectedIndex(Math.min(3, selectedIndex + 1)); // 0-2 for lines, 3 for back
} else if (key.return) {
if (selectedIndex === 3) {
onBack();
} else {
onSelect(selectedIndex);
}
}
});
// Show powerline theme warning if applicable
if (isThemeManaged) {
return (
<Box flexDirection='column'>
<Text bold>{title ?? 'Select Line'}</Text>
<Box marginTop={1}>
<Text color='yellow'>
⚠ Colors are currently managed by the Powerline theme:
{' ' + powerlineTheme.charAt(0).toUpperCase() + powerlineTheme.slice(1)}
</Text>
</Box>
<Box marginTop={1}>
<Text dimColor>To customize colors, either:</Text>
</Box>
<Box marginLeft={2}>
<Text dimColor>• Change to 'Custom' theme in Powerline Configuration → Themes</Text>
</Box>
<Box marginLeft={2}>
<Text dimColor>• Disable Powerline mode in Powerline Configuration</Text>
</Box>
<Box marginTop={2}>
<Text>Press any key to go back...</Text>
</Box>
</Box>
);
}
return (
<Box flexDirection='column'>
<Text bold>{title ?? 'Select Line to Edit'}</Text>
<Text dimColor>Choose which status line to configure (up to 3 lines supported)</Text>
<Text dimColor>Press ESC to go back</Text>
<Box marginTop={1} flexDirection='column'>
<Box>
<Text color={selectedIndex === 0 ? 'green' : undefined}>
{selectedIndex === 0 ? '▶ ' : ' '}
☰ Line 1
{lines[0] && lines[0].length > 0 ? ` (${lines[0].length} items)` : ' (empty)'}
</Text>
</Box>
<Box>
<Text color={selectedIndex === 1 ? 'green' : undefined}>
{selectedIndex === 1 ? '▶ ' : ' '}
☰ Line 2
{lines[1] && lines[1].length > 0 ? ` (${lines[1].length} items)` : ' (empty)'}
</Text>
</Box>
<Box>
<Text color={selectedIndex === 2 ? 'green' : undefined}>
{selectedIndex === 2 ? '▶ ' : ' '}
☰ Line 3
{lines[2] && lines[2].length > 0 ? ` (${lines[2].length} items)` : ' (empty)'}
</Text>
</Box>
<Box marginTop={1}>
<Text color={selectedIndex === 3 ? 'green' : undefined}>
{selectedIndex === 3 ? '▶ ' : ' '}
← Back
</Text>
</Box>
</Box>
</Box>
);
};
export { LineSelector, type LineSelectorProps };
+120
View File
@@ -0,0 +1,120 @@
import {
Box,
Text,
useInput
} from 'ink';
import React, { useState } from 'react';
import type { Settings } from '../../types/Settings';
import { type PowerlineFontStatus } from '../../utils/powerline';
export interface MainMenuProps {
onSelect: (value: string) => void;
isClaudeInstalled: boolean;
hasChanges: boolean;
initialSelection?: number;
powerlineFontStatus: PowerlineFontStatus;
settings: Settings | null;
previewIsTruncated?: boolean;
}
export const MainMenu: React.FC<MainMenuProps> = ({ onSelect, isClaudeInstalled, hasChanges, initialSelection = 0, powerlineFontStatus, settings, previewIsTruncated }) => {
const [selectedIndex, setSelectedIndex] = useState(initialSelection);
// Build menu structure with visual gaps
const menuItems = [
{ label: '📝 Edit Lines', value: 'lines', selectable: true },
{ label: '🎨 Edit Colors', value: 'colors', selectable: true },
{ label: '⚡ Powerline Setup', value: 'powerline', selectable: true },
{ label: '', value: '_gap1', selectable: false }, // Visual gap
{ label: '💻 Terminal Options', value: 'terminalConfig', selectable: true },
{ label: '🌐 Global Overrides', value: 'globalOverrides', selectable: true },
{ label: '', value: '_gap2', selectable: false }, // Visual gap
{ label: isClaudeInstalled ? '🗑️ Uninstall from Claude Code' : '📦 Install to Claude Code', value: 'install', selectable: true }
];
if (hasChanges) {
menuItems.push(
{ label: '💾 Save & Exit', value: 'save', selectable: true },
{ label: '❌ Exit without saving', value: 'exit', selectable: true }
);
} else {
menuItems.push({ label: '🚪 Exit', value: 'exit', selectable: true });
}
// Get only selectable items for navigation
const selectableItems = menuItems.filter(item => item.selectable);
useInput((input, key) => {
if (key.upArrow) {
setSelectedIndex(Math.max(0, selectedIndex - 1));
} else if (key.downArrow) {
setSelectedIndex(Math.min(selectableItems.length - 1, selectedIndex + 1));
} else if (key.return) {
const item = selectableItems[selectedIndex];
if (item) {
onSelect(item.value);
}
}
});
// Get description for selected item
const getDescription = (value: string): string => {
const descriptions: Record<string, string> = {
lines: 'Configure up to 3 status lines with various widgets like model info, git status, and token usage',
colors: 'Customize colors for each widget including foreground, background, and bold styling',
powerline: 'Install Powerline fonts for enhanced visual separators and symbols in your status line',
globalOverrides: 'Set global padding, separators, and color overrides that apply to all widgets',
install: isClaudeInstalled
? 'Remove ccstatusline from your Claude Code settings'
: 'Add ccstatusline to your Claude Code settings for automatic status line rendering',
terminalConfig: 'Configure terminal-specific settings for optimal display',
save: 'Save all changes and exit the configuration tool',
exit: hasChanges
? 'Exit without saving your changes'
: 'Exit the configuration tool'
};
return descriptions[value] ?? '';
};
const selectedItem = selectableItems[selectedIndex];
const description = selectedItem ? getDescription(selectedItem.value) : '';
// Check if we should show the truncation warning
const showTruncationWarning = previewIsTruncated && settings?.flexMode === 'full-minus-40';
return (
<Box flexDirection='column'>
{showTruncationWarning && (
<Box marginBottom={1}>
<Text color='yellow'>⚠ Some lines are truncated, see Terminal Options → Terminal Width for info</Text>
</Box>
)}
<Text bold>Main Menu</Text>
<Box marginTop={1} flexDirection='column'>
{menuItems.map((item, idx) => {
if (!item.selectable && item.value.startsWith('_gap')) {
return <Text key={item.value}> </Text>;
}
const selectableIdx = selectableItems.indexOf(item);
const isSelected = selectableIdx === selectedIndex;
return (
<Text
key={item.value}
color={isSelected ? 'green' : undefined}
>
{isSelected ? '▶ ' : ' '}
{item.label}
</Text>
);
})}
</Box>
{description && (
<Box marginTop={1} paddingLeft={2}>
<Text dimColor wrap='wrap'>{description}</Text>
</Box>
)}
</Box>
);
};
@@ -0,0 +1,319 @@
import {
Box,
Text,
useInput
} from 'ink';
import React, { useState } from 'react';
import type { Settings } from '../../types/Settings';
export type EditorMode = 'separator' | 'startCap' | 'endCap';
export interface PowerlineSeparatorEditorProps {
settings: Settings;
mode: EditorMode;
onUpdate: (settings: Settings) => void;
onBack: () => void;
}
export const PowerlineSeparatorEditor: React.FC<PowerlineSeparatorEditorProps> = ({
settings,
mode,
onUpdate,
onBack
}) => {
const powerlineConfig = settings.powerline;
// Get the appropriate array based on mode
const getItems = () => {
switch (mode) {
case 'separator':
return powerlineConfig.separators;
case 'startCap':
return powerlineConfig.startCaps;
case 'endCap':
return powerlineConfig.endCaps;
}
};
const separators = getItems();
const invertBgs = mode === 'separator'
? powerlineConfig.separatorInvertBackground
: [];
const [selectedIndex, setSelectedIndex] = useState(0);
const [hexInputMode, setHexInputMode] = useState(false);
const [hexInput, setHexInput] = useState('');
const [cursorPos, setCursorPos] = useState(0);
// Get presets based on mode
const getPresets = () => {
if (mode === 'separator') {
return [
{ char: '\uE0B0', name: 'Triangle Right', hex: 'E0B0' },
{ char: '\uE0B2', name: 'Triangle Left', hex: 'E0B2' },
{ char: '\uE0B4', name: 'Round Right', hex: 'E0B4' },
{ char: '\uE0B6', name: 'Round Left', hex: 'E0B6' }
];
} else if (mode === 'startCap') {
return [
{ char: '\uE0B2', name: 'Triangle', hex: 'E0B2' },
{ char: '\uE0B6', name: 'Round', hex: 'E0B6' },
{ char: '\uE0BA', name: 'Lower Triangle', hex: 'E0BA' },
{ char: '\uE0BE', name: 'Diagonal', hex: 'E0BE' }
];
} else {
return [
{ char: '\uE0B0', name: 'Triangle', hex: 'E0B0' },
{ char: '\uE0B4', name: 'Round', hex: 'E0B4' },
{ char: '\uE0B8', name: 'Lower Triangle', hex: 'E0B8' },
{ char: '\uE0BC', name: 'Diagonal', hex: 'E0BC' }
];
}
};
const presetSeparators = getPresets();
const getSeparatorDisplay = (char: string, index: number): string => {
const preset = presetSeparators.find(p => p.char === char);
const invertBg = invertBgs[index] ?? false;
if (preset) {
// Show inversion status for all separators in separator mode
const inversionText = mode === 'separator' && invertBg ? ' [Inverted]' : '';
return `${preset.char} - ${preset.name}${inversionText}`;
}
const hexCode = char.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0');
return `${char} - Custom (\\u${hexCode})${invertBg ? ' [Inverted]' : ''}`;
};
const updateSeparators = (newSeparators: string[], newInvertBgs?: boolean[]) => {
const updatedPowerline = { ...powerlineConfig };
switch (mode) {
case 'separator':
updatedPowerline.separators = newSeparators;
updatedPowerline.separatorInvertBackground = newInvertBgs ?? newSeparators.map((_, i) => invertBgs[i] ?? false);
break;
case 'startCap':
updatedPowerline.startCaps = newSeparators;
break;
case 'endCap':
updatedPowerline.endCaps = newSeparators;
break;
}
onUpdate({
...settings,
powerline: updatedPowerline
});
};
useInput((input, key) => {
if (hexInputMode) {
// Hex input mode
if (key.escape) {
setHexInputMode(false);
setHexInput('');
setCursorPos(0);
} else if (key.return) {
if (hexInput.length === 4) {
const char = String.fromCharCode(parseInt(hexInput, 16));
const newSeparators = [...separators];
if (separators.length === 0) {
// Add new item if list is empty
newSeparators.push(char);
} else {
newSeparators[selectedIndex] = char;
}
updateSeparators(newSeparators);
setHexInputMode(false);
setHexInput('');
setCursorPos(0);
}
} else if (key.backspace && cursorPos > 0) {
setHexInput(hexInput.slice(0, cursorPos - 1) + hexInput.slice(cursorPos));
setCursorPos(cursorPos - 1);
} else if (input && /[0-9a-fA-F]/.test(input) && hexInput.length < 4) {
setHexInput(hexInput.slice(0, cursorPos) + input.toUpperCase() + hexInput.slice(cursorPos));
setCursorPos(cursorPos + 1);
}
} else {
// Normal mode
if (key.escape) {
onBack();
} else if (key.upArrow) {
setSelectedIndex(Math.max(0, selectedIndex - 1));
} else if (key.downArrow && separators.length > 0) {
setSelectedIndex(Math.min(separators.length - 1, selectedIndex + 1));
} else if ((key.leftArrow || key.rightArrow) && separators.length > 0) {
// Cycle through preset separators
const currentChar = separators[selectedIndex] ?? '\uE0B0';
const currentPresetIndex = presetSeparators.findIndex(p => p.char === currentChar);
const newSeparators = [...separators];
const newInvertBgs = mode === 'separator' ? [...invertBgs] : [];
let newIndex;
if (currentPresetIndex !== -1) {
// It's a preset, cycle to next/prev preset
if (key.rightArrow) {
newIndex = (currentPresetIndex + 1) % presetSeparators.length;
} else {
newIndex = currentPresetIndex === 0 ? presetSeparators.length - 1 : currentPresetIndex - 1;
}
} else {
// It's a custom separator, cycle to first or last preset
if (key.rightArrow) {
newIndex = 0; // Go to first preset
} else {
newIndex = presetSeparators.length - 1; // Go to last preset
}
}
const newChar = presetSeparators[newIndex]?.char ?? presetSeparators[0]?.char ?? '\uE0B0';
newSeparators[selectedIndex] = newChar;
// Auto-set inversion for left-facing separators (Triangle Left \uE0B2 or Round Left \uE0B6)
if (mode === 'separator') {
const isLeftFacing = newChar === '\uE0B2' || newChar === '\uE0B6';
newInvertBgs[selectedIndex] = isLeftFacing;
}
updateSeparators(newSeparators, mode === 'separator' ? newInvertBgs : undefined);
} else if ((input === 'a' || input === 'A') && (mode === 'separator' || separators.length < 3)) {
// Add after current (max 3 for caps)
const newSeparators = [...separators];
const newInvertBgs = mode === 'separator' ? [...invertBgs] : [];
const defaultChar = presetSeparators[0]?.char ?? '\uE0B0';
const isLeftFacing = defaultChar === '\uE0B2' || defaultChar === '\uE0B6';
if (separators.length === 0) {
// If empty, just add at the beginning
newSeparators.push(defaultChar);
if (mode === 'separator') {
newInvertBgs.push(isLeftFacing);
}
updateSeparators(newSeparators, newInvertBgs);
setSelectedIndex(0);
} else {
// Add after current selected item
newSeparators.splice(selectedIndex + 1, 0, defaultChar);
if (mode === 'separator') {
newInvertBgs.splice(selectedIndex + 1, 0, isLeftFacing);
}
updateSeparators(newSeparators, newInvertBgs);
setSelectedIndex(selectedIndex + 1);
}
} else if ((input === 'i' || input === 'I') && (mode === 'separator' || separators.length < 3)) {
// Insert before current (max 3 for caps)
const newSeparators = [...separators];
const newInvertBgs = mode === 'separator' ? [...invertBgs] : [];
const defaultChar = presetSeparators[0]?.char ?? '\uE0B0';
const isLeftFacing = defaultChar === '\uE0B2' || defaultChar === '\uE0B6';
if (separators.length === 0) {
// If empty, just add at the beginning
newSeparators.push(defaultChar);
if (mode === 'separator') {
newInvertBgs.push(isLeftFacing);
}
updateSeparators(newSeparators, newInvertBgs);
setSelectedIndex(0);
} else {
// Insert before current selected item
newSeparators.splice(selectedIndex, 0, defaultChar);
if (mode === 'separator') {
newInvertBgs.splice(selectedIndex, 0, isLeftFacing);
}
updateSeparators(newSeparators, newInvertBgs);
// Keep selection on the newly inserted item (which is now at selectedIndex)
}
} else if ((input === 'd' || input === 'D') && (mode !== 'separator' || separators.length > 1)) {
// Delete current (min 1 for separator, no min for caps)
const newSeparators = separators.filter((_, i) => i !== selectedIndex);
const newInvertBgs = mode === 'separator' ? invertBgs.filter((_, i) => i !== selectedIndex) : [];
updateSeparators(newSeparators, newInvertBgs);
setSelectedIndex(Math.min(selectedIndex, Math.max(0, newSeparators.length - 1)));
} else if (input === 'c' || input === 'C') {
// Clear all
if (mode === 'separator') {
// Reset to default right-facing separator with no inversion
updateSeparators(['\uE0B0'], [false]);
} else {
updateSeparators([]);
}
setSelectedIndex(0);
} else if (input === 'h' || input === 'H') {
// Enter hex input mode
setHexInputMode(true);
setHexInput('');
setCursorPos(0);
} else if ((input === 't' || input === 'T') && mode === 'separator') {
// Toggle background inversion (for all separators in separator mode)
const newInvertBgs = [...invertBgs];
newInvertBgs[selectedIndex] = !(newInvertBgs[selectedIndex] ?? false);
updateSeparators(separators, newInvertBgs);
}
}
});
const getTitle = () => {
switch (mode) {
case 'separator':
return 'Powerline Separator Configuration';
case 'startCap':
return 'Powerline Start Cap Configuration';
case 'endCap':
return 'Powerline End Cap Configuration';
}
};
const canAdd = mode === 'separator' || separators.length < 3;
const canDelete = mode !== 'separator' || separators.length > 1;
return (
<Box flexDirection='column'>
<Text bold>{getTitle()}</Text>
{hexInputMode ? (
<Box marginTop={2} flexDirection='column'>
<Text>
Enter 4-digit hex code for
{' '}
{mode === 'separator' ? 'separator' : 'cap'}
{separators.length > 0 ? ` ${selectedIndex + 1}` : ''}
:
</Text>
<Text>
\u
{hexInput.slice(0, cursorPos)}
<Text backgroundColor='gray' color='black'>{hexInput[cursorPos] ?? '_'}</Text>
{hexInput.slice(cursorPos + 1)}
{hexInput.length < 4 && hexInput.length === cursorPos && <Text dimColor>{'_'.repeat(4 - hexInput.length - 1)}</Text>}
</Text>
<Text dimColor>Enter 4 hex digits (0-9, A-F), then press Enter. ESC to cancel.</Text>
</Box>
) : (
<>
<Box>
<Text dimColor>
{`↑↓ select, ← → cycle${canAdd ? ', (a)dd, (i)nsert' : ''}${canDelete ? ', (d)elete' : ''}, (c)lear, (h)ex${mode === 'separator' ? ', (t)oggle invert' : ''}, ESC back`}
</Text>
</Box>
<Box marginTop={2} flexDirection='column'>
{separators.length > 0 ? (
separators.map((sep, index) => (
<Box key={index}>
<Text color={index === selectedIndex ? 'green' : undefined}>
{index === selectedIndex ? '▶ ' : ' '}
{`${index + 1}: ${getSeparatorDisplay(sep, index)}`}
</Text>
</Box>
))
) : (
<Text dimColor>(none configured - press 'a' to add)</Text>
)}
</Box>
</>
)}
</Box>
);
};
+461
View File
@@ -0,0 +1,461 @@
import {
Box,
Text,
useInput
} from 'ink';
import * as os from 'os';
import React, { useState } from 'react';
import type { Settings } from '../../types/Settings';
import { getDefaultPowerlineTheme } from '../../utils/colors';
import { type PowerlineFontStatus } from '../../utils/powerline';
import { ConfirmDialog } from './ConfirmDialog';
import { PowerlineSeparatorEditor } from './PowerlineSeparatorEditor';
import { PowerlineThemeSelector } from './PowerlineThemeSelector';
export interface PowerlineSetupProps {
settings: Settings;
powerlineFontStatus: PowerlineFontStatus;
onUpdate: (settings: Settings) => void;
onBack: () => void;
onInstallFonts: () => void;
installingFonts: boolean;
fontInstallMessage: string | null;
onClearMessage: () => void;
}
type Screen = 'menu' | 'separator' | 'startCap' | 'endCap' | 'themes';
export const PowerlineSetup: React.FC<PowerlineSetupProps> = ({
settings,
powerlineFontStatus,
onUpdate,
onBack,
onInstallFonts,
installingFonts,
fontInstallMessage,
onClearMessage
}) => {
const powerlineConfig = settings.powerline;
const [screen, setScreen] = useState<Screen>('menu');
const [selectedMenuItem, setSelectedMenuItem] = useState(0);
const [confirmingEnable, setConfirmingEnable] = useState(false);
const [confirmingFontInstall, setConfirmingFontInstall] = useState(false);
// Check if there are any separators or flex-separators in the current configuration
const hasSeparatorItems = settings.lines.some(line => line.some(item => item.type === 'separator' || item.type === 'flex-separator'));
// Menu items for navigation
const menuItems = [
{ label: 'Separator', value: 'separator' },
{ label: 'Start Cap', value: 'startCap' },
{ label: 'End Cap', value: 'endCap' },
{ label: 'Themes', value: 'themes' },
{ label: '← Back', value: 'back' }
];
// Helper functions for display
const getSeparatorDisplay = (): string => {
const seps = powerlineConfig.separators;
if (seps.length > 1) {
return 'multiple';
}
const sep = seps[0] ?? '\uE0B0';
const presets = [
{ char: '\uE0B0', name: 'Triangle Right' },
{ char: '\uE0B2', name: 'Triangle Left' },
{ char: '\uE0B4', name: 'Round Right' },
{ char: '\uE0B6', name: 'Round Left' }
];
const preset = presets.find(p => p.char === sep);
if (preset) {
return `${preset.char} - ${preset.name}`;
}
return `${sep} - Custom`;
};
const getCapDisplay = (type: 'start' | 'end'): string => {
const caps = type === 'start'
? powerlineConfig.startCaps
: powerlineConfig.endCaps;
if (caps.length === 0)
return 'none';
if (caps.length > 1)
return 'multiple';
const cap = caps[0];
if (!cap)
return 'none';
const presets = type === 'start' ? [
{ char: '\uE0B2', name: 'Triangle' },
{ char: '\uE0B6', name: 'Round' },
{ char: '\uE0BA', name: 'Lower Triangle' },
{ char: '\uE0BE', name: 'Diagonal' }
] : [
{ char: '\uE0B0', name: 'Triangle' },
{ char: '\uE0B4', name: 'Round' },
{ char: '\uE0B8', name: 'Lower Triangle' },
{ char: '\uE0BC', name: 'Diagonal' }
];
const preset = presets.find(c => c.char === cap);
if (preset) {
return `${preset.char} - ${preset.name}`;
}
return `${cap} - Custom`;
};
const getThemeDisplay = (): string => {
const theme = powerlineConfig.theme;
if (!theme || theme === 'custom')
return 'Custom';
return theme.charAt(0).toUpperCase() + theme.slice(1);
};
useInput((input, key) => {
// Block all input handling when font installation message is shown or installing
if (fontInstallMessage || installingFonts) {
// Only clear message on non-escape keys when message is shown
if (fontInstallMessage && !key.escape) {
onClearMessage();
}
// Always return early to prevent any other input handling
return;
}
// Skip input handling when confirmations are active - let ConfirmDialog handle it
if (confirmingFontInstall || confirmingEnable) {
return;
}
if (screen === 'menu') {
// Menu navigation mode
if (key.escape) {
onBack();
} else if (key.upArrow) {
setSelectedMenuItem(Math.max(0, selectedMenuItem - 1));
} else if (key.downArrow) {
setSelectedMenuItem(Math.min(menuItems.length - 1, selectedMenuItem + 1));
} else if (key.return) {
const selected = menuItems[selectedMenuItem];
if (selected) {
if (selected.value === 'back') {
onBack();
} else if (powerlineConfig.enabled) {
setScreen(selected.value as Screen);
}
}
} else if (input === 't' || input === 'T') {
// Toggle powerline mode
if (!powerlineConfig.enabled) {
// Only show confirmation when enabling if there are separators to remove
if (hasSeparatorItems) {
setConfirmingEnable(true);
} else {
// Set to nord theme if currently custom or undefined (first time enabling)
const theme = (!powerlineConfig.theme || powerlineConfig.theme === 'custom')
? getDefaultPowerlineTheme()
: powerlineConfig.theme;
// Enable directly without confirmation since there are no separators
const updatedSettings = {
...settings,
powerline: {
...powerlineConfig,
enabled: true,
theme,
// Separators are already initialized by Zod
separators: powerlineConfig.separators,
separatorInvertBackground: powerlineConfig.separatorInvertBackground
},
defaultPadding: ' ' // Set padding to space when enabling powerline
};
onUpdate(updatedSettings);
}
} else {
// Disable without confirmation
const newConfig = { ...powerlineConfig, enabled: false };
onUpdate({ ...settings, powerline: newConfig });
}
} else if (input === 'i' || input === 'I') {
// Show font installation consent prompt
setConfirmingFontInstall(true);
}
}
});
// Render sub-screens
if (screen === 'separator') {
return (
<PowerlineSeparatorEditor
settings={settings}
mode='separator'
onUpdate={onUpdate}
onBack={() => { setScreen('menu'); }}
/>
);
}
if (screen === 'startCap') {
return (
<PowerlineSeparatorEditor
settings={settings}
mode='startCap'
onUpdate={onUpdate}
onBack={() => { setScreen('menu'); }}
/>
);
}
if (screen === 'endCap') {
return (
<PowerlineSeparatorEditor
settings={settings}
mode='endCap'
onUpdate={onUpdate}
onBack={() => { setScreen('menu'); }}
/>
);
}
if (screen === 'themes') {
return (
<PowerlineThemeSelector
settings={settings}
onUpdate={onUpdate}
onBack={() => { setScreen('menu'); }}
/>
);
}
// Main menu screen
return (
<Box flexDirection='column'>
{!confirmingFontInstall && !installingFonts && !fontInstallMessage && (
<Text bold>Powerline Setup</Text>
)}
{confirmingFontInstall ? (
<Box flexDirection='column'>
<Box marginBottom={1}>
<Text color='cyan' bold>Font Installation</Text>
</Box>
<Box marginBottom={1} flexDirection='column'>
<Text bold>What will happen:</Text>
<Text>
<Text dimColor>• Clone fonts from </Text>
<Text color='blue'>https://github.com/powerline/fonts</Text>
</Text>
{os.platform() === 'darwin' && (
<>
<Text dimColor>• Run install.sh script which will:</Text>
<Text dimColor> - Copy all .ttf/.otf files to ~/Library/Fonts</Text>
<Text dimColor> - Register fonts with macOS</Text>
</>
)}
{os.platform() === 'linux' && (
<>
<Text dimColor>• Run install.sh script which will:</Text>
<Text dimColor> - Copy all .ttf/.otf files to ~/.local/share/fonts</Text>
<Text dimColor> - Run fc-cache to update font cache</Text>
</>
)}
{os.platform() === 'win32' && (
<>
<Text dimColor>• Copy Powerline .ttf/.otf files to:</Text>
<Text dimColor> AppData\Local\Microsoft\Windows\Fonts</Text>
</>
)}
<Text dimColor>• Clean up temporary files</Text>
</Box>
<Box marginBottom={1}>
<Text color='yellow' bold>Requirements: </Text>
<Text dimColor>Git installed, Internet connection, Write permissions</Text>
</Box>
<Box marginBottom={1} flexDirection='column'>
<Text color='green' bold>After install:</Text>
<Text dimColor>• Restart terminal</Text>
<Text dimColor>• Select a Powerline font</Text>
<Text dimColor> (e.g. "Meslo LG S for Powerline")</Text>
</Box>
<Box marginTop={1}>
<Text>Proceed? </Text>
</Box>
<Box marginTop={1}>
<ConfirmDialog
inline={true}
onConfirm={() => {
setConfirmingFontInstall(false);
onInstallFonts();
}}
onCancel={() => {
setConfirmingFontInstall(false);
}}
/>
</Box>
</Box>
) : confirmingEnable ? (
<Box flexDirection='column' marginTop={1}>
{hasSeparatorItems && (
<>
<Box>
<Text color='yellow'>⚠ Warning: Enabling Powerline mode will remove all existing separators and flex-separators from your status lines.</Text>
</Box>
<Box marginBottom={1}>
<Text dimColor>Powerline mode uses its own separator system and is incompatible with manual separators.</Text>
</Box>
</>
)}
<Box marginTop={hasSeparatorItems ? 1 : 0}>
<Text>Do you want to continue? </Text>
</Box>
<Box marginTop={1}>
<ConfirmDialog
inline={true}
onConfirm={() => {
// Set to nord theme if currently custom or undefined (first time enabling)
const theme = (!powerlineConfig.theme || powerlineConfig.theme === 'custom')
? getDefaultPowerlineTheme()
: powerlineConfig.theme;
// Remove all separators and flex-separators from lines
// Also set default padding to a space when enabling powerline
const updatedSettings = {
...settings,
powerline: {
...powerlineConfig,
enabled: true,
theme,
// Separators are already initialized by Zod
separators: powerlineConfig.separators,
separatorInvertBackground: powerlineConfig.separatorInvertBackground
},
defaultPadding: ' ', // Set padding to space when enabling powerline
lines: settings.lines.map(line => line.filter(item => item.type !== 'separator' && item.type !== 'flex-separator')
)
};
onUpdate(updatedSettings);
setConfirmingEnable(false);
}}
onCancel={() => {
setConfirmingEnable(false);
}}
/>
</Box>
</Box>
) : installingFonts ? (
<Box>
<Text color='yellow'>Installing Powerline fonts... This may take a moment.</Text>
</Box>
) : fontInstallMessage ? (
<Box flexDirection='column'>
<Text color={fontInstallMessage.includes('success') ? 'green' : 'red'}>
{fontInstallMessage}
</Text>
<Box marginTop={1}>
<Text dimColor>Press any key to continue...</Text>
</Box>
</Box>
) : (
<>
<Box flexDirection='column'>
<Text>
{' Font Status: '}
{powerlineFontStatus.installed ? (
<>
<Text color='green'>✓ Installed</Text>
<Text dimColor> - Ensure fonts are active in your terminal</Text>
</>
) : (
<>
<Text color='yellow'>✗ Not Installed</Text>
<Text dimColor> - Press (i) to install Powerline fonts</Text>
</>
)}
</Text>
</Box>
<Box>
<Text>Powerline Mode: </Text>
<Text color={powerlineConfig.enabled ? 'green' : 'red'}>
{powerlineConfig.enabled ? '✓ Enabled ' : '✗ Disabled '}
</Text>
<Text dimColor> - Press (t) to toggle</Text>
</Box>
{powerlineConfig.enabled && (
<Box flexDirection='column' marginTop={1}>
<Text dimColor>
When enabled, global overrides are disabled and powerline separators are used
</Text>
</Box>
)}
<Box marginTop={1} flexDirection='column'>
{powerlineConfig.enabled ? (
<>
{menuItems.map((item, index) => {
const isSelected = index === selectedMenuItem;
let displayValue = '';
switch (item.value) {
case 'separator':
displayValue = getSeparatorDisplay();
break;
case 'startCap':
displayValue = getCapDisplay('start');
break;
case 'endCap':
displayValue = getCapDisplay('end');
break;
case 'themes':
displayValue = getThemeDisplay();
break;
case 'back':
displayValue = '';
break;
}
if (item.value === 'back') {
return (
<Box key={item.value} marginTop={1}>
<Text color={isSelected ? 'green' : undefined}>
{isSelected ? '▶ ' : ' '}
{item.label}
</Text>
</Box>
);
}
return (
<Box key={item.value}>
<Text color={isSelected ? 'green' : undefined}>
{isSelected ? '▶ ' : ' '}
{item.label.padEnd(11, ' ')}
<Text dimColor>
{displayValue && `(${displayValue})`}
</Text>
</Text>
</Box>
);
})}
</>
) : (
// When powerline is disabled, show ESC to go back message
<Box marginTop={1}>
<Text dimColor>Press ESC to go back</Text>
</Box>
)}
</Box>
</>
)}
</Box>
);
};
@@ -0,0 +1,226 @@
import {
Box,
Text,
useInput
} from 'ink';
import React, {
useRef,
useState
} from 'react';
import { getColorLevelString } from '../../types/ColorLevel';
import type { Settings } from '../../types/Settings';
import {
getPowerlineTheme,
getPowerlineThemes
} from '../../utils/colors';
import { ConfirmDialog } from './ConfirmDialog';
export interface PowerlineThemeSelectorProps {
settings: Settings;
onUpdate: (settings: Settings) => void;
onBack: () => void;
}
export const PowerlineThemeSelector: React.FC<PowerlineThemeSelectorProps> = ({
settings,
onUpdate,
onBack
}) => {
const themes = getPowerlineThemes();
const currentTheme = settings.powerline.theme ?? 'custom';
const [selectedIndex, setSelectedIndex] = useState(Math.max(0, themes.indexOf(currentTheme)));
const [showCustomizeConfirm, setShowCustomizeConfirm] = useState(false);
const originalThemeRef = useRef(currentTheme);
const originalSettingsRef = useRef(settings);
const applyTheme = (themeName: string) => {
// Simply change the theme setting, don't modify widget colors
const updatedSettings = {
...settings,
powerline: {
...settings.powerline,
theme: themeName
}
};
onUpdate(updatedSettings);
};
const customizeTheme = () => {
// Copy current theme's colors to widgets and switch to custom theme
const currentThemeName = themes[selectedIndex];
if (!currentThemeName) {
return;
}
const theme = getPowerlineTheme(currentThemeName);
if (!theme || currentThemeName === 'custom') {
// If already on custom, just go back
onBack();
return;
}
const colorLevel = getColorLevelString(settings.colorLevel);
const colorLevelKey = colorLevel === 'ansi16' ? '1' : colorLevel === 'ansi256' ? '2' : '3';
const themeColors = theme[colorLevelKey];
if (themeColors) {
// Apply theme colors to widgets
const newLines = settings.lines.map((line) => {
let widgetColorIndex = 0;
return line.map((widget) => {
// Skip separators
if (widget.type === 'separator' || widget.type === 'flex-separator') {
return widget;
}
const fgColor = themeColors.fg[widgetColorIndex % themeColors.fg.length];
const bgColor = themeColors.bg[widgetColorIndex % themeColors.bg.length];
widgetColorIndex++;
return {
...widget,
color: fgColor,
backgroundColor: bgColor
};
});
});
const updatedSettings = {
...settings,
powerline: {
...settings.powerline,
theme: 'custom'
},
lines: newLines
};
onUpdate(updatedSettings);
}
onBack();
};
useInput((input, key) => {
// Skip input handling when confirmation is active - let ConfirmDialog handle it
if (showCustomizeConfirm) {
return;
}
{
// Normal input handling
if (key.escape) {
// Restore original settings completely when canceling
onUpdate(originalSettingsRef.current);
onBack();
} else if (key.upArrow) {
const newIndex = Math.max(0, selectedIndex - 1);
setSelectedIndex(newIndex);
const newTheme = themes[newIndex];
if (newTheme) {
applyTheme(newTheme);
}
} else if (key.downArrow) {
const newIndex = Math.min(themes.length - 1, selectedIndex + 1);
setSelectedIndex(newIndex);
const newTheme = themes[newIndex];
if (newTheme) {
applyTheme(newTheme);
}
} else if (key.return) {
// User confirmed their selection, so we keep the current theme
onBack();
} else if (input === 'c' || input === 'C') {
// Customize theme - copy theme colors to widgets
const currentThemeName = themes[selectedIndex];
if (currentThemeName && currentThemeName !== 'custom') {
setShowCustomizeConfirm(true);
}
}
}
});
const selectedThemeName = themes[selectedIndex];
const selectedTheme = selectedThemeName ? getPowerlineTheme(selectedThemeName) : undefined;
if (showCustomizeConfirm) {
return (
<Box flexDirection='column'>
<Text bold color='yellow'>⚠ Confirm Customization</Text>
<Box marginTop={1} flexDirection='column'>
<Text>This will copy the current theme colors to your widgets</Text>
<Text>and switch to Custom theme mode.</Text>
<Text color='red'>This will overwrite any existing custom colors!</Text>
</Box>
<Box marginTop={2}>
<Text>Continue?</Text>
</Box>
<Box marginTop={1}>
<ConfirmDialog
inline={true}
onConfirm={() => {
customizeTheme();
setShowCustomizeConfirm(false);
}}
onCancel={() => {
setShowCustomizeConfirm(false);
}}
/>
</Box>
</Box>
);
}
return (
<Box flexDirection='column'>
<Text bold>
{`Powerline Theme Selection | `}
<Text dimColor>
{`Original: ${originalThemeRef.current}`}
</Text>
</Text>
<Box>
<Text dimColor>
{`↑↓ navigate, Enter apply${selectedThemeName && selectedThemeName !== 'custom' ? ', (c)ustomize theme' : ''}, ESC cancel`}
</Text>
</Box>
<Box marginTop={1} flexDirection='column'>
{themes.map((themeName, index) => {
const theme = getPowerlineTheme(themeName);
const isSelected = index === selectedIndex;
const isOriginal = themeName === originalThemeRef.current;
return (
<Box key={themeName}>
<Text color={isSelected ? 'green' : undefined}>
{isSelected ? '▶ ' : ' '}
{theme?.name ?? themeName}
{isOriginal && <Text dimColor> (original)</Text>}
</Text>
</Box>
);
})}
</Box>
{selectedTheme && (
<Box marginTop={2} flexDirection='column'>
<Text dimColor>Description:</Text>
<Box marginLeft={2}>
<Text>{selectedTheme.description}</Text>
</Box>
{selectedThemeName && selectedThemeName !== 'custom' && (
<Box marginTop={1}>
<Text dimColor>Press (c) to customize this theme - copies colors to widgets</Text>
</Box>
)}
{settings.colorLevel === 1 && (
<Box>
<Text color='yellow'>⚠ 16 color mode themes have a very limited palette, we recommend switching color level in Terminal Options</Text>
</Box>
)}
</Box>
)}
</Box>
);
};
+98
View File
@@ -0,0 +1,98 @@
import chalk from 'chalk';
import {
Box,
Text
} from 'ink';
import React from 'react';
import type { Settings } from '../../types/Settings';
import type { WidgetItem } from '../../types/Widget';
import {
renderStatusLineWithInfo,
type RenderContext,
type RenderResult
} from '../../utils/renderer';
import { canDetectTerminalWidth } from '../../utils/terminal';
export interface StatusLinePreviewProps {
lines: WidgetItem[][];
terminalWidth: number;
settings?: Settings;
onTruncationChange?: (isTruncated: boolean) => void;
}
const renderSingleLine = (
widgets: WidgetItem[],
terminalWidth: number,
widthDetectionAvailable: boolean,
settings: Settings,
lineIndex: number,
globalSeparatorIndex: number
): RenderResult => {
// Create render context for preview
const context: RenderContext = {
terminalWidth,
isPreview: true,
lineIndex,
globalSeparatorIndex
};
return renderStatusLineWithInfo(widgets, settings, context);
};
export const StatusLinePreview: React.FC<StatusLinePreviewProps> = ({ lines, terminalWidth, settings, onTruncationChange }) => {
const widthDetectionAvailable = React.useMemo(() => canDetectTerminalWidth(), []);
// Render each configured line
// Pass the full terminal width - the renderer will handle preview adjustments
const { renderedLines, anyTruncated } = React.useMemo(() => {
if (!settings)
return { renderedLines: [], anyTruncated: false };
let globalSeparatorIndex = 0;
const result: string[] = [];
let truncated = false;
for (let i = 0; i < lines.length; i++) {
const lineItems = lines[i];
if (lineItems && lineItems.length > 0) {
const renderResult = renderSingleLine(lineItems, terminalWidth, widthDetectionAvailable, settings, i, globalSeparatorIndex);
result.push(renderResult.line);
if (renderResult.wasTruncated) {
truncated = true;
}
// Count separators used in this line (widgets - 1, excluding merged widgets)
const nonMergedWidgets = lineItems.filter((_, idx) => idx === lineItems.length - 1 || !lineItems[idx]?.merge);
if (nonMergedWidgets.length > 1) {
globalSeparatorIndex += nonMergedWidgets.length - 1;
}
}
}
return { renderedLines: result, anyTruncated: truncated };
}, [lines, terminalWidth, widthDetectionAvailable, settings]);
// Notify parent when truncation status changes
React.useEffect(() => {
onTruncationChange?.(anyTruncated);
}, [anyTruncated, onTruncationChange]);
return (
<Box flexDirection='column'>
<Box borderStyle='round' borderColor='gray' borderDimColor width='100%' paddingLeft={1}>
<Text>
&gt;
<Text dimColor> Preview (ctrl+s to save configuration at any time)</Text>
</Text>
</Box>
{renderedLines.map((line, index) => (
<Text key={index}>
{' '}
{line}
{chalk.reset('')}
</Text>
))}
</Box>
);
};
+230
View File
@@ -0,0 +1,230 @@
import chalk from 'chalk';
import {
Box,
Text,
useInput
} from 'ink';
import React, { useState } from 'react';
import type { Settings } from '../../types/Settings';
import type { WidgetItem } from '../../types/Widget';
import { getWidget } from '../../utils/widgets';
import { ConfirmDialog } from './ConfirmDialog';
export interface TerminalOptionsMenuProps {
settings: Settings;
onUpdate: (settings: Settings) => void;
onBack: (target?: string) => void;
}
export const TerminalOptionsMenu: React.FC<TerminalOptionsMenuProps> = ({ settings, onUpdate, onBack }) => {
const [showColorWarning, setShowColorWarning] = useState(false);
const [pendingColorLevel, setPendingColorLevel] = useState<0 | 1 | 2 | 3 | null>(null);
const [selectedIndex, setSelectedIndex] = useState(0);
const handleSelect = () => {
if (selectedIndex === 2) {
// Back button
onBack();
} else if (selectedIndex === 0) {
// Terminal Width Options
onBack('width');
} else if (selectedIndex === 1) {
// Color Level
// Check if there are any custom colors that would be lost
const hasCustomColors = settings.lines.some((line: WidgetItem[]) => line.some((widget: WidgetItem) => Boolean(widget.color && (widget.color.startsWith('ansi256:') || widget.color.startsWith('hex:')))
|| Boolean(widget.backgroundColor && (widget.backgroundColor.startsWith('ansi256:') || widget.backgroundColor.startsWith('hex:')))
)
);
const currentLevel = settings.colorLevel;
const nextLevel = ((currentLevel + 1) % 4) as 0 | 1 | 2 | 3;
// Warn if switching away from mode that supports custom colors
if (hasCustomColors
&& ((currentLevel === 2 && nextLevel !== 2) // Switching from 256 color mode
|| (currentLevel === 3 && nextLevel !== 3))) { // Switching from truecolor mode
setShowColorWarning(true);
setPendingColorLevel(nextLevel);
} else {
// Update chalk level immediately
chalk.level = nextLevel;
// Clean up incompatible custom colors even when no warning is shown
const cleanedLines = settings.lines.map(line => line.map((widget) => {
const newWidget = { ...widget };
// Remove custom colors incompatible with the new mode
if (nextLevel === 2) {
// Switching to 256 color mode - remove hex colors
if (widget.color?.startsWith('hex:')) {
if (widget.type !== 'separator' && widget.type !== 'flex-separator') {
const widgetImpl = getWidget(widget.type);
newWidget.color = widgetImpl.getDefaultColor();
}
}
if (widget.backgroundColor?.startsWith('hex:')) {
newWidget.backgroundColor = undefined;
}
} else if (nextLevel === 3) {
// Switching to truecolor mode - remove ansi256 colors
if (widget.color?.startsWith('ansi256:')) {
if (widget.type !== 'separator' && widget.type !== 'flex-separator') {
const widgetImpl = getWidget(widget.type);
newWidget.color = widgetImpl.getDefaultColor();
}
}
if (widget.backgroundColor?.startsWith('ansi256:')) {
newWidget.backgroundColor = undefined;
}
} else {
// Switching to 16 color mode - remove all custom colors
if (widget.color?.startsWith('ansi256:') || widget.color?.startsWith('hex:')) {
if (widget.type !== 'separator' && widget.type !== 'flex-separator') {
const widgetImpl = getWidget(widget.type);
newWidget.color = widgetImpl.getDefaultColor();
}
}
if (widget.backgroundColor?.startsWith('ansi256:') || widget.backgroundColor?.startsWith('hex:')) {
newWidget.backgroundColor = undefined;
}
}
return newWidget;
})
);
onUpdate({
...settings,
lines: cleanedLines,
colorLevel: nextLevel
});
}
}
};
const handleColorConfirm = () => {
// Proceed with color level change and clean up custom colors
if (pendingColorLevel !== null) {
chalk.level = pendingColorLevel;
// Clean up custom colors if switching away from modes that support them
const cleanedLines = settings.lines.map(line => line.map((widget) => {
const newWidget = { ...widget };
// Remove custom colors if switching to a mode that doesn't support them
if ((pendingColorLevel !== 2 && pendingColorLevel !== 3)
|| (pendingColorLevel === 2 && (widget.color?.startsWith('hex:') || widget.backgroundColor?.startsWith('hex:')))
|| (pendingColorLevel === 3 && (widget.color?.startsWith('ansi256:') || widget.backgroundColor?.startsWith('ansi256:')))) {
// Reset custom colors to defaults
if (widget.color?.startsWith('ansi256:') || widget.color?.startsWith('hex:')) {
if (widget.type !== 'separator' && widget.type !== 'flex-separator') {
const widgetImpl = getWidget(widget.type);
newWidget.color = widgetImpl.getDefaultColor();
}
}
if (widget.backgroundColor?.startsWith('ansi256:') || widget.backgroundColor?.startsWith('hex:')) {
newWidget.backgroundColor = undefined;
}
}
return newWidget;
})
);
onUpdate({
...settings,
lines: cleanedLines,
colorLevel: pendingColorLevel
});
}
setShowColorWarning(false);
setPendingColorLevel(null);
};
const handleColorCancel = () => {
setShowColorWarning(false);
setPendingColorLevel(null);
};
useInput((input, key) => {
if (key.escape) {
if (!showColorWarning) {
onBack();
}
} else if (!showColorWarning) {
if (key.upArrow) {
setSelectedIndex(Math.max(0, selectedIndex - 1));
} else if (key.downArrow) {
setSelectedIndex(Math.min(2, selectedIndex + 1));
} else if (key.return) {
handleSelect();
}
}
});
return (
<Box flexDirection='column'>
<Text bold>Terminal Options</Text>
{showColorWarning ? (
<Box flexDirection='column' marginTop={1}>
<Text color='yellow'>⚠ Warning: Custom colors detected!</Text>
<Text>Switching color modes will reset custom ansi256 or hex colors to defaults.</Text>
<Box marginTop={1}>
<ConfirmDialog
message='Continue?'
onConfirm={handleColorConfirm}
onCancel={handleColorCancel}
inline
/>
</Box>
</Box>
) : (
<>
<Text color='white'>Configure terminal-specific settings for optimal display</Text>
<Box marginTop={1} flexDirection='column'>
<Box>
<Text color={selectedIndex === 0 ? 'green' : undefined}>
{selectedIndex === 0 ? '▶ ' : ' '}
◱ Terminal Width
</Text>
</Box>
<Box>
<Text color={selectedIndex === 1 ? 'green' : undefined}>
{selectedIndex === 1 ? '▶ ' : ' '}
▓ Color Level:
{' '}
{getColorLevelLabel(settings.colorLevel)}
</Text>
</Box>
<Box marginTop={1}>
<Text color={selectedIndex === 2 ? 'green' : undefined}>
{selectedIndex === 2 ? '▶ ' : ' '}
← Back
</Text>
</Box>
</Box>
{selectedIndex === 1 && (
<Box marginTop={1} flexDirection='column'>
<Text dimColor>Color level affects how colors are rendered:</Text>
<Text dimColor>• Truecolor: Full 24-bit RGB colors (16.7M colors)</Text>
<Text dimColor>• 256 Color: Extended color palette (256 colors)</Text>
<Text dimColor>• Basic: Standard 16-color terminal palette</Text>
<Text dimColor>• No Color: Disables all color output</Text>
</Box>
)}
</>
)}
</Box>
);
};
export const getColorLevelLabel = (level?: 0 | 1 | 2 | 3): string => {
switch (level) {
case 0: return 'No Color';
case 1: return 'Basic';
case 2:
case undefined: return '256 Color (default)';
case 3: return 'Truecolor';
default: return '256 Color (default)';
}
};
+177
View File
@@ -0,0 +1,177 @@
import {
Box,
Text,
useInput
} from 'ink';
import React, { useState } from 'react';
import type { FlexMode } from '../../types/FlexMode';
import type { Settings } from '../../types/Settings';
export interface TerminalWidthMenuProps {
settings: Settings;
onUpdate: (settings: Settings) => void;
onBack: () => void;
}
export const TerminalWidthMenu: React.FC<TerminalWidthMenuProps> = ({ settings, onUpdate, onBack }) => {
const [selectedOption, setSelectedOption] = useState<FlexMode>(settings.flexMode);
const [compactThreshold, setCompactThreshold] = useState(settings.compactThreshold);
const [editingThreshold, setEditingThreshold] = useState(false);
const [thresholdInput, setThresholdInput] = useState(String(settings.compactThreshold));
const [validationError, setValidationError] = useState<string | null>(null);
// For manual navigation: 0-2 for options, 3 for back
const [selectedIndex, setSelectedIndex] = useState(() => {
const options: FlexMode[] = ['full', 'full-minus-40', 'full-until-compact'];
return options.indexOf(settings.flexMode);
});
const options: FlexMode[] = ['full', 'full-minus-40', 'full-until-compact'];
useInput((input, key) => {
if (editingThreshold) {
if (key.return) {
const value = parseInt(thresholdInput, 10);
if (isNaN(value)) {
setValidationError('Please enter a valid number');
} else if (value < 1 || value > 99) {
setValidationError(`Value must be between 1 and 99 (you entered ${value})`);
} else {
setCompactThreshold(value);
// Update settings with both flexMode and the new threshold
const updatedSettings = {
...settings,
flexMode: selectedOption,
compactThreshold: value
};
onUpdate(updatedSettings);
setEditingThreshold(false);
setValidationError(null);
}
} else if (key.escape) {
setThresholdInput(String(compactThreshold));
setEditingThreshold(false);
setValidationError(null);
} else if (key.backspace) {
setThresholdInput(thresholdInput.slice(0, -1));
setValidationError(null);
} else if (key.delete) {
// For simple number inputs, forward delete does nothing since there's no cursor position
} else if (input && /\d/.test(input)) {
const newValue = thresholdInput + input;
if (newValue.length <= 2) {
setThresholdInput(newValue);
setValidationError(null);
}
}
} else {
if (key.escape) {
onBack();
} else if (key.upArrow) {
setSelectedIndex(Math.max(0, selectedIndex - 1));
} else if (key.downArrow) {
setSelectedIndex(Math.min(3, selectedIndex + 1)); // 0-2 for options, 3 for back
} else if (key.return) {
if (selectedIndex === 3) {
onBack();
} else if (selectedIndex >= 0 && selectedIndex < options.length) {
const mode = options[selectedIndex];
if (mode) {
setSelectedOption(mode);
// Update settings
const updatedSettings = {
...settings,
flexMode: mode,
compactThreshold: compactThreshold
};
onUpdate(updatedSettings);
if (mode === 'full-until-compact') {
// Prompt for threshold editing
setEditingThreshold(true);
}
}
}
}
}
});
const optionDetails = [
{
value: 'full' as FlexMode,
label: 'Full width always',
description: 'Uses the full terminal width minus 4 characters for terminal padding. If the auto-compact message appears, it may cause the line to wrap.\n\nNOTE: If /ide integration is enabled, it\'s not recommended to use this mode.'
},
{
value: 'full-minus-40' as FlexMode,
label: 'Full width minus 40 (default)',
description: 'Leaves a gap to the right of the status line to accommodate the auto-compact message. This prevents wrapping but may leave unused space. This limitation exists because we cannot detect when the message will appear.'
},
{
value: 'full-until-compact' as FlexMode,
label: 'Full width until compact',
description: `Dynamically adjusts width based on context usage. When context reaches ${compactThreshold}%, it switches to leaving space for the auto-compact message.\n\nNOTE: If /ide integration is enabled, it's not recommended to use this mode.`
}
];
const currentOption = selectedIndex < 3 ? optionDetails[selectedIndex] : null;
return (
<Box flexDirection='column'>
<Text bold>Terminal Width</Text>
<Text color='white'>These settings affect where long lines are truncated, and where right-alignment occurs when using flex separators</Text>
<Text dimColor wrap='wrap'>Claude code does not currently provide an available width variable for the statusline and features like IDE integration, auto-compaction notices, etc all cause the statusline to wrap if we do not truncate it</Text>
{editingThreshold ? (
<Box marginTop={1} flexDirection='column'>
<Text>
Enter compact threshold (1-99):
{' '}
{thresholdInput}
%
</Text>
{validationError ? (
<Text color='red'>{validationError}</Text>
) : (
<Text dimColor>Press Enter to confirm, ESC to cancel</Text>
)}
</Box>
) : (
<>
<Box marginTop={1} flexDirection='column'>
{optionDetails.map((opt, index) => (
<Box key={opt.value}>
<Text color={selectedIndex === index ? 'green' : undefined}>
{selectedIndex === index ? '▶ ' : ' '}
{opt.label}
{opt.value === selectedOption ? ' ✓' : ''}
</Text>
</Box>
))}
<Box marginTop={1}>
<Text color={selectedIndex === 3 ? 'green' : undefined}>
{selectedIndex === 3 ? '▶ ' : ' '}
← Back
</Text>
</Box>
</Box>
{currentOption && (
<Box marginTop={1} marginBottom={1} borderStyle='round' borderColor='dim' paddingX={1}>
<Box flexDirection='column'>
<Text>
<Text color='yellow'>{currentOption.label}</Text>
{currentOption.value === 'full-until-compact' && ` | Current threshold: ${compactThreshold}%`}
</Text>
<Text dimColor wrap='wrap'>{currentOption.description}</Text>
</Box>
</Box>
)}
</>
)}
</Box>
);
};
+12
View File
@@ -0,0 +1,12 @@
// Barrel file - exports all components and their types
export * from './ColorMenu';
export * from './ConfirmDialog';
export * from './GlobalOverridesMenu';
export * from './InstallMenu';
export * from './ItemsEditor';
export * from './LineSelector';
export * from './MainMenu';
export * from './PowerlineSetup';
export * from './StatusLinePreview';
export * from './TerminalOptionsMenu';
export * from './TerminalWidthMenu';
+1
View File
@@ -0,0 +1 @@
export { runTUI } from './App';
+12
View File
@@ -0,0 +1,12 @@
export interface ClaudeSettings {
permissions?: {
allow?: string[];
deny?: string[];
};
statusLine?: {
type: string;
command: string;
padding?: number;
};
[key: string]: unknown;
}
+10
View File
@@ -0,0 +1,10 @@
import type { ChalkInstance } from 'chalk';
export interface ColorEntry {
name: string;
displayName: string;
isBackground: boolean;
ansi16: ChalkInstance;
ansi256: ChalkInstance;
truecolor: ChalkInstance;
}
+29
View File
@@ -0,0 +1,29 @@
import { z } from 'zod';
// Schema for chalk color level
export const ColorLevelSchema = z.union([
z.literal(0), // No colors
z.literal(1), // Basic 16 colors
z.literal(2), // 256 colors
z.literal(3) // Truecolor (16 million)
]);
// Inferred type from schema
export type ColorLevel = z.infer<typeof ColorLevelSchema>;
// String representation for different color levels
export type ColorLevelString = 'ansi16' | 'ansi256' | 'truecolor';
// Helper to get color level as string for chalk
export function getColorLevelString(level: ColorLevel | undefined): ColorLevelString {
switch (level) {
case 0:
case 1:
return 'ansi16';
case 3:
return 'truecolor';
case 2:
default:
return 'ansi256';
}
}
+7
View File
@@ -0,0 +1,7 @@
import { z } from 'zod';
// Schema for terminal width handling modes
export const FlexModeSchema = z.enum(['full', 'full-minus-40', 'full-until-compact']);
// Inferred type from schema
export type FlexMode = z.infer<typeof FlexModeSchema>;
+14
View File
@@ -0,0 +1,14 @@
import { z } from 'zod';
// Schema for powerline configuration
export const PowerlineConfigSchema = z.object({
enabled: z.boolean().default(false),
separators: z.array(z.string()).default(['\uE0B0']),
separatorInvertBackground: z.array(z.boolean()).default([false]),
startCaps: z.array(z.string()).default([]),
endCaps: z.array(z.string()).default([]),
theme: z.string().optional()
});
// Inferred type from schema
export type PowerlineConfig = z.infer<typeof PowerlineConfigSchema>;
+4
View File
@@ -0,0 +1,4 @@
export interface PowerlineFontStatus {
installed: boolean;
checkedSymbol?: string;
}
+12
View File
@@ -0,0 +1,12 @@
import type { StatusJSON } from './StatusJSON';
import type { TokenMetrics } from './TokenMetrics';
export interface RenderContext {
data?: StatusJSON;
tokenMetrics?: TokenMetrics | null;
sessionDuration?: string | null;
terminalWidth?: number | null;
isPreview?: boolean;
lineIndex?: number; // Index of the current line being rendered (for theme cycling)
globalSeparatorIndex?: number; // Global separator index that continues across lines
}
+70
View File
@@ -0,0 +1,70 @@
import { z } from 'zod';
import { ColorLevelSchema } from './ColorLevel';
import { FlexModeSchema } from './FlexMode';
import { PowerlineConfigSchema } from './PowerlineConfig';
import { WidgetItemSchema } from './Widget';
// Current version - bump this when making breaking changes to the schema
export const CURRENT_VERSION = 2;
// Schema for v1 settings (before version field was added)
export const SettingsSchema_v1 = z.object({
lines: z.array(z.array(WidgetItemSchema)).optional(),
flexMode: FlexModeSchema.optional(),
compactThreshold: z.number().optional(),
colorLevel: ColorLevelSchema.optional(),
defaultSeparator: z.string().optional(),
defaultPadding: z.string().optional(),
inheritSeparatorColors: z.boolean().optional(),
overrideBackgroundColor: z.string().optional(),
overrideForegroundColor: z.string().optional(),
globalBold: z.boolean().optional()
});
// Main settings schema with defaults
export const SettingsSchema = z.object({
version: z.number().default(CURRENT_VERSION),
lines: z.array(z.array(WidgetItemSchema))
.min(1)
.max(3)
.default([
[
{ id: '1', type: 'model', color: 'cyan' },
{ id: '2', type: 'separator' },
{ id: '3', type: 'context-length', color: 'brightBlack' },
{ id: '4', type: 'separator' },
{ id: '5', type: 'git-branch', color: 'magenta' },
{ id: '6', type: 'separator' },
{ id: '7', type: 'git-changes', color: 'yellow' }
]
])
.transform(lines => lines.slice(0, 3)), // Ensure max 3 lines
flexMode: FlexModeSchema.default('full-minus-40'),
compactThreshold: z.number().min(1).max(99).default(60),
colorLevel: ColorLevelSchema.default(2),
defaultSeparator: z.string().optional(),
defaultPadding: z.string().optional(),
inheritSeparatorColors: z.boolean().default(false),
overrideBackgroundColor: z.string().optional(),
overrideForegroundColor: z.string().optional(),
globalBold: z.boolean().default(false),
powerline: PowerlineConfigSchema.default({
enabled: false,
separators: ['\uE0B0'],
separatorInvertBackground: [false],
startCaps: [],
endCaps: [],
theme: undefined
}),
updatemessage: z.object({
message: z.string().nullable().optional(),
remaining: z.number().nullable().optional()
}).optional()
});
// Inferred type from schema
export type Settings = z.infer<typeof SettingsSchema>;
// Export a default settings constant for reference
export const DEFAULT_SETTINGS: Settings = SettingsSchema.parse({});
+20
View File
@@ -0,0 +1,20 @@
import { z } from 'zod';
export const StatusJSONSchema = z.looseObject({
hook_event_name: z.string().optional(),
session_id: z.string().optional(),
transcript_path: z.string().optional(),
cwd: z.string().optional(),
model: z.object({
id: z.string().optional(),
display_name: z.string().optional()
}).optional(),
workspace: z.object({
current_dir: z.string().optional(),
project_dir: z.string().optional()
}).optional(),
version: z.string().optional(),
output_style: z.object({ name: z.string().optional() }).optional()
});
export type StatusJSON = z.infer<typeof StatusJSONSchema>;
+20
View File
@@ -0,0 +1,20 @@
export interface TokenUsage {
input_tokens: number;
output_tokens: number;
cache_creation_input_tokens?: number;
cache_read_input_tokens?: number;
}
export interface TranscriptLine {
message?: { usage?: TokenUsage };
isSidechain?: boolean;
timestamp?: string;
}
export interface TokenMetrics {
inputTokens: number;
outputTokens: number;
cachedTokens: number;
totalTokens: number;
contextLength: number;
}
+54
View File
@@ -0,0 +1,54 @@
import { z } from 'zod';
import type { RenderContext } from './RenderContext';
import type { Settings } from './Settings';
// Widget item schema - accepts any string type for forward compatibility
export const WidgetItemSchema = z.object({
id: z.string(),
type: z.string(),
color: z.string().optional(),
backgroundColor: z.string().optional(),
bold: z.boolean().optional(),
character: z.string().optional(),
rawValue: z.boolean().optional(),
customText: z.string().optional(),
commandPath: z.string().optional(),
maxWidth: z.number().optional(),
preserveColors: z.boolean().optional(),
timeout: z.number().optional(),
merge: z.union([z.boolean(), z.literal('no-padding')]).optional()
});
// Inferred types from Zod schemas
export type WidgetItem = z.infer<typeof WidgetItemSchema>;
export type WidgetItemType = string; // Allow any string for forward compatibility
export interface WidgetEditorDisplay {
displayText: string;
modifierText?: string;
}
export interface Widget {
getDefaultColor(): string;
getDisplayName(): string;
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay;
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null;
getCustomKeybinds?(): CustomKeybind[];
renderEditor?(props: WidgetEditorProps): React.ReactElement | null;
supportsRawValue(): boolean;
supportsColors(item: WidgetItem): boolean;
setEditorAction?(action: string): void;
}
export interface WidgetEditorProps {
widget: WidgetItem;
onComplete: (updatedWidget: WidgetItem) => void;
onCancel: () => void;
}
export interface CustomKeybind {
key: string;
label: string;
action: string;
}
+19
View File
@@ -0,0 +1,19 @@
// Central export file for all types
export type {
CustomKeybind,
Widget,
WidgetEditorProps,
WidgetItem,
WidgetItemType
} from './Widget';
export type { Settings } from './Settings';
export type { FlexMode } from './FlexMode';
export type { PowerlineConfig } from './PowerlineConfig';
export type { ColorLevel, ColorLevelString } from './ColorLevel';
export { getColorLevelString } from './ColorLevel';
export type { StatusJSON } from './StatusJSON';
export type { TokenMetrics, TokenUsage, TranscriptLine } from './TokenMetrics';
export type { RenderContext } from './RenderContext';
export type { PowerlineFontStatus } from './PowerlineFontStatus';
export type { ClaudeSettings } from './ClaudeSettings';
export type { ColorEntry } from './ColorEntry';
+28 -26
View File
@@ -1,35 +1,27 @@
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { promisify } from 'util';
import * as path from 'path';
// 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);
import type { ClaudeSettings } from '../types/ClaudeSettings';
// Re-export for backward compatibility
export type { ClaudeSettings };
// Use fs.promises directly
const readFile = fs.promises.readFile;
const writeFile = fs.promises.writeFile;
const mkdir = fs.promises.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);
return JSON.parse(content) as ClaudeSettings;
} catch {
return {};
}
@@ -43,18 +35,28 @@ export async function saveClaudeSettings(settings: ClaudeSettings): Promise<void
export async function isInstalled(): Promise<boolean> {
const settings = await loadClaudeSettings();
// Check if command is correct AND padding is 0 (or undefined for new installs)
return settings.statusLine?.command === 'npx -y ccstatusline@latest' &&
(settings.statusLine.padding === 0 || settings.statusLine.padding === undefined);
// Check if command is either npx or bunx version AND padding is 0 (or undefined for new installs)
const validCommands = ['npx -y ccstatusline@latest', 'bunx -y ccstatusline@latest'];
return validCommands.includes(settings.statusLine?.command ?? '')
&& (settings.statusLine?.padding === 0 || settings.statusLine?.padding === undefined);
}
export async function installStatusLine(): Promise<void> {
export function isBunxAvailable(): boolean {
try {
execSync('which bunx', { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
export async function installStatusLine(useBunx = false): Promise<void> {
const settings = await loadClaudeSettings();
// Update settings with our status line (confirmation already handled in TUI)
settings.statusLine = {
type: 'command',
command: 'npx -y ccstatusline@latest',
command: useBunx ? 'bunx -y ccstatusline@latest' : 'npx -y ccstatusline@latest',
padding: 0
};
@@ -72,5 +74,5 @@ export async function uninstallStatusLine(): Promise<void> {
export async function getExistingStatusLine(): Promise<string | null> {
const settings = await loadClaudeSettings();
return settings.statusLine?.command || null;
return settings.statusLine?.command ?? null;
}
+444
View File
@@ -0,0 +1,444 @@
import chalk, { type ChalkInstance } from 'chalk';
import type { ColorEntry } from '../types/ColorEntry';
// Re-export for backward compatibility
export type { ColorEntry };
// Create COLOR_MAP as a function so chalk instances are created with the correct level
function createColorMap(): ColorEntry[] {
return [
// Regular colors - for ansi256 mode, use fixed palette colors (16-231) instead of theme colors (0-15)
{ name: 'black', displayName: 'Black', isBackground: false, ansi16: chalk.black, ansi256: chalk.ansi256(16), truecolor: chalk.hex('#000000') },
{ name: 'red', displayName: 'Red', isBackground: false, ansi16: chalk.red, ansi256: chalk.ansi256(160), truecolor: chalk.hex('#cc0000') },
{ name: 'green', displayName: 'Green', isBackground: false, ansi16: chalk.green, ansi256: chalk.ansi256(70), truecolor: chalk.hex('#4e9a06') },
{ name: 'yellow', displayName: 'Yellow', isBackground: false, ansi16: chalk.yellow, ansi256: chalk.ansi256(178), truecolor: chalk.hex('#c4a000') },
{ name: 'blue', displayName: 'Blue', isBackground: false, ansi16: chalk.blue, ansi256: chalk.ansi256(26), truecolor: chalk.hex('#3465a4') },
{ name: 'magenta', displayName: 'Magenta', isBackground: false, ansi16: chalk.magenta, ansi256: chalk.ansi256(96), truecolor: chalk.hex('#75507b') },
{ name: 'cyan', displayName: 'Cyan', isBackground: false, ansi16: chalk.cyan, ansi256: chalk.ansi256(30), truecolor: chalk.hex('#06989a') },
{ name: 'white', displayName: 'White', isBackground: false, ansi16: chalk.white, ansi256: chalk.ansi256(188), truecolor: chalk.hex('#d3d7cf') },
// Bright colors - use brighter fixed palette colors
{ name: 'brightBlack', displayName: 'Bright Black', isBackground: false, ansi16: chalk.blackBright, ansi256: chalk.ansi256(59), truecolor: chalk.hex('#555753') },
{ name: 'brightRed', displayName: 'Bright Red', isBackground: false, ansi16: chalk.redBright, ansi256: chalk.ansi256(203), truecolor: chalk.hex('#ef2929') },
{ name: 'brightGreen', displayName: 'Bright Green', isBackground: false, ansi16: chalk.greenBright, ansi256: chalk.ansi256(155), truecolor: chalk.hex('#8ae234') },
{ name: 'brightYellow', displayName: 'Bright Yellow', isBackground: false, ansi16: chalk.yellowBright, ansi256: chalk.ansi256(227), truecolor: chalk.hex('#fce94f') },
{ name: 'brightBlue', displayName: 'Bright Blue', isBackground: false, ansi16: chalk.blueBright, ansi256: chalk.ansi256(111), truecolor: chalk.hex('#729fcf') },
{ name: 'brightMagenta', displayName: 'Bright Magenta', isBackground: false, ansi16: chalk.magentaBright, ansi256: chalk.ansi256(140), truecolor: chalk.hex('#ad7fa8') },
{ name: 'brightCyan', displayName: 'Bright Cyan', isBackground: false, ansi16: chalk.cyanBright, ansi256: chalk.ansi256(80), truecolor: chalk.hex('#34e2e2') },
{ name: 'brightWhite', displayName: 'Bright White', isBackground: false, ansi16: chalk.whiteBright, ansi256: chalk.ansi256(231), truecolor: chalk.hex('#eeeeec') },
// Background colors - match foreground indices for consistency
{ name: 'bgBlack', displayName: 'Black', isBackground: true, ansi16: chalk.bgBlack, ansi256: chalk.bgAnsi256(16), truecolor: chalk.bgHex('#000000') },
{ name: 'bgRed', displayName: 'Red', isBackground: true, ansi16: chalk.bgRed, ansi256: chalk.bgAnsi256(160), truecolor: chalk.bgHex('#cc0000') },
{ name: 'bgGreen', displayName: 'Green', isBackground: true, ansi16: chalk.bgGreen, ansi256: chalk.bgAnsi256(70), truecolor: chalk.bgHex('#4e9a06') },
{ name: 'bgYellow', displayName: 'Yellow', isBackground: true, ansi16: chalk.bgYellow, ansi256: chalk.bgAnsi256(178), truecolor: chalk.bgHex('#c4a000') },
{ name: 'bgBlue', displayName: 'Blue', isBackground: true, ansi16: chalk.bgBlue, ansi256: chalk.bgAnsi256(26), truecolor: chalk.bgHex('#3465a4') },
{ name: 'bgMagenta', displayName: 'Magenta', isBackground: true, ansi16: chalk.bgMagenta, ansi256: chalk.bgAnsi256(96), truecolor: chalk.bgHex('#75507b') },
{ name: 'bgCyan', displayName: 'Cyan', isBackground: true, ansi16: chalk.bgCyan, ansi256: chalk.bgAnsi256(30), truecolor: chalk.bgHex('#06989a') },
{ name: 'bgWhite', displayName: 'White', isBackground: true, ansi16: chalk.bgWhite, ansi256: chalk.bgAnsi256(188), truecolor: chalk.bgHex('#d3d7cf') },
// Bright background colors - match bright foreground indices
{ name: 'bgBrightBlack', displayName: 'Bright Black', isBackground: true, ansi16: chalk.bgBlackBright, ansi256: chalk.bgAnsi256(59), truecolor: chalk.bgHex('#555753') },
{ name: 'bgBrightRed', displayName: 'Bright Red', isBackground: true, ansi16: chalk.bgRedBright, ansi256: chalk.bgAnsi256(203), truecolor: chalk.bgHex('#ef2929') },
{ name: 'bgBrightGreen', displayName: 'Bright Green', isBackground: true, ansi16: chalk.bgGreenBright, ansi256: chalk.bgAnsi256(155), truecolor: chalk.bgHex('#8ae234') },
{ name: 'bgBrightYellow', displayName: 'Bright Yellow', isBackground: true, ansi16: chalk.bgYellowBright, ansi256: chalk.bgAnsi256(227), truecolor: chalk.bgHex('#fce94f') },
{ name: 'bgBrightBlue', displayName: 'Bright Blue', isBackground: true, ansi16: chalk.bgBlueBright, ansi256: chalk.bgAnsi256(111), truecolor: chalk.bgHex('#729fcf') },
{ name: 'bgBrightMagenta', displayName: 'Bright Magenta', isBackground: true, ansi16: chalk.bgMagentaBright, ansi256: chalk.bgAnsi256(140), truecolor: chalk.bgHex('#ad7fa8') },
{ name: 'bgBrightCyan', displayName: 'Bright Cyan', isBackground: true, ansi16: chalk.bgCyanBright, ansi256: chalk.bgAnsi256(80), truecolor: chalk.bgHex('#34e2e2') },
{ name: 'bgBrightWhite', displayName: 'Bright White', isBackground: true, ansi16: chalk.bgWhiteBright, ansi256: chalk.bgAnsi256(231), truecolor: chalk.bgHex('#eeeeec') }
];
}
// Initialize with default, will be recreated when chalk.level is set
export let COLOR_MAP: ColorEntry[] = createColorMap();
// Function to recreate COLOR_MAP after chalk.level is set
export function updateColorMap(): void {
COLOR_MAP = createColorMap();
}
export function bgToFg(colorName: string | undefined): string | undefined {
if (!colorName)
return undefined;
// Custom formats pass through unchanged (ansi256:X and hex:XXXXXX)
if (colorName.startsWith('ansi256:') || colorName.startsWith('hex:')) {
return colorName;
}
// Convert background color names to foreground equivalents
if (colorName.startsWith('bgBright')) {
// bgBrightRed -> brightRed
const baseName = colorName.substring(8);
return 'bright' + baseName.charAt(0).toUpperCase() + baseName.slice(1).toLowerCase();
} else if (colorName.startsWith('bg')) {
// bgRed -> red
const baseName = colorName.substring(2);
return baseName.charAt(0).toLowerCase() + baseName.slice(1);
}
// Already a foreground color
return colorName;
}
export function getChalkColor(colorName: string | undefined, colorLevel: 'ansi16' | 'ansi256' | 'truecolor' = 'ansi16', isBackground = false): ChalkInstance | undefined {
if (!colorName)
return undefined;
// Handle ansi256:X format
if (colorName.startsWith('ansi256:')) {
const code = parseInt(colorName.substring(8), 10);
if (!isNaN(code) && code >= 0 && code <= 255) {
return isBackground ? chalk.bgAnsi256(code) : chalk.ansi256(code);
}
return undefined;
}
// Handle hex:XXXXXX format
if (colorName.startsWith('hex:')) {
const hex = colorName.substring(4);
if (/^[0-9A-Fa-f]{6}$/.test(hex)) {
return isBackground ? chalk.bgHex('#' + hex) : chalk.hex('#' + hex);
}
return undefined;
}
const colorEntry = COLOR_MAP.find(c => c.name === colorName);
if (!colorEntry) {
return undefined;
}
switch (colorLevel) {
case 'ansi256':
return colorEntry.ansi256;
case 'truecolor':
return colorEntry.truecolor;
case 'ansi16':
default:
return colorEntry.ansi16;
}
}
export function applyColors(
text: string,
foregroundColor?: string,
backgroundColor?: string,
bold?: boolean,
colorLevel: 'ansi16' | 'ansi256' | 'truecolor' = 'ansi16'
): string {
if (!foregroundColor && !backgroundColor && !bold) {
return text;
}
let result = text;
// Apply background color first
// This ensures the background is properly established before other styles
if (backgroundColor) {
const bgChalk = getChalkColor(backgroundColor, colorLevel, true);
if (bgChalk) {
result = bgChalk(result);
}
}
// Apply foreground color second
if (foregroundColor) {
const fgChalk = getChalkColor(foregroundColor, colorLevel, false);
if (fgChalk) {
result = fgChalk(result);
}
}
// Apply bold last if needed
if (bold) {
result = chalk.bold(result);
}
return result;
}
// Get raw ANSI codes for a color without the reset codes
export function getColorAnsiCode(colorName: string | undefined, colorLevel: 'ansi16' | 'ansi256' | 'truecolor' = 'ansi16', isBackground = false): string {
if (!colorName)
return '';
// Handle ansi256:X format
if (colorName.startsWith('ansi256:')) {
const code = parseInt(colorName.substring(8), 10);
if (!isNaN(code) && code >= 0 && code <= 255) {
return isBackground ? `\x1b[48;5;${code}m` : `\x1b[38;5;${code}m`;
}
return '';
}
// Handle hex:XXXXXX format
if (colorName.startsWith('hex:')) {
const hex = colorName.substring(4);
if (/^[0-9A-Fa-f]{6}$/.test(hex)) {
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return isBackground ? `\x1b[48;2;${r};${g};${b}m` : `\x1b[38;2;${r};${g};${b}m`;
}
return '';
}
// Find the color in COLOR_MAP
const colorEntry = COLOR_MAP.find(c => c.name === colorName);
if (!colorEntry)
return '';
// Now that chalk.level is set correctly, we can use chalk to generate the codes
let chalkFn: ChalkInstance;
switch (colorLevel) {
case 'ansi256':
chalkFn = colorEntry.ansi256;
break;
case 'truecolor':
chalkFn = colorEntry.truecolor;
break;
default:
chalkFn = colorEntry.ansi16;
break;
}
// Apply the color and extract the opening ANSI code
const colored = chalkFn('TEST');
const escapeChar = '\u001b'; // ESC character
const ansiRegex = new RegExp(`^(${escapeChar.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\[[^m]+m)`);
const match = ansiRegex.exec(colored);
return match?.[1] ?? '';
}
export function getColorDisplayName(colorName: string): string {
const colorEntry = COLOR_MAP.find(c => c.name === colorName);
return colorEntry?.displayName ?? colorName;
}
export function getAvailableColors(): string[] {
return COLOR_MAP.map(c => c.name);
}
export function getAvailableColorsForUI(): { name: string; value: string }[] {
// Add default option, then filter for non-background colors
return [
{ name: 'Default', value: '' },
...COLOR_MAP
.filter(c => !c.isBackground)
.map(c => ({ name: c.displayName, value: c.name }))
];
}
export function getAvailableBackgroundColorsForUI(): { name: string; value: string }[] {
// Add default/none option, then filter for background colors
return [
{ name: 'Default', value: '' },
...COLOR_MAP
.filter(c => c.isBackground)
.map(c => ({ name: c.displayName, value: c.name }))
];
}
export function getBackgroundColorsForPowerline(): string[] {
// Get background colors excluding black for better visibility in powerline mode
return COLOR_MAP
.filter(c => c.isBackground && c.name !== 'bgBlack')
.map(c => c.name);
}
// Powerline theme definitions
export interface PowerlineThemeColors {
fg: string[];
bg: string[];
}
export interface PowerlineTheme {
name: string;
description: string;
// Color levels: 1 = basic 16, 2 = 256, 3 = truecolor
1?: PowerlineThemeColors;
2?: PowerlineThemeColors;
3?: PowerlineThemeColors;
}
export const POWERLINE_THEMES: Record<string, PowerlineTheme> = {
'custom': {
name: 'Custom',
description: 'Uses individual widget background colors'
// No color definitions - handled specially
},
'nord': {
name: 'Nord',
description: 'Arctic, north-bluish color palette',
1: {
fg: ['black', 'brightWhite', 'brightWhite', 'black', 'black'],
bg: ['bgBrightCyan', 'bgBrightBlack', 'bgBlue', 'bgBrightYellow', 'bgBrightGreen']
},
2: {
fg: ['ansi256:16', 'ansi256:254', 'ansi256:231', 'ansi256:231', 'ansi256:16'],
bg: ['ansi256:73', 'ansi256:239', 'ansi256:25', 'ansi256:96', 'ansi256:152']
},
3: {
fg: ['hex:2E3440', 'hex:D8DEE9', 'hex:FDF6E3', 'hex:2E3440', 'hex:2E3440'],
bg: ['hex:88C0D0', 'hex:4C566A', 'hex:5E81AC', 'hex:B48EAD', 'hex:A3BE8C']
}
},
'nord-aurora': {
name: 'Nord Aurora',
description: 'Nord theme with aurora colors',
1: {
fg: ['brightWhite', 'black', 'black', 'black', 'black'],
bg: ['bgRed', 'bgBrightYellow', 'bgBrightBlue', 'bgGreen', 'bgBrightMagenta']
},
2: {
fg: ['ansi256:231', 'ansi256:16', 'ansi256:231', 'ansi256:16', 'ansi256:16'],
bg: ['ansi256:131', 'ansi256:220', 'ansi256:68', 'ansi256:108', 'ansi256:176']
},
3: {
fg: ['hex:ECEFF4', 'hex:2E3440', 'hex:FDF6E3', 'hex:2E3440', 'hex:2E3440'],
bg: ['hex:BF616A', 'hex:EBCB8B', 'hex:5E81AC', 'hex:A3BE8C', 'hex:B48EAD']
}
},
'monokai': {
name: 'Monokai',
description: 'Dark background with vibrant colors',
1: {
fg: ['black', 'brightWhite', 'black', 'white', 'black'],
bg: ['bgBrightGreen', 'bgBrightBlack', 'bgBrightYellow', 'bgMagenta', 'bgBrightCyan']
},
2: {
fg: ['ansi256:235', 'ansi256:255', 'ansi256:235', 'ansi256:16', 'ansi256:235'],
bg: ['ansi256:148', 'ansi256:238', 'ansi256:186', 'ansi256:141', 'ansi256:81']
},
3: {
fg: ['hex:272822', 'hex:F8F8F2', 'hex:272822', 'hex:272822', 'hex:272822'],
bg: ['hex:A6E22E', 'hex:49483E', 'hex:E6DB74', 'hex:AE81FF', 'hex:66D9EF']
}
},
'solarized': {
name: 'Solarized',
description: 'Precision colors for readability',
1: {
fg: ['brightWhite', 'black', 'brightWhite', 'black', 'black'],
bg: ['bgBlue', 'bgBrightYellow', 'bgBrightBlack', 'bgCyan', 'bgBrightWhite']
},
2: {
fg: ['ansi256:231', 'ansi256:234', 'ansi256:254', 'ansi256:16', 'ansi256:234'],
bg: ['ansi256:33', 'ansi256:136', 'ansi256:240', 'ansi256:37', 'ansi256:254']
},
3: {
fg: ['hex:073642', 'hex:073642', 'hex:FDF6E3', 'hex:073642', 'hex:073642'],
bg: ['hex:268BD2', 'hex:B58900', 'hex:586E75', 'hex:2AA198', 'hex:EEE8D5']
}
},
'minimal': {
name: 'Minimal',
description: 'Clean monochrome theme',
1: {
fg: ['brightWhite', 'black', 'white', 'black', 'black'],
bg: ['bgBrightBlack', 'bgBrightWhite', 'bgBlack', 'bgWhite', 'bgBrightWhite']
},
2: {
fg: ['ansi256:255', 'ansi256:232', 'ansi256:255', 'ansi256:232', 'ansi256:252'],
bg: ['ansi256:240', 'ansi256:251', 'ansi256:233', 'ansi256:248', 'ansi256:236']
},
3: {
fg: ['hex:FFFFFF', 'hex:1C1C1C', 'hex:FFFFFF', 'hex:1C1C1C', 'hex:E4E4E4'],
bg: ['hex:585858', 'hex:D0D0D0', 'hex:1A1A1A', 'hex:A8A8A8', 'hex:303030']
}
},
'dracula': {
name: 'Dracula',
description: 'Dark theme with purple accents',
1: {
fg: ['brightWhite', 'black', 'brightWhite', 'black', 'white'],
bg: ['bgMagenta', 'bgBrightWhite', 'bgRed', 'bgBrightCyan', 'bgBrightBlack']
},
2: {
fg: ['ansi256:235', 'ansi256:235', 'ansi256:235', 'ansi256:235', 'ansi256:231'],
bg: ['ansi256:141', 'ansi256:253', 'ansi256:204', 'ansi256:117', 'ansi256:236']
},
3: {
fg: ['hex:282A36', 'hex:282A36', 'hex:282A36', 'hex:282A36', 'hex:F8F8F2'],
bg: ['hex:BD93F9', 'hex:F8F8F2', 'hex:FF5555', 'hex:8BE9FD', 'hex:44475A']
}
},
'catppuccin': {
name: 'Catppuccin',
description: 'Soothing pastel theme',
1: {
fg: ['black', 'brightWhite', 'black', 'brightWhite', 'black'],
bg: ['bgBrightMagenta', 'bgBrightBlack', 'bgBrightGreen', 'bgBlue', 'bgBrightYellow']
},
2: {
fg: ['ansi256:235', 'ansi256:255', 'ansi256:235', 'ansi256:235', 'ansi256:235'],
bg: ['ansi256:176', 'ansi256:238', 'ansi256:150', 'ansi256:210', 'ansi256:111']
},
3: {
fg: ['hex:1E1E2E', 'hex:CDD6F4', 'hex:1E1E2E', 'hex:1E1E2E', 'hex:CDD6F4'],
bg: ['hex:CBA6F7', 'hex:45475A', 'hex:A6E3A1', 'hex:F38BA8', 'hex:585B70']
}
},
'gruvbox': {
name: 'Gruvbox',
description: 'Retro groove color scheme',
1: {
fg: ['brightWhite', 'black', 'black', 'brightWhite', 'black'],
bg: ['bgRed', 'bgBrightYellow', 'bgBrightWhite', 'bgBlue', 'bgBrightGreen']
},
2: {
fg: ['ansi256:16', 'ansi256:235', 'ansi256:235', 'ansi256:16', 'ansi256:235'],
bg: ['ansi256:167', 'ansi256:214', 'ansi256:246', 'ansi256:109', 'ansi256:142']
},
3: {
fg: ['hex:EBDBB2', 'hex:282828', 'hex:282828', 'hex:FDF6E3', 'hex:282828'],
bg: ['hex:CC241D', 'hex:FABD2F', 'hex:A89984', 'hex:458588', 'hex:98971A']
}
},
'onedark': {
name: 'One Dark',
description: 'Atom-inspired dark theme',
1: {
fg: ['black', 'brightWhite', 'black', 'brightWhite', 'black'],
bg: ['bgBrightBlue', 'bgBrightBlack', 'bgBrightGreen', 'bgRed', 'bgBrightYellow']
},
2: {
fg: ['ansi256:235', 'ansi256:251', 'ansi256:235', 'ansi256:16', 'ansi256:235'],
bg: ['ansi256:75', 'ansi256:237', 'ansi256:114', 'ansi256:204', 'ansi256:180']
},
3: {
fg: ['hex:282C34', 'hex:ABB2BF', 'hex:282C34', 'hex:282C34', 'hex:282C34'],
bg: ['hex:61AFEF', 'hex:3E4452', 'hex:98C379', 'hex:E06C75', 'hex:E5C07B']
}
},
'tokyonight': {
name: 'Tokyo Night',
description: 'Clean, dark theme inspired by Tokyo nightlife',
1: {
fg: ['brightWhite', 'black', 'brightWhite', 'black', 'black'],
bg: ['bgBlue', 'bgBrightWhite', 'bgMagenta', 'bgBrightYellow', 'bgBrightCyan']
},
2: {
fg: ['ansi256:16', 'ansi256:234', 'ansi256:16', 'ansi256:234', 'ansi256:234'],
bg: ['ansi256:111', 'ansi256:248', 'ansi256:176', 'ansi256:221', 'ansi256:80']
},
3: {
fg: ['hex:1A1B26', 'hex:1A1B26', 'hex:1A1B26', 'hex:1A1B26', 'hex:1A1B26'],
bg: ['hex:7AA2F7', 'hex:D5D6DB', 'hex:BB9AF7', 'hex:E0AF68', 'hex:7DCFFF']
}
}
};
export function getPowerlineThemes(): string[] {
return Object.keys(POWERLINE_THEMES);
}
export function getPowerlineTheme(name: string): PowerlineTheme | undefined {
return POWERLINE_THEMES[name];
}
export function getDefaultPowerlineTheme(): string {
return 'nord-aurora';
}
+100 -147
View File
@@ -1,171 +1,124 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { promisify } from 'util';
import * as path from 'path';
// 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);
import {
CURRENT_VERSION,
SettingsSchema,
SettingsSchema_v1,
type Settings
} from '../types/Settings';
export type StatusItemType = 'model' | 'git-branch' | 'git-changes' | 'separator' | 'flex-separator' |
'tokens-input' | 'tokens-output' | 'tokens-cached' | 'tokens-total' | 'context-length' | 'context-percentage' | 'context-percentage-usable' | 'terminal-width' | 'session-clock' | 'version' | 'custom-text' | 'custom-command';
import {
migrateConfig,
needsMigration
} from './migrations';
export interface StatusItem {
id: string;
type: StatusItemType;
color?: string;
backgroundColor?: string; // Background color for the item
bold?: boolean; // Bold text styling
character?: string; // For separator and flex-separator types
rawValue?: boolean; // Show value without label prefix
customText?: string; // For custom-text type
commandPath?: string; // For custom-command type - the command to execute
maxWidth?: number; // For custom-command type - max width of output
preserveColors?: boolean; // For custom-command type - preserve ANSI colors from command output
timeout?: number; // For custom-command type - timeout in milliseconds (default: 1000)
}
export type FlexMode = 'full' | 'full-minus-40' | 'full-until-compact';
export interface Settings {
items?: StatusItem[]; // Legacy single line support
lines?: StatusItem[][]; // Multiple lines (up to 3)
flexMode?: FlexMode; // How to handle terminal width for flex separators
compactThreshold?: number; // Context percentage (50-99) for 'full-until-compact' mode
defaultSeparator?: string; // Default separator character to insert between items
defaultPadding?: string; // Default padding to add around all items
inheritSeparatorColors?: boolean; // Whether default separators inherit colors from preceding widget
overrideBackgroundColor?: string; // Override background color for all items (e.g., 'none', 'bgRed', etc.)
overrideForegroundColor?: string; // Override foreground color for all items (e.g., 'red', 'cyan', etc.)
globalBold?: boolean; // Apply bold formatting to all items
}
// Use fs.promises directly (always available in modern Node.js)
const readFile = fs.promises.readFile;
const writeFile = fs.promises.writeFile;
const mkdir = fs.promises.mkdir;
const CONFIG_DIR = path.join(os.homedir(), '.config', 'ccstatusline');
const SETTINGS_PATH = path.join(CONFIG_DIR, 'settings.json');
const SETTINGS_BACKUP_PATH = path.join(CONFIG_DIR, 'settings.bak');
export const DEFAULT_SETTINGS: Settings = {
lines: [
[
{
"id": "1",
"type": "model",
"color": "cyan"
},
{
"id": "2",
"type": "separator"
},
{
"id": "3",
"type": "context-length",
"color": "gray"
},
{
"id": "4",
"type": "separator"
},
{
"id": "5",
"type": "git-branch",
"color": "magenta"
},
{
"id": "6",
"type": "separator"
},
{
"id": "7",
"type": "git-changes",
"color": "yellow"
}
]
],
flexMode: 'full-minus-40',
compactThreshold: 60,
};
export async function loadSettings(): Promise<Settings> {
async function backupBadSettings(): Promise<void> {
try {
// Use Node.js-compatible file reading
if (!fs.existsSync(SETTINGS_PATH)) {
return DEFAULT_SETTINGS;
if (fs.existsSync(SETTINGS_PATH)) {
const content = await readFile(SETTINGS_PATH, 'utf-8');
await writeFile(SETTINGS_BACKUP_PATH, content, 'utf-8');
console.error(`Bad settings backed up to ${SETTINGS_BACKUP_PATH}`);
}
const content = await readFile(SETTINGS_PATH, 'utf-8');
let loaded: any;
try {
loaded = JSON.parse(content);
} catch (parseError) {
// If we can't parse the settings, return defaults
console.error('Failed to parse settings.json, using defaults');
return DEFAULT_SETTINGS;
}
// Migrate from old format with elements/layout
if (loaded.elements || loaded.layout) {
return migrateOldSettings(loaded);
}
// Migrate from single items array to lines array
if (loaded.items && !loaded.lines) {
loaded.lines = [loaded.items];
delete loaded.items;
}
// Ensure lines is an array and limit to 3 lines
if (loaded.lines) {
if (!Array.isArray(loaded.lines)) {
loaded.lines = [[]];
}
loaded.lines = loaded.lines.slice(0, 3);
}
return { ...DEFAULT_SETTINGS, ...loaded };
} catch (error) {
// Any other error, return defaults
console.error('Error loading settings:', error);
return DEFAULT_SETTINGS;
console.error('Failed to backup bad settings:', error);
}
}
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 {
lines: [items], // Put migrated items in first line
flexMode: DEFAULT_SETTINGS.flexMode,
compactThreshold: DEFAULT_SETTINGS.compactThreshold,
async function writeDefaultSettings(): Promise<Settings> {
const defaults = SettingsSchema.parse({});
const settingsWithVersion = {
...defaults,
version: CURRENT_VERSION
};
try {
await mkdir(CONFIG_DIR, { recursive: true });
await writeFile(SETTINGS_PATH, JSON.stringify(settingsWithVersion, null, 2), 'utf-8');
console.error(`Default settings written to ${SETTINGS_PATH}`);
} catch (error) {
console.error('Failed to write default settings:', error);
}
return defaults;
}
export async function loadSettings(): Promise<Settings> {
try {
// Check if settings file exists
if (!fs.existsSync(SETTINGS_PATH))
return await writeDefaultSettings();
const content = await readFile(SETTINGS_PATH, 'utf-8');
let rawData: unknown;
try {
rawData = JSON.parse(content);
} catch {
// If we can't parse the JSON, backup and write defaults
console.error('Failed to parse settings.json, backing up and using defaults');
await backupBadSettings();
return await writeDefaultSettings();
}
// Check if this is a v1 config (no version field)
const hasVersion = typeof rawData === 'object' && rawData !== null && 'version' in rawData;
if (!hasVersion) {
// Parse as v1 to validate before migration
const v1Result = SettingsSchema_v1.safeParse(rawData);
if (!v1Result.success) {
console.error('Invalid v1 settings format:', v1Result.error);
await backupBadSettings();
return await writeDefaultSettings();
}
// Migrate v1 to current version and save the migrated settings back to disk
rawData = migrateConfig(rawData, CURRENT_VERSION);
await writeFile(SETTINGS_PATH, JSON.stringify(rawData, null, 2), 'utf-8');
} else if (needsMigration(rawData, CURRENT_VERSION)) {
// Handle migrations for versioned configs (v2+) and save the migrated settings back to disk
rawData = migrateConfig(rawData, CURRENT_VERSION);
await writeFile(SETTINGS_PATH, JSON.stringify(rawData, null, 2), 'utf-8');
}
// At this point, data should be in current format with version field
// Parse with main schema which will apply all defaults
const result = SettingsSchema.safeParse(rawData);
if (!result.success) {
console.error('Failed to parse settings:', result.error);
await backupBadSettings();
return await writeDefaultSettings();
}
return result.data;
} catch (error) {
// Any other error, backup and write defaults
console.error('Error loading settings:', error);
await backupBadSettings();
return await writeDefaultSettings();
}
}
export async function saveSettings(settings: Settings): Promise<void> {
// Ensure config directory exists
await mkdir(CONFIG_DIR, { recursive: true });
// Always include version when saving
const settingsWithVersion = {
...settings,
version: CURRENT_VERSION
};
// Write settings using Node.js-compatible API
await writeFile(SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf-8');
await writeFile(SETTINGS_PATH, JSON.stringify(settingsWithVersion, null, 2), 'utf-8');
}
+11
View File
@@ -0,0 +1,11 @@
/**
* Generate a GUID (UUID v4)
* Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
*/
export function generateGuid(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
+141
View File
@@ -0,0 +1,141 @@
import type { WidgetItem } from '../types/Widget';
import { generateGuid } from './guid';
// Type for migration functions
interface Migration {
fromVersion: number;
toVersion: number;
description: string;
migrate: (data: Record<string, unknown>) => Record<string, unknown>;
}
// Type guards for checking data structure
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
// Define all migrations here
export const migrations: Migration[] = [
{
fromVersion: 1,
toVersion: 2,
description: 'Migrate from v1 to v2',
migrate: (data) => {
// Build a new v2 config from v1 data, only copying known fields
const migrated: Record<string, unknown> = {};
// Process lines: strip separators if needed and assign GUIDs
if (data.lines && Array.isArray(data.lines)) {
const processedLines: WidgetItem[][] = [];
for (const line of data.lines) {
if (Array.isArray(line)) {
// Filter out separators if defaultSeparator is enabled
let processedLine = line;
if (data.defaultSeparator) {
processedLine = line.filter((item: unknown) => {
if (isRecord(item)) {
return item.type !== 'separator';
}
return true;
});
}
// Assign GUIDs to all items and build typed array
const typedLine: WidgetItem[] = [];
for (const item of processedLine) {
if (isRecord(item) && typeof item.type === 'string') {
typedLine.push({
...item,
id: generateGuid(),
type: item.type
} as WidgetItem);
}
}
processedLines.push(typedLine);
}
}
migrated.lines = processedLines;
}
// Copy all v1 fields that exist
if (typeof data.flexMode === 'string')
migrated.flexMode = data.flexMode;
if (typeof data.compactThreshold === 'number')
migrated.compactThreshold = data.compactThreshold;
if (typeof data.colorLevel === 'number')
migrated.colorLevel = data.colorLevel;
if (typeof data.defaultSeparator === 'string')
migrated.defaultSeparator = data.defaultSeparator;
if (typeof data.defaultPadding === 'string')
migrated.defaultPadding = data.defaultPadding;
if (typeof data.inheritSeparatorColors === 'boolean')
migrated.inheritSeparatorColors = data.inheritSeparatorColors;
if (typeof data.overrideBackgroundColor === 'string')
migrated.overrideBackgroundColor = data.overrideBackgroundColor;
if (typeof data.overrideForegroundColor === 'string')
migrated.overrideForegroundColor = data.overrideForegroundColor;
if (typeof data.globalBold === 'boolean')
migrated.globalBold = data.globalBold;
// Add version field for v2
migrated.version = 2;
// Add update message for v2 migration
migrated.updatemessage = {
message: 'ccstatusline updated to v2.0.0, launch tui to use new settings',
remaining: 12
};
return migrated;
}
}
];
/**
* Detect the version of the config data
*/
export function detectVersion(data: unknown): number {
if (!isRecord(data))
return 1;
// If it has a version field, use it
if (typeof data.version === 'number')
return data.version;
// No version field means it's the old v1 format
return 1;
}
/**
* Migrate config data from its current version to the target version
*/
export function migrateConfig(data: unknown, targetVersion: number): unknown {
if (!isRecord(data))
return data;
let currentVersion = detectVersion(data);
let migrated: Record<string, unknown> = { ...data };
// Apply migrations sequentially
while (currentVersion < targetVersion) {
const migration = migrations.find(m => m.fromVersion === currentVersion);
if (!migration)
break;
migrated = migration.migrate(migrated);
currentVersion = migration.toVersion;
}
return migrated;
}
/**
* Check if a migration is needed
*/
export function needsMigration(data: unknown, targetVersion: number): boolean {
return detectVersion(data) < targetVersion;
}
+336
View File
@@ -0,0 +1,336 @@
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import type { PowerlineFontStatus } from '../types/PowerlineFontStatus';
// Re-export for backward compatibility
export type { PowerlineFontStatus };
// Track if fonts were installed during this session (for DEBUG_FONT_INSTALL)
let fontsInstalledThisSession = false;
/**
* Check if Powerline fonts are installed by testing if Powerline symbols render correctly
*/
export function checkPowerlineFonts(): PowerlineFontStatus {
// Debug mode: pretend fonts aren't installed (unless we installed them this session)
if (process.env.DEBUG_FONT_INSTALL === '1' && !fontsInstalledThisSession) {
return {
installed: false,
checkedSymbol: '\uE0B0'
};
}
try {
// Test if we can display the common Powerline separator symbols
// These are the key characters that require Powerline fonts
const testSymbols = {
rightArrow: '\uE0B0', //
rightThinArrow: '\uE0B1', //
leftArrow: '\uE0B2', //
leftThinArrow: '\uE0B3' //
};
// Try to detect if fonts are available
// This is a heuristic check - we assume if the system has common Powerline font files, they're available
const platform = os.platform();
let fontPaths: string[] = [];
if (platform === 'darwin') {
fontPaths = [
path.join(os.homedir(), 'Library', 'Fonts'),
'/Library/Fonts',
'/System/Library/Fonts'
];
} else if (platform === 'linux') {
fontPaths = [
path.join(os.homedir(), '.local', 'share', 'fonts'),
path.join(os.homedir(), '.fonts'),
'/usr/share/fonts',
'/usr/local/share/fonts'
];
} else if (platform === 'win32') {
fontPaths = [
path.join(os.homedir(), 'AppData', 'Local', 'Microsoft', 'Windows', 'Fonts'),
'C:\\Windows\\Fonts'
];
}
// Common Powerline font patterns to look for
const powerlineFontPatterns = [
/powerline/i,
/nerd font/i,
/for powerline/i,
/meslo.*lg/i, // Meslo LG fonts often include Powerline
/source.*code.*pro.*powerline/i,
/dejavu.*powerline/i,
/ubuntu.*mono.*powerline/i,
/cascadia.*code.*pl/i, // Cascadia Code PL
/fira.*code.*nerd/i
];
// Check if any Powerline fonts exist
for (const fontPath of fontPaths) {
if (fs.existsSync(fontPath)) {
try {
const files = fs.readdirSync(fontPath);
for (const file of files) {
for (const pattern of powerlineFontPatterns) {
if (pattern.test(file)) {
return {
installed: true,
checkedSymbol: testSymbols.rightArrow
};
}
}
}
} catch {
// Ignore read errors for specific directories
}
}
}
return {
installed: false,
checkedSymbol: testSymbols.rightArrow
};
} catch {
return { installed: false };
}
}
/**
* Check if Powerline fonts are installed (async version with fc-list check)
*/
export async function checkPowerlineFontsAsync(): Promise<PowerlineFontStatus> {
// Ensure this is always async
await Promise.resolve();
// Debug mode: pretend fonts aren't installed (unless we installed them this session)
if (process.env.DEBUG_FONT_INSTALL === '1' && !fontsInstalledThisSession) {
return {
installed: false,
checkedSymbol: '\uE0B0'
};
}
try {
// First do the quick synchronous check
const quickCheck = checkPowerlineFonts();
if (quickCheck.installed) {
return quickCheck;
}
// Additional check: See if fontconfig knows about Powerline fonts (Linux/macOS)
const platform = os.platform();
if (platform === 'linux' || platform === 'darwin') {
try {
const { exec } = await import('child_process');
const { promisify } = await import('util');
const execAsync = promisify(exec);
const { stdout } = await execAsync('fc-list 2>/dev/null | grep -i powerline', { encoding: 'utf8' });
if (stdout.trim()) {
return {
installed: true,
checkedSymbol: '\uE0B0'
};
}
} catch {
// fc-list not available or no Powerline fonts found
}
}
return quickCheck;
} catch {
return { installed: false };
}
}
/**
* Install Powerline fonts on the system
*/
export async function installPowerlineFonts(): Promise<{ success: boolean; message: string }> {
// Ensure this is always async
await Promise.resolve();
try {
const platform = os.platform();
let fontDir: string;
// Determine font directory based on platform
if (platform === 'darwin') {
fontDir = path.join(os.homedir(), 'Library', 'Fonts');
} else if (platform === 'linux') {
fontDir = path.join(os.homedir(), '.local', 'share', 'fonts');
} else if (platform === 'win32') {
fontDir = path.join(
os.homedir(),
'AppData',
'Local',
'Microsoft',
'Windows',
'Fonts'
);
} else {
return {
success: false,
message: 'Unsupported platform for font installation'
};
}
// Create font directory if it doesn't exist
if (!fs.existsSync(fontDir)) {
fs.mkdirSync(fontDir, { recursive: true });
}
// Create temporary directory for font download
const tempDir = path.join(os.tmpdir(), `ccstatusline-powerline-fonts-${Date.now()}`);
try {
// Clean up if temp directory exists
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
// Clone Powerline fonts repository
execSync(
`git clone --depth=1 https://github.com/powerline/fonts.git "${tempDir}"`,
{
stdio: 'pipe',
encoding: 'utf8'
}
);
// Run the install script based on platform
if (platform === 'darwin' || platform === 'linux') {
const installScript = path.join(tempDir, 'install.sh');
if (fs.existsSync(installScript)) {
// Make script executable
fs.chmodSync(installScript, 0o755);
// Run install script
execSync(`cd "${tempDir}" && ./install.sh`, {
stdio: 'pipe',
encoding: 'utf8',
shell: '/bin/bash'
});
// On Linux, update font cache
if (platform === 'linux') {
try {
execSync('fc-cache -f -v', {
stdio: 'pipe',
encoding: 'utf8'
});
} catch {
// fc-cache might not be available
}
}
// Mark as installed for DEBUG_FONT_INSTALL mode
if (process.env.DEBUG_FONT_INSTALL === '1') {
fontsInstalledThisSession = true;
}
return {
success: true,
message: 'Powerline fonts installed successfully! Please restart your terminal and select a Powerline font (e.g., "Source Code Pro for Powerline", "Meslo LG S for Powerline", etc.)'
};
} else {
throw new Error('Install script not found in Powerline fonts repository');
}
} else {
// For Windows, we need to copy font files manually
const fontFiles: string[] = [];
// Find all font files in the repository
function findFontFiles(dir: string): void {
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory() && !file.startsWith('.')) {
findFontFiles(filePath);
} else if (file.endsWith('.ttf') || file.endsWith('.otf')) {
// Only include fonts that have "Powerline" in their path
if (filePath.toLowerCase().includes('powerline')) {
fontFiles.push(filePath);
}
}
}
}
findFontFiles(tempDir);
// Copy font files to Windows font directory
let installedCount = 0;
for (const fontFile of fontFiles) {
const fileName = path.basename(fontFile);
const destPath = path.join(fontDir, fileName);
try {
fs.copyFileSync(fontFile, destPath);
installedCount++;
} catch {
// Ignore individual file errors
}
}
if (installedCount > 0) {
return {
success: true,
message: `Installed ${installedCount} Powerline fonts. Please restart your terminal and select a Powerline font from your terminal settings.`
};
} else {
throw new Error('No fonts were installed');
}
}
return {
success: false,
message: 'Platform-specific installation not implemented'
};
} finally {
// Clean up temporary directory
if (fs.existsSync(tempDir)) {
try {
fs.rmSync(tempDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
}
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
// Provide helpful error messages
if (errorMessage.includes('git')) {
return {
success: false,
message: 'Git is required to install Powerline fonts. Please install Git and try again.'
};
}
return {
success: false,
message: `Failed to install Powerline fonts: ${errorMessage}. You can manually install fonts from: https://github.com/powerline/fonts`
};
}
}
/**
* Get a user-friendly message about Powerline font status
*/
export function getPowerlineStatusMessage(status: PowerlineFontStatus): string {
if (status.installed) {
return `✓ Powerline fonts detected`;
} else {
return `✗ Powerline fonts not detected - some symbols may not display correctly`;
}
}
+613 -487
View File
File diff suppressed because it is too large. Load diff
+107
View File
@@ -0,0 +1,107 @@
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
// Get package version
export function getPackageVersion(): string {
try {
const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) as { version?: string };
return packageJson.version ?? '';
} catch {
return '';
}
}
// Get terminal width
export function getTerminalWidth(): number | null {
try {
// First try to get the tty of the parent process
const tty = execSync('ps -o tty= -p $(ps -o ppid= -p $$)', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
shell: '/bin/sh'
}).trim();
// Check if we got a valid tty (not ?? which means no tty)
if (tty && tty !== '??' && tty !== '?') {
// Now get the terminal size
const width = execSync(
`stty size < /dev/${tty} | awk '{print $2}'`,
{
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
shell: '/bin/sh'
}
).trim();
const parsed = parseInt(width, 10);
if (!isNaN(parsed) && parsed > 0) {
return parsed;
}
}
} catch {
// Command failed, width detection not available
}
// Fallback: try tput cols which might work in some environments
try {
const width = execSync('tput cols 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
const parsed = parseInt(width, 10);
if (!isNaN(parsed) && parsed > 0) {
return parsed;
}
} catch {
// tput also failed
}
return null;
}
// Check if terminal width detection is available
export function canDetectTerminalWidth(): boolean {
try {
// First try to get the tty of the parent process
const tty = execSync('ps -o tty= -p $(ps -o ppid= -p $$)', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
shell: '/bin/sh'
}).trim();
// Check if we got a valid tty
if (tty && tty !== '??' && tty !== '?') {
const width = execSync(
`stty size < /dev/${tty} | awk '{print $2}'`,
{
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
shell: '/bin/sh'
}
).trim();
const parsed = parseInt(width, 10);
if (!isNaN(parsed) && parsed > 0) {
return true;
}
}
} catch {
// Try fallback
}
// Fallback: try tput cols
try {
const width = execSync('tput cols 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
const parsed = parseInt(width, 10);
return !isNaN(parsed) && parsed > 0;
} catch {
return false;
}
}
+54
View File
@@ -0,0 +1,54 @@
import type { Settings } from '../types/Settings';
import type {
Widget,
WidgetItemType
} from '../types/Widget';
import * as widgets from '../widgets';
// Create widget registry
const widgetRegistry = new Map<WidgetItemType, Widget>([
['model', new widgets.ModelWidget()],
['output-style', new widgets.OutputStyleWidget()],
['git-branch', new widgets.GitBranchWidget()],
['git-changes', new widgets.GitChangesWidget()],
['tokens-input', new widgets.TokensInputWidget()],
['tokens-output', new widgets.TokensOutputWidget()],
['tokens-cached', new widgets.TokensCachedWidget()],
['tokens-total', new widgets.TokensTotalWidget()],
['context-length', new widgets.ContextLengthWidget()],
['context-percentage', new widgets.ContextPercentageWidget()],
['context-percentage-usable', new widgets.ContextPercentageUsableWidget()],
['session-clock', new widgets.SessionClockWidget()],
['terminal-width', new widgets.TerminalWidthWidget()],
['version', new widgets.VersionWidget()],
['custom-text', new widgets.CustomTextWidget()],
['custom-command', new widgets.CustomCommandWidget()]
]);
export function getWidget(type: WidgetItemType): Widget {
const widget = widgetRegistry.get(type);
if (!widget) {
throw new Error(`Unknown widget type: ${type}`);
}
return widget;
}
export function getAllWidgetTypes(settings: Settings): WidgetItemType[] {
const allTypes = Array.from(widgetRegistry.keys());
// Add separator types based on settings
if (!settings.powerline.enabled) {
if (!settings.defaultSeparator) {
allTypes.push('separator');
}
allTypes.push('flex-separator');
}
return allTypes;
}
export function isWidgetType(type: string): boolean {
return widgetRegistry.has(type)
|| type === 'separator'
|| type === 'flex-separator';
}
+28
View File
@@ -0,0 +1,28 @@
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
Widget,
WidgetEditorDisplay,
WidgetItem
} from '../types/Widget';
import { formatTokens } from '../utils/renderer';
export class ContextLengthWidget implements Widget {
getDefaultColor(): string { return 'brightBlack'; }
getDisplayName(): string { return 'Context Length'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return { displayText: this.getDisplayName() };
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
if (context.isPreview) {
return item.rawValue ? '18.6k' : 'Ctx: 18.6k';
} else if (context.tokenMetrics) {
return item.rawValue ? formatTokens(context.tokenMetrics.contextLength) : `Ctx: ${formatTokens(context.tokenMetrics.contextLength)}`;
}
return null;
}
supportsRawValue(): boolean { return true; }
supportsColors(item: WidgetItem): boolean { return true; }
}
+28
View File
@@ -0,0 +1,28 @@
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
Widget,
WidgetEditorDisplay,
WidgetItem
} from '../types/Widget';
export class ContextPercentageWidget implements Widget {
getDefaultColor(): string { return 'blue'; }
getDisplayName(): string { return 'Context %'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return { displayText: this.getDisplayName() };
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
if (context.isPreview) {
return item.rawValue ? '9.3%' : 'Ctx: 9.3%';
} else if (context.tokenMetrics) {
const percentage = Math.min(100, (context.tokenMetrics.contextLength / 200000) * 100);
return item.rawValue ? `${percentage.toFixed(1)}%` : `Ctx: ${percentage.toFixed(1)}%`;
}
return null;
}
supportsRawValue(): boolean { return true; }
supportsColors(item: WidgetItem): boolean { return true; }
}
+28
View File
@@ -0,0 +1,28 @@
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
Widget,
WidgetEditorDisplay,
WidgetItem
} from '../types/Widget';
export class ContextPercentageUsableWidget implements Widget {
getDefaultColor(): string { return 'green'; }
getDisplayName(): string { return 'Context % (usable)'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return { displayText: this.getDisplayName() };
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
if (context.isPreview) {
return item.rawValue ? '11.6%' : 'Ctx(u): 11.6%';
} else if (context.tokenMetrics) {
const percentage = Math.min(100, (context.tokenMetrics.contextLength / 160000) * 100);
return item.rawValue ? `${percentage.toFixed(1)}%` : `Ctx(u): ${percentage.toFixed(1)}%`;
}
return null;
}
supportsRawValue(): boolean { return true; }
supportsColors(item: WidgetItem): boolean { return true; }
}
+219
View File
@@ -0,0 +1,219 @@
import { execSync } from 'child_process';
import {
Box,
Text,
useInput
} from 'ink';
import React, { useState } from 'react';
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
CustomKeybind,
Widget,
WidgetEditorDisplay,
WidgetEditorProps,
WidgetItem
} from '../types/Widget';
export class CustomCommandWidget implements Widget {
private currentAction: string = 'edit-command';
getDefaultColor(): string { return 'white'; }
getDisplayName(): string { return 'Custom Command'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
const cmd = item.commandPath ?? 'No command';
const truncatedCmd = cmd.length > 20 ? `${cmd.substring(0, 17)}...` : cmd;
const displayText = `${this.getDisplayName()} (${truncatedCmd})`;
// Build modifiers string
const modifiers: string[] = [];
if (item.maxWidth) {
modifiers.push(`max:${item.maxWidth}`);
}
if (item.timeout && item.timeout !== 1000) {
modifiers.push(`timeout:${item.timeout}ms`);
}
if (item.preserveColors) {
modifiers.push('preserve');
}
return {
displayText,
modifierText: modifiers.length > 0 ? `(${modifiers.join(', ')})` : undefined
};
}
setEditorAction(action: string): void {
this.currentAction = action;
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
if (context.isPreview) {
return item.commandPath ? `[cmd: ${item.commandPath.substring(0, 20)}${item.commandPath.length > 20 ? '...' : ''}]` : '[No command]';
} else if (item.commandPath && context.data) {
try {
const timeout = item.timeout ?? 1000;
const jsonInput = JSON.stringify(context.data);
let output = execSync(item.commandPath, {
encoding: 'utf8',
input: jsonInput,
timeout: timeout,
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
// Strip ANSI codes if preserveColors is false
if (!item.preserveColors) {
// Strip all ANSI escape sequences
output = output.replace(/\x1b\[[0-9;]*m/g, '');
}
if (item.maxWidth && output.length > item.maxWidth) {
output = output.substring(0, item.maxWidth - 3) + '...';
}
return output || null;
} catch {
return '[Error]';
}
}
return null;
}
getCustomKeybinds(): CustomKeybind[] {
return [
{ key: 'e', label: '(e)dit cmd', action: 'edit-command' },
{ key: 'w', label: '(w)idth', action: 'edit-width' },
{ key: 't', label: '(t)imeout', action: 'edit-timeout' },
{ key: 'p', label: '(p)reserve colors', action: 'toggle-preserve' }
];
}
renderEditor(props: WidgetEditorProps): React.ReactElement {
return <CustomCommandEditor {...props} action={this.currentAction} />;
}
supportsRawValue(): boolean { return false; }
supportsColors(item: WidgetItem): boolean {
// Only supports colors if preserveColors is false
return !item.preserveColors;
}
}
interface EditorMode { type: 'command' | 'width' | 'timeout' | null }
const CustomCommandEditor: React.FC<WidgetEditorProps & { action: string }> = ({ widget, onComplete, onCancel, action }) => {
const getMode = (): EditorMode['type'] => {
switch (action) {
case 'edit-command': return 'command';
case 'edit-width': return 'width';
case 'edit-timeout': return 'timeout';
default: return 'command';
}
};
const mode = getMode();
const [commandInput, setCommandInput] = useState(widget.commandPath ?? '');
const [commandCursorPos, setCommandCursorPos] = useState(commandInput.length);
const [widthInput, setWidthInput] = useState(widget.maxWidth?.toString() ?? '');
const [timeoutInput, setTimeoutInput] = useState(widget.timeout?.toString() ?? '1000');
useInput((input, key) => {
if (mode === 'command') {
if (key.return) {
onComplete({ ...widget, commandPath: commandInput });
} else if (key.escape) {
onCancel();
} else if (key.leftArrow) {
setCommandCursorPos(Math.max(0, commandCursorPos - 1));
} else if (key.rightArrow) {
setCommandCursorPos(Math.min(commandInput.length, commandCursorPos + 1));
} else if (key.backspace) {
if (commandCursorPos > 0) {
setCommandInput(commandInput.slice(0, commandCursorPos - 1) + commandInput.slice(commandCursorPos));
setCommandCursorPos(commandCursorPos - 1);
}
} else if (key.delete) {
if (commandCursorPos < commandInput.length) {
setCommandInput(commandInput.slice(0, commandCursorPos) + commandInput.slice(commandCursorPos + 1));
}
} else if (input) {
setCommandInput(commandInput.slice(0, commandCursorPos) + input + commandInput.slice(commandCursorPos));
setCommandCursorPos(commandCursorPos + input.length);
}
} else if (mode === 'width') {
if (key.return) {
const width = parseInt(widthInput, 10);
if (!isNaN(width) && width > 0) {
onComplete({ ...widget, maxWidth: width });
} else {
const { maxWidth, ...rest } = widget;
void maxWidth; // Intentionally unused
onComplete(rest);
}
} else if (key.escape) {
onCancel();
} else if (key.backspace) {
setWidthInput(widthInput.slice(0, -1));
} else if (input && /\d/.test(input)) {
setWidthInput(widthInput + input);
}
} else if (mode === 'timeout') {
if (key.return) {
const timeout = parseInt(timeoutInput, 10);
if (!isNaN(timeout) && timeout > 0) {
onComplete({ ...widget, timeout });
} else {
const { timeout, ...rest } = widget;
void timeout; // Intentionally unused
onComplete(rest);
}
} else if (key.escape) {
onCancel();
} else if (key.backspace) {
setTimeoutInput(timeoutInput.slice(0, -1));
} else if (input && /\d/.test(input)) {
setTimeoutInput(timeoutInput + input);
}
}
});
if (mode === 'command') {
return (
<Box flexDirection='column'>
<Text>
Enter command path:
{' '}
{commandInput.slice(0, commandCursorPos)}
<Text backgroundColor='gray' color='black'>{commandInput[commandCursorPos] ?? ' '}</Text>
{commandInput.slice(commandCursorPos + 1)}
</Text>
<Text dimColor>←→ move cursor, Enter save, ESC cancel</Text>
</Box>
);
} else if (mode === 'width') {
return (
<Box flexDirection='column'>
<Text>
Enter max width (blank for no limit):
{' '}
{widthInput}
</Text>
<Text dimColor>Press Enter to save, ESC to cancel</Text>
</Box>
);
} else if (mode === 'timeout') {
return (
<Box flexDirection='column'>
<Text>
Enter timeout in milliseconds (default 1000):
{' '}
{timeoutInput}
</Text>
<Text dimColor>Press Enter to save, ESC to cancel</Text>
</Box>
);
}
return <Text>Unknown editor mode</Text>;
};
+91
View File
@@ -0,0 +1,91 @@
import {
Box,
Text,
useInput
} from 'ink';
import React, { useState } from 'react';
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
CustomKeybind,
Widget,
WidgetEditorDisplay,
WidgetEditorProps,
WidgetItem
} from '../types/Widget';
export class CustomTextWidget implements Widget {
getDefaultColor(): string { return 'white'; }
getDisplayName(): string { return 'Custom Text'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
const text = item.customText ?? 'Empty';
return { displayText: `${this.getDisplayName()} (${text})` };
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
return item.customText ?? '';
}
getCustomKeybinds(): CustomKeybind[] {
return [{
key: 'e',
label: '(e)dit text',
action: 'edit-text'
}];
}
renderEditor(props: WidgetEditorProps): React.ReactElement {
return <CustomTextEditor {...props} />;
}
supportsRawValue(): boolean { return false; }
supportsColors(item: WidgetItem): boolean { return true; }
}
const CustomTextEditor: React.FC<WidgetEditorProps> = ({ widget, onComplete, onCancel }) => {
const [text, setText] = useState(widget.customText ?? '');
const [cursorPos, setCursorPos] = useState(text.length);
useInput((input, key) => {
if (key.return) {
onComplete({ ...widget, customText: text });
} else if (key.escape) {
onCancel();
} else if (key.leftArrow) {
setCursorPos(Math.max(0, cursorPos - 1));
} else if (key.rightArrow) {
setCursorPos(Math.min(text.length, cursorPos + 1));
} else if (key.ctrl && input === 'ArrowLeft') {
setCursorPos(0);
} else if (key.ctrl && input === 'ArrowRight') {
setCursorPos(text.length);
} else if (key.backspace) {
if (cursorPos > 0) {
setText(text.slice(0, cursorPos - 1) + text.slice(cursorPos));
setCursorPos(cursorPos - 1);
}
} else if (key.delete) {
if (cursorPos < text.length) {
setText(text.slice(0, cursorPos) + text.slice(cursorPos + 1));
}
} else if (input && input.length === 1) {
setText(text.slice(0, cursorPos) + input + text.slice(cursorPos));
setCursorPos(cursorPos + 1);
}
});
return (
<Box flexDirection='column'>
<Text>
Enter custom text:
{' '}
{text.slice(0, cursorPos)}
<Text backgroundColor='gray' color='black'>{text[cursorPos] ?? ' '}</Text>
{text.slice(cursorPos + 1)}
</Text>
<Text dimColor>←→ move cursor, Ctrl+←→ jump to start/end, Enter save, ESC cancel</Text>
</Box>
);
};
+44
View File
@@ -0,0 +1,44 @@
import { execSync } from 'child_process';
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
Widget,
WidgetEditorDisplay,
WidgetItem
} from '../types/Widget';
export class GitBranchWidget implements Widget {
getDefaultColor(): string { return 'magenta'; }
getDisplayName(): string { return 'Git Branch'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return { displayText: this.getDisplayName() };
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
if (context.isPreview) {
return item.rawValue ? 'main' : '⎇ main';
}
const branch = this.getGitBranch();
if (branch)
return item.rawValue ? branch : `⎇ ${branch}`;
return '⎇ no git';
}
private 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;
}
}
supportsRawValue(): boolean { return true; }
supportsColors(item: WidgetItem): boolean { return true; }
}
+67
View File
@@ -0,0 +1,67 @@
import { execSync } from 'child_process';
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
Widget,
WidgetEditorDisplay,
WidgetItem
} from '../types/Widget';
export class GitChangesWidget implements Widget {
getDefaultColor(): string { return 'yellow'; }
getDisplayName(): string { return 'Git Changes'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return { displayText: this.getDisplayName() };
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
if (context.isPreview) {
return '(+42,-10)';
}
const changes = this.getGitChanges();
if (changes)
return `(+${changes.insertions},-${changes.deletions})`;
else
return '(no git)';
}
private getGitChanges(): { insertions: number; deletions: number } | null {
try {
let totalInsertions = 0;
let totalDeletions = 0;
const unstagedStat = execSync('git diff --shortstat 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
const stagedStat = execSync('git diff --cached --shortstat 2>/dev/null', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore']
}).trim();
if (unstagedStat) {
const insertMatch = /(\d+) insertion/.exec(unstagedStat);
const deleteMatch = /(\d+) deletion/.exec(unstagedStat);
totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
}
if (stagedStat) {
const insertMatch = /(\d+) insertion/.exec(stagedStat);
const deleteMatch = /(\d+) deletion/.exec(stagedStat);
totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
}
return { insertions: totalInsertions, deletions: totalDeletions };
} catch {
return null;
}
}
supportsRawValue(): boolean { return false; }
supportsColors(item: WidgetItem): boolean { return true; }
}
+27
View File
@@ -0,0 +1,27 @@
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
Widget,
WidgetEditorDisplay,
WidgetItem
} from '../types/Widget';
export class ModelWidget implements Widget {
getDefaultColor(): string { return 'cyan'; }
getDisplayName(): string { return 'Model'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return { displayText: this.getDisplayName() };
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
if (context.isPreview) {
return item.rawValue ? 'Claude' : 'Model: Claude';
} else if (context.data?.model?.display_name) {
return item.rawValue ? context.data.model.display_name : `Model: ${context.data.model.display_name}`;
}
return null;
}
supportsRawValue(): boolean { return true; }
supportsColors(item: WidgetItem): boolean { return true; }
}
+27
View File
@@ -0,0 +1,27 @@
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
Widget,
WidgetEditorDisplay,
WidgetItem
} from '../types/Widget';
export class OutputStyleWidget implements Widget {
getDefaultColor(): string { return 'cyan'; }
getDisplayName(): string { return 'Output Style'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return { displayText: this.getDisplayName() };
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
if (context.isPreview) {
return item.rawValue ? 'default' : 'Style: default';
} else if (context.data?.output_style?.name) {
return item.rawValue ? context.data.output_style.name : `Style: ${context.data.output_style.name}`;
}
return null;
}
supportsRawValue(): boolean { return true; }
supportsColors(item: WidgetItem): boolean { return true; }
}
+27
View File
@@ -0,0 +1,27 @@
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
Widget,
WidgetEditorDisplay,
WidgetItem
} from '../types/Widget';
export class SessionClockWidget implements Widget {
getDefaultColor(): string { return 'yellow'; }
getDisplayName(): string { return 'Session Clock'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return { displayText: this.getDisplayName() };
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
if (context.isPreview) {
return item.rawValue ? '2hr 15m' : 'Session: 2hr 15m';
} else if (context.sessionDuration) {
return item.rawValue ? context.sessionDuration : `Session: ${context.sessionDuration}`;
}
return null;
}
supportsRawValue(): boolean { return true; }
supportsColors(item: WidgetItem): boolean { return true; }
}
+30
View File
@@ -0,0 +1,30 @@
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
Widget,
WidgetEditorDisplay,
WidgetItem
} from '../types/Widget';
import { getTerminalWidth } from '../utils/renderer';
export class TerminalWidthWidget implements Widget {
getDefaultColor(): string { return 'gray'; }
getDisplayName(): string { return 'Terminal Width'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return { displayText: this.getDisplayName() };
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
const width = context.terminalWidth ?? getTerminalWidth();
if (context.isPreview) {
const detectedWidth = width ?? '??';
return item.rawValue ? `${detectedWidth}` : `Term: ${detectedWidth}`;
} else if (width) {
return item.rawValue ? `${width}` : `Term: ${width}`;
}
return null;
}
supportsRawValue(): boolean { return true; }
supportsColors(item: WidgetItem): boolean { return true; }
}
+28
View File
@@ -0,0 +1,28 @@
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
Widget,
WidgetEditorDisplay,
WidgetItem
} from '../types/Widget';
import { formatTokens } from '../utils/renderer';
export class TokensCachedWidget implements Widget {
getDefaultColor(): string { return 'cyan'; }
getDisplayName(): string { return 'Tokens Cached'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return { displayText: this.getDisplayName() };
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
if (context.isPreview) {
return item.rawValue ? '12k' : 'Cached: 12k';
} else if (context.tokenMetrics) {
return item.rawValue ? formatTokens(context.tokenMetrics.cachedTokens) : `Cached: ${formatTokens(context.tokenMetrics.cachedTokens)}`;
}
return null;
}
supportsRawValue(): boolean { return true; }
supportsColors(item: WidgetItem): boolean { return true; }
}
+28
View File
@@ -0,0 +1,28 @@
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
Widget,
WidgetEditorDisplay,
WidgetItem
} from '../types/Widget';
import { formatTokens } from '../utils/renderer';
export class TokensInputWidget implements Widget {
getDefaultColor(): string { return 'blue'; }
getDisplayName(): string { return 'Tokens Input'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return { displayText: this.getDisplayName() };
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
if (context.isPreview) {
return item.rawValue ? '15.2k' : 'In: 15.2k';
} else if (context.tokenMetrics) {
return item.rawValue ? formatTokens(context.tokenMetrics.inputTokens) : `In: ${formatTokens(context.tokenMetrics.inputTokens)}`;
}
return null;
}
supportsRawValue(): boolean { return true; }
supportsColors(item: WidgetItem): boolean { return true; }
}
+28
View File
@@ -0,0 +1,28 @@
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
Widget,
WidgetEditorDisplay,
WidgetItem
} from '../types/Widget';
import { formatTokens } from '../utils/renderer';
export class TokensOutputWidget implements Widget {
getDefaultColor(): string { return 'white'; }
getDisplayName(): string { return 'Tokens Output'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return { displayText: this.getDisplayName() };
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
if (context.isPreview) {
return item.rawValue ? '3.4k' : 'Out: 3.4k';
} else if (context.tokenMetrics) {
return item.rawValue ? formatTokens(context.tokenMetrics.outputTokens) : `Out: ${formatTokens(context.tokenMetrics.outputTokens)}`;
}
return null;
}
supportsRawValue(): boolean { return true; }
supportsColors(item: WidgetItem): boolean { return true; }
}
+28
View File
@@ -0,0 +1,28 @@
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
Widget,
WidgetEditorDisplay,
WidgetItem
} from '../types/Widget';
import { formatTokens } from '../utils/renderer';
export class TokensTotalWidget implements Widget {
getDefaultColor(): string { return 'cyan'; }
getDisplayName(): string { return 'Tokens Total'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return { displayText: this.getDisplayName() };
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
if (context.isPreview) {
return item.rawValue ? '30.6k' : 'Total: 30.6k';
} else if (context.tokenMetrics) {
return item.rawValue ? formatTokens(context.tokenMetrics.totalTokens) : `Total: ${formatTokens(context.tokenMetrics.totalTokens)}`;
}
return null;
}
supportsRawValue(): boolean { return true; }
supportsColors(item: WidgetItem): boolean { return true; }
}
+27
View File
@@ -0,0 +1,27 @@
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
Widget,
WidgetEditorDisplay,
WidgetItem
} from '../types/Widget';
export class VersionWidget implements Widget {
getDefaultColor(): string { return 'gray'; }
getDisplayName(): string { return 'Version'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return { displayText: this.getDisplayName() };
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
if (context.isPreview) {
return item.rawValue ? '1.0.0' : 'v1.0.0';
} else if (context.data?.version) {
return item.rawValue ? context.data.version : `v${context.data.version}`;
}
return null;
}
supportsRawValue(): boolean { return true; }
supportsColors(item: WidgetItem): boolean { return true; }
}
+16
View File
@@ -0,0 +1,16 @@
export { ModelWidget } from './Model';
export { OutputStyleWidget } from './OutputStyle';
export { GitBranchWidget } from './GitBranch';
export { GitChangesWidget } from './GitChanges';
export { TokensInputWidget } from './TokensInput';
export { TokensOutputWidget } from './TokensOutput';
export { TokensCachedWidget } from './TokensCached';
export { TokensTotalWidget } from './TokensTotal';
export { ContextLengthWidget } from './ContextLength';
export { ContextPercentageWidget } from './ContextPercentage';
export { ContextPercentageUsableWidget } from './ContextPercentageUsable';
export { SessionClockWidget } from './SessionClock';
export { TerminalWidthWidget } from './TerminalWidth';
export { VersionWidget } from './Version';
export { CustomTextWidget } from './CustomText';
export { CustomCommandWidget } from './CustomCommand';