diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml new file mode 100644 index 0000000..e4e24c6 --- /dev/null +++ b/.github/workflows/autofix.yml @@ -0,0 +1,46 @@ +name: autofix.ci # needed to securely identify the workflow + +on: + pull_request: + push: + branches: [ "main", "master" ] + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + +jobs: + autofix: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-autofix-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-autofix- + ${{ runner.os }}-cargo- + + # Fix lint errors and warnings + - name: Fix clippy issues + run: cargo clippy --fix --workspace --allow-dirty --allow-staged + + # Format code + - name: Format code + run: cargo fmt --all + + - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 \ No newline at end of file diff --git a/.gitignore b/.gitignore index aefc99a..5212b83 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ claude-powerline-features.md CLAUDE.md PRD.md -.claude/ \ No newline at end of file +.claude/ +tests/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 642d13c..e5313e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.4] - 2025-08-28 + +### Added +- **Interactive Main Menu**: Direct execution now shows TUI menu instead of hanging +- **Claude Code Patcher**: `--patch` command to disable context warnings and enable verbose mode +- **Three New Segments**: Extended statusline with additional information + - **Cost Segment**: Shows monetary cost with intelligent zero-cost handling + - **Session Segment**: Displays session duration and line changes + - **OutputStyle Segment**: Shows current output style name +- **Enhanced Theme System**: Comprehensive theme architecture with 9 built-in themes + - Modular theme organization with individual theme modules + - 4 new Powerline theme variants (dark, light, rose pine, tokyo night) + - Enhanced existing themes (cometix, default, minimal, gruvbox, nord) +- **Model Management System**: Intelligent model recognition and configuration + +### Fixed +- **Direct Execution Hanging**: No longer hangs when executed without stdin input +- **Help Component Styling**: Consistent key highlighting across all TUI help displays +- **Cross-platform Path Support**: Enhanced Windows %USERPROFILE% and Unix ~/ path handling + + ## [1.0.3] - 2025-08-17 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 11f60f8..cd78c0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -115,9 +124,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" [[package]] name = "bumpalo" @@ -142,16 +151,16 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.33" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ "shlex", ] [[package]] name = "ccometixline-packycc" -version = "1.0.3" +version = "1.0.4" dependencies = [ "ansi-to-tui", "ansi_term", @@ -160,6 +169,7 @@ dependencies = [ "crossterm", "dirs", "ratatui", + "regex", "semver", "serde", "serde_json", @@ -169,9 +179,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "chrono" @@ -190,9 +200,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.45" +version = "4.5.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" +checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" dependencies = [ "clap_builder", "clap_derive", @@ -200,9 +210,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.44" +version = "4.5.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" dependencies = [ "anstream", "anstyle", @@ -401,9 +411,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -554,9 +564,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -575,9 +585,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", "hashbrown", @@ -787,9 +797,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "potential_utf" @@ -859,6 +869,35 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + [[package]] name = "ring" version = "0.17.14" @@ -967,9 +1006,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", @@ -1234,13 +1273,14 @@ dependencies = [ [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -1642,9 +1682,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 79d9aed..46122ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ccometixline-packycc" -version = "1.0.3" +version = "1.0.4" edition = "2021" description = "CCometixLine (ccline) - High-performance Claude Code StatusLine tool written in Rust" authors = ["ding113"] @@ -11,28 +11,27 @@ keywords = ["claude", "statusline", "powerline", "rust", "claude-code"] categories = ["command-line-utilities", "development-tools"] [dependencies] -# 核心依赖 serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" clap = { version = "4.0", features = ["derive"] } toml = "0.8" -# TUI 依赖 ratatui = { version = "0.29", optional = true } crossterm = { version = "0.28", optional = true } -# 颜色处理 ansi_term = { version = "0.12", optional = true } ansi-to-tui = { version = "7.0", optional = true } -# 可选:更新功能 ureq = { version = "2.10", features = ["json"], optional = true } semver = { version = "1.0", optional = true } chrono = { version = "0.4", features = ["serde"], optional = true } dirs = { version = "5.0", optional = true } +regex = "1.0" + + [features] -default = ["tui", "self-update", "quota"] +default = ["tui", "self-update", "quota", "dirs"] tui = ["ratatui", "crossterm", "ansi_term", "ansi-to-tui", "chrono"] self-update = ["ureq", "semver", "chrono", "dirs"] quota = ["ureq", "dirs"] diff --git a/README.md b/README.md index f77144d..86489a1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [English](README.md) | [中文](README.zh.md) -A high-performance Claude Code statusline tool written in Rust with Git integration, real-time usage tracking, and intelligent API quota monitoring. +A high-performance Claude Code statusline tool written in Rust with Git integration, usage tracking, interactive TUI configuration, and Claude Code enhancement utilities with intelligent API quota monitoring. > This is a specially adapted version of CCometixLine for PackyCode service. The original CCometixLine was created by [Haleclipse](https://github.com/Haleclipse/CCometixLine) under MIT License. This project is also released under MIT License. > @@ -20,14 +20,26 @@ The statusline shows: Model | Directory | Git Branch Status | Context Window | A ## Features -- **High performance** with Rust native speed +### Core Functionality - **Git integration** with branch, status, and tracking info - **Model display** with simplified Claude model names - **Usage tracking** based on transcript analysis - **Smart API quota monitoring** with intelligent dual-endpoint detection - **Directory display** showing current workspace - **Minimal design** using Nerd Font icons -- **Simple configuration** via command line options + +### Interactive TUI Features +- **Interactive main menu** when executed without input +- **TUI configuration interface** with real-time preview +- **Theme system** with multiple built-in presets +- **Segment customization** with granular control +- **Configuration management** (init, check, edit) + +### Claude Code Enhancement +- **Context warning disabler** - Remove annoying "Context low" messages +- **Verbose mode enabler** - Enhanced output detail +- **Robust patcher** - Survives Claude Code version updates +- **Automatic backups** - Safe modification with easy recovery ## Installation @@ -55,6 +67,7 @@ After installation: - ✅ Global command `ccline` is available everywhere - ✅ Automatically configured for Claude Code at `~/.claude/ccline/ccline` - ✅ Ready to use immediately! +- 🎨 Run `ccline --config` to open configuration panel for theme selection ### Update @@ -144,6 +157,83 @@ Add to your Claude Code `settings.json`: } ``` +**Fallback (npm installation):** +```json +{ + "statusLine": { + "type": "command", + "command": "ccline", + "padding": 0 + } +} +``` +*Use this if npm global installation is available in PATH* + +### Update + +```bash +npm update -g @cometix/ccline +``` + +
+Manual Installation (Click to expand) + +Alternatively, download from [Releases](https://github.com/Haleclipse/CCometixLine/releases): + +#### Linux + +#### Option 1: Dynamic Binary (Recommended) +```bash +mkdir -p ~/.claude/ccline +wget https://github.com/Haleclipse/CCometixLine/releases/latest/download/ccline-linux-x64.tar.gz +tar -xzf ccline-linux-x64.tar.gz +cp ccline ~/.claude/ccline/ +chmod +x ~/.claude/ccline/ccline +``` +*Requires: Ubuntu 22.04+, CentOS 9+, Debian 11+, RHEL 9+ (glibc 2.35+)* + +#### Option 2: Static Binary (Universal Compatibility) +```bash +mkdir -p ~/.claude/ccline +wget https://github.com/Haleclipse/CCometixLine/releases/latest/download/ccline-linux-x64-static.tar.gz +tar -xzf ccline-linux-x64-static.tar.gz +cp ccline ~/.claude/ccline/ +chmod +x ~/.claude/ccline/ccline +``` +*Works on any Linux distribution (static, no dependencies)* + +#### macOS (Intel) + +```bash +mkdir -p ~/.claude/ccline +wget https://github.com/Haleclipse/CCometixLine/releases/latest/download/ccline-macos-x64.tar.gz +tar -xzf ccline-macos-x64.tar.gz +cp ccline ~/.claude/ccline/ +chmod +x ~/.claude/ccline/ccline +``` + +#### macOS (Apple Silicon) + +```bash +mkdir -p ~/.claude/ccline +wget https://github.com/Haleclipse/CCometixLine/releases/latest/download/ccline-macos-arm64.tar.gz +tar -xzf ccline-macos-arm64.tar.gz +cp ccline ~/.claude/ccline/ +chmod +x ~/.claude/ccline/ccline +``` + +#### Windows + +```powershell +# Create directory and download +New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.claude\ccline" +Invoke-WebRequest -Uri "https://github.com/Haleclipse/CCometixLine/releases/latest/download/ccline-windows-x64.zip" -OutFile "ccline-windows-x64.zip" +Expand-Archive -Path "ccline-windows-x64.zip" -DestinationPath "." +Move-Item "ccline.exe" "$env:USERPROFILE\.claude\ccline\" +``` + +
+ ### Build from Source ```bash @@ -163,18 +253,44 @@ copy target\release\ccometixline.exe "$env:USERPROFILE\.claude\ccline\ccline.exe ## Usage +### Configuration Management + ```bash -# Basic usage (displays all enabled segments) -ccline +# Initialize configuration file +ccline --init -# Show help -ccline --help +# Check configuration validity +ccline --check -# Print default configuration -ccline --print-config +# Print current configuration +ccline --print -# TUI configuration mode (planned) -ccline --configure +# Enter TUI configuration mode +ccline --config +``` + +### Theme Override + +```bash +# Temporarily use specific theme (overrides config file) +ccline --theme cometix +ccline --theme minimal +ccline --theme gruvbox +ccline --theme nord +ccline --theme powerline-dark + +# Or use custom theme files from ~/.claude/ccline/themes/ +ccline --theme my-custom-theme +``` + +### Claude Code Enhancement + +```bash +# Disable context warnings and enable verbose mode +ccline --patch /path/to/claude-code/cli.js + +# Example for common installation +ccline --patch ~/.local/share/fnm/node-versions/v24.4.1/installation/lib/node_modules/@anthropic-ai/claude-code/cli.js ``` ## Default Segments @@ -213,13 +329,23 @@ Supports multiple API key sources: ## Configuration -Configuration support is planned for future releases. Currently uses sensible defaults for all segments. +CCometixLine supports full configuration via TOML files and interactive TUI: + +- **Configuration file**: `~/.claude/ccline/config.toml` +- **Interactive TUI**: `ccline --config` for real-time editing with preview +- **Theme files**: `~/.claude/ccline/themes/*.toml` for custom themes +- **Automatic initialization**: `ccline --init` creates default configuration + +### Available Segments + +All segments are configurable with: +- Enable/disable toggle +- Custom separators and icons +- Color customization +- Format options -## Performance +Supported segments: Directory, Git, Model, Usage, Time, Cost, OutputStyle -- **Startup time**: < 50ms (vs ~200ms for TypeScript equivalents) -- **Memory usage**: < 10MB (vs ~25MB for Node.js tools) -- **Binary size**: ~2MB optimized release build ## Requirements @@ -244,11 +370,11 @@ cargo build --release ## Roadmap -- [ ] TOML configuration file support -- [ ] TUI configuration interface -- [ ] Custom themes -- [ ] Plugin system -- [ ] Cross-platform binaries +- [x] TOML configuration file support +- [x] TUI configuration interface +- [x] Custom themes +- [x] Interactive main menu +- [x] Claude Code enhancement tools ## Contributing diff --git a/README.zh.md b/README.zh.md index d5e9b5a..656eb35 100644 --- a/README.zh.md +++ b/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | [中文](README.zh.md) -基于 Rust 的高性能 Claude Code 状态栏工具,集成 Git 信息、实时使用量跟踪和智能 API 配额监控。 +基于 Rust 的高性能 Claude Code 状态栏工具,集成 Git 信息、使用量跟踪、交互式 TUI 配置、Claude Code 增强工具和智能 API 配额监控。 > 这是专为 PackyCode 服务特别适配的 CCometixLine 版本。原版 CCometixLine 由 [Haleclipse](https://github.com/Haleclipse/CCometixLine) 基于 MIT License 创建。本项目也基于 MIT License 发布。 > diff --git a/src/cli.rs b/src/cli.rs index cb28b11..1619cc7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -27,6 +27,10 @@ pub struct Cli { /// Check for updates #[arg(short = 'u', long = "update")] pub update: bool, + + /// Patch Claude Code cli.js to disable context warnings + #[arg(long = "patch")] + pub patch: Option, } impl Cli { diff --git a/src/config/loader.rs b/src/config/loader.rs index dc9b239..1e5d9c3 100644 --- a/src/config/loader.rs +++ b/src/config/loader.rs @@ -23,6 +23,7 @@ impl ConfigLoader { fs::create_dir_all(&themes_dir)?; let builtin_themes = [ + "cometix", "default", "minimal", "gruvbox", @@ -80,6 +81,7 @@ impl ConfigLoader { "minimal", "gruvbox", "nord", + "cometix", "powerline-dark", "powerline-light", "powerline-rose-pine", diff --git a/src/config/mod.rs b/src/config/mod.rs index 5bc17c6..281aaf8 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,6 +1,8 @@ pub mod defaults; pub mod loader; +pub mod models; pub mod types; pub use loader::ConfigLoader; +pub use models::*; pub use types::*; diff --git a/src/config/models.rs b/src/config/models.rs new file mode 100644 index 0000000..788f1c3 --- /dev/null +++ b/src/config/models.rs @@ -0,0 +1,212 @@ +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::Path; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ModelConfig { + pub default_context_limit: u32, + #[serde(rename = "models")] + pub model_entries: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ModelEntry { + pub pattern: String, + pub display_name: String, + pub context_limit: u32, +} + +impl ModelConfig { + /// Load model configuration from TOML file + pub fn load_from_file>(path: P) -> Result> { + let content = fs::read_to_string(path)?; + let config: ModelConfig = toml::from_str(&content)?; + Ok(config) + } + + /// Load model configuration with fallback locations + pub fn load() -> Self { + // Try loading from user config directory first + let config_paths = [ + dirs::home_dir().map(|d| d.join(".claude").join("ccline").join("models.toml")), + Some(Path::new("models.toml").to_path_buf()), + ]; + + for path in config_paths.iter().flatten() { + if path.exists() { + if let Ok(config) = Self::load_from_file(path) { + return config; + } + } + } + + // Fallback to default configuration if no file found + Self::default() + } + + /// Get context limit for a model based on ID pattern matching + /// Checks external config first, then falls back to built-in config + pub fn get_context_limit(&self, model_id: &str) -> u32 { + let model_lower = model_id.to_lowercase(); + + // First check external config if it exists + if let Ok(external_config) = Self::load_external_config() { + for entry in &external_config.model_entries { + if model_lower.contains(&entry.pattern.to_lowercase()) { + return entry.context_limit; + } + } + } + + // Fall back to built-in config + for entry in &self.model_entries { + if model_lower.contains(&entry.pattern.to_lowercase()) { + return entry.context_limit; + } + } + + self.default_context_limit + } + + /// Get display name for a model based on ID pattern matching + /// Checks external config first, then falls back to built-in config + /// Returns None if no match found (should use fallback display_name) + pub fn get_display_name(&self, model_id: &str) -> Option { + let model_lower = model_id.to_lowercase(); + + // First check external config if it exists + if let Ok(external_config) = Self::load_external_config() { + for entry in &external_config.model_entries { + if model_lower.contains(&entry.pattern.to_lowercase()) { + return Some(entry.display_name.clone()); + } + } + } + + // Fall back to built-in config + for entry in &self.model_entries { + if model_lower.contains(&entry.pattern.to_lowercase()) { + return Some(entry.display_name.clone()); + } + } + + None + } + + /// Load external configuration file only + fn load_external_config() -> Result> { + let config_paths = [ + dirs::home_dir().map(|d| d.join(".claude").join("ccline").join("models.toml")), + Some(Path::new("models.toml").to_path_buf()), + ]; + + for path in config_paths.iter().flatten() { + if path.exists() { + return Self::load_from_file(path); + } + } + + Err("No external config file found".into()) + } + + /// Create default model configuration file with minimal template + pub fn create_default_file>(path: P) -> Result<(), Box> { + // Create a minimal template config (not the full fallback config) + let template_config = Self { + default_context_limit: 200_000, + model_entries: vec![], // Empty - just provide the structure + }; + + let toml_content = toml::to_string_pretty(&template_config)?; + + // Add comments and examples to the template + let template_content = format!( + "# CCometixLine Model Configuration\n\ + # This file defines model display names and context limits for different LLM models\n\ + # File location: ~/.claude/ccline/models.toml\n\ + \n\ + {}\n\ + \n\ + # Model configurations\n\ + # Each [[models]] section defines a model pattern and its properties\n\ + # Order matters: first match wins, so put more specific patterns first\n\ + \n\ + # Example of how to add new models:\n\ + # [[models]]\n\ + # pattern = \"glm-4.5\"\n\ + # display_name = \"GLM-4.5\"\n\ + # context_limit = 128000\n", + toml_content.trim() + ); + + // Create parent directory if it doesn't exist + if let Some(parent) = path.as_ref().parent() { + fs::create_dir_all(parent)?; + } + + fs::write(path, template_content)?; + Ok(()) + } +} + +impl Default for ModelConfig { + fn default() -> Self { + Self { + default_context_limit: 200_000, + model_entries: vec![ + // 1M context models (put first for priority matching) + ModelEntry { + pattern: "[1m]".to_string(), + display_name: "Sonnet 4 1M".to_string(), + context_limit: 1_000_000, + }, + ModelEntry { + pattern: "claude-sonnet-4".to_string(), + display_name: "Sonnet 4".to_string(), + context_limit: 200_000, + }, + ModelEntry { + pattern: "claude-4-sonnet".to_string(), + display_name: "Sonnet 4".to_string(), + context_limit: 200_000, + }, + ModelEntry { + pattern: "claude-4-opus".to_string(), + display_name: "Opus 4".to_string(), + context_limit: 200_000, + }, + ModelEntry { + pattern: "sonnet-4".to_string(), + display_name: "Sonnet 4".to_string(), + context_limit: 200_000, + }, + ModelEntry { + pattern: "claude-3-7-sonnet".to_string(), + display_name: "Sonnet 3.7".to_string(), + context_limit: 200_000, + }, + // Third-party models + ModelEntry { + pattern: "glm-4.5".to_string(), + display_name: "GLM-4.5".to_string(), + context_limit: 128_000, + }, + ModelEntry { + pattern: "kimi-k2-turbo".to_string(), + display_name: "Kimi K2 Turbo".to_string(), + context_limit: 128_000, + }, + ModelEntry { + pattern: "kimi-k2".to_string(), + display_name: "Kimi K2".to_string(), + context_limit: 128_000, + }, + ModelEntry { + pattern: "qwen3-coder".to_string(), + display_name: "Qwen Coder".to_string(), + context_limit: 256_000, + }, + ], + } + } +} diff --git a/src/config/types.rs b/src/config/types.rs index 2be0154..853033f 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -20,9 +20,9 @@ pub struct StyleConfig { #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum StyleMode { - Plain, // emoji + 颜色 - NerdFont, // Nerd Font 图标 + 颜色 - Powerline, // 未来支持 + Plain, + NerdFont, + Powerline, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -68,6 +68,9 @@ pub enum SegmentId { Directory, Git, Usage, + Cost, + Session, + OutputStyle, Update, Quota, } @@ -84,6 +87,7 @@ pub struct SegmentsConfig { // Data structures compatible with existing main.rs #[derive(Deserialize)] pub struct Model { + pub id: String, pub display_name: String, } @@ -92,11 +96,27 @@ pub struct Workspace { pub current_dir: String, } +#[derive(Deserialize)] +pub struct Cost { + pub total_cost_usd: Option, + pub total_duration_ms: Option, + pub total_api_duration_ms: Option, + pub total_lines_added: Option, + pub total_lines_removed: Option, +} + +#[derive(Deserialize)] +pub struct OutputStyle { + pub name: String, +} + #[derive(Deserialize)] pub struct InputData { pub model: Model, pub workspace: Workspace, pub transcript_path: String, + pub cost: Option, + pub output_style: Option, } // OpenAI-style nested token details @@ -357,4 +377,10 @@ pub struct Message { pub struct TranscriptEntry { pub r#type: Option, pub message: Option, + #[serde(rename = "leafUuid")] + pub leaf_uuid: Option, + pub uuid: Option, + #[serde(rename = "parentUuid")] + pub parent_uuid: Option, + pub summary: Option, } diff --git a/src/core/segments/cost.rs b/src/core/segments/cost.rs new file mode 100644 index 0000000..494832e --- /dev/null +++ b/src/core/segments/cost.rs @@ -0,0 +1,47 @@ +use super::{Segment, SegmentData}; +use crate::config::{InputData, SegmentId}; +use std::collections::HashMap; + +#[derive(Default)] +pub struct CostSegment; + +impl CostSegment { + pub fn new() -> Self { + Self + } +} + +impl Segment for CostSegment { + fn collect(&self, input: &InputData) -> Option { + let cost_data = input.cost.as_ref()?; + + // Primary display: total cost + let primary = if let Some(cost) = cost_data.total_cost_usd { + if cost == 0.0 || cost < 0.01 { + "$0".to_string() + } else { + format!("${:.2}", cost) + } + } else { + return None; + }; + + // Secondary display: empty for cost segment + let secondary = String::new(); + + let mut metadata = HashMap::new(); + if let Some(cost) = cost_data.total_cost_usd { + metadata.insert("cost".to_string(), cost.to_string()); + } + + Some(SegmentData { + primary, + secondary, + metadata, + }) + } + + fn id(&self) -> SegmentId { + SegmentId::Cost + } +} diff --git a/src/core/segments/mod.rs b/src/core/segments/mod.rs index 831c52f..1af7331 100644 --- a/src/core/segments/mod.rs +++ b/src/core/segments/mod.rs @@ -1,7 +1,10 @@ +pub mod cost; pub mod directory; pub mod git; pub mod model; +pub mod output_style; pub mod quota; +pub mod session; pub mod update; pub mod usage; @@ -22,9 +25,12 @@ pub struct SegmentData { } // Re-export all segment types +pub use cost::CostSegment; pub use directory::DirectorySegment; pub use git::GitSegment; pub use model::ModelSegment; +pub use output_style::OutputStyleSegment; pub use quota::QuotaSegment; +pub use session::SessionSegment; pub use update::UpdateSegment; pub use usage::UsageSegment; diff --git a/src/core/segments/model.rs b/src/core/segments/model.rs index 13679b8..7265fc5 100644 --- a/src/core/segments/model.rs +++ b/src/core/segments/model.rs @@ -1,5 +1,5 @@ use super::{Segment, SegmentData}; -use crate::config::{InputData, SegmentId}; +use crate::config::{InputData, ModelConfig, SegmentId}; use std::collections::HashMap; #[derive(Default)] @@ -13,10 +13,14 @@ impl ModelSegment { impl Segment for ModelSegment { fn collect(&self, input: &InputData) -> Option { + let mut metadata = HashMap::new(); + metadata.insert("model_id".to_string(), input.model.id.clone()); + metadata.insert("display_name".to_string(), input.model.display_name.clone()); + Some(SegmentData { - primary: self.format_model_name(&input.model.display_name), + primary: self.format_model_name(&input.model.id, &input.model.display_name), secondary: String::new(), - metadata: HashMap::new(), + metadata, }) } @@ -26,17 +30,15 @@ impl Segment for ModelSegment { } impl ModelSegment { - fn format_model_name(&self, display_name: &str) -> String { - // Simplify model display names - match display_name { - name if name.contains("claude-3-5-sonnet") => "Sonnet 3.5".to_string(), - name if name.contains("claude-3-7-sonnet") => "Sonnet 3.7".to_string(), - name if name.contains("claude-3-sonnet") => "Sonnet 3".to_string(), - name if name.contains("claude-3-haiku") => "Haiku 3".to_string(), - name if name.contains("claude-4-sonnet") => "Sonnet 4".to_string(), - name if name.contains("claude-4-opus") => "Opus 4".to_string(), - name if name.contains("sonnet-4") => "Sonnet 4".to_string(), - _ => display_name.to_string(), + fn format_model_name(&self, id: &str, display_name: &str) -> String { + let model_config = ModelConfig::load(); + + // Try to get display name from external config first + if let Some(config_name) = model_config.get_display_name(id) { + config_name + } else { + // Fallback to Claude Code's official display_name for unrecognized models + display_name.to_string() } } } diff --git a/src/core/segments/output_style.rs b/src/core/segments/output_style.rs new file mode 100644 index 0000000..c09eeb1 --- /dev/null +++ b/src/core/segments/output_style.rs @@ -0,0 +1,34 @@ +use super::{Segment, SegmentData}; +use crate::config::{InputData, SegmentId}; +use std::collections::HashMap; + +#[derive(Default)] +pub struct OutputStyleSegment; + +impl OutputStyleSegment { + pub fn new() -> Self { + Self + } +} + +impl Segment for OutputStyleSegment { + fn collect(&self, input: &InputData) -> Option { + let output_style = input.output_style.as_ref()?; + + // Primary display: style name + let primary = output_style.name.clone(); + + let mut metadata = HashMap::new(); + metadata.insert("style_name".to_string(), output_style.name.clone()); + + Some(SegmentData { + primary, + secondary: String::new(), + metadata, + }) + } + + fn id(&self) -> SegmentId { + SegmentId::OutputStyle + } +} diff --git a/src/core/segments/session.rs b/src/core/segments/session.rs new file mode 100644 index 0000000..1c77c16 --- /dev/null +++ b/src/core/segments/session.rs @@ -0,0 +1,88 @@ +use super::{Segment, SegmentData}; +use crate::config::{InputData, SegmentId}; +use std::collections::HashMap; + +#[derive(Default)] +pub struct SessionSegment; + +impl SessionSegment { + pub fn new() -> Self { + Self + } + + fn format_duration(ms: u64) -> String { + if ms < 1000 { + format!("{}ms", ms) + } else if ms < 60_000 { + let seconds = ms / 1000; + format!("{}s", seconds) + } else if ms < 3_600_000 { + let minutes = ms / 60_000; + let seconds = (ms % 60_000) / 1000; + if seconds == 0 { + format!("{}m", minutes) + } else { + format!("{}m{}s", minutes, seconds) + } + } else { + let hours = ms / 3_600_000; + let minutes = (ms % 3_600_000) / 60_000; + if minutes == 0 { + format!("{}h", hours) + } else { + format!("{}h{}m", hours, minutes) + } + } + } +} + +impl Segment for SessionSegment { + fn collect(&self, input: &InputData) -> Option { + let cost_data = input.cost.as_ref()?; + + // Primary display: total duration + let primary = if let Some(duration) = cost_data.total_duration_ms { + Self::format_duration(duration) + } else { + return None; + }; + + // Secondary display: line changes if available + let secondary = match (cost_data.total_lines_added, cost_data.total_lines_removed) { + (Some(added), Some(removed)) if added > 0 || removed > 0 => { + format!("+{} -{}", added, removed) + } + (Some(added), None) if added > 0 => { + format!("+{}", added) + } + (None, Some(removed)) if removed > 0 => { + format!("-{}", removed) + } + _ => String::new(), + }; + + let mut metadata = HashMap::new(); + if let Some(duration) = cost_data.total_duration_ms { + metadata.insert("duration_ms".to_string(), duration.to_string()); + } + if let Some(api_duration) = cost_data.total_api_duration_ms { + metadata.insert("api_duration_ms".to_string(), api_duration.to_string()); + } + if let Some(added) = cost_data.total_lines_added { + metadata.insert("lines_added".to_string(), added.to_string()); + } + if let Some(removed) = cost_data.total_lines_removed { + metadata.insert("lines_removed".to_string(), removed.to_string()); + } + + Some(SegmentData { + primary, + secondary, + metadata, + }) + } + + fn id(&self) -> SegmentId { + SegmentId::Session + } +} diff --git a/src/core/segments/usage.rs b/src/core/segments/usage.rs index 61021a4..58714c4 100644 --- a/src/core/segments/usage.rs +++ b/src/core/segments/usage.rs @@ -1,9 +1,9 @@ use super::{Segment, SegmentData}; -use crate::config::{InputData, SegmentId, TranscriptEntry}; +use crate::config::{InputData, ModelConfig, SegmentId, TranscriptEntry}; use std::collections::HashMap; use std::fs; use std::io::{BufRead, BufReader}; -use std::path::Path; +use std::path::{Path, PathBuf}; /// Get context limit for a specific model /// Returns 1M for Sonnet[1M] models, 200K for all others @@ -22,19 +22,25 @@ impl UsageSegment { pub fn new() -> Self { Self } + + /// Get context limit for the specified model + fn get_context_limit_for_model(model_id: &str) -> u32 { + let model_config = ModelConfig::load(); + model_config.get_context_limit(model_id) + } } impl Segment for UsageSegment { fn collect(&self, input: &InputData) -> Option { + // Dynamically determine context limit based on current model ID + let context_limit = Self::get_context_limit_for_model(&input.model.id); + let context_used_token = if input.transcript_path == "mock_preview" { // Hardcoded mock data for preview 150000 } else { parse_transcript_usage(&input.transcript_path) }; - - // Use dynamic context limit based on model - let context_limit = get_context_limit(&input.model.display_name); let context_used_rate = (context_used_token as f64 / context_limit as f64) * 100.0; let percentage_display = if context_used_rate.fract() == 0.0 { @@ -58,6 +64,7 @@ impl Segment for UsageSegment { metadata.insert("tokens".to_string(), context_used_token.to_string()); metadata.insert("percentage".to_string(), context_used_rate.to_string()); metadata.insert("limit".to_string(), context_limit.to_string()); + metadata.insert("model".to_string(), input.model.id.clone()); Some(SegmentData { primary: format!("{} · {} tokens", percentage_display, tokens_display), @@ -72,17 +79,48 @@ impl Segment for UsageSegment { } fn parse_transcript_usage>(transcript_path: P) -> u32 { - let file = match fs::File::open(&transcript_path) { - Ok(file) => file, - Err(_) => return 0, - }; + let path = transcript_path.as_ref(); + + // Try to parse from current transcript file + if let Some(usage) = try_parse_transcript_file(path) { + return usage; + } + + // If file doesn't exist, try to find usage from project history + if !path.exists() { + if let Some(usage) = try_find_usage_from_project_history(path) { + return usage; + } + } + + 0 +} +fn try_parse_transcript_file(path: &Path) -> Option { + let file = fs::File::open(path).ok()?; let reader = BufReader::new(file); let lines: Vec = reader .lines() .collect::, _>>() .unwrap_or_default(); + if lines.is_empty() { + return None; + } + + // Check if the last line is a summary + let last_line = lines.last()?.trim(); + if let Ok(entry) = serde_json::from_str::(last_line) { + if entry.r#type.as_deref() == Some("summary") { + // Handle summary case: find usage by leafUuid + if let Some(leaf_uuid) = &entry.leaf_uuid { + let project_dir = path.parent()?; + return find_usage_by_leaf_uuid(leaf_uuid, project_dir); + } + } + } + + // Normal case: find the last assistant message in current file for line in lines.iter().rev() { let line = line.trim(); if line.is_empty() { @@ -94,12 +132,136 @@ fn parse_transcript_usage>(transcript_path: P) -> u32 { if let Some(message) = &entry.message { if let Some(raw_usage) = &message.usage { let normalized = raw_usage.clone().normalize(); - return normalized.display_tokens(); + return Some(normalized.display_tokens()); } } } } } - 0 + None +} + +fn find_usage_by_leaf_uuid(leaf_uuid: &str, project_dir: &Path) -> Option { + // Search for the leafUuid across all session files in the project directory + let entries = fs::read_dir(project_dir).ok()?; + + for entry in entries { + let entry = entry.ok()?; + let path = entry.path(); + + if path.extension().and_then(|s| s.to_str()) != Some("jsonl") { + continue; + } + + if let Some(usage) = search_uuid_in_file(&path, leaf_uuid) { + return Some(usage); + } + } + + None +} + +fn search_uuid_in_file(path: &Path, target_uuid: &str) -> Option { + let file = fs::File::open(path).ok()?; + let reader = BufReader::new(file); + let lines: Vec = reader + .lines() + .collect::, _>>() + .unwrap_or_default(); + + // Find the message with target_uuid + for line in &lines { + let line = line.trim(); + if line.is_empty() { + continue; + } + + if let Ok(entry) = serde_json::from_str::(line) { + if let Some(uuid) = &entry.uuid { + if uuid == target_uuid { + // Found the target message, check its type + if entry.r#type.as_deref() == Some("assistant") { + // Direct assistant message with usage + if let Some(message) = &entry.message { + if let Some(raw_usage) = &message.usage { + let normalized = raw_usage.clone().normalize(); + return Some(normalized.display_tokens()); + } + } + } else if entry.r#type.as_deref() == Some("user") { + // User message, need to find the parent assistant message + if let Some(parent_uuid) = &entry.parent_uuid { + return find_assistant_message_by_uuid(&lines, parent_uuid); + } + } + break; + } + } + } + } + + None +} + +fn find_assistant_message_by_uuid(lines: &[String], target_uuid: &str) -> Option { + for line in lines { + let line = line.trim(); + if line.is_empty() { + continue; + } + + if let Ok(entry) = serde_json::from_str::(line) { + if let Some(uuid) = &entry.uuid { + if uuid == target_uuid && entry.r#type.as_deref() == Some("assistant") { + if let Some(message) = &entry.message { + if let Some(raw_usage) = &message.usage { + let normalized = raw_usage.clone().normalize(); + return Some(normalized.display_tokens()); + } + } + } + } + } + } + + None +} + +fn try_find_usage_from_project_history(transcript_path: &Path) -> Option { + let project_dir = transcript_path.parent()?; + + // Find the most recent session file in the project directory + let mut session_files: Vec = Vec::new(); + let entries = fs::read_dir(project_dir).ok()?; + + for entry in entries { + let entry = entry.ok()?; + let path = entry.path(); + + if path.extension().and_then(|s| s.to_str()) == Some("jsonl") { + session_files.push(path); + } + } + + if session_files.is_empty() { + return None; + } + + // Sort by modification time (most recent first) + session_files.sort_by_key(|path| { + fs::metadata(path) + .and_then(|m| m.modified()) + .unwrap_or(std::time::UNIX_EPOCH) + }); + session_files.reverse(); + + // Try to find usage from the most recent session + for session_path in &session_files { + if let Some(usage) = try_parse_transcript_file(session_path) { + return Some(usage); + } + } + + None } diff --git a/src/core/statusline.rs b/src/core/statusline.rs index 676ce7d..0601052 100644 --- a/src/core/statusline.rs +++ b/src/core/statusline.rs @@ -481,6 +481,18 @@ pub fn collect_all_segments( let segment = UsageSegment::new(); segment.collect(input) } + crate::config::SegmentId::Cost => { + let segment = CostSegment::new(); + segment.collect(input) + } + crate::config::SegmentId::Session => { + let segment = SessionSegment::new(); + segment.collect(input) + } + crate::config::SegmentId::OutputStyle => { + let segment = OutputStyleSegment::new(); + segment.collect(input) + } crate::config::SegmentId::Update => { let segment = UpdateSegment::new(); segment.collect(input) diff --git a/src/lib.rs b/src/lib.rs index af45d02..f6e60fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod cli; pub mod config; pub mod core; pub mod ui; +pub mod utils; #[cfg(feature = "self-update")] pub mod updater; diff --git a/src/main.rs b/src/main.rs index c70f83f..da658f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use ccometixline_packycc::cli::Cli; use ccometixline_packycc::config::{Config, InputData}; use ccometixline_packycc::core::{collect_all_segments, StatusLineGenerator}; -use std::io; +use std::io::{self, IsTerminal}; fn main() -> Result<(), Box> { let cli = Cli::parse_args(); @@ -56,6 +56,41 @@ fn main() -> Result<(), Box> { return Ok(()); } + // Handle Claude Code patcher + if let Some(claude_path) = cli.patch { + use ccometixline::utils::ClaudeCodePatcher; + + println!("🔧 Claude Code Context Warning Disabler"); + println!("Target file: {}", claude_path); + + // Create backup in same directory + let backup_path = format!("{}.backup", claude_path); + std::fs::copy(&claude_path, &backup_path)?; + println!("📦 Created backup: {}", backup_path); + + // Load and patch + let mut patcher = ClaudeCodePatcher::new(&claude_path)?; + + // Apply both modifications + println!("\n🔄 Applying patches..."); + + // 1. Set verbose property to true + if let Err(e) = patcher.write_verbose_property(true) { + println!("⚠️ Could not modify verbose property: {}", e); + } + + // 2. Disable context low warnings + patcher.disable_context_low_warnings()?; + + patcher.save()?; + + println!("✅ All patches applied successfully!"); + println!("💡 To restore warnings, replace your cli.js with the backup file:"); + println!(" cp {} {}", backup_path, claude_path); + + return Ok(()); + } + // Load configuration let mut config = Config::load().unwrap_or_else(|_| Config::default()); @@ -64,6 +99,42 @@ fn main() -> Result<(), Box> { config = ccometixline_packycc::ui::themes::ThemePresets::get_theme(&theme); } + // Check if stdin has data + if io::stdin().is_terminal() { + // No input data available, show main menu + #[cfg(feature = "tui")] + { + use ccometixline::ui::{MainMenu, MenuResult}; + + if let Some(result) = MainMenu::run()? { + match result { + MenuResult::LaunchConfigurator => { + ccometixline::ui::run_configurator()?; + } + MenuResult::InitConfig => { + ccometixline::config::Config::init()?; + println!("Configuration initialized successfully!"); + } + MenuResult::CheckConfig => { + let config = ccometixline::config::Config::load()?; + config.check()?; + println!("Configuration is valid!"); + } + MenuResult::Exit => { + // Exit gracefully + } + } + } + } + #[cfg(not(feature = "tui"))] + { + eprintln!("No input data provided and TUI feature is not enabled."); + eprintln!("Usage: echo '{{...}}' | ccline"); + eprintln!(" or: ccline --help"); + } + return Ok(()); + } + // Read Claude Code data from stdin let stdin = io::stdin(); let input: InputData = serde_json::from_reader(stdin.lock())?; diff --git a/src/themes/powerline_importer.rs b/src/themes/powerline_importer.rs deleted file mode 100644 index 4226ae8..0000000 --- a/src/themes/powerline_importer.rs +++ /dev/null @@ -1,199 +0,0 @@ -use crate::config::{AnsiColor, Config, SegmentConfig, SegmentId, ColorConfig, StyleConfig, IconConfig, StylesConfig, StyleMode}; - -/// Convert hex color string to RGB AnsiColor -fn hex_to_rgb(hex: &str) -> Result { - let hex = hex.trim_start_matches('#'); - if hex.len() != 6 { - return Err(format!("Invalid hex color: #{}", hex)); - } - - let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_| "Invalid red component")?; - let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| "Invalid green component")?; - let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_| "Invalid blue component")?; - - Ok(AnsiColor::Rgb { r, g, b }) -} - -/// Create a Powerline-compatible theme from claude-powerline color scheme -pub fn create_powerline_theme( - theme_name: &str, - directory_colors: (&str, &str), - git_colors: (&str, &str), - model_colors: (&str, &str), - usage_colors: Option<(&str, &str)>, - update_colors: Option<(&str, &str)>, -) -> Result { - let mut config = Config::default(); - - // Set theme name and use Powerline separator - config.theme = theme_name.to_string(); - config.style.separator = "\u{e0b0}".to_string(); - config.style.mode = StyleMode::NerdFont; - - // Create segments with Powerline colors - let mut segments = Vec::new(); - - // Model segment - segments.push(SegmentConfig { - id: SegmentId::Model, - enabled: true, - icon: IconConfig { - plain: "🔮".to_string(), - nerd_font: "\u{f02a2}".to_string(), - }, - colors: ColorConfig { - icon: Some(hex_to_rgb(model_colors.1)?), - text: Some(hex_to_rgb(model_colors.1)?), - background: Some(hex_to_rgb(model_colors.0)?), - }, - styles: StylesConfig { - text_bold: false, - }, - options: std::collections::HashMap::new(), - }); - - // Directory segment - segments.push(SegmentConfig { - id: SegmentId::Directory, - enabled: true, - icon: IconConfig { - plain: "📁".to_string(), - nerd_font: "\u{f115}".to_string(), - }, - colors: ColorConfig { - icon: Some(hex_to_rgb(directory_colors.1)?), - text: Some(hex_to_rgb(directory_colors.1)?), - background: Some(hex_to_rgb(directory_colors.0)?), - }, - styles: StylesConfig { - text_bold: false, - }, - options: std::collections::HashMap::new(), - }); - - // Git segment - segments.push(SegmentConfig { - id: SegmentId::Git, - enabled: true, - icon: IconConfig { - plain: "🔗".to_string(), - nerd_font: "\u{f1d3}".to_string(), - }, - colors: ColorConfig { - icon: Some(hex_to_rgb(git_colors.1)?), - text: Some(hex_to_rgb(git_colors.1)?), - background: Some(hex_to_rgb(git_colors.0)?), - }, - styles: StylesConfig { - text_bold: false, - }, - options: std::collections::HashMap::new(), - }); - - // Usage segment (if provided) - if let Some(usage_colors) = usage_colors { - segments.push(SegmentConfig { - id: SegmentId::Usage, - enabled: true, - icon: IconConfig { - plain: "💰".to_string(), - nerd_font: "\u{f111}".to_string(), - }, - colors: ColorConfig { - icon: Some(hex_to_rgb(usage_colors.1)?), - text: Some(hex_to_rgb(usage_colors.1)?), - background: Some(hex_to_rgb(usage_colors.0)?), - }, - styles: StylesConfig { - text_bold: false, - }, - options: std::collections::HashMap::new(), - }); - } - - // Update segment (if provided) - if let Some(update_colors) = update_colors { - segments.push(SegmentConfig { - id: SegmentId::Update, - enabled: true, - icon: IconConfig { - plain: "⬆️".to_string(), - nerd_font: "\u{f062}".to_string(), - }, - colors: ColorConfig { - icon: Some(hex_to_rgb(update_colors.1)?), - text: Some(hex_to_rgb(update_colors.1)?), - background: Some(hex_to_rgb(update_colors.0)?), - }, - styles: StylesConfig { - text_bold: false, - }, - options: std::collections::HashMap::new(), - }); - } - - config.segments = segments; - Ok(config) -} - -/// Generate all Powerline themes based on claude-powerline configurations -pub fn generate_powerline_themes() -> Result, String> { - let mut themes = Vec::new(); - - // Dark theme - let dark_theme = create_powerline_theme( - "powerline-dark", - ("#8b4513", "#ffffff"), // directory: brown bg, white fg - ("#404040", "#ffffff"), // git: dark gray bg, white fg - ("#2d2d2d", "#ffffff"), // model: darker gray bg, white fg - Some(("#374151", "#d1d5db")), // usage: metrics colors - Some(("#3a3a4a", "#b8b8d0")), // update: version colors - )?; - themes.push(("powerline-dark".to_string(), dark_theme)); - - // Light theme - let light_theme = create_powerline_theme( - "powerline-light", - ("#ff6b47", "#ffffff"), // directory: coral bg, white fg - ("#4fb3d9", "#ffffff"), // git: sky blue bg, white fg - ("#87ceeb", "#000000"), // model: sky blue bg, black fg - Some(("#6b7280", "#ffffff")), // usage: gray bg, white fg - Some(("#8b7dd8", "#ffffff")), // update: purple bg, white fg - )?; - themes.push(("powerline-light".to_string(), light_theme)); - - // Nord theme - let nord_theme = create_powerline_theme( - "powerline-nord", - ("#434c5e", "#d8dee9"), // directory: nord gray bg, light fg - ("#3b4252", "#a3be8c"), // git: nord dark bg, green fg - ("#4c566a", "#81a1c1"), // model: nord blue-gray bg, blue fg - Some(("#b48ead", "#2e3440")), // usage: nord purple bg, dark fg - Some(("#434c5e", "#88c0d0")), // update: nord gray bg, cyan fg - )?; - themes.push(("powerline-nord".to_string(), nord_theme)); - - // Tokyo Night theme - let tokyo_night_theme = create_powerline_theme( - "powerline-tokyo-night", - ("#2f334d", "#82aaff"), // directory: tokyo blue-gray bg, blue fg - ("#1e2030", "#c3e88d"), // git: tokyo dark bg, green fg - ("#191b29", "#fca7ea"), // model: tokyo darker bg, pink fg - Some(("#3d59a1", "#c0caf5")), // usage: tokyo blue bg, light fg - Some(("#292e42", "#bb9af7")), // update: tokyo purple-gray bg, purple fg - )?; - themes.push(("powerline-tokyo-night".to_string(), tokyo_night_theme)); - - // Rose Pine theme - let rose_pine_theme = create_powerline_theme( - "powerline-rose-pine", - ("#26233a", "#c4a7e7"), // directory: rose dark bg, purple fg - ("#1f1d2e", "#9ccfd8"), // git: rose darker bg, cyan fg - ("#191724", "#ebbcba"), // model: rose darkest bg, pink fg - Some(("#524f67", "#e0def4")), // usage: rose gray bg, light fg - Some(("#2a273f", "#c4a7e7")), // update: rose medium bg, purple fg - )?; - themes.push(("powerline-rose-pine".to_string(), rose_pine_theme)); - - Ok(themes) -} \ No newline at end of file diff --git a/src/ui/app.rs b/src/ui/app.rs index f1e07b2..178b78c 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -498,6 +498,9 @@ impl App { SegmentId::Directory => "Directory", SegmentId::Git => "Git", SegmentId::Usage => "Usage", + SegmentId::Cost => "Cost", + SegmentId::Session => "Session", + SegmentId::OutputStyle => "Output Style", SegmentId::Update => "Update", SegmentId::Quota => "Quota", }; @@ -522,6 +525,9 @@ impl App { SegmentId::Directory => "Directory", SegmentId::Git => "Git", SegmentId::Usage => "Usage", + SegmentId::Cost => "Cost", + SegmentId::Session => "Session", + SegmentId::OutputStyle => "Output Style", SegmentId::Update => "Update", SegmentId::Quota => "Quota", }; diff --git a/src/ui/components/help.rs b/src/ui/components/help.rs index 178d333..5b18cfd 100644 --- a/src/ui/components/help.rs +++ b/src/ui/components/help.rs @@ -1,5 +1,7 @@ use ratatui::{ layout::Rect, + style::{Color, Modifier, Style}, + text::{Line, Span, Text}, widgets::{Block, Borders, Paragraph}, Frame, }; @@ -22,32 +24,32 @@ impl HelpComponent { ) { let help_items = if color_picker_open { vec![ - "[↑↓] Navigate", - "[Tab] Mode", - "[Enter] Select", - "[Esc] Cancel", + ("[↑↓]", "Navigate"), + ("[Tab]", "Mode"), + ("[Enter]", "Select"), + ("[Esc]", "Cancel"), ] } else if icon_selector_open { vec![ - "[↑↓] Navigate", - "[Tab] Style", - "[C] Custom", - "[Enter] Select", - "[Esc] Cancel", + ("[↑↓]", "Navigate"), + ("[Tab]", "Style"), + ("[C]", "Custom"), + ("[Enter]", "Select"), + ("[Esc]", "Cancel"), ] } else { vec![ - "[Tab] Switch Panel", - "[Enter] Toggle/Edit", - "[Shift+↑↓] Reorder", - "[1-4] Theme", - "[P] Switch Theme", - "[R] Reset", - "[E] Edit Separator", - "[S] Save Config", - "[W] Write Theme", - "[Ctrl+S] Save Theme", - "[Esc] Quit", + ("[Tab]", "Switch Panel"), + ("[Enter]", "Toggle/Edit"), + ("[Shift+↑↓]", "Reorder"), + ("[1-4]", "Theme"), + ("[P]", "Switch Theme"), + ("[R]", "Reset"), + ("[E]", "Edit Separator"), + ("[S]", "Save Config"), + ("[W]", "Write Theme"), + ("[Ctrl+S]", "Save Theme"), + ("[Esc]", "Quit"), ] }; @@ -56,15 +58,15 @@ impl HelpComponent { // Build help text with smart wrapping - keep each shortcut as a unit let content_width = area.width.saturating_sub(2); // Remove borders let mut lines = Vec::new(); - let mut current_line = String::new(); + let mut current_line_spans = Vec::new(); let mut current_width = 0usize; - for (i, item) in help_items.iter().enumerate() { - // Calculate item display width (character count) - let item_width = item.chars().count(); + for (i, (key, description)) in help_items.iter().enumerate() { + // Calculate item display width + let item_width = key.chars().count() + description.chars().count() + 1; // +1 for space // Add separator for non-first items on the same line - let needs_separator = i > 0 && !current_line.is_empty(); + let needs_separator = i > 0 && !current_line_spans.is_empty(); let separator_width = if needs_separator { 2 } else { 0 }; let total_width = item_width + separator_width; @@ -72,31 +74,59 @@ impl HelpComponent { if current_width + total_width <= content_width as usize { // Item fits, add to current line if needs_separator { - current_line.push_str(" "); + current_line_spans.push(Span::styled(" ", Style::default())); current_width += 2; } - current_line.push_str(item); + + // Add highlighted key and description + current_line_spans.push(Span::styled( + *key, + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + )); + current_line_spans.push(Span::styled( + format!(" {}", description), + Style::default().fg(Color::Gray), + )); current_width += item_width; } else { // Item doesn't fit, start new line - if !current_line.is_empty() { - lines.push(current_line); + if !current_line_spans.is_empty() { + lines.push(Line::from(current_line_spans)); + current_line_spans = Vec::new(); } - current_line = item.to_string(); + + // Start new line with this item + current_line_spans.push(Span::styled( + *key, + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + )); + current_line_spans.push(Span::styled( + format!(" {}", description), + Style::default().fg(Color::Gray), + )); current_width = item_width; } } // Add last line if not empty - if !current_line.is_empty() { - lines.push(current_line); + if !current_line_spans.is_empty() { + lines.push(Line::from(current_line_spans)); } - let mut help_text = lines.join("\n"); + // Add status message if present if !status.is_empty() { - help_text = format!("{}\n{}", help_text, status); + lines.push(Line::from("")); + lines.push(Line::from(Span::styled( + status, + Style::default().fg(Color::Green), + ))); } + let help_text = Text::from(lines); let help_paragraph = Paragraph::new(help_text) .block(Block::default().borders(Borders::ALL).title("Help")) .wrap(ratatui::widgets::Wrap { trim: false }); diff --git a/src/ui/components/preview.rs b/src/ui/components/preview.rs index 3f42c58..05062c8 100644 --- a/src/ui/components/preview.rs +++ b/src/ui/components/preview.rs @@ -136,6 +136,35 @@ impl PreviewComponent { map }, }, + SegmentId::Cost => SegmentData { + primary: "$0.02".to_string(), + secondary: "".to_string(), + metadata: { + let mut map = HashMap::new(); + map.insert("cost".to_string(), "0.01234".to_string()); + map + }, + }, + SegmentId::Session => SegmentData { + primary: "3m45s".to_string(), + secondary: "+156 -23".to_string(), + metadata: { + let mut map = HashMap::new(); + map.insert("duration_ms".to_string(), "225000".to_string()); + map.insert("lines_added".to_string(), "156".to_string()); + map.insert("lines_removed".to_string(), "23".to_string()); + map + }, + }, + SegmentId::OutputStyle => SegmentData { + primary: "default".to_string(), + secondary: "".to_string(), + metadata: { + let mut map = HashMap::new(); + map.insert("style_name".to_string(), "default".to_string()); + map + }, + }, SegmentId::Update => SegmentData { primary: format!("v{}", env!("CARGO_PKG_VERSION")), secondary: "".to_string(), diff --git a/src/ui/components/segment_list.rs b/src/ui/components/segment_list.rs index e1dd02a..f0f100e 100644 --- a/src/ui/components/segment_list.rs +++ b/src/ui/components/segment_list.rs @@ -52,6 +52,9 @@ impl SegmentListComponent { SegmentId::Directory => "Directory", SegmentId::Git => "Git", SegmentId::Usage => "Usage", + SegmentId::Cost => "Cost", + SegmentId::Session => "Session", + SegmentId::OutputStyle => "Output Style", SegmentId::Update => "Update", SegmentId::Quota => "Quota", }; diff --git a/src/ui/components/settings.rs b/src/ui/components/settings.rs index 098d8e0..6565091 100644 --- a/src/ui/components/settings.rs +++ b/src/ui/components/settings.rs @@ -31,6 +31,9 @@ impl SettingsComponent { SegmentId::Directory => "Directory", SegmentId::Git => "Git", SegmentId::Usage => "Usage", + SegmentId::Cost => "Cost", + SegmentId::Session => "Session", + SegmentId::OutputStyle => "Output Style", SegmentId::Update => "Update", SegmentId::Quota => "Quota", }; diff --git a/src/ui/main_menu.rs b/src/ui/main_menu.rs new file mode 100644 index 0000000..73ac2b7 --- /dev/null +++ b/src/ui/main_menu.rs @@ -0,0 +1,323 @@ +use crossterm::{ + event::{self, Event, KeyCode, KeyEventKind}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ratatui::{ + backend::CrosstermBackend, + layout::{Alignment, Constraint, Direction, Layout}, + style::{Color, Modifier, Style}, + text::{Line, Span, Text}, + widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap}, + Frame, Terminal, +}; +use std::io; + +#[derive(Default)] +pub struct MainMenu { + selected_item: usize, + should_quit: bool, + show_about: bool, +} + +#[derive(Debug)] +pub enum MenuResult { + LaunchConfigurator, + InitConfig, + CheckConfig, + Exit, +} + +impl MainMenu { + pub fn new() -> Self { + Self::default() + } + + pub fn run() -> Result, Box> { + // Setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let mut app = MainMenu::new(); + let result = app.main_loop(&mut terminal)?; + + // Restore terminal + disable_raw_mode()?; + execute!(terminal.backend_mut(), LeaveAlternateScreen)?; + terminal.show_cursor()?; + + Ok(result) + } + + fn main_loop( + &mut self, + terminal: &mut Terminal>, + ) -> Result, Box> { + loop { + terminal.draw(|f| self.ui(f))?; + + if let Event::Key(key) = event::read()? { + if key.kind != KeyEventKind::Press { + continue; + } + + if self.show_about { + // In about dialog, any key closes it + self.show_about = false; + continue; + } + + match key.code { + KeyCode::Esc | KeyCode::Char('q') => { + self.should_quit = true; + } + KeyCode::Up => { + if self.selected_item > 0 { + self.selected_item -= 1; + } + } + KeyCode::Down => { + let menu_items = self.get_menu_items(); + if self.selected_item < menu_items.len() - 1 { + self.selected_item += 1; + } + } + KeyCode::Enter => { + return Ok(Some(self.handle_selection()?)); + } + _ => {} + } + } + + if self.should_quit { + return Ok(Some(MenuResult::Exit)); + } + } + } + + fn get_menu_items(&self) -> Vec<(&str, &str)> { + vec![ + (" Configuration Mode", "Enter TUI configuration interface"), + (" Initialize Config", "Create default configuration"), + (" Check Configuration", "Validate configuration file"), + (" About", "Show application information"), + (" Exit", "Exit CCometixLine"), + ] + } + + fn handle_selection(&mut self) -> Result> { + match self.selected_item { + 0 => Ok(MenuResult::LaunchConfigurator), + 1 => Ok(MenuResult::InitConfig), + 2 => Ok(MenuResult::CheckConfig), + 3 => { + self.show_about = true; + // Return to loop to show about dialog + self.main_loop_once() + } + 4 => Ok(MenuResult::Exit), + _ => Ok(MenuResult::Exit), + } + } + + fn main_loop_once(&mut self) -> Result> { + // This is a placeholder - in the actual flow, we'd continue the main loop + // but for now, let's just show about and continue + Ok(MenuResult::Exit) // This won't actually be used + } + + fn ui(&mut self, f: &mut Frame) { + let size = f.area(); + + // Main layout + let main_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(5), // Header + Constraint::Min(10), // Menu + Constraint::Length(3), // Footer + ]) + .split(size); + + // Header + let header_text = Text::from(vec![ + Line::from(vec![ + Span::styled( + "CCometixLine", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + Span::styled(" v", Style::default().fg(Color::Gray)), + Span::styled( + env!("CARGO_PKG_VERSION"), + Style::default().fg(Color::Yellow), + ), + ]), + Line::from(""), + Line::from(Span::styled( + "High-performance Claude Code StatusLine Configuration", + Style::default().fg(Color::Gray), + )), + ]); + + let header = Paragraph::new(header_text) + .block(Block::default().borders(Borders::ALL).title("Welcome")) + .alignment(Alignment::Center) + .wrap(Wrap { trim: true }); + + f.render_widget(header, main_layout[0]); + + // Menu + let menu_items = self.get_menu_items(); + let list_items: Vec = menu_items + .iter() + .enumerate() + .map(|(i, (title, desc))| { + let style = if i == self.selected_item { + Style::default().fg(Color::Black).bg(Color::Cyan) + } else { + Style::default().fg(Color::White) + }; + + let content = Line::from(vec![ + Span::styled(*title, style), + Span::styled(format!(" - {}", desc), Style::default().fg(Color::Gray)), + ]); + + ListItem::new(content).style(style) + }) + .collect(); + + let menu_list = List::new(list_items) + .block( + Block::default() + .borders(Borders::ALL) + .title("Main Menu") + .title_style(Style::default().fg(Color::Green)), + ) + .highlight_style(Style::default().bg(Color::Cyan).fg(Color::Black)) + .highlight_symbol("▶ "); + + let mut list_state = ListState::default(); + list_state.select(Some(self.selected_item)); + + f.render_stateful_widget(menu_list, main_layout[1], &mut list_state); + + // Footer + let footer_text = Text::from(vec![Line::from(vec![ + Span::styled( + "[↑↓]", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::styled(" Navigate ", Style::default().fg(Color::Gray)), + Span::styled( + "[Enter]", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::styled(" Select ", Style::default().fg(Color::Gray)), + Span::styled( + "[Esc/Q]", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + Span::styled(" Exit", Style::default().fg(Color::Gray)), + ])]); + + let footer = Paragraph::new(footer_text) + .block(Block::default().borders(Borders::ALL)) + .alignment(Alignment::Center); + + f.render_widget(footer, main_layout[2]); + + // About dialog overlay + if self.show_about { + self.render_about_dialog(f, size); + } + } + + fn render_about_dialog(&self, f: &mut Frame, area: ratatui::layout::Rect) { + // Calculate popup area (centered) + let popup_area = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(20), + Constraint::Percentage(60), + Constraint::Percentage(20), + ]) + .split(area)[1]; + + let popup_area = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(20), + Constraint::Percentage(60), + Constraint::Percentage(20), + ]) + .split(popup_area)[1]; + + // Clear the background + f.render_widget(Clear, popup_area); + + let about_text = Text::from(vec![ + Line::from(""), + Line::from(vec![ + Span::styled( + "CCometixLine ", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + Span::styled("v", Style::default().fg(Color::Gray)), + Span::styled( + env!("CARGO_PKG_VERSION"), + Style::default().fg(Color::Yellow), + ), + ]), + Line::from(""), + Line::from(Span::styled( + "Features:", + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD), + )), + Line::from("• 🎨 TUI Configuration Interface"), + Line::from("• 🎯 Multiple Built-in Themes"), + Line::from("• ⚡ Real-time Usage Tracking"), + Line::from("• 💰 Cost Monitoring"), + Line::from("• 📊 Session Statistics"), + Line::from("• 🎨 Nerd Font Support"), + Line::from("• 🔧 Highly Customizable"), + Line::from(""), + Line::from(Span::styled( + "Press any key to continue...", + Style::default().fg(Color::Yellow), + )), + ]); + + let about_dialog = Paragraph::new(about_text) + .block( + Block::default() + .borders(Borders::ALL) + .title("About CCometixLine") + .title_style( + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ) + .border_style(Style::default().fg(Color::Cyan)), + ) + .alignment(Alignment::Center) + .wrap(Wrap { trim: true }); + + f.render_widget(about_dialog, popup_area); + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 7b913ff..5d44e8a 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -7,10 +7,14 @@ pub mod events; #[cfg(feature = "tui")] pub mod layout; #[cfg(feature = "tui")] +pub mod main_menu; +#[cfg(feature = "tui")] pub mod themes; #[cfg(feature = "tui")] pub use app::App; +#[cfg(feature = "tui")] +pub use main_menu::{MainMenu, MenuResult}; #[cfg(feature = "tui")] pub fn run_configurator() -> Result<(), Box> { diff --git a/src/ui/themes/mod.rs b/src/ui/themes/mod.rs index 26ddf98..f79c1c5 100644 --- a/src/ui/themes/mod.rs +++ b/src/ui/themes/mod.rs @@ -1,3 +1,12 @@ pub mod presets; +pub mod theme_cometix; +pub mod theme_default; +pub mod theme_gruvbox; +pub mod theme_minimal; +pub mod theme_nord; +pub mod theme_powerline_dark; +pub mod theme_powerline_light; +pub mod theme_powerline_rose_pine; +pub mod theme_powerline_tokyo_night; pub use presets::*; diff --git a/src/ui/themes/presets.rs b/src/ui/themes/presets.rs index a05166b..26da41e 100644 --- a/src/ui/themes/presets.rs +++ b/src/ui/themes/presets.rs @@ -1,10 +1,12 @@ // Theme presets for TUI configuration -use crate::config::{ - AnsiColor, ColorConfig, Config, IconConfig, SegmentConfig, SegmentId, StyleConfig, StyleMode, - TextStyleConfig, +use crate::config::{Config, StyleConfig, StyleMode}; + +// Import all theme modules +use super::{ + theme_cometix, theme_default, theme_gruvbox, theme_minimal, theme_nord, theme_powerline_dark, + theme_powerline_light, theme_powerline_rose_pine, theme_powerline_tokyo_night, }; -use std::collections::HashMap; pub struct ThemePresets; @@ -17,8 +19,10 @@ impl ThemePresets { // Fallback to built-in themes match theme_name { - "minimal" => Self::get_minimal(), + "cometix" => Self::get_cometix(), + "default" => Self::get_default(), "gruvbox" => Self::get_gruvbox(), + "minimal" => Self::get_minimal(), "nord" => Self::get_nord(), "powerline-dark" => Self::get_powerline_dark(), "powerline-light" => Self::get_powerline_light(), @@ -76,6 +80,7 @@ impl ThemePresets { /// List all available themes (built-in + custom) pub fn list_available_themes() -> Vec { let mut themes = vec![ + "cometix".to_string(), "default".to_string(), "minimal".to_string(), "gruvbox".to_string(), @@ -105,6 +110,7 @@ impl ThemePresets { pub fn get_available_themes() -> Vec<(&'static str, &'static str)> { vec![ + ("cometix", "Cometix theme"), ("default", "Default theme with emoji icons"), ("minimal", "Minimal theme with reduced colors"), ("gruvbox", "Gruvbox color scheme"), @@ -116,96 +122,41 @@ impl ThemePresets { ] } - pub fn get_default() -> Config { + pub fn get_cometix() -> Config { Config { style: StyleConfig { - mode: StyleMode::Plain, + mode: StyleMode::NerdFont, separator: " | ".to_string(), }, segments: vec![ - Self::model_segment(), - Self::directory_segment(), - Self::git_segment(), - Self::usage_segment(), - Self::quota_segment(), + theme_cometix::model_segment(), + theme_cometix::directory_segment(), + theme_cometix::git_segment(), + theme_cometix::usage_segment(), + theme_cometix::cost_segment(), + theme_cometix::session_segment(), + theme_cometix::output_style_segment(), ], - theme: "default".to_string(), - } - } - - fn model_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Model, - enabled: true, - icon: IconConfig { - plain: "🤖".to_string(), - nerd_font: "\u{e26d}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Color16 { c16: 14 }), // Cyan - text: Some(AnsiColor::Color16 { c16: 14 }), - background: None, - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - fn directory_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Directory, - enabled: true, - icon: IconConfig { - plain: "📁".to_string(), - nerd_font: "\u{f024b}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Color16 { c16: 11 }), // Yellow - text: Some(AnsiColor::Color16 { c16: 10 }), // Green - background: None, - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - fn git_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Git, - enabled: true, - icon: IconConfig { - plain: "🌿".to_string(), - nerd_font: "\u{f02a2}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Color16 { c16: 12 }), // Blue - text: Some(AnsiColor::Color16 { c16: 12 }), - background: None, - }, - styles: TextStyleConfig::default(), - options: { - let mut opts = HashMap::new(); - opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); - opts - }, + theme: "cometix".to_string(), } } - fn usage_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Usage, - enabled: true, - icon: IconConfig { - plain: "⚡".to_string(), - nerd_font: "\u{f49b}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Color16 { c16: 13 }), // Magenta - text: Some(AnsiColor::Color16 { c16: 13 }), - background: None, + pub fn get_default() -> Config { + Config { + style: StyleConfig { + mode: StyleMode::Plain, + separator: " | ".to_string(), }, - styles: TextStyleConfig::default(), - options: HashMap::new(), + segments: vec![ + theme_default::model_segment(), + theme_default::directory_segment(), + theme_default::git_segment(), + theme_default::usage_segment(), + theme_default::cost_segment(), + theme_default::session_segment(), + theme_default::output_style_segment(), + ], + theme: "default".to_string(), } } @@ -213,14 +164,16 @@ impl ThemePresets { Config { style: StyleConfig { mode: StyleMode::Plain, - separator: " │ ".to_string(), // Thin vertical bar + separator: " │ ".to_string(), }, segments: vec![ - Self::minimal_model_segment(), - Self::minimal_directory_segment(), - Self::minimal_git_segment(), - Self::minimal_usage_segment(), - Self::minimal_quota_segment(), + theme_minimal::model_segment(), + theme_minimal::directory_segment(), + theme_minimal::git_segment(), + theme_minimal::usage_segment(), + theme_minimal::cost_segment(), + theme_minimal::session_segment(), + theme_minimal::output_style_segment(), ], theme: "minimal".to_string(), } @@ -233,11 +186,13 @@ impl ThemePresets { separator: " | ".to_string(), }, segments: vec![ - Self::gruvbox_model_segment(), - Self::gruvbox_directory_segment(), - Self::gruvbox_git_segment(), - Self::gruvbox_usage_segment(), - Self::gruvbox_quota_segment(), + theme_gruvbox::model_segment(), + theme_gruvbox::directory_segment(), + theme_gruvbox::git_segment(), + theme_gruvbox::usage_segment(), + theme_gruvbox::cost_segment(), + theme_gruvbox::session_segment(), + theme_gruvbox::output_style_segment(), ], theme: "gruvbox".to_string(), } @@ -250,1056 +205,91 @@ impl ThemePresets { separator: "".to_string(), }, segments: vec![ - Self::nord_model_segment(), - Self::nord_directory_segment(), - Self::nord_git_segment(), - Self::nord_usage_segment(), - Self::nord_quota_segment(), + theme_nord::model_segment(), + theme_nord::directory_segment(), + theme_nord::git_segment(), + theme_nord::usage_segment(), + theme_nord::cost_segment(), + theme_nord::session_segment(), + theme_nord::output_style_segment(), ], theme: "nord".to_string(), } } - // Minimal theme segments - fn minimal_model_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Model, - enabled: true, - icon: IconConfig { - plain: "✽".to_string(), - nerd_font: "\u{f2d0}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Color16 { c16: 7 }), - text: Some(AnsiColor::Color16 { c16: 7 }), - background: None, - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - fn minimal_directory_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Directory, - enabled: true, - icon: IconConfig { - plain: "~".to_string(), - nerd_font: "\u{f024b}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Color16 { c16: 8 }), - text: Some(AnsiColor::Color16 { c16: 7 }), - background: None, - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - fn minimal_git_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Git, - enabled: true, - icon: IconConfig { - plain: "⑂".to_string(), - nerd_font: "\u{f02a2}".to_string(), - }, - colors: ColorConfig { - icon: None, - text: Some(AnsiColor::Color16 { c16: 8 }), - background: None, - }, - styles: TextStyleConfig::default(), - options: { - let mut opts = HashMap::new(); - opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); - opts - }, - } - } - - fn minimal_usage_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Usage, - enabled: true, - icon: IconConfig { - plain: "◐".to_string(), - nerd_font: "\u{f49b}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Color16 { c16: 13 }), - text: Some(AnsiColor::Color16 { c16: 13 }), - background: None, - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - fn minimal_quota_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Quota, - enabled: true, - icon: IconConfig { - plain: "$".to_string(), - nerd_font: "\u{f155}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Color16 { c16: 8 }), - text: Some(AnsiColor::Color16 { c16: 8 }), - background: None, - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - // Gruvbox theme segments - fn gruvbox_model_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Model, - enabled: true, - icon: IconConfig { - plain: "🤖".to_string(), - nerd_font: "\u{e26d}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Color16 { c16: 14 }), - text: Some(AnsiColor::Color16 { c16: 14 }), - background: None, - }, - styles: TextStyleConfig { text_bold: true }, - options: HashMap::new(), - } - } - - fn gruvbox_directory_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Directory, - enabled: true, - icon: IconConfig { - plain: "📁".to_string(), - nerd_font: "\u{f024b}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Color16 { c16: 11 }), - text: Some(AnsiColor::Color16 { c16: 10 }), - background: None, - }, - styles: TextStyleConfig { text_bold: true }, - options: HashMap::new(), - } - } - - fn gruvbox_git_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Git, - enabled: true, - icon: IconConfig { - plain: "🌿".to_string(), - nerd_font: "\u{f02a2}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Color16 { c16: 4 }), - text: Some(AnsiColor::Color16 { c16: 4 }), - background: None, - }, - styles: TextStyleConfig { text_bold: true }, - options: { - let mut opts = HashMap::new(); - opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); - opts - }, - } - } - - fn gruvbox_usage_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Usage, - enabled: true, - icon: IconConfig { - plain: "⚡".to_string(), - nerd_font: "\u{f49b}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Color16 { c16: 5 }), - text: Some(AnsiColor::Color16 { c16: 5 }), - background: None, - }, - styles: TextStyleConfig { text_bold: true }, - options: HashMap::new(), - } - } - - fn gruvbox_quota_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Quota, - enabled: true, - icon: IconConfig { - plain: "💰".to_string(), - nerd_font: "\u{f155}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Color16 { c16: 3 }), - text: Some(AnsiColor::Color16 { c16: 3 }), - background: None, - }, - styles: TextStyleConfig { text_bold: true }, - options: HashMap::new(), - } - } - - // Nord theme segments - fn nord_model_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Model, - enabled: true, - icon: IconConfig { - plain: "🤖".to_string(), - nerd_font: "\u{e26d}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 191, - g: 97, - b: 106, - }), - text: Some(AnsiColor::Rgb { - r: 191, - g: 97, - b: 106, - }), - background: Some(AnsiColor::Rgb { - r: 76, - g: 86, - b: 106, - }), - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - fn nord_directory_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Directory, - enabled: true, - icon: IconConfig { - plain: "📁".to_string(), - nerd_font: "\u{f024b}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 235, - g: 203, - b: 139, - }), - text: Some(AnsiColor::Rgb { - r: 163, - g: 190, - b: 140, - }), - background: Some(AnsiColor::Rgb { - r: 67, - g: 76, - b: 94, - }), - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - fn nord_git_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Git, - enabled: true, - icon: IconConfig { - plain: "🌿".to_string(), - nerd_font: "\u{f02a2}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 136, - g: 192, - b: 208, - }), - text: Some(AnsiColor::Rgb { - r: 136, - g: 192, - b: 208, - }), - background: Some(AnsiColor::Rgb { - r: 59, - g: 66, - b: 82, - }), - }, - styles: TextStyleConfig::default(), - options: { - let mut opts = HashMap::new(); - opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); - opts - }, - } - } - - fn nord_usage_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Usage, - enabled: true, - icon: IconConfig { - plain: "⚡".to_string(), - nerd_font: "\u{f49b}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 46, - g: 52, - b: 64, - }), - text: Some(AnsiColor::Rgb { - r: 46, - g: 52, - b: 64, - }), - background: Some(AnsiColor::Rgb { - r: 180, - g: 142, - b: 173, - }), - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - fn nord_quota_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Quota, - enabled: true, - icon: IconConfig { - plain: "💰".to_string(), - nerd_font: "\u{f155}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 208, - g: 135, - b: 112, - }), - text: Some(AnsiColor::Rgb { - r: 208, - g: 135, - b: 112, - }), - background: Some(AnsiColor::Rgb { - r: 76, - g: 86, - b: 106, - }), - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - // Powerline Dark theme pub fn get_powerline_dark() -> Config { Config { style: StyleConfig { mode: StyleMode::NerdFont, - separator: "".to_string(), + separator: "".to_string(), }, segments: vec![ - Self::powerline_dark_model_segment(), - Self::powerline_dark_directory_segment(), - Self::powerline_dark_git_segment(), - Self::powerline_dark_usage_segment(), - Self::powerline_dark_quota_segment(), + theme_powerline_dark::model_segment(), + theme_powerline_dark::directory_segment(), + theme_powerline_dark::git_segment(), + theme_powerline_dark::usage_segment(), + theme_powerline_dark::cost_segment(), + theme_powerline_dark::session_segment(), + theme_powerline_dark::output_style_segment(), ], theme: "powerline-dark".to_string(), } } - fn powerline_dark_model_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Model, - enabled: true, - icon: IconConfig { - plain: "🤖".to_string(), - nerd_font: "\u{e26d}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 255, - g: 255, - b: 255, - }), - text: Some(AnsiColor::Rgb { - r: 255, - g: 255, - b: 255, - }), - background: Some(AnsiColor::Rgb { - r: 45, - g: 45, - b: 45, - }), - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - fn powerline_dark_directory_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Directory, - enabled: true, - icon: IconConfig { - plain: "📁".to_string(), - nerd_font: "\u{f024b}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 255, - g: 255, - b: 255, - }), - text: Some(AnsiColor::Rgb { - r: 255, - g: 255, - b: 255, - }), - background: Some(AnsiColor::Rgb { - r: 139, - g: 69, - b: 19, - }), - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - fn powerline_dark_git_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Git, - enabled: true, - icon: IconConfig { - plain: "🌿".to_string(), - nerd_font: "\u{f02a2}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 255, - g: 255, - b: 255, - }), - text: Some(AnsiColor::Rgb { - r: 255, - g: 255, - b: 255, - }), - background: Some(AnsiColor::Rgb { - r: 64, - g: 64, - b: 64, - }), - }, - styles: TextStyleConfig::default(), - options: { - let mut opts = HashMap::new(); - opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); - opts - }, - } - } - - fn powerline_dark_usage_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Usage, - enabled: true, - icon: IconConfig { - plain: "⚡".to_string(), - nerd_font: "\u{f49b}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 209, - g: 213, - b: 219, - }), - text: Some(AnsiColor::Rgb { - r: 209, - g: 213, - b: 219, - }), - background: Some(AnsiColor::Rgb { - r: 55, - g: 65, - b: 81, - }), - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - fn powerline_dark_quota_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Quota, - enabled: true, - icon: IconConfig { - plain: "💰".to_string(), - nerd_font: "\u{f155}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 255, - g: 255, - b: 255, - }), - text: Some(AnsiColor::Rgb { - r: 255, - g: 255, - b: 255, - }), - background: Some(AnsiColor::Rgb { - r: 85, - g: 85, - b: 85, - }), - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - // Powerline Light theme pub fn get_powerline_light() -> Config { Config { style: StyleConfig { mode: StyleMode::NerdFont, - separator: "".to_string(), + separator: "".to_string(), }, segments: vec![ - Self::powerline_light_model_segment(), - Self::powerline_light_directory_segment(), - Self::powerline_light_git_segment(), - Self::powerline_light_usage_segment(), - Self::powerline_light_quota_segment(), + theme_powerline_light::model_segment(), + theme_powerline_light::directory_segment(), + theme_powerline_light::git_segment(), + theme_powerline_light::usage_segment(), + theme_powerline_light::cost_segment(), + theme_powerline_light::session_segment(), + theme_powerline_light::output_style_segment(), ], theme: "powerline-light".to_string(), } } - fn powerline_light_model_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Model, - enabled: true, - icon: IconConfig { - plain: "🤖".to_string(), - nerd_font: "\u{e26d}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { r: 0, g: 0, b: 0 }), - text: Some(AnsiColor::Rgb { r: 0, g: 0, b: 0 }), - background: Some(AnsiColor::Rgb { - r: 135, - g: 206, - b: 235, - }), - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - fn powerline_light_directory_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Directory, - enabled: true, - icon: IconConfig { - plain: "📁".to_string(), - nerd_font: "\u{f024b}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 255, - g: 255, - b: 255, - }), - text: Some(AnsiColor::Rgb { - r: 255, - g: 255, - b: 255, - }), - background: Some(AnsiColor::Rgb { - r: 255, - g: 107, - b: 71, - }), - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - fn powerline_light_git_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Git, - enabled: true, - icon: IconConfig { - plain: "🌿".to_string(), - nerd_font: "\u{f02a2}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 255, - g: 255, - b: 255, - }), - text: Some(AnsiColor::Rgb { - r: 255, - g: 255, - b: 255, - }), - background: Some(AnsiColor::Rgb { - r: 79, - g: 179, - b: 217, - }), - }, - styles: TextStyleConfig::default(), - options: { - let mut opts = HashMap::new(); - opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); - opts - }, - } - } - - fn powerline_light_usage_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Usage, - enabled: true, - icon: IconConfig { - plain: "⚡".to_string(), - nerd_font: "\u{f49b}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 255, - g: 255, - b: 255, - }), - text: Some(AnsiColor::Rgb { - r: 255, - g: 255, - b: 255, - }), - background: Some(AnsiColor::Rgb { - r: 107, - g: 114, - b: 128, - }), - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - fn powerline_light_quota_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Quota, - enabled: true, - icon: IconConfig { - plain: "💰".to_string(), - nerd_font: "\u{f155}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 255, - g: 255, - b: 255, - }), - text: Some(AnsiColor::Rgb { - r: 255, - g: 255, - b: 255, - }), - background: Some(AnsiColor::Rgb { - r: 76, - g: 175, - b: 80, - }), - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - // Powerline Rose Pine theme pub fn get_powerline_rose_pine() -> Config { Config { style: StyleConfig { mode: StyleMode::NerdFont, - separator: "".to_string(), + separator: "".to_string(), }, segments: vec![ - Self::powerline_rose_pine_model_segment(), - Self::powerline_rose_pine_directory_segment(), - Self::powerline_rose_pine_git_segment(), - Self::powerline_rose_pine_usage_segment(), - Self::powerline_rose_pine_quota_segment(), + theme_powerline_rose_pine::model_segment(), + theme_powerline_rose_pine::directory_segment(), + theme_powerline_rose_pine::git_segment(), + theme_powerline_rose_pine::usage_segment(), + theme_powerline_rose_pine::cost_segment(), + theme_powerline_rose_pine::session_segment(), + theme_powerline_rose_pine::output_style_segment(), ], theme: "powerline-rose-pine".to_string(), } } - fn powerline_rose_pine_model_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Model, - enabled: true, - icon: IconConfig { - plain: "🤖".to_string(), - nerd_font: "\u{e26d}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 235, - g: 188, - b: 186, - }), - text: Some(AnsiColor::Rgb { - r: 235, - g: 188, - b: 186, - }), - background: Some(AnsiColor::Rgb { - r: 25, - g: 23, - b: 36, - }), - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - fn powerline_rose_pine_directory_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Directory, - enabled: true, - icon: IconConfig { - plain: "📁".to_string(), - nerd_font: "\u{f024b}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 196, - g: 167, - b: 231, - }), - text: Some(AnsiColor::Rgb { - r: 196, - g: 167, - b: 231, - }), - background: Some(AnsiColor::Rgb { - r: 38, - g: 35, - b: 58, - }), - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - fn powerline_rose_pine_git_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Git, - enabled: true, - icon: IconConfig { - plain: "🌿".to_string(), - nerd_font: "\u{f02a2}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 156, - g: 207, - b: 216, - }), - text: Some(AnsiColor::Rgb { - r: 156, - g: 207, - b: 216, - }), - background: Some(AnsiColor::Rgb { - r: 31, - g: 29, - b: 46, - }), - }, - styles: TextStyleConfig::default(), - options: { - let mut opts = HashMap::new(); - opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); - opts - }, - } - } - - fn powerline_rose_pine_usage_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Usage, - enabled: true, - icon: IconConfig { - plain: "⚡".to_string(), - nerd_font: "\u{f49b}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 224, - g: 222, - b: 244, - }), - text: Some(AnsiColor::Rgb { - r: 224, - g: 222, - b: 244, - }), - background: Some(AnsiColor::Rgb { - r: 82, - g: 79, - b: 103, - }), - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - fn powerline_rose_pine_quota_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Quota, - enabled: true, - icon: IconConfig { - plain: "💰".to_string(), - nerd_font: "\u{f155}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 235, - g: 188, - b: 186, - }), - text: Some(AnsiColor::Rgb { - r: 235, - g: 188, - b: 186, - }), - background: Some(AnsiColor::Rgb { - r: 49, - g: 46, - b: 65, - }), - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - // Powerline Tokyo Night theme pub fn get_powerline_tokyo_night() -> Config { Config { style: StyleConfig { mode: StyleMode::NerdFont, - separator: "".to_string(), + separator: "".to_string(), }, segments: vec![ - Self::powerline_tokyo_night_model_segment(), - Self::powerline_tokyo_night_directory_segment(), - Self::powerline_tokyo_night_git_segment(), - Self::powerline_tokyo_night_usage_segment(), - Self::powerline_tokyo_night_quota_segment(), + theme_powerline_tokyo_night::model_segment(), + theme_powerline_tokyo_night::directory_segment(), + theme_powerline_tokyo_night::git_segment(), + theme_powerline_tokyo_night::usage_segment(), + theme_powerline_tokyo_night::cost_segment(), + theme_powerline_tokyo_night::session_segment(), + theme_powerline_tokyo_night::output_style_segment(), ], theme: "powerline-tokyo-night".to_string(), } } - - fn powerline_tokyo_night_model_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Model, - enabled: true, - icon: IconConfig { - plain: "🤖".to_string(), - nerd_font: "\u{e26d}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 252, - g: 167, - b: 234, - }), - text: Some(AnsiColor::Rgb { - r: 252, - g: 167, - b: 234, - }), - background: Some(AnsiColor::Rgb { - r: 25, - g: 27, - b: 41, - }), - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - fn powerline_tokyo_night_directory_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Directory, - enabled: true, - icon: IconConfig { - plain: "📁".to_string(), - nerd_font: "\u{f024b}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 130, - g: 170, - b: 255, - }), - text: Some(AnsiColor::Rgb { - r: 130, - g: 170, - b: 255, - }), - background: Some(AnsiColor::Rgb { - r: 47, - g: 51, - b: 77, - }), - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - fn powerline_tokyo_night_git_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Git, - enabled: true, - icon: IconConfig { - plain: "🌿".to_string(), - nerd_font: "\u{f02a2}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 195, - g: 232, - b: 141, - }), - text: Some(AnsiColor::Rgb { - r: 195, - g: 232, - b: 141, - }), - background: Some(AnsiColor::Rgb { - r: 30, - g: 32, - b: 48, - }), - }, - styles: TextStyleConfig::default(), - options: { - let mut opts = HashMap::new(); - opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); - opts - }, - } - } - - fn powerline_tokyo_night_usage_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Usage, - enabled: true, - icon: IconConfig { - plain: "⚡".to_string(), - nerd_font: "\u{f49b}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 192, - g: 202, - b: 245, - }), - text: Some(AnsiColor::Rgb { - r: 192, - g: 202, - b: 245, - }), - background: Some(AnsiColor::Rgb { - r: 61, - g: 89, - b: 161, - }), - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - fn powerline_tokyo_night_quota_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Quota, - enabled: true, - icon: IconConfig { - plain: "💰".to_string(), - nerd_font: "\u{f155}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Rgb { - r: 247, - g: 118, - b: 142, - }), - text: Some(AnsiColor::Rgb { - r: 247, - g: 118, - b: 142, - }), - background: Some(AnsiColor::Rgb { - r: 36, - g: 40, - b: 59, - }), - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } - - fn quota_segment() -> SegmentConfig { - SegmentConfig { - id: SegmentId::Quota, - enabled: true, - icon: IconConfig { - plain: "💰".to_string(), - nerd_font: "\u{f155}".to_string(), - }, - colors: ColorConfig { - icon: Some(AnsiColor::Color16 { c16: 11 }), // Yellow - text: Some(AnsiColor::Color16 { c16: 11 }), - background: None, - }, - styles: TextStyleConfig::default(), - options: HashMap::new(), - } - } } diff --git a/src/ui/themes/theme_cometix.rs b/src/ui/themes/theme_cometix.rs new file mode 100644 index 0000000..e1777ea --- /dev/null +++ b/src/ui/themes/theme_cometix.rs @@ -0,0 +1,134 @@ +use crate::config::{ + AnsiColor, ColorConfig, IconConfig, SegmentConfig, SegmentId, TextStyleConfig, +}; +use std::collections::HashMap; + +pub fn model_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Model, + enabled: true, + icon: IconConfig { + plain: "🤖".to_string(), + nerd_font: "\u{e26d}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 14 }), + text: Some(AnsiColor::Color16 { c16: 14 }), + background: None, + }, + styles: TextStyleConfig { text_bold: true }, + options: HashMap::new(), + } +} + +pub fn directory_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Directory, + enabled: true, + icon: IconConfig { + plain: "📁".to_string(), + nerd_font: "\u{f024b}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 11 }), + text: Some(AnsiColor::Color16 { c16: 10 }), + background: None, + }, + styles: TextStyleConfig { text_bold: true }, + options: HashMap::new(), + } +} + +pub fn git_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Git, + enabled: true, + icon: IconConfig { + plain: "🌿".to_string(), + nerd_font: "\u{f02a2}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 12 }), + text: Some(AnsiColor::Color16 { c16: 12 }), + background: None, + }, + styles: TextStyleConfig { text_bold: true }, + options: { + let mut opts = HashMap::new(); + opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); + opts + }, + } +} + +pub fn usage_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Usage, + enabled: true, + icon: IconConfig { + plain: "⚡️".to_string(), + nerd_font: "\u{f49b}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 13 }), + text: Some(AnsiColor::Color16 { c16: 13 }), + background: None, + }, + styles: TextStyleConfig { text_bold: true }, + options: HashMap::new(), + } +} + +pub fn cost_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Cost, + enabled: false, + icon: IconConfig { + plain: "💰".to_string(), + nerd_font: "\u{eec1}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 3 }), + text: Some(AnsiColor::Color16 { c16: 3 }), + background: None, + }, + styles: TextStyleConfig { text_bold: true }, + options: HashMap::new(), + } +} + +pub fn session_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Session, + enabled: false, + icon: IconConfig { + plain: "⏱️".to_string(), + nerd_font: "\u{f19bb}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 2 }), + text: Some(AnsiColor::Color16 { c16: 2 }), + background: None, + }, + styles: TextStyleConfig { text_bold: true }, + options: HashMap::new(), + } +} + +pub fn output_style_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::OutputStyle, + enabled: false, + icon: IconConfig { + plain: "🎯".to_string(), + nerd_font: "\u{f12f5}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 6 }), + text: Some(AnsiColor::Color16 { c16: 6 }), + background: None, + }, + styles: TextStyleConfig { text_bold: true }, + options: HashMap::new(), + } +} diff --git a/src/ui/themes/theme_default.rs b/src/ui/themes/theme_default.rs new file mode 100644 index 0000000..bc0a611 --- /dev/null +++ b/src/ui/themes/theme_default.rs @@ -0,0 +1,134 @@ +use crate::config::{ + AnsiColor, ColorConfig, IconConfig, SegmentConfig, SegmentId, TextStyleConfig, +}; +use std::collections::HashMap; + +pub fn model_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Model, + enabled: true, + icon: IconConfig { + plain: "🤖".to_string(), + nerd_font: "\u{e26d}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 14 }), // Cyan + text: Some(AnsiColor::Color16 { c16: 14 }), + background: None, + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn directory_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Directory, + enabled: true, + icon: IconConfig { + plain: "📁".to_string(), + nerd_font: "\u{f024b}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 11 }), // Yellow + text: Some(AnsiColor::Color16 { c16: 10 }), // Green + background: None, + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn git_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Git, + enabled: true, + icon: IconConfig { + plain: "🌿".to_string(), + nerd_font: "\u{f02a2}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 12 }), // Blue + text: Some(AnsiColor::Color16 { c16: 12 }), + background: None, + }, + styles: TextStyleConfig::default(), + options: { + let mut opts = HashMap::new(); + opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); + opts + }, + } +} + +pub fn usage_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Usage, + enabled: true, + icon: IconConfig { + plain: "⚡️".to_string(), + nerd_font: "\u{f49b}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 13 }), // Magenta + text: Some(AnsiColor::Color16 { c16: 13 }), + background: None, + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn cost_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Cost, + enabled: false, + icon: IconConfig { + plain: "💰".to_string(), + nerd_font: "\u{eec1}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 3 }), // Yellow + text: Some(AnsiColor::Color16 { c16: 3 }), + background: None, + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn session_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Session, + enabled: false, + icon: IconConfig { + plain: "⏱️".to_string(), + nerd_font: "\u{f19bb}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 2 }), // Green + text: Some(AnsiColor::Color16 { c16: 2 }), + background: None, + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn output_style_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::OutputStyle, + enabled: false, + icon: IconConfig { + plain: "🎯".to_string(), + nerd_font: "\u{f12f5}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 6 }), // Cyan + text: Some(AnsiColor::Color16 { c16: 6 }), + background: None, + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} diff --git a/src/ui/themes/theme_gruvbox.rs b/src/ui/themes/theme_gruvbox.rs new file mode 100644 index 0000000..a14861c --- /dev/null +++ b/src/ui/themes/theme_gruvbox.rs @@ -0,0 +1,134 @@ +use crate::config::{ + AnsiColor, ColorConfig, IconConfig, SegmentConfig, SegmentId, TextStyleConfig, +}; +use std::collections::HashMap; + +pub fn model_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Model, + enabled: true, + icon: IconConfig { + plain: "🤖".to_string(), + nerd_font: "\u{e26d}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color256 { c256: 208 }), // Gruvbox orange + text: Some(AnsiColor::Color256 { c256: 208 }), + background: None, + }, + styles: TextStyleConfig { text_bold: true }, + options: HashMap::new(), + } +} + +pub fn directory_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Directory, + enabled: true, + icon: IconConfig { + plain: "📁".to_string(), + nerd_font: "\u{f024b}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color256 { c256: 142 }), // Gruvbox green + text: Some(AnsiColor::Color256 { c256: 142 }), + background: None, + }, + styles: TextStyleConfig { text_bold: true }, + options: HashMap::new(), + } +} + +pub fn git_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Git, + enabled: true, + icon: IconConfig { + plain: "🌿".to_string(), + nerd_font: "\u{f02a2}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color256 { c256: 109 }), // Gruvbox cyan + text: Some(AnsiColor::Color256 { c256: 109 }), + background: None, + }, + styles: TextStyleConfig { text_bold: true }, + options: { + let mut opts = HashMap::new(); + opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); + opts + }, + } +} + +pub fn usage_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Usage, + enabled: true, + icon: IconConfig { + plain: "⚡️".to_string(), + nerd_font: "\u{f49b}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 5 }), + text: Some(AnsiColor::Color16 { c16: 5 }), + background: None, + }, + styles: TextStyleConfig { text_bold: true }, + options: HashMap::new(), + } +} + +pub fn cost_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Cost, + enabled: false, + icon: IconConfig { + plain: "💰".to_string(), + nerd_font: "\u{eec1}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color256 { c256: 214 }), // Gruvbox yellow + text: Some(AnsiColor::Color256 { c256: 214 }), + background: None, + }, + styles: TextStyleConfig { text_bold: true }, + options: HashMap::new(), + } +} + +pub fn session_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Session, + enabled: false, + icon: IconConfig { + plain: "⏱️".to_string(), + nerd_font: "\u{f19bb}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color256 { c256: 142 }), // Gruvbox green + text: Some(AnsiColor::Color256 { c256: 142 }), + background: None, + }, + styles: TextStyleConfig { text_bold: true }, + options: HashMap::new(), + } +} + +pub fn output_style_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::OutputStyle, + enabled: false, + icon: IconConfig { + plain: "🎯".to_string(), + nerd_font: "\u{f12f5}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color256 { c256: 109 }), // Gruvbox cyan + text: Some(AnsiColor::Color256 { c256: 109 }), + background: None, + }, + styles: TextStyleConfig { text_bold: true }, + options: HashMap::new(), + } +} diff --git a/src/ui/themes/theme_minimal.rs b/src/ui/themes/theme_minimal.rs new file mode 100644 index 0000000..1b191a5 --- /dev/null +++ b/src/ui/themes/theme_minimal.rs @@ -0,0 +1,134 @@ +use crate::config::{ + AnsiColor, ColorConfig, IconConfig, SegmentConfig, SegmentId, TextStyleConfig, +}; +use std::collections::HashMap; + +pub fn model_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Model, + enabled: true, + icon: IconConfig { + plain: "✽".to_string(), + nerd_font: "\u{f2d0}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 14 }), + text: Some(AnsiColor::Color16 { c16: 14 }), + background: None, + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn directory_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Directory, + enabled: true, + icon: IconConfig { + plain: "◐".to_string(), + nerd_font: "\u{f024b}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 11 }), + text: Some(AnsiColor::Color16 { c16: 10 }), + background: None, + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn git_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Git, + enabled: true, + icon: IconConfig { + plain: "※".to_string(), + nerd_font: "\u{f02a2}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 12 }), + text: Some(AnsiColor::Color16 { c16: 12 }), + background: None, + }, + styles: TextStyleConfig::default(), + options: { + let mut opts = HashMap::new(); + opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); + opts + }, + } +} + +pub fn usage_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Usage, + enabled: true, + icon: IconConfig { + plain: "◐".to_string(), + nerd_font: "\u{f49b}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 13 }), + text: Some(AnsiColor::Color16 { c16: 13 }), + background: None, + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn cost_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Cost, + enabled: false, + icon: IconConfig { + plain: "💰".to_string(), + nerd_font: "\u{eec1}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 3 }), + text: Some(AnsiColor::Color16 { c16: 3 }), + background: None, + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn session_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Session, + enabled: false, + icon: IconConfig { + plain: "⏱️".to_string(), + nerd_font: "\u{f19bb}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 2 }), + text: Some(AnsiColor::Color16 { c16: 2 }), + background: None, + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn output_style_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::OutputStyle, + enabled: false, + icon: IconConfig { + plain: "🎯".to_string(), + nerd_font: "\u{f12f5}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Color16 { c16: 6 }), + text: Some(AnsiColor::Color16 { c16: 6 }), + background: None, + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} diff --git a/src/ui/themes/theme_nord.rs b/src/ui/themes/theme_nord.rs new file mode 100644 index 0000000..4426a3b --- /dev/null +++ b/src/ui/themes/theme_nord.rs @@ -0,0 +1,218 @@ +use crate::config::{ + AnsiColor, ColorConfig, IconConfig, SegmentConfig, SegmentId, TextStyleConfig, +}; +use std::collections::HashMap; + +pub fn model_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Model, + enabled: true, + icon: IconConfig { + plain: "🤖".to_string(), + nerd_font: "\u{e26d}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 46, + g: 52, + b: 64, + }), + text: Some(AnsiColor::Rgb { + r: 46, + g: 52, + b: 64, + }), + background: Some(AnsiColor::Rgb { + r: 136, + g: 192, + b: 208, + }), + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn directory_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Directory, + enabled: true, + icon: IconConfig { + plain: "📁".to_string(), + nerd_font: "\u{f024b}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 46, + g: 52, + b: 64, + }), + text: Some(AnsiColor::Rgb { + r: 46, + g: 52, + b: 64, + }), + background: Some(AnsiColor::Rgb { + r: 163, + g: 190, + b: 140, + }), + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn git_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Git, + enabled: true, + icon: IconConfig { + plain: "🌿".to_string(), + nerd_font: "\u{f02a2}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 46, + g: 52, + b: 64, + }), + text: Some(AnsiColor::Rgb { + r: 46, + g: 52, + b: 64, + }), + background: Some(AnsiColor::Rgb { + r: 129, + g: 161, + b: 193, + }), + }, + styles: TextStyleConfig::default(), + options: { + let mut opts = HashMap::new(); + opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); + opts + }, + } +} + +pub fn usage_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Usage, + enabled: true, + icon: IconConfig { + plain: "⚡️".to_string(), + nerd_font: "\u{f49b}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 46, + g: 52, + b: 64, + }), + text: Some(AnsiColor::Rgb { + r: 46, + g: 52, + b: 64, + }), + background: Some(AnsiColor::Rgb { + r: 180, + g: 142, + b: 173, + }), + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn cost_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Cost, + enabled: false, + icon: IconConfig { + plain: "💰".to_string(), + nerd_font: "\u{eec1}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 46, + g: 52, + b: 64, + }), + text: Some(AnsiColor::Rgb { + r: 46, + g: 52, + b: 64, + }), + background: Some(AnsiColor::Rgb { + r: 235, + g: 203, + b: 139, + }), // Nord yellow background + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn session_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Session, + enabled: false, + icon: IconConfig { + plain: "⏱️".to_string(), + nerd_font: "\u{f19bb}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 46, + g: 52, + b: 64, + }), + text: Some(AnsiColor::Rgb { + r: 46, + g: 52, + b: 64, + }), + background: Some(AnsiColor::Rgb { + r: 163, + g: 190, + b: 140, + }), // Nord green background + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn output_style_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::OutputStyle, + enabled: false, + icon: IconConfig { + plain: "🎯".to_string(), + nerd_font: "\u{f12f5}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 46, + g: 52, + b: 64, + }), + text: Some(AnsiColor::Rgb { + r: 46, + g: 52, + b: 64, + }), + background: Some(AnsiColor::Rgb { + r: 136, + g: 192, + b: 208, + }), // Nord cyan background + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} diff --git a/src/ui/themes/theme_powerline_dark.rs b/src/ui/themes/theme_powerline_dark.rs new file mode 100644 index 0000000..5a7e860 --- /dev/null +++ b/src/ui/themes/theme_powerline_dark.rs @@ -0,0 +1,218 @@ +use crate::config::{ + AnsiColor, ColorConfig, IconConfig, SegmentConfig, SegmentId, TextStyleConfig, +}; +use std::collections::HashMap; + +pub fn model_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Model, + enabled: true, + icon: IconConfig { + plain: "🤖".to_string(), + nerd_font: "\u{e26d}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 255, + g: 255, + b: 255, + }), + text: Some(AnsiColor::Rgb { + r: 255, + g: 255, + b: 255, + }), + background: Some(AnsiColor::Rgb { + r: 45, + g: 45, + b: 45, + }), + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn directory_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Directory, + enabled: true, + icon: IconConfig { + plain: "📁".to_string(), + nerd_font: "\u{f024b}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 255, + g: 255, + b: 255, + }), + text: Some(AnsiColor::Rgb { + r: 255, + g: 255, + b: 255, + }), + background: Some(AnsiColor::Rgb { + r: 139, + g: 69, + b: 19, + }), + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn git_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Git, + enabled: true, + icon: IconConfig { + plain: "🌿".to_string(), + nerd_font: "\u{f02a2}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 255, + g: 255, + b: 255, + }), + text: Some(AnsiColor::Rgb { + r: 255, + g: 255, + b: 255, + }), + background: Some(AnsiColor::Rgb { + r: 64, + g: 64, + b: 64, + }), + }, + styles: TextStyleConfig::default(), + options: { + let mut opts = HashMap::new(); + opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); + opts + }, + } +} + +pub fn usage_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Usage, + enabled: true, + icon: IconConfig { + plain: "⚡️".to_string(), + nerd_font: "\u{f49b}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 209, + g: 213, + b: 219, + }), + text: Some(AnsiColor::Rgb { + r: 209, + g: 213, + b: 219, + }), + background: Some(AnsiColor::Rgb { + r: 55, + g: 65, + b: 81, + }), + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn cost_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Cost, + enabled: false, + icon: IconConfig { + plain: "💰".to_string(), + nerd_font: "\u{eec1}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 229, + g: 192, + b: 123, + }), + text: Some(AnsiColor::Rgb { + r: 229, + g: 192, + b: 123, + }), + background: Some(AnsiColor::Rgb { + r: 40, + g: 44, + b: 52, + }), // Powerline dark background + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn session_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Session, + enabled: false, + icon: IconConfig { + plain: "⏱️".to_string(), + nerd_font: "\u{f19bb}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 163, + g: 190, + b: 140, + }), + text: Some(AnsiColor::Rgb { + r: 163, + g: 190, + b: 140, + }), + background: Some(AnsiColor::Rgb { + r: 45, + g: 50, + b: 59, + }), // Powerline darker background + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn output_style_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::OutputStyle, + enabled: false, + icon: IconConfig { + plain: "🎯".to_string(), + nerd_font: "\u{f12f5}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 129, + g: 161, + b: 193, + }), + text: Some(AnsiColor::Rgb { + r: 129, + g: 161, + b: 193, + }), + background: Some(AnsiColor::Rgb { + r: 50, + g: 56, + b: 66, + }), // Powerline darkest background + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} diff --git a/src/ui/themes/theme_powerline_light.rs b/src/ui/themes/theme_powerline_light.rs new file mode 100644 index 0000000..58b2c4b --- /dev/null +++ b/src/ui/themes/theme_powerline_light.rs @@ -0,0 +1,210 @@ +use crate::config::{ + AnsiColor, ColorConfig, IconConfig, SegmentConfig, SegmentId, TextStyleConfig, +}; +use std::collections::HashMap; + +pub fn model_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Model, + enabled: true, + icon: IconConfig { + plain: "🤖".to_string(), + nerd_font: "\u{e26d}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { r: 0, g: 0, b: 0 }), + text: Some(AnsiColor::Rgb { r: 0, g: 0, b: 0 }), + background: Some(AnsiColor::Rgb { + r: 135, + g: 206, + b: 235, + }), + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn directory_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Directory, + enabled: true, + icon: IconConfig { + plain: "📁".to_string(), + nerd_font: "\u{f024b}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 255, + g: 255, + b: 255, + }), + text: Some(AnsiColor::Rgb { + r: 255, + g: 255, + b: 255, + }), + background: Some(AnsiColor::Rgb { + r: 255, + g: 107, + b: 71, + }), + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn git_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Git, + enabled: true, + icon: IconConfig { + plain: "🌿".to_string(), + nerd_font: "\u{f02a2}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 255, + g: 255, + b: 255, + }), + text: Some(AnsiColor::Rgb { + r: 255, + g: 255, + b: 255, + }), + background: Some(AnsiColor::Rgb { + r: 79, + g: 179, + b: 217, + }), + }, + styles: TextStyleConfig::default(), + options: { + let mut opts = HashMap::new(); + opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); + opts + }, + } +} + +pub fn usage_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Usage, + enabled: true, + icon: IconConfig { + plain: "⚡️".to_string(), + nerd_font: "\u{f49b}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 255, + g: 255, + b: 255, + }), + text: Some(AnsiColor::Rgb { + r: 255, + g: 255, + b: 255, + }), + background: Some(AnsiColor::Rgb { + r: 107, + g: 114, + b: 128, + }), + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn cost_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Cost, + enabled: false, + icon: IconConfig { + plain: "💰".to_string(), + nerd_font: "\u{eec1}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 255, + g: 255, + b: 255, + }), + text: Some(AnsiColor::Rgb { + r: 255, + g: 255, + b: 255, + }), + background: Some(AnsiColor::Rgb { + r: 255, + g: 193, + b: 7, + }), + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn session_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Session, + enabled: false, + icon: IconConfig { + plain: "⏱️".to_string(), + nerd_font: "\u{f19bb}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 255, + g: 255, + b: 255, + }), + text: Some(AnsiColor::Rgb { + r: 255, + g: 255, + b: 255, + }), + background: Some(AnsiColor::Rgb { + r: 40, + g: 167, + b: 69, + }), + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn output_style_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::OutputStyle, + enabled: false, + icon: IconConfig { + plain: "🎯".to_string(), + nerd_font: "\u{f12f5}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 255, + g: 255, + b: 255, + }), + text: Some(AnsiColor::Rgb { + r: 255, + g: 255, + b: 255, + }), + background: Some(AnsiColor::Rgb { + r: 32, + g: 201, + b: 151, + }), + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} diff --git a/src/ui/themes/theme_powerline_rose_pine.rs b/src/ui/themes/theme_powerline_rose_pine.rs new file mode 100644 index 0000000..45a4060 --- /dev/null +++ b/src/ui/themes/theme_powerline_rose_pine.rs @@ -0,0 +1,218 @@ +use crate::config::{ + AnsiColor, ColorConfig, IconConfig, SegmentConfig, SegmentId, TextStyleConfig, +}; +use std::collections::HashMap; + +pub fn model_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Model, + enabled: true, + icon: IconConfig { + plain: "🤖".to_string(), + nerd_font: "\u{e26d}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 235, + g: 188, + b: 186, + }), + text: Some(AnsiColor::Rgb { + r: 235, + g: 188, + b: 186, + }), + background: Some(AnsiColor::Rgb { + r: 25, + g: 23, + b: 36, + }), + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn directory_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Directory, + enabled: true, + icon: IconConfig { + plain: "📁".to_string(), + nerd_font: "\u{f024b}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 196, + g: 167, + b: 231, + }), + text: Some(AnsiColor::Rgb { + r: 196, + g: 167, + b: 231, + }), + background: Some(AnsiColor::Rgb { + r: 38, + g: 35, + b: 58, + }), + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn git_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Git, + enabled: true, + icon: IconConfig { + plain: "🌿".to_string(), + nerd_font: "\u{f02a2}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 156, + g: 207, + b: 216, + }), + text: Some(AnsiColor::Rgb { + r: 156, + g: 207, + b: 216, + }), + background: Some(AnsiColor::Rgb { + r: 31, + g: 29, + b: 46, + }), + }, + styles: TextStyleConfig::default(), + options: { + let mut opts = HashMap::new(); + opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); + opts + }, + } +} + +pub fn usage_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Usage, + enabled: true, + icon: IconConfig { + plain: "⚡️".to_string(), + nerd_font: "\u{f49b}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 224, + g: 222, + b: 244, + }), + text: Some(AnsiColor::Rgb { + r: 224, + g: 222, + b: 244, + }), + background: Some(AnsiColor::Rgb { + r: 82, + g: 79, + b: 103, + }), + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn cost_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Cost, + enabled: false, + icon: IconConfig { + plain: "💰".to_string(), + nerd_font: "\u{eec1}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 246, + g: 193, + b: 119, + }), + text: Some(AnsiColor::Rgb { + r: 246, + g: 193, + b: 119, + }), + background: Some(AnsiColor::Rgb { + r: 35, + g: 33, + b: 54, + }), // Rose Pine dark background + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn session_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Session, + enabled: false, + icon: IconConfig { + plain: "⏱️".to_string(), + nerd_font: "\u{f19bb}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 156, + g: 207, + b: 216, + }), + text: Some(AnsiColor::Rgb { + r: 156, + g: 207, + b: 216, + }), + background: Some(AnsiColor::Rgb { + r: 42, + g: 39, + b: 63, + }), // Rose Pine darker background + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn output_style_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::OutputStyle, + enabled: false, + icon: IconConfig { + plain: "🎯".to_string(), + nerd_font: "\u{f12f5}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 49, + g: 116, + b: 143, + }), + text: Some(AnsiColor::Rgb { + r: 49, + g: 116, + b: 143, + }), + background: Some(AnsiColor::Rgb { + r: 38, + g: 35, + b: 58, + }), // Rose Pine darkest background + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} diff --git a/src/ui/themes/theme_powerline_tokyo_night.rs b/src/ui/themes/theme_powerline_tokyo_night.rs new file mode 100644 index 0000000..ecf81bb --- /dev/null +++ b/src/ui/themes/theme_powerline_tokyo_night.rs @@ -0,0 +1,218 @@ +use crate::config::{ + AnsiColor, ColorConfig, IconConfig, SegmentConfig, SegmentId, TextStyleConfig, +}; +use std::collections::HashMap; + +pub fn model_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Model, + enabled: true, + icon: IconConfig { + plain: "🤖".to_string(), + nerd_font: "\u{e26d}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 252, + g: 167, + b: 234, + }), + text: Some(AnsiColor::Rgb { + r: 252, + g: 167, + b: 234, + }), + background: Some(AnsiColor::Rgb { + r: 25, + g: 27, + b: 41, + }), + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn directory_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Directory, + enabled: true, + icon: IconConfig { + plain: "📁".to_string(), + nerd_font: "\u{f024b}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 130, + g: 170, + b: 255, + }), + text: Some(AnsiColor::Rgb { + r: 130, + g: 170, + b: 255, + }), + background: Some(AnsiColor::Rgb { + r: 47, + g: 51, + b: 77, + }), + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn git_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Git, + enabled: true, + icon: IconConfig { + plain: "🌿".to_string(), + nerd_font: "\u{f02a2}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 195, + g: 232, + b: 141, + }), + text: Some(AnsiColor::Rgb { + r: 195, + g: 232, + b: 141, + }), + background: Some(AnsiColor::Rgb { + r: 30, + g: 32, + b: 48, + }), + }, + styles: TextStyleConfig::default(), + options: { + let mut opts = HashMap::new(); + opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); + opts + }, + } +} + +pub fn usage_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Usage, + enabled: true, + icon: IconConfig { + plain: "⚡️️".to_string(), + nerd_font: "\u{f49b}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 192, + g: 202, + b: 245, + }), + text: Some(AnsiColor::Rgb { + r: 192, + g: 202, + b: 245, + }), + background: Some(AnsiColor::Rgb { + r: 61, + g: 89, + b: 161, + }), + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn cost_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Cost, + enabled: false, + icon: IconConfig { + plain: "💰".to_string(), + nerd_font: "\u{eec1}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 224, + g: 175, + b: 104, + }), + text: Some(AnsiColor::Rgb { + r: 224, + g: 175, + b: 104, + }), + background: Some(AnsiColor::Rgb { + r: 36, + g: 40, + b: 59, + }), // Tokyo Night dark background + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn session_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::Session, + enabled: false, + icon: IconConfig { + plain: "⏱️".to_string(), + nerd_font: "\u{f1ad3}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 158, + g: 206, + b: 106, + }), + text: Some(AnsiColor::Rgb { + r: 158, + g: 206, + b: 106, + }), + background: Some(AnsiColor::Rgb { + r: 41, + g: 46, + b: 66, + }), // Tokyo Night darker background + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} + +pub fn output_style_segment() -> SegmentConfig { + SegmentConfig { + id: SegmentId::OutputStyle, + enabled: false, + icon: IconConfig { + plain: "🎯".to_string(), + nerd_font: "\u{f12f5}".to_string(), + }, + colors: ColorConfig { + icon: Some(AnsiColor::Rgb { + r: 125, + g: 207, + b: 255, + }), + text: Some(AnsiColor::Rgb { + r: 125, + g: 207, + b: 255, + }), + background: Some(AnsiColor::Rgb { + r: 32, + g: 35, + b: 52, + }), // Tokyo Night darkest background + }, + styles: TextStyleConfig::default(), + options: HashMap::new(), + } +} diff --git a/src/utils/claude_code_patcher.rs b/src/utils/claude_code_patcher.rs new file mode 100644 index 0000000..bb729d0 --- /dev/null +++ b/src/utils/claude_code_patcher.rs @@ -0,0 +1,294 @@ +use regex::Regex; +use std::fs; +use std::path::Path; + +#[derive(Debug, Clone)] +pub struct LocationResult { + pub start_index: usize, + pub end_index: usize, + pub variable_name: Option, +} + +#[derive(Debug)] +pub struct ClaudeCodePatcher { + file_content: String, + file_path: String, +} + +impl ClaudeCodePatcher { + pub fn new>(file_path: P) -> Result> { + let path = file_path.as_ref(); + let content = fs::read_to_string(path)?; + + Ok(Self { + file_content: content, + file_path: path.to_string_lossy().to_string(), + }) + } + + /// Find the verbose property location in Claude Code's cli.js + /// Based on the pattern from patching.ts getVerbosePropertyLocation function + pub fn get_verbose_property_location(&self) -> Option { + // Step 1: Find createElement pattern with spinnerTip and overrideMessage + let create_element_pattern = + Regex::new(r"createElement\([$\w]+,\{[^}]+spinnerTip[^}]+overrideMessage[^}]+\}") + .ok()?; + + let create_element_match = create_element_pattern.find(&self.file_content)?; + let extracted_string = + &self.file_content[create_element_match.start()..create_element_match.end()]; + + println!( + "Found createElement match at: {}-{}", + create_element_match.start(), + create_element_match.end() + ); + println!( + "Extracted string: {}", + &extracted_string[..std::cmp::min(200, extracted_string.len())] + ); + + // Step 2: Find verbose property within the createElement match + let verbose_pattern = Regex::new(r"verbose:[^,}]+").ok()?; + let verbose_match = verbose_pattern.find(extracted_string)?; + + println!( + "Found verbose match at: {}-{}", + verbose_match.start(), + verbose_match.end() + ); + println!("Verbose string: {}", verbose_match.as_str()); + + // Calculate absolute positions in the original file + let absolute_verbose_start = create_element_match.start() + verbose_match.start(); + let absolute_verbose_end = absolute_verbose_start + verbose_match.len(); + + Some(LocationResult { + start_index: absolute_verbose_start, + end_index: absolute_verbose_end, + variable_name: None, + }) + } + + /// Write the verbose property with new value + pub fn write_verbose_property( + &mut self, + value: bool, + ) -> Result<(), Box> { + let location = self + .get_verbose_property_location() + .ok_or("Failed to find verbose property location")?; + + let new_code = format!("verbose:{}", value); + + let new_content = format!( + "{}{}{}", + &self.file_content[..location.start_index], + new_code, + &self.file_content[location.end_index..] + ); + + self.show_diff(&new_code, location.start_index, location.end_index); + self.file_content = new_content; + + Ok(()) + } + + /// Save the modified content back to file + pub fn save(&self) -> Result<(), Box> { + fs::write(&self.file_path, &self.file_content)?; + Ok(()) + } + + /// Get a reference to the file content (for testing purposes) + pub fn get_file_content(&self) -> &str { + &self.file_content + } + + /// Show a diff of the changes (for debugging) + fn show_diff(&self, injected_text: &str, start_index: usize, end_index: usize) { + let context_start = start_index.saturating_sub(50); + let context_end_old = std::cmp::min(self.file_content.len(), end_index + 50); + + let old_before = &self.file_content[context_start..start_index]; + let old_changed = &self.file_content[start_index..end_index]; + let old_after = &self.file_content[end_index..context_end_old]; + + println!("\n--- Verbose Property Diff ---"); + println!( + "OLD: {}\x1b[31m{}\x1b[0m{}", + old_before, old_changed, old_after + ); + println!( + "NEW: {}\x1b[32m{}\x1b[0m{}", + old_before, injected_text, old_after + ); + println!("--- End Diff ---\n"); + } + + /// Find the context low message location in Claude Code's cli.js + /// Pattern: "Context low (",B,"% remaining) · Run /compact to compact & continue" + /// where B is a variable name + pub fn get_context_low_message_location(&self) -> Option { + // Pattern to match: "Context low (",{variable},"% remaining) · Run /compact to compact & continue" + let context_low_pattern = Regex::new( + r#""Context low \(",([^,]+),"% remaining\) · Run /compact to compact & continue""#, + ) + .ok()?; + + let context_low_match = context_low_pattern.find(&self.file_content)?; + + println!( + "Found context low match at: {}-{}", + context_low_match.start(), + context_low_match.end() + ); + println!("Context low string: {}", context_low_match.as_str()); + + // Extract the variable name from the capture group + let captures = context_low_pattern.captures(&self.file_content)?; + let variable_name = captures.get(1)?.as_str(); + + println!("Variable name: {}", variable_name); + + Some(LocationResult { + start_index: context_low_match.start(), + end_index: context_low_match.end(), + variable_name: Some(variable_name.to_string()), + }) + } + + /// Core robust function locator using anchor-based expansion + /// Uses stable text patterns to survive Claude Code version updates + pub fn find_context_low_function_robust(&self) -> Option { + // Step 1: Locate stable anchor text that survives obfuscation + let primary_anchor = "Context low ("; + let anchor_pos = self.file_content.find(primary_anchor)?; + + // Step 2: Search backward within reasonable range to find function declarations + let search_range = 800; // Optimized range based on actual function size (~466 chars) + let search_start = anchor_pos.saturating_sub(search_range); + let backward_text = &self.file_content[search_start..anchor_pos]; + + // Find the function declaration that contains our anchor + let mut function_candidates = Vec::new(); + let mut start = 0; + + while let Some(func_pos) = backward_text[start..].find("function ") { + let absolute_func_pos = search_start + start + func_pos; + + // Check if this function contains the expected stable patterns + let func_to_anchor_text = &self.file_content[absolute_func_pos..anchor_pos + 100]; + + if func_to_anchor_text.contains("tokenUsage:") { + function_candidates.push(absolute_func_pos); + println!("Found function candidate at: {}", absolute_func_pos); + } + + start += func_pos + 9; // Move past "function " + } + + // Use the closest function to anchor (last candidate found) + if let Some(&func_start) = function_candidates.last() { + println!("Selected function start at: {}", func_start); + + // We only need the function start for condition replacement + // Return a minimal range that includes the condition + let condition_search_end = anchor_pos + 100; // Small range after anchor + + Some(LocationResult { + start_index: func_start, + end_index: condition_search_end, + variable_name: Some("context_function".to_string()), + }) + } else { + println!("❌ No suitable function candidate found"); + None + } + } + + /// Core robust condition locator that finds the if statement to patch + /// Returns the exact location of 'if(!Q||D)return null' for replacement with 'if(true)return null' + pub fn get_context_low_condition_location_robust(&self) -> Option { + // Find the function using stable patterns + let function_location = self.find_context_low_function_robust()?; + let function_content = + &self.file_content[function_location.start_index..function_location.end_index]; + + // Look for if condition pattern using regex - match any condition that returns null + let if_pattern = Regex::new(r"if\([^)]+\)return null").ok()?; + + if let Some(if_match) = if_pattern.find(function_content) { + let absolute_start = function_location.start_index + if_match.start(); + let absolute_end = function_location.start_index + if_match.end(); + + println!("Found if condition: '{}'", if_match.as_str()); + + Some(LocationResult { + start_index: absolute_start, + end_index: absolute_end, + variable_name: Some(if_match.as_str().to_string()), + }) + } else { + println!("❌ Could not find if condition in context function"); + None + } + } + + /// Disable context low warnings by modifying the if condition to always return null + /// Uses robust pattern matching based on stable identifiers + pub fn disable_context_low_warnings(&mut self) -> Result<(), Box> { + if let Some(location) = self.get_context_low_condition_location_robust() { + let replacement_condition = "if(true)return null"; + + let new_content = format!( + "{}{}{}", + &self.file_content[..location.start_index], + replacement_condition, + &self.file_content[location.end_index..] + ); + + self.show_diff( + replacement_condition, + location.start_index, + location.end_index, + ); + self.file_content = new_content; + + println!("✅ Context low warnings disabled successfully"); + Ok(()) + } else { + Err("Could not locate context low condition using robust method".into()) + } + } + + /// Write a replacement for the context low message + pub fn write_context_low_message( + &mut self, + new_message: &str, + variable_name: &str, + ) -> Result<(), Box> { + let location = self + .get_context_low_message_location() + .ok_or("Failed to find context low message location")?; + + let new_code = format!( + r#""{}","{}","{}""#, + new_message.split(',').nth(0).unwrap_or(new_message), + variable_name, + new_message.split(',').nth(1).unwrap_or("") + ); + + let new_content = format!( + "{}{}{}", + &self.file_content[..location.start_index], + new_code, + &self.file_content[location.end_index..] + ); + + self.show_diff(&new_code, location.start_index, location.end_index); + self.file_content = new_content; + + Ok(()) + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..f58fce4 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,3 @@ +pub mod claude_code_patcher; + +pub use claude_code_patcher::{ClaudeCodePatcher, LocationResult};