mirror of
https://github.com/tiennm99/ccstatusline.git
synced 2026-10-13 06:15:01 +00:00
Powerline capability, better color support, custom colors, multiple install options, Output Style widget, UI cleanup
This commit is contained in:
1 parent
bd7010fcd2
commit
365b64092a
62 files changed
+7484
-2411
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
+467
@@ -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 />);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@@ -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)';
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -0,0 +1 @@
|
||||
export { runTUI } from './App';
|
||||
@@ -0,0 +1,12 @@
|
||||
export interface ClaudeSettings {
|
||||
permissions?: {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
};
|
||||
statusLine?: {
|
||||
type: string;
|
||||
command: string;
|
||||
padding?: number;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { ChalkInstance } from 'chalk';
|
||||
|
||||
export interface ColorEntry {
|
||||
name: string;
|
||||
displayName: string;
|
||||
isBackground: boolean;
|
||||
ansi16: ChalkInstance;
|
||||
ansi256: ChalkInstance;
|
||||
truecolor: ChalkInstance;
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
@@ -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>;
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface PowerlineFontStatus {
|
||||
installed: boolean;
|
||||
checkedSymbol?: string;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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({});
|
||||
@@ -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>;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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');
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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';
|
||||
Reference in new issue
Block a user