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