├── assets └── img1.png ├── src ├── core │ ├── mod.rs │ └── segments │ │ ├── update.rs │ │ ├── output_style.rs │ │ ├── mod.rs │ │ ├── cost.rs │ │ ├── model.rs │ │ ├── directory.rs │ │ ├── session.rs │ │ ├── git.rs │ │ ├── context_window.rs │ │ └── usage.rs ├── utils │ ├── mod.rs │ └── credentials.rs ├── lib.rs ├── config │ ├── mod.rs │ ├── defaults.rs │ ├── loader.rs │ └── models.rs ├── ui │ ├── components │ │ ├── mod.rs │ │ ├── editor.rs │ │ ├── theme_selector.rs │ │ ├── segment_list.rs │ │ ├── name_input.rs │ │ ├── help.rs │ │ ├── separator_editor.rs │ │ └── preview.rs │ ├── themes │ │ ├── mod.rs │ │ ├── theme_minimal.rs │ │ ├── theme_cometix.rs │ │ ├── theme_default.rs │ │ ├── theme_gruvbox.rs │ │ ├── theme_powerline_light.rs │ │ ├── theme_nord.rs │ │ ├── theme_powerline_dark.rs │ │ ├── theme_powerline_rose_pine.rs │ │ ├── theme_powerline_tokyo_night.rs │ │ └── presets.rs │ ├── mod.rs │ ├── events.rs │ └── layout.rs ├── cli.rs └── main.rs ├── .gitignore ├── lefthook.yml ├── npm ├── platforms │ ├── linux-x64 │ │ └── package.json │ ├── darwin-x64 │ │ └── package.json │ ├── win32-x64 │ │ └── package.json │ ├── darwin-arm64 │ │ └── package.json │ └── linux-x64-musl │ │ └── package.json ├── main │ ├── package.json │ ├── README.md │ ├── bin │ │ └── ccline.js │ └── scripts │ │ └── postinstall.js └── scripts │ └── prepare-packages.js ├── Cargo.toml ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── README.zh.md ├── README.md └── CHANGELOG.md /assets/img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haleclipse/CCometixLine/HEAD/assets/img1.png -------------------------------------------------------------------------------- /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod segments; 2 | pub mod statusline; 3 | 4 | pub use statusline::{collect_all_segments, StatusLineGenerator}; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | # Documentation and development files 4 | claude-powerline-features.md 5 | CLAUDE.md 6 | PRD.md 7 | .claude/ 8 | tests/ -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod claude_code_patcher; 2 | pub mod credentials; 3 | 4 | pub use claude_code_patcher::{ClaudeCodePatcher, LocationResult}; 5 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod cli; 2 | pub mod config; 3 | pub mod core; 4 | pub mod ui; 5 | pub mod utils; 6 | 7 | #[cfg(feature = "self-update")] 8 | pub mod updater; 9 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | fmt-check: 5 | run: cargo fmt --all -- --check 6 | clippy: 7 | run: cargo clippy -- -D warnings 8 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod defaults; 2 | pub mod loader; 3 | pub mod models; 4 | pub mod types; 5 | 6 | pub use loader::{ConfigLoader, InitResult}; 7 | pub use models::*; 8 | pub use types::*; 9 | -------------------------------------------------------------------------------- /src/ui/components/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod color_picker; 2 | pub mod editor; 3 | pub mod help; 4 | pub mod icon_selector; 5 | pub mod name_input; 6 | pub mod preview; 7 | pub mod segment_list; 8 | pub mod separator_editor; 9 | pub mod settings; 10 | pub mod theme_selector; 11 | -------------------------------------------------------------------------------- /src/ui/themes/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod presets; 2 | pub mod theme_cometix; 3 | pub mod theme_default; 4 | pub mod theme_gruvbox; 5 | pub mod theme_minimal; 6 | pub mod theme_nord; 7 | pub mod theme_powerline_dark; 8 | pub mod theme_powerline_light; 9 | pub mod theme_powerline_rose_pine; 10 | pub mod theme_powerline_tokyo_night; 11 | 12 | pub use presets::*; 13 | -------------------------------------------------------------------------------- /src/config/defaults.rs: -------------------------------------------------------------------------------- 1 | // Legacy defaults - now using ui/themes/presets.rs for configuration 2 | // This file kept for backward compatibility 3 | 4 | use super::types::Config; 5 | 6 | impl Default for Config { 7 | fn default() -> Self { 8 | // Use the theme presets as the source of truth 9 | crate::ui::themes::ThemePresets::get_default() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /npm/platforms/linux-x64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cometix/ccline-linux-x64", 3 | "version": "0.0.0", 4 | "description": "Linux x64 binary for CCometixLine", 5 | "files": ["ccline"], 6 | "os": ["linux"], 7 | "cpu": ["x64"], 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/Haleclipse/CCometixLine" 11 | }, 12 | "author": "Haleclipse", 13 | "license": "MIT" 14 | } -------------------------------------------------------------------------------- /npm/platforms/darwin-x64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cometix/ccline-darwin-x64", 3 | "version": "0.0.0", 4 | "description": "macOS x64 binary for CCometixLine", 5 | "files": ["ccline"], 6 | "os": ["darwin"], 7 | "cpu": ["x64"], 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/Haleclipse/CCometixLine" 11 | }, 12 | "author": "Haleclipse", 13 | "license": "MIT" 14 | } -------------------------------------------------------------------------------- /npm/platforms/win32-x64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cometix/ccline-win32-x64", 3 | "version": "0.0.0", 4 | "description": "Windows x64 binary for CCometixLine", 5 | "files": ["ccline.exe"], 6 | "os": ["win32"], 7 | "cpu": ["x64"], 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/Haleclipse/CCometixLine" 11 | }, 12 | "author": "Haleclipse", 13 | "license": "MIT" 14 | } -------------------------------------------------------------------------------- /npm/platforms/darwin-arm64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cometix/ccline-darwin-arm64", 3 | "version": "0.0.0", 4 | "description": "macOS ARM64 binary for CCometixLine", 5 | "files": ["ccline"], 6 | "os": ["darwin"], 7 | "cpu": ["arm64"], 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/Haleclipse/CCometixLine" 11 | }, 12 | "author": "Haleclipse", 13 | "license": "MIT" 14 | } -------------------------------------------------------------------------------- /npm/platforms/linux-x64-musl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cometix/ccline-linux-x64-musl", 3 | "version": "0.0.0", 4 | "description": "Linux x64 static binary for CCometixLine (musl libc)", 5 | "files": ["ccline"], 6 | "os": ["linux"], 7 | "cpu": ["x64"], 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/Haleclipse/CCometixLine" 11 | }, 12 | "author": "Haleclipse", 13 | "license": "MIT" 14 | } -------------------------------------------------------------------------------- /src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "tui")] 2 | pub mod app; 3 | #[cfg(feature = "tui")] 4 | pub mod components; 5 | #[cfg(feature = "tui")] 6 | pub mod events; 7 | #[cfg(feature = "tui")] 8 | pub mod layout; 9 | #[cfg(feature = "tui")] 10 | pub mod main_menu; 11 | #[cfg(feature = "tui")] 12 | pub mod themes; 13 | 14 | #[cfg(feature = "tui")] 15 | pub use app::App; 16 | #[cfg(feature = "tui")] 17 | pub use main_menu::{MainMenu, MenuResult}; 18 | 19 | #[cfg(feature = "tui")] 20 | pub fn run_configurator() -> Result<(), Box> { 21 | App::run() 22 | } 23 | 24 | #[cfg(not(feature = "tui"))] 25 | pub fn run_configurator() -> Result<(), Box> { 26 | eprintln!("TUI feature is not enabled. Please install with --features tui"); 27 | std::process::exit(1); 28 | } 29 | -------------------------------------------------------------------------------- /src/ui/components/editor.rs: -------------------------------------------------------------------------------- 1 | // Configuration editor component 2 | 3 | use crate::config::SegmentId; 4 | 5 | pub struct EditorComponent { 6 | pub editing_segment: Option, 7 | } 8 | 9 | impl Default for EditorComponent { 10 | fn default() -> Self { 11 | Self::new() 12 | } 13 | } 14 | 15 | impl EditorComponent { 16 | pub fn new() -> Self { 17 | Self { 18 | editing_segment: None, 19 | } 20 | } 21 | 22 | pub fn edit_segment(&mut self, segment_id: SegmentId) { 23 | self.editing_segment = Some(segment_id); 24 | } 25 | 26 | pub fn stop_editing(&mut self) { 27 | self.editing_segment = None; 28 | } 29 | 30 | pub fn is_editing(&self, segment_id: SegmentId) -> bool { 31 | self.editing_segment == Some(segment_id) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/core/segments/update.rs: -------------------------------------------------------------------------------- 1 | use super::{Segment, SegmentData}; 2 | use crate::config::{InputData, SegmentId}; 3 | use crate::updater::UpdateState; 4 | 5 | #[derive(Default)] 6 | pub struct UpdateSegment; 7 | 8 | impl UpdateSegment { 9 | pub fn new() -> Self { 10 | Self 11 | } 12 | } 13 | 14 | impl Segment for UpdateSegment { 15 | fn collect(&self, _input: &InputData) -> Option { 16 | // Load update state and check for update status 17 | let update_state = UpdateState::load(); 18 | 19 | update_state.status_text().map(|status_text| SegmentData { 20 | primary: status_text, 21 | secondary: String::new(), 22 | metadata: std::collections::HashMap::new(), 23 | }) 24 | } 25 | 26 | fn id(&self) -> SegmentId { 27 | SegmentId::Update 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /npm/main/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cometix/ccline", 3 | "version": "0.0.0", 4 | "description": "CCometixLine - High-performance Claude Code StatusLine tool", 5 | "bin": { 6 | "ccline": "./bin/ccline.js" 7 | }, 8 | "scripts": { 9 | "postinstall": "node scripts/postinstall.js" 10 | }, 11 | "optionalDependencies": { 12 | "@cometix/ccline-darwin-x64": "0.0.0", 13 | "@cometix/ccline-darwin-arm64": "0.0.0", 14 | "@cometix/ccline-linux-x64": "0.0.0", 15 | "@cometix/ccline-linux-x64-musl": "0.0.0", 16 | "@cometix/ccline-win32-x64": "0.0.0" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/Haleclipse/CCometixLine" 21 | }, 22 | "keywords": ["claude", "statusline", "claude-code", "rust", "cli"], 23 | "author": "Haleclipse", 24 | "license": "MIT", 25 | "engines": { 26 | "node": ">=14.0.0" 27 | } 28 | } -------------------------------------------------------------------------------- /src/ui/events.rs: -------------------------------------------------------------------------------- 1 | // Event handling utilities 2 | 3 | use crossterm::event::{KeyCode, KeyEvent}; 4 | 5 | #[derive(Debug, Clone, PartialEq)] 6 | pub enum AppEvent { 7 | Quit, 8 | Save, 9 | MoveUp, 10 | MoveDown, 11 | Edit, 12 | Toggle, 13 | SwitchPanel, 14 | OpenColorPicker, 15 | OpenIconSelector, 16 | Unknown, 17 | } 18 | 19 | pub fn handle_key_event(key: KeyEvent) -> AppEvent { 20 | match key.code { 21 | KeyCode::Char('q') => AppEvent::Quit, 22 | KeyCode::Char('s') => AppEvent::Save, 23 | KeyCode::Up => AppEvent::MoveUp, 24 | KeyCode::Down => AppEvent::MoveDown, 25 | KeyCode::Enter => AppEvent::Edit, 26 | KeyCode::Char(' ') => AppEvent::Toggle, 27 | KeyCode::Tab => AppEvent::SwitchPanel, 28 | KeyCode::Char('c') => AppEvent::OpenColorPicker, 29 | KeyCode::Char('i') => AppEvent::OpenIconSelector, 30 | _ => AppEvent::Unknown, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/core/segments/output_style.rs: -------------------------------------------------------------------------------- 1 | use super::{Segment, SegmentData}; 2 | use crate::config::{InputData, SegmentId}; 3 | use std::collections::HashMap; 4 | 5 | #[derive(Default)] 6 | pub struct OutputStyleSegment; 7 | 8 | impl OutputStyleSegment { 9 | pub fn new() -> Self { 10 | Self 11 | } 12 | } 13 | 14 | impl Segment for OutputStyleSegment { 15 | fn collect(&self, input: &InputData) -> Option { 16 | let output_style = input.output_style.as_ref()?; 17 | 18 | // Primary display: style name 19 | let primary = output_style.name.clone(); 20 | 21 | let mut metadata = HashMap::new(); 22 | metadata.insert("style_name".to_string(), output_style.name.clone()); 23 | 24 | Some(SegmentData { 25 | primary, 26 | secondary: String::new(), 27 | metadata, 28 | }) 29 | } 30 | 31 | fn id(&self) -> SegmentId { 32 | SegmentId::OutputStyle 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/core/segments/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod context_window; 2 | pub mod cost; 3 | pub mod directory; 4 | pub mod git; 5 | pub mod model; 6 | pub mod output_style; 7 | pub mod session; 8 | pub mod update; 9 | pub mod usage; 10 | 11 | use crate::config::{InputData, SegmentId}; 12 | use std::collections::HashMap; 13 | 14 | // New Segment trait for data collection only 15 | pub trait Segment { 16 | fn collect(&self, input: &InputData) -> Option; 17 | fn id(&self) -> SegmentId; 18 | } 19 | 20 | #[derive(Debug, Clone)] 21 | pub struct SegmentData { 22 | pub primary: String, 23 | pub secondary: String, 24 | pub metadata: HashMap, 25 | } 26 | 27 | // Re-export all segment types 28 | pub use context_window::ContextWindowSegment; 29 | pub use cost::CostSegment; 30 | pub use directory::DirectorySegment; 31 | pub use git::GitSegment; 32 | pub use model::ModelSegment; 33 | pub use output_style::OutputStyleSegment; 34 | pub use session::SessionSegment; 35 | pub use update::UpdateSegment; 36 | pub use usage::UsageSegment; 37 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | #[derive(Parser, Debug)] 4 | #[command(name = "ccline")] 5 | #[command(version, about = "High-performance Claude Code StatusLine")] 6 | pub struct Cli { 7 | /// Enter TUI configuration mode 8 | #[arg(short = 'c', long = "config")] 9 | pub config: bool, 10 | 11 | /// Set theme 12 | #[arg(short = 't', long = "theme")] 13 | pub theme: Option, 14 | 15 | /// Print current configuration 16 | #[arg(long = "print")] 17 | pub print: bool, 18 | 19 | /// Initialize config file 20 | #[arg(long = "init")] 21 | pub init: bool, 22 | 23 | /// Check configuration 24 | #[arg(long = "check")] 25 | pub check: bool, 26 | 27 | /// Check for updates 28 | #[arg(short = 'u', long = "update")] 29 | pub update: bool, 30 | 31 | /// Patch Claude Code cli.js to disable context warnings 32 | #[arg(long = "patch")] 33 | pub patch: Option, 34 | } 35 | 36 | impl Cli { 37 | pub fn parse_args() -> Self { 38 | Self::parse() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /npm/main/README.md: -------------------------------------------------------------------------------- 1 | # @cometix/ccline 2 | 3 | CCometixLine - High-performance Claude Code StatusLine tool 4 | 5 | ## Installation 6 | 7 | ```bash 8 | npm install -g @cometix/ccline 9 | ``` 10 | 11 | ## Features 12 | 13 | - 🚀 **Fast**: Written in Rust for maximum performance 14 | - 🌍 **Cross-platform**: Works on Windows, macOS, and Linux 15 | - 📦 **Easy installation**: One command via npm 16 | - 🔄 **Auto-update**: Built-in update notifications 17 | - 🎨 **Beautiful**: Nerd Font icons and colors 18 | 19 | ## Usage 20 | 21 | After installation, ccline is automatically configured for Claude Code at `~/.claude/ccline/ccline`. 22 | 23 | You can also use it directly: 24 | 25 | ```bash 26 | ccline --help 27 | ccline --version 28 | ``` 29 | 30 | ## For Users in China 31 | 32 | Use npm mirror for faster installation: 33 | 34 | ```bash 35 | npm install -g @cometix/ccline --registry https://registry.npmmirror.com 36 | ``` 37 | 38 | ## More Information 39 | 40 | - GitHub: https://github.com/Haleclipse/CCometixLine 41 | - Issues: https://github.com/Haleclipse/CCometixLine/issues 42 | - License: MIT -------------------------------------------------------------------------------- /src/ui/layout.rs: -------------------------------------------------------------------------------- 1 | // Layout utilities for TUI 2 | 3 | use ratatui::layout::{Constraint, Direction, Layout, Rect}; 4 | 5 | pub struct AppLayout; 6 | 7 | impl AppLayout { 8 | /// Create the main layout with title, preview, style selector, main content, and help 9 | pub fn main_layout(area: Rect) -> Vec { 10 | Layout::default() 11 | .direction(Direction::Vertical) 12 | .constraints([ 13 | Constraint::Length(3), // Title 14 | Constraint::Length(3), // Preview 15 | Constraint::Length(3), // Style selector 16 | Constraint::Min(10), // Main content 17 | Constraint::Length(3), // Help 18 | ]) 19 | .split(area) 20 | .to_vec() 21 | } 22 | 23 | /// Create the horizontal split for segment list and settings panel 24 | pub fn content_layout(area: Rect) -> Vec { 25 | Layout::default() 26 | .direction(Direction::Horizontal) 27 | .constraints([ 28 | Constraint::Percentage(30), // Segment list 29 | Constraint::Percentage(70), // Settings panel 30 | ]) 31 | .split(area) 32 | .to_vec() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ccometixline" 3 | version = "1.0.8" 4 | edition = "2021" 5 | description = "CCometixLine (ccline) - High-performance Claude Code StatusLine tool written in Rust" 6 | authors = ["Haleclipse"] 7 | license = "MIT" 8 | repository = "https://github.com/username/CCometixLine" 9 | readme = "README.md" 10 | keywords = ["claude", "statusline", "powerline", "rust", "claude-code"] 11 | categories = ["command-line-utilities", "development-tools"] 12 | 13 | [dependencies] 14 | serde = { version = "1.0", features = ["derive"] } 15 | serde_json = "1.0" 16 | clap = { version = "4.0", features = ["derive"] } 17 | toml = "0.8" 18 | 19 | ratatui = { version = "0.29", optional = true } 20 | crossterm = { version = "0.28", optional = true } 21 | 22 | ansi_term = { version = "0.12", optional = true } 23 | ansi-to-tui = { version = "7.0", optional = true } 24 | 25 | ureq = { version = "2.10", features = ["json"], optional = true } 26 | semver = { version = "1.0", optional = true } 27 | chrono = { version = "0.4", features = ["serde"], optional = true } 28 | dirs = { version = "5.0", optional = true } 29 | regex = "1.0" 30 | 31 | 32 | 33 | [features] 34 | default = ["tui", "self-update", "dirs"] 35 | tui = ["ratatui", "crossterm", "ansi_term", "ansi-to-tui", "chrono"] 36 | self-update = ["ureq", "semver", "chrono", "dirs"] 37 | -------------------------------------------------------------------------------- /src/core/segments/cost.rs: -------------------------------------------------------------------------------- 1 | use super::{Segment, SegmentData}; 2 | use crate::config::{InputData, SegmentId}; 3 | use std::collections::HashMap; 4 | 5 | #[derive(Default)] 6 | pub struct CostSegment; 7 | 8 | impl CostSegment { 9 | pub fn new() -> Self { 10 | Self 11 | } 12 | } 13 | 14 | impl Segment for CostSegment { 15 | fn collect(&self, input: &InputData) -> Option { 16 | let cost_data = input.cost.as_ref()?; 17 | 18 | // Primary display: total cost 19 | let primary = if let Some(cost) = cost_data.total_cost_usd { 20 | if cost == 0.0 || cost < 0.01 { 21 | "$0".to_string() 22 | } else { 23 | format!("${:.2}", cost) 24 | } 25 | } else { 26 | return None; 27 | }; 28 | 29 | // Secondary display: empty for cost segment 30 | let secondary = String::new(); 31 | 32 | let mut metadata = HashMap::new(); 33 | if let Some(cost) = cost_data.total_cost_usd { 34 | metadata.insert("cost".to_string(), cost.to_string()); 35 | } 36 | 37 | Some(SegmentData { 38 | primary, 39 | secondary, 40 | metadata, 41 | }) 42 | } 43 | 44 | fn id(&self) -> SegmentId { 45 | SegmentId::Cost 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master, main ] 6 | pull_request: 7 | branches: [ master, main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | test: 14 | name: Test Suite 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Install Rust 21 | uses: dtolnay/rust-toolchain@stable 22 | with: 23 | components: rustfmt, clippy 24 | 25 | - name: Cache cargo registry 26 | uses: actions/cache@v4 27 | with: 28 | path: | 29 | ~/.cargo/registry 30 | ~/.cargo/git 31 | target 32 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 33 | 34 | - name: Run tests 35 | run: cargo test --verbose 36 | 37 | - name: Check formatting 38 | run: cargo fmt -- --check 39 | 40 | - name: Run clippy 41 | run: cargo clippy -- -D warnings 42 | 43 | build: 44 | name: Build Check 45 | runs-on: ${{ matrix.os }} 46 | strategy: 47 | matrix: 48 | os: [ubuntu-latest, windows-latest, macos-latest] 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | 53 | - name: Install Rust 54 | uses: dtolnay/rust-toolchain@stable 55 | 56 | - name: Build 57 | run: cargo build --release -------------------------------------------------------------------------------- /src/core/segments/model.rs: -------------------------------------------------------------------------------- 1 | use super::{Segment, SegmentData}; 2 | use crate::config::{InputData, ModelConfig, SegmentId}; 3 | use std::collections::HashMap; 4 | 5 | #[derive(Default)] 6 | pub struct ModelSegment; 7 | 8 | impl ModelSegment { 9 | pub fn new() -> Self { 10 | Self 11 | } 12 | } 13 | 14 | impl Segment for ModelSegment { 15 | fn collect(&self, input: &InputData) -> Option { 16 | let mut metadata = HashMap::new(); 17 | metadata.insert("model_id".to_string(), input.model.id.clone()); 18 | metadata.insert("display_name".to_string(), input.model.display_name.clone()); 19 | 20 | Some(SegmentData { 21 | primary: self.format_model_name(&input.model.id, &input.model.display_name), 22 | secondary: String::new(), 23 | metadata, 24 | }) 25 | } 26 | 27 | fn id(&self) -> SegmentId { 28 | SegmentId::Model 29 | } 30 | } 31 | 32 | impl ModelSegment { 33 | fn format_model_name(&self, id: &str, display_name: &str) -> String { 34 | let model_config = ModelConfig::load(); 35 | 36 | // Try to get display name from external config first 37 | if let Some(config_name) = model_config.get_display_name(id) { 38 | config_name 39 | } else { 40 | // Fallback to Claude Code's official display_name for unrecognized models 41 | display_name.to_string() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/core/segments/directory.rs: -------------------------------------------------------------------------------- 1 | use super::{Segment, SegmentData}; 2 | use crate::config::{InputData, SegmentId}; 3 | use std::collections::HashMap; 4 | 5 | #[derive(Default)] 6 | pub struct DirectorySegment; 7 | 8 | impl DirectorySegment { 9 | pub fn new() -> Self { 10 | Self 11 | } 12 | 13 | /// Extract directory name from path, handling both Unix and Windows separators 14 | fn extract_directory_name(path: &str) -> String { 15 | // Handle both Unix and Windows separators by trying both 16 | let unix_name = path.split('/').next_back().unwrap_or(""); 17 | let windows_name = path.split('\\').next_back().unwrap_or(""); 18 | 19 | // Choose the name that indicates actual path splitting occurred 20 | let result = if windows_name.len() < path.len() { 21 | // Windows path separator was found 22 | windows_name 23 | } else if unix_name.len() < path.len() { 24 | // Unix path separator was found 25 | unix_name 26 | } else { 27 | // No separator found, use the whole path 28 | path 29 | }; 30 | 31 | if result.is_empty() { 32 | "root".to_string() 33 | } else { 34 | result.to_string() 35 | } 36 | } 37 | } 38 | 39 | impl Segment for DirectorySegment { 40 | fn collect(&self, input: &InputData) -> Option { 41 | let current_dir = &input.workspace.current_dir; 42 | 43 | // Handle cross-platform path separators manually for better compatibility 44 | let dir_name = Self::extract_directory_name(current_dir); 45 | 46 | // Store the full path in metadata for potential use 47 | let mut metadata = HashMap::new(); 48 | metadata.insert("full_path".to_string(), current_dir.clone()); 49 | 50 | Some(SegmentData { 51 | primary: dir_name, 52 | secondary: String::new(), 53 | metadata, 54 | }) 55 | } 56 | 57 | fn id(&self) -> SegmentId { 58 | SegmentId::Directory 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/utils/credentials.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::path::PathBuf; 3 | 4 | #[derive(Debug, Deserialize, Serialize)] 5 | struct OAuthCredentials { 6 | #[serde(rename = "accessToken")] 7 | access_token: String, 8 | #[serde(rename = "refreshToken")] 9 | refresh_token: Option, 10 | #[serde(rename = "expiresAt")] 11 | expires_at: Option, 12 | scopes: Option>, 13 | #[serde(rename = "subscriptionType")] 14 | subscription_type: Option, 15 | } 16 | 17 | #[derive(Debug, Deserialize, Serialize)] 18 | struct CredentialsFile { 19 | #[serde(rename = "claudeAiOauth")] 20 | claude_ai_oauth: Option, 21 | } 22 | 23 | pub fn get_oauth_token() -> Option { 24 | if cfg!(target_os = "macos") { 25 | get_oauth_token_macos() 26 | } else { 27 | get_oauth_token_file() 28 | } 29 | } 30 | 31 | fn get_oauth_token_macos() -> Option { 32 | use std::process::Command; 33 | 34 | let user = std::env::var("USER").unwrap_or_else(|_| "user".to_string()); 35 | 36 | let output = Command::new("security") 37 | .args([ 38 | "find-generic-password", 39 | "-a", 40 | &user, 41 | "-w", 42 | "-s", 43 | "Claude Code-credentials", 44 | ]) 45 | .output(); 46 | 47 | match output { 48 | Ok(output) if output.status.success() => { 49 | let json_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); 50 | if !json_str.is_empty() { 51 | if let Ok(creds_file) = serde_json::from_str::(&json_str) { 52 | return creds_file.claude_ai_oauth.map(|oauth| oauth.access_token); 53 | } 54 | } 55 | None 56 | } 57 | _ => { 58 | // Fallback to file-based credentials 59 | get_oauth_token_file() 60 | } 61 | } 62 | } 63 | 64 | fn get_oauth_token_file() -> Option { 65 | let credentials_path = get_credentials_path()?; 66 | 67 | if !credentials_path.exists() { 68 | return None; 69 | } 70 | 71 | let content = std::fs::read_to_string(&credentials_path).ok()?; 72 | let creds_file: CredentialsFile = serde_json::from_str(&content).ok()?; 73 | 74 | creds_file.claude_ai_oauth.map(|oauth| oauth.access_token) 75 | } 76 | 77 | fn get_credentials_path() -> Option { 78 | let home = dirs::home_dir()?; 79 | Some(home.join(".claude").join(".credentials.json")) 80 | } 81 | -------------------------------------------------------------------------------- /src/ui/components/theme_selector.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use ratatui::{ 3 | layout::Rect, 4 | widgets::{Block, Borders, Paragraph}, 5 | Frame, 6 | }; 7 | 8 | #[derive(Default)] 9 | pub struct ThemeSelectorComponent; 10 | 11 | impl ThemeSelectorComponent { 12 | pub fn new() -> Self { 13 | Self 14 | } 15 | 16 | pub fn render(&self, f: &mut Frame, area: Rect, config: &Config) { 17 | let is_modified = config.is_modified_from_theme(); 18 | let modified_indicator = if is_modified { "*" } else { "" }; 19 | 20 | // Get all available themes dynamically 21 | let available_themes = crate::ui::themes::ThemePresets::list_available_themes(); 22 | 23 | // Calculate available width (minus borders and spacing) 24 | let content_width = area.width.saturating_sub(2); // Remove borders 25 | 26 | // Build theme options with auto-wrapping 27 | let mut lines = Vec::new(); 28 | let mut current_line = String::new(); 29 | let mut first_line = true; 30 | 31 | for (i, theme) in available_themes.iter().enumerate() { 32 | let marker = if config.theme == *theme { 33 | "[✓]" 34 | } else { 35 | "[ ]" 36 | }; 37 | let theme_part = format!("{} {}", marker, theme); 38 | let separator = if i == 0 { "" } else { " " }; 39 | let part_with_sep = format!("{}{}", separator, theme_part); 40 | 41 | // Check if this part fits in current line 42 | let would_fit = current_line.len() + part_with_sep.len() <= content_width as usize; 43 | 44 | if would_fit || first_line { 45 | current_line.push_str(&part_with_sep); 46 | first_line = false; 47 | } else { 48 | // Start new line 49 | lines.push(current_line); 50 | current_line = theme_part; // No indent for continuation lines 51 | } 52 | } 53 | 54 | if !current_line.trim().is_empty() { 55 | lines.push(current_line); 56 | } 57 | 58 | // Add separator display at the end 59 | let separator_display = format!("\nSeparator: \"{}\"", config.style.separator); 60 | 61 | let full_text = format!("{}{}", lines.join("\n"), separator_display); 62 | let title = format!("Themes{}", modified_indicator); 63 | let theme_selector = Paragraph::new(full_text) 64 | .block(Block::default().borders(Borders::ALL).title(title)) 65 | .wrap(ratatui::widgets::Wrap { trim: false }); 66 | f.render_widget(theme_selector, area); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /npm/scripts/prepare-packages.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | const version = process.env.GITHUB_REF?.replace('refs/tags/v', '') || process.argv[2]; 6 | if (!version) { 7 | console.error('Error: Version not provided'); 8 | console.error('Usage: GITHUB_REF=refs/tags/v1.0.0 node prepare-packages.js'); 9 | console.error(' or: node prepare-packages.js 1.0.0'); 10 | process.exit(1); 11 | } 12 | 13 | console.log(`🚀 Preparing packages for version ${version}`); 14 | 15 | // Define platform structures 16 | const platforms = [ 17 | 'darwin-x64', 18 | 'darwin-arm64', 19 | 'linux-x64', 20 | 'linux-x64-musl', 21 | 'win32-x64' 22 | ]; 23 | 24 | // Prepare platform packages 25 | platforms.forEach(platform => { 26 | const sourceDir = path.join(__dirname, '..', 'platforms', platform); 27 | const targetDir = path.join(__dirname, '..', '..', 'npm-publish', platform); 28 | 29 | // Create directory 30 | fs.mkdirSync(targetDir, { recursive: true }); 31 | 32 | // Read template package.json 33 | const templatePath = path.join(sourceDir, 'package.json'); 34 | const packageJson = JSON.parse(fs.readFileSync(templatePath, 'utf8')); 35 | 36 | // Update version 37 | packageJson.version = version; 38 | 39 | // Write to target directory 40 | fs.writeFileSync( 41 | path.join(targetDir, 'package.json'), 42 | JSON.stringify(packageJson, null, 2) + '\n' 43 | ); 44 | 45 | console.log(`✓ Prepared @cometix/ccline-${platform} v${version}`); 46 | }); 47 | 48 | // Prepare main package 49 | const mainSource = path.join(__dirname, '..', 'main'); 50 | const mainTarget = path.join(__dirname, '..', '..', 'npm-publish', 'main'); 51 | 52 | // Copy main package files 53 | fs.cpSync(mainSource, mainTarget, { recursive: true }); 54 | 55 | // Update main package.json 56 | const mainPackageJsonPath = path.join(mainTarget, 'package.json'); 57 | const mainPackageJson = JSON.parse(fs.readFileSync(mainPackageJsonPath, 'utf8')); 58 | 59 | mainPackageJson.version = version; 60 | 61 | // Update optionalDependencies versions 62 | if (mainPackageJson.optionalDependencies) { 63 | Object.keys(mainPackageJson.optionalDependencies).forEach(dep => { 64 | if (dep.startsWith('@cometix/ccline-')) { 65 | mainPackageJson.optionalDependencies[dep] = version; 66 | } 67 | }); 68 | } 69 | 70 | fs.writeFileSync( 71 | mainPackageJsonPath, 72 | JSON.stringify(mainPackageJson, null, 2) + '\n' 73 | ); 74 | 75 | console.log(`✓ Prepared @cometix/ccline v${version}`); 76 | console.log(`\n🎉 All packages prepared for version ${version}`); 77 | console.log('\nNext steps:'); 78 | console.log('1. Copy binaries to platform directories'); 79 | console.log('2. Publish platform packages first'); 80 | console.log('3. Publish main package last'); -------------------------------------------------------------------------------- /src/ui/components/segment_list.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{Config, SegmentId}; 2 | use ratatui::{ 3 | layout::Rect, 4 | style::{Color, Style}, 5 | text::{Line, Span}, 6 | widgets::{Block, Borders, List, ListItem}, 7 | Frame, 8 | }; 9 | 10 | #[derive(Debug, Clone, PartialEq)] 11 | pub enum Panel { 12 | SegmentList, 13 | Settings, 14 | } 15 | 16 | #[derive(Debug, Clone, PartialEq)] 17 | pub enum FieldSelection { 18 | Enabled, 19 | Icon, 20 | IconColor, 21 | TextColor, 22 | BackgroundColor, 23 | TextStyle, 24 | Options, 25 | } 26 | 27 | #[derive(Default)] 28 | pub struct SegmentListComponent; 29 | 30 | impl SegmentListComponent { 31 | pub fn new() -> Self { 32 | Self 33 | } 34 | 35 | pub fn render( 36 | &self, 37 | f: &mut Frame, 38 | area: Rect, 39 | config: &Config, 40 | selected_segment: usize, 41 | selected_panel: &Panel, 42 | ) { 43 | let items: Vec = config 44 | .segments 45 | .iter() 46 | .enumerate() 47 | .map(|(i, segment)| { 48 | let is_selected = i == selected_segment && *selected_panel == Panel::SegmentList; 49 | let enabled_marker = if segment.enabled { "●" } else { "○" }; 50 | let segment_name = match segment.id { 51 | SegmentId::Model => "Model", 52 | SegmentId::Directory => "Directory", 53 | SegmentId::Git => "Git", 54 | SegmentId::ContextWindow => "Context Window", 55 | SegmentId::Usage => "Usage", 56 | SegmentId::Cost => "Cost", 57 | SegmentId::Session => "Session", 58 | SegmentId::OutputStyle => "Output Style", 59 | SegmentId::Update => "Update", 60 | }; 61 | 62 | if is_selected { 63 | // Selected item with colored cursor 64 | ListItem::new(Line::from(vec![ 65 | Span::styled("▶ ", Style::default().fg(Color::Cyan)), 66 | Span::raw(format!("{} {}", enabled_marker, segment_name)), 67 | ])) 68 | } else { 69 | // Non-selected item 70 | ListItem::new(format!(" {} {}", enabled_marker, segment_name)) 71 | } 72 | }) 73 | .collect(); 74 | let segments_block = Block::default() 75 | .borders(Borders::ALL) 76 | .title("Segments") 77 | .border_style(if *selected_panel == Panel::SegmentList { 78 | Style::default().fg(Color::Cyan) 79 | } else { 80 | Style::default() 81 | }); 82 | let segments_list = List::new(items).block(segments_block); 83 | f.render_widget(segments_list, area); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/core/segments/session.rs: -------------------------------------------------------------------------------- 1 | use super::{Segment, SegmentData}; 2 | use crate::config::{InputData, SegmentId}; 3 | use std::collections::HashMap; 4 | 5 | #[derive(Default)] 6 | pub struct SessionSegment; 7 | 8 | impl SessionSegment { 9 | pub fn new() -> Self { 10 | Self 11 | } 12 | 13 | fn format_duration(ms: u64) -> String { 14 | if ms < 1000 { 15 | format!("{}ms", ms) 16 | } else if ms < 60_000 { 17 | let seconds = ms / 1000; 18 | format!("{}s", seconds) 19 | } else if ms < 3_600_000 { 20 | let minutes = ms / 60_000; 21 | let seconds = (ms % 60_000) / 1000; 22 | if seconds == 0 { 23 | format!("{}m", minutes) 24 | } else { 25 | format!("{}m{}s", minutes, seconds) 26 | } 27 | } else { 28 | let hours = ms / 3_600_000; 29 | let minutes = (ms % 3_600_000) / 60_000; 30 | if minutes == 0 { 31 | format!("{}h", hours) 32 | } else { 33 | format!("{}h{}m", hours, minutes) 34 | } 35 | } 36 | } 37 | } 38 | 39 | impl Segment for SessionSegment { 40 | fn collect(&self, input: &InputData) -> Option { 41 | let cost_data = input.cost.as_ref()?; 42 | 43 | // Primary display: total duration 44 | let primary = if let Some(duration) = cost_data.total_duration_ms { 45 | Self::format_duration(duration) 46 | } else { 47 | return None; 48 | }; 49 | 50 | // Secondary display: line changes if available 51 | let secondary = match (cost_data.total_lines_added, cost_data.total_lines_removed) { 52 | (Some(added), Some(removed)) if added > 0 || removed > 0 => { 53 | format!("+{} -{}", added, removed) 54 | } 55 | (Some(added), None) if added > 0 => { 56 | format!("+{}", added) 57 | } 58 | (None, Some(removed)) if removed > 0 => { 59 | format!("-{}", removed) 60 | } 61 | _ => String::new(), 62 | }; 63 | 64 | let mut metadata = HashMap::new(); 65 | if let Some(duration) = cost_data.total_duration_ms { 66 | metadata.insert("duration_ms".to_string(), duration.to_string()); 67 | } 68 | if let Some(api_duration) = cost_data.total_api_duration_ms { 69 | metadata.insert("api_duration_ms".to_string(), api_duration.to_string()); 70 | } 71 | if let Some(added) = cost_data.total_lines_added { 72 | metadata.insert("lines_added".to_string(), added.to_string()); 73 | } 74 | if let Some(removed) = cost_data.total_lines_removed { 75 | metadata.insert("lines_removed".to_string(), removed.to_string()); 76 | } 77 | 78 | Some(SegmentData { 79 | primary, 80 | secondary, 81 | metadata, 82 | }) 83 | } 84 | 85 | fn id(&self) -> SegmentId { 86 | SegmentId::Session 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /npm/main/bin/ccline.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { spawnSync } = require('child_process'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const os = require('os'); 6 | 7 | // 1. Priority: Use ~/.claude/ccline/ccline if exists 8 | const claudePath = path.join( 9 | os.homedir(), 10 | '.claude', 11 | 'ccline', 12 | process.platform === 'win32' ? 'ccline.exe' : 'ccline' 13 | ); 14 | 15 | if (fs.existsSync(claudePath)) { 16 | const result = spawnSync(claudePath, process.argv.slice(2), { 17 | stdio: 'inherit', 18 | shell: false 19 | }); 20 | process.exit(result.status || 0); 21 | } 22 | 23 | // 2. Fallback: Use npm package binary 24 | const platform = process.platform; 25 | const arch = process.arch; 26 | 27 | // Handle special cases 28 | let platformKey = `${platform}-${arch}`; 29 | if (platform === 'linux') { 30 | // Detect if static linking is needed based on glibc version 31 | function shouldUseStaticBinary() { 32 | try { 33 | const { execSync } = require('child_process'); 34 | const lddOutput = execSync('ldd --version 2>/dev/null || echo ""', { 35 | encoding: 'utf8', 36 | timeout: 1000 37 | }); 38 | 39 | // Parse "ldd (GNU libc) 2.35" format 40 | const match = lddOutput.match(/(?:GNU libc|GLIBC).*?(\d+)\.(\d+)/); 41 | if (match) { 42 | const major = parseInt(match[1]); 43 | const minor = parseInt(match[2]); 44 | // Use static binary if glibc < 2.35 45 | return major < 2 || (major === 2 && minor < 35); 46 | } 47 | } catch (e) { 48 | // If detection fails, default to dynamic binary 49 | return false; 50 | } 51 | 52 | return false; 53 | } 54 | 55 | if (shouldUseStaticBinary()) { 56 | platformKey = 'linux-x64-musl'; 57 | } 58 | } 59 | 60 | const packageMap = { 61 | 'darwin-x64': '@cometix/ccline-darwin-x64', 62 | 'darwin-arm64': '@cometix/ccline-darwin-arm64', 63 | 'linux-x64': '@cometix/ccline-linux-x64', 64 | 'linux-x64-musl': '@cometix/ccline-linux-x64-musl', 65 | 'win32-x64': '@cometix/ccline-win32-x64', 66 | 'win32-ia32': '@cometix/ccline-win32-x64', // Use 64-bit for 32-bit systems 67 | }; 68 | 69 | const packageName = packageMap[platformKey]; 70 | if (!packageName) { 71 | console.error(`Error: Unsupported platform ${platformKey}`); 72 | console.error('Supported platforms: darwin (x64/arm64), linux (x64), win32 (x64)'); 73 | console.error('Please visit https://github.com/Haleclipse/CCometixLine for manual installation'); 74 | process.exit(1); 75 | } 76 | 77 | const binaryName = platform === 'win32' ? 'ccline.exe' : 'ccline'; 78 | const binaryPath = path.join(__dirname, '..', 'node_modules', packageName, binaryName); 79 | 80 | if (!fs.existsSync(binaryPath)) { 81 | console.error(`Error: Binary not found at ${binaryPath}`); 82 | console.error('This might indicate a failed installation or unsupported platform.'); 83 | console.error('Please try reinstalling: npm install -g @cometix/ccline'); 84 | console.error(`Expected package: ${packageName}`); 85 | process.exit(1); 86 | } 87 | 88 | const result = spawnSync(binaryPath, process.argv.slice(2), { 89 | stdio: 'inherit', 90 | shell: false 91 | }); 92 | 93 | process.exit(result.status || 0); -------------------------------------------------------------------------------- /src/ui/components/name_input.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | layout::{Constraint, Direction, Layout, Rect}, 3 | style::{Color, Style}, 4 | widgets::{Block, Borders, Clear, Paragraph}, 5 | Frame, 6 | }; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct NameInputComponent { 10 | pub is_open: bool, 11 | pub input: String, 12 | pub title: String, 13 | pub placeholder: String, 14 | } 15 | 16 | impl Default for NameInputComponent { 17 | fn default() -> Self { 18 | Self::new() 19 | } 20 | } 21 | 22 | impl NameInputComponent { 23 | pub fn new() -> Self { 24 | Self { 25 | is_open: false, 26 | input: String::new(), 27 | title: "Input Name".to_string(), 28 | placeholder: "Enter name...".to_string(), 29 | } 30 | } 31 | 32 | pub fn open(&mut self, title: &str, placeholder: &str) { 33 | self.is_open = true; 34 | self.input.clear(); 35 | self.title = title.to_string(); 36 | self.placeholder = placeholder.to_string(); 37 | } 38 | 39 | pub fn close(&mut self) { 40 | self.is_open = false; 41 | self.input.clear(); 42 | } 43 | 44 | pub fn input_char(&mut self, c: char) { 45 | if c.is_ascii_alphanumeric() || c == '_' || c == '-' { 46 | self.input.push(c); 47 | } 48 | } 49 | 50 | pub fn backspace(&mut self) { 51 | self.input.pop(); 52 | } 53 | 54 | pub fn get_input(&self) -> Option { 55 | if self.input.trim().is_empty() { 56 | None 57 | } else { 58 | Some(self.input.trim().to_string()) 59 | } 60 | } 61 | 62 | pub fn render(&self, f: &mut Frame, area: Rect) { 63 | if !self.is_open { 64 | return; 65 | } 66 | 67 | // Calculate popup area that avoids covering the bottom help area 68 | let popup_width = 60_u16.min(area.width.saturating_sub(4)); 69 | let popup_height = 8_u16; // Fixed height for content 70 | 71 | // Ensure popup doesn't cover bottom help area (reserve at least 4 lines for help) 72 | let max_y = area.height.saturating_sub(popup_height + 4); 73 | let popup_y = if max_y > 2 { 74 | (area.height.saturating_sub(popup_height)) / 2 75 | } else { 76 | 2 // Minimum top margin 77 | }; 78 | 79 | let popup_area = Rect { 80 | x: (area.width.saturating_sub(popup_width)) / 2, 81 | y: popup_y.min(max_y), 82 | width: popup_width, 83 | height: popup_height, 84 | }; 85 | 86 | // Clear the popup area first 87 | f.render_widget(Clear, popup_area); 88 | 89 | let popup_block = Block::default() 90 | .borders(Borders::ALL) 91 | .title(self.title.as_str()); 92 | let inner = popup_block.inner(popup_area); 93 | f.render_widget(popup_block, popup_area); 94 | 95 | let chunks = Layout::default() 96 | .direction(Direction::Vertical) 97 | .constraints([ 98 | Constraint::Length(3), // Input field 99 | Constraint::Length(3), // Actions 100 | ]) 101 | .split(inner); 102 | 103 | // Input field 104 | let input_text = if self.input.is_empty() { 105 | format!("> {} <", self.placeholder) 106 | } else { 107 | format!("> {} <", self.input) 108 | }; 109 | 110 | f.render_widget( 111 | Paragraph::new(input_text) 112 | .style(if self.input.is_empty() { 113 | Style::default().fg(Color::DarkGray) 114 | } else { 115 | Style::default().fg(Color::Yellow) 116 | }) 117 | .block(Block::default().borders(Borders::ALL).title("Name")), 118 | chunks[0], 119 | ); 120 | 121 | // Actions 122 | f.render_widget( 123 | Paragraph::new("[Enter] Confirm [Esc] Cancel") 124 | .block(Block::default().borders(Borders::ALL)), 125 | chunks[1], 126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use ccometixline::cli::Cli; 2 | use ccometixline::config::{Config, InputData}; 3 | use ccometixline::core::{collect_all_segments, StatusLineGenerator}; 4 | use std::io::{self, IsTerminal}; 5 | 6 | fn main() -> Result<(), Box> { 7 | let cli = Cli::parse_args(); 8 | 9 | // Handle configuration commands 10 | if cli.init { 11 | use ccometixline::config::InitResult; 12 | match Config::init()? { 13 | InitResult::Created(path) => println!("Created config at {}", path.display()), 14 | InitResult::AlreadyExists(path) => { 15 | println!("Config already exists at {}", path.display()) 16 | } 17 | } 18 | return Ok(()); 19 | } 20 | 21 | if cli.print { 22 | let mut config = Config::load().unwrap_or_else(|_| Config::default()); 23 | 24 | // Apply theme override if provided 25 | if let Some(theme) = cli.theme { 26 | config = ccometixline::ui::themes::ThemePresets::get_theme(&theme); 27 | } 28 | 29 | config.print()?; 30 | return Ok(()); 31 | } 32 | 33 | if cli.check { 34 | let config = Config::load()?; 35 | config.check()?; 36 | println!("✓ Configuration valid"); 37 | return Ok(()); 38 | } 39 | 40 | if cli.config { 41 | #[cfg(feature = "tui")] 42 | { 43 | ccometixline::ui::run_configurator()?; 44 | } 45 | #[cfg(not(feature = "tui"))] 46 | { 47 | eprintln!("TUI feature is not enabled. Please install with --features tui"); 48 | std::process::exit(1); 49 | } 50 | return Ok(()); 51 | } 52 | 53 | if cli.update { 54 | #[cfg(feature = "self-update")] 55 | { 56 | println!("Update feature not implemented in new architecture yet"); 57 | } 58 | #[cfg(not(feature = "self-update"))] 59 | { 60 | println!("Update check not available (self-update feature disabled)"); 61 | } 62 | return Ok(()); 63 | } 64 | 65 | // Handle Claude Code patcher 66 | if let Some(claude_path) = cli.patch { 67 | use ccometixline::utils::ClaudeCodePatcher; 68 | 69 | println!("🔧 Claude Code Context Warning Disabler"); 70 | println!("Target file: {}", claude_path); 71 | 72 | // Create backup in same directory 73 | let backup_path = format!("{}.backup", claude_path); 74 | std::fs::copy(&claude_path, &backup_path)?; 75 | println!("📦 Created backup: {}", backup_path); 76 | 77 | // Load and patch 78 | let mut patcher = ClaudeCodePatcher::new(&claude_path)?; 79 | 80 | println!("\n🔄 Applying patches..."); 81 | let results = patcher.apply_all_patches(); 82 | patcher.save()?; 83 | 84 | ClaudeCodePatcher::print_summary(&results); 85 | println!("💡 To restore warnings, replace your cli.js with the backup file:"); 86 | println!(" cp {} {}", backup_path, claude_path); 87 | 88 | return Ok(()); 89 | } 90 | 91 | // Load configuration 92 | let mut config = Config::load().unwrap_or_else(|_| Config::default()); 93 | 94 | // Apply theme override if provided 95 | if let Some(theme) = cli.theme { 96 | config = ccometixline::ui::themes::ThemePresets::get_theme(&theme); 97 | } 98 | 99 | // Check if stdin has data 100 | if io::stdin().is_terminal() { 101 | // No input data available, show main menu 102 | #[cfg(feature = "tui")] 103 | { 104 | use ccometixline::ui::{MainMenu, MenuResult}; 105 | 106 | if let Some(result) = MainMenu::run()? { 107 | match result { 108 | MenuResult::LaunchConfigurator => { 109 | ccometixline::ui::run_configurator()?; 110 | } 111 | MenuResult::InitConfig | MenuResult::CheckConfig => { 112 | // These are now handled internally by the menu 113 | // and should not be returned, but handle gracefully 114 | } 115 | MenuResult::Exit => { 116 | // Exit gracefully 117 | } 118 | } 119 | } 120 | } 121 | #[cfg(not(feature = "tui"))] 122 | { 123 | eprintln!("No input data provided and TUI feature is not enabled."); 124 | eprintln!("Usage: echo '{{...}}' | ccline"); 125 | eprintln!(" or: ccline --help"); 126 | } 127 | return Ok(()); 128 | } 129 | 130 | // Read Claude Code data from stdin 131 | let stdin = io::stdin(); 132 | let input: InputData = serde_json::from_reader(stdin.lock())?; 133 | 134 | // Collect segment data 135 | let segments_data = collect_all_segments(&config, &input); 136 | 137 | // Render statusline 138 | let generator = StatusLineGenerator::new(config); 139 | let statusline = generator.generate(segments_data); 140 | 141 | println!("{}", statusline); 142 | 143 | Ok(()) 144 | } 145 | -------------------------------------------------------------------------------- /src/ui/components/help.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | layout::Rect, 3 | style::{Color, Modifier, Style}, 4 | text::{Line, Span, Text}, 5 | widgets::{Block, Borders, Paragraph}, 6 | Frame, 7 | }; 8 | 9 | #[derive(Default)] 10 | pub struct HelpComponent; 11 | 12 | impl HelpComponent { 13 | pub fn new() -> Self { 14 | Self 15 | } 16 | 17 | pub fn render( 18 | &self, 19 | f: &mut Frame, 20 | area: Rect, 21 | status_message: Option<&str>, 22 | color_picker_open: bool, 23 | icon_selector_open: bool, 24 | ) { 25 | let help_items = if color_picker_open { 26 | vec![ 27 | ("[↑↓]", "Navigate"), 28 | ("[Tab]", "Mode"), 29 | ("[Enter]", "Select"), 30 | ("[Esc]", "Cancel"), 31 | ] 32 | } else if icon_selector_open { 33 | vec![ 34 | ("[↑↓]", "Navigate"), 35 | ("[Tab]", "Style"), 36 | ("[C]", "Custom"), 37 | ("[Enter]", "Select"), 38 | ("[Esc]", "Cancel"), 39 | ] 40 | } else { 41 | vec![ 42 | ("[Tab]", "Switch Panel"), 43 | ("[Enter]", "Toggle/Edit"), 44 | ("[Shift+↑↓]", "Reorder"), 45 | ("[1-4]", "Theme"), 46 | ("[P]", "Switch Theme"), 47 | ("[R]", "Reset"), 48 | ("[E]", "Edit Separator"), 49 | ("[S]", "Save Config"), 50 | ("[W]", "Write Theme"), 51 | ("[Ctrl+S]", "Save Theme"), 52 | ("[Esc]", "Quit"), 53 | ] 54 | }; 55 | 56 | let status = status_message.unwrap_or(""); 57 | 58 | // Build help text with smart wrapping - keep each shortcut as a unit 59 | let content_width = area.width.saturating_sub(2); // Remove borders 60 | let mut lines = Vec::new(); 61 | let mut current_line_spans = Vec::new(); 62 | let mut current_width = 0usize; 63 | 64 | for (i, (key, description)) in help_items.iter().enumerate() { 65 | // Calculate item display width 66 | let item_width = key.chars().count() + description.chars().count() + 1; // +1 for space 67 | 68 | // Add separator for non-first items on the same line 69 | let needs_separator = i > 0 && !current_line_spans.is_empty(); 70 | let separator_width = if needs_separator { 2 } else { 0 }; 71 | let total_width = item_width + separator_width; 72 | 73 | // Check if item fits on current line 74 | if current_width + total_width <= content_width as usize { 75 | // Item fits, add to current line 76 | if needs_separator { 77 | current_line_spans.push(Span::styled(" ", Style::default())); 78 | current_width += 2; 79 | } 80 | 81 | // Add highlighted key and description 82 | current_line_spans.push(Span::styled( 83 | *key, 84 | Style::default() 85 | .fg(Color::Yellow) 86 | .add_modifier(Modifier::BOLD), 87 | )); 88 | current_line_spans.push(Span::styled( 89 | format!(" {}", description), 90 | Style::default().fg(Color::Gray), 91 | )); 92 | current_width += item_width; 93 | } else { 94 | // Item doesn't fit, start new line 95 | if !current_line_spans.is_empty() { 96 | lines.push(Line::from(current_line_spans)); 97 | current_line_spans = Vec::new(); 98 | } 99 | 100 | // Start new line with this item 101 | current_line_spans.push(Span::styled( 102 | *key, 103 | Style::default() 104 | .fg(Color::Yellow) 105 | .add_modifier(Modifier::BOLD), 106 | )); 107 | current_line_spans.push(Span::styled( 108 | format!(" {}", description), 109 | Style::default().fg(Color::Gray), 110 | )); 111 | current_width = item_width; 112 | } 113 | } 114 | 115 | // Add last line if not empty 116 | if !current_line_spans.is_empty() { 117 | lines.push(Line::from(current_line_spans)); 118 | } 119 | 120 | // Add status message if present 121 | if !status.is_empty() { 122 | lines.push(Line::from("")); 123 | lines.push(Line::from(Span::styled( 124 | status, 125 | Style::default().fg(Color::Green), 126 | ))); 127 | } 128 | 129 | let help_text = Text::from(lines); 130 | let help_paragraph = Paragraph::new(help_text) 131 | .block(Block::default().borders(Borders::ALL).title("Help")) 132 | .wrap(ratatui::widgets::Wrap { trim: false }); 133 | f.render_widget(help_paragraph, area); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/ui/themes/theme_minimal.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{ 2 | AnsiColor, ColorConfig, IconConfig, SegmentConfig, SegmentId, TextStyleConfig, 3 | }; 4 | use std::collections::HashMap; 5 | 6 | pub fn model_segment() -> SegmentConfig { 7 | SegmentConfig { 8 | id: SegmentId::Model, 9 | enabled: true, 10 | icon: IconConfig { 11 | plain: "✽".to_string(), 12 | nerd_font: "\u{f2d0}".to_string(), 13 | }, 14 | colors: ColorConfig { 15 | icon: Some(AnsiColor::Color16 { c16: 14 }), 16 | text: Some(AnsiColor::Color16 { c16: 14 }), 17 | background: None, 18 | }, 19 | styles: TextStyleConfig::default(), 20 | options: HashMap::new(), 21 | } 22 | } 23 | 24 | pub fn directory_segment() -> SegmentConfig { 25 | SegmentConfig { 26 | id: SegmentId::Directory, 27 | enabled: true, 28 | icon: IconConfig { 29 | plain: "◐".to_string(), 30 | nerd_font: "\u{f024b}".to_string(), 31 | }, 32 | colors: ColorConfig { 33 | icon: Some(AnsiColor::Color16 { c16: 11 }), 34 | text: Some(AnsiColor::Color16 { c16: 10 }), 35 | background: None, 36 | }, 37 | styles: TextStyleConfig::default(), 38 | options: HashMap::new(), 39 | } 40 | } 41 | 42 | pub fn git_segment() -> SegmentConfig { 43 | SegmentConfig { 44 | id: SegmentId::Git, 45 | enabled: true, 46 | icon: IconConfig { 47 | plain: "※".to_string(), 48 | nerd_font: "\u{f02a2}".to_string(), 49 | }, 50 | colors: ColorConfig { 51 | icon: Some(AnsiColor::Color16 { c16: 12 }), 52 | text: Some(AnsiColor::Color16 { c16: 12 }), 53 | background: None, 54 | }, 55 | styles: TextStyleConfig::default(), 56 | options: { 57 | let mut opts = HashMap::new(); 58 | opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); 59 | opts 60 | }, 61 | } 62 | } 63 | 64 | pub fn context_window_segment() -> SegmentConfig { 65 | SegmentConfig { 66 | id: SegmentId::ContextWindow, 67 | enabled: true, 68 | icon: IconConfig { 69 | plain: "◐".to_string(), 70 | nerd_font: "\u{f49b}".to_string(), 71 | }, 72 | colors: ColorConfig { 73 | icon: Some(AnsiColor::Color16 { c16: 13 }), 74 | text: Some(AnsiColor::Color16 { c16: 13 }), 75 | background: None, 76 | }, 77 | styles: TextStyleConfig::default(), 78 | options: HashMap::new(), 79 | } 80 | } 81 | 82 | pub fn cost_segment() -> SegmentConfig { 83 | SegmentConfig { 84 | id: SegmentId::Cost, 85 | enabled: false, 86 | icon: IconConfig { 87 | plain: "💰".to_string(), 88 | nerd_font: "\u{eec1}".to_string(), 89 | }, 90 | colors: ColorConfig { 91 | icon: Some(AnsiColor::Color16 { c16: 3 }), 92 | text: Some(AnsiColor::Color16 { c16: 3 }), 93 | background: None, 94 | }, 95 | styles: TextStyleConfig::default(), 96 | options: HashMap::new(), 97 | } 98 | } 99 | 100 | pub fn session_segment() -> SegmentConfig { 101 | SegmentConfig { 102 | id: SegmentId::Session, 103 | enabled: false, 104 | icon: IconConfig { 105 | plain: "⏱️".to_string(), 106 | nerd_font: "\u{f19bb}".to_string(), 107 | }, 108 | colors: ColorConfig { 109 | icon: Some(AnsiColor::Color16 { c16: 2 }), 110 | text: Some(AnsiColor::Color16 { c16: 2 }), 111 | background: None, 112 | }, 113 | styles: TextStyleConfig::default(), 114 | options: HashMap::new(), 115 | } 116 | } 117 | 118 | pub fn output_style_segment() -> SegmentConfig { 119 | SegmentConfig { 120 | id: SegmentId::OutputStyle, 121 | enabled: false, 122 | icon: IconConfig { 123 | plain: "🎯".to_string(), 124 | nerd_font: "\u{f12f5}".to_string(), 125 | }, 126 | colors: ColorConfig { 127 | icon: Some(AnsiColor::Color16 { c16: 6 }), 128 | text: Some(AnsiColor::Color16 { c16: 6 }), 129 | background: None, 130 | }, 131 | styles: TextStyleConfig::default(), 132 | options: HashMap::new(), 133 | } 134 | } 135 | 136 | pub fn usage_segment() -> SegmentConfig { 137 | SegmentConfig { 138 | id: SegmentId::Usage, 139 | enabled: false, 140 | icon: IconConfig { 141 | plain: "📊".to_string(), 142 | nerd_font: "\u{f0a9e}".to_string(), 143 | }, 144 | colors: ColorConfig { 145 | icon: Some(AnsiColor::Color16 { c16: 14 }), 146 | text: Some(AnsiColor::Color16 { c16: 14 }), 147 | background: None, 148 | }, 149 | styles: TextStyleConfig::default(), 150 | options: { 151 | let mut opts = HashMap::new(); 152 | opts.insert( 153 | "api_base_url".to_string(), 154 | serde_json::Value::String("https://api.anthropic.com".to_string()), 155 | ); 156 | opts.insert( 157 | "cache_duration".to_string(), 158 | serde_json::Value::Number(180.into()), 159 | ); 160 | opts.insert("timeout".to_string(), serde_json::Value::Number(2.into())); 161 | opts 162 | }, 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/ui/themes/theme_cometix.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{ 2 | AnsiColor, ColorConfig, IconConfig, SegmentConfig, SegmentId, TextStyleConfig, 3 | }; 4 | use std::collections::HashMap; 5 | 6 | pub fn model_segment() -> SegmentConfig { 7 | SegmentConfig { 8 | id: SegmentId::Model, 9 | enabled: true, 10 | icon: IconConfig { 11 | plain: "🤖".to_string(), 12 | nerd_font: "\u{e26d}".to_string(), 13 | }, 14 | colors: ColorConfig { 15 | icon: Some(AnsiColor::Color16 { c16: 14 }), 16 | text: Some(AnsiColor::Color16 { c16: 14 }), 17 | background: None, 18 | }, 19 | styles: TextStyleConfig { text_bold: true }, 20 | options: HashMap::new(), 21 | } 22 | } 23 | 24 | pub fn directory_segment() -> SegmentConfig { 25 | SegmentConfig { 26 | id: SegmentId::Directory, 27 | enabled: true, 28 | icon: IconConfig { 29 | plain: "📁".to_string(), 30 | nerd_font: "\u{f024b}".to_string(), 31 | }, 32 | colors: ColorConfig { 33 | icon: Some(AnsiColor::Color16 { c16: 11 }), 34 | text: Some(AnsiColor::Color16 { c16: 10 }), 35 | background: None, 36 | }, 37 | styles: TextStyleConfig { text_bold: true }, 38 | options: HashMap::new(), 39 | } 40 | } 41 | 42 | pub fn git_segment() -> SegmentConfig { 43 | SegmentConfig { 44 | id: SegmentId::Git, 45 | enabled: true, 46 | icon: IconConfig { 47 | plain: "🌿".to_string(), 48 | nerd_font: "\u{f02a2}".to_string(), 49 | }, 50 | colors: ColorConfig { 51 | icon: Some(AnsiColor::Color16 { c16: 12 }), 52 | text: Some(AnsiColor::Color16 { c16: 12 }), 53 | background: None, 54 | }, 55 | styles: TextStyleConfig { text_bold: true }, 56 | options: { 57 | let mut opts = HashMap::new(); 58 | opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); 59 | opts 60 | }, 61 | } 62 | } 63 | 64 | pub fn context_window_segment() -> SegmentConfig { 65 | SegmentConfig { 66 | id: SegmentId::ContextWindow, 67 | enabled: true, 68 | icon: IconConfig { 69 | plain: "⚡️".to_string(), 70 | nerd_font: "\u{f49b}".to_string(), 71 | }, 72 | colors: ColorConfig { 73 | icon: Some(AnsiColor::Color16 { c16: 13 }), 74 | text: Some(AnsiColor::Color16 { c16: 13 }), 75 | background: None, 76 | }, 77 | styles: TextStyleConfig { text_bold: true }, 78 | options: HashMap::new(), 79 | } 80 | } 81 | 82 | pub fn cost_segment() -> SegmentConfig { 83 | SegmentConfig { 84 | id: SegmentId::Cost, 85 | enabled: false, 86 | icon: IconConfig { 87 | plain: "💰".to_string(), 88 | nerd_font: "\u{eec1}".to_string(), 89 | }, 90 | colors: ColorConfig { 91 | icon: Some(AnsiColor::Color16 { c16: 3 }), 92 | text: Some(AnsiColor::Color16 { c16: 3 }), 93 | background: None, 94 | }, 95 | styles: TextStyleConfig { text_bold: true }, 96 | options: HashMap::new(), 97 | } 98 | } 99 | 100 | pub fn session_segment() -> SegmentConfig { 101 | SegmentConfig { 102 | id: SegmentId::Session, 103 | enabled: false, 104 | icon: IconConfig { 105 | plain: "⏱️".to_string(), 106 | nerd_font: "\u{f19bb}".to_string(), 107 | }, 108 | colors: ColorConfig { 109 | icon: Some(AnsiColor::Color16 { c16: 2 }), 110 | text: Some(AnsiColor::Color16 { c16: 2 }), 111 | background: None, 112 | }, 113 | styles: TextStyleConfig { text_bold: true }, 114 | options: HashMap::new(), 115 | } 116 | } 117 | 118 | pub fn output_style_segment() -> SegmentConfig { 119 | SegmentConfig { 120 | id: SegmentId::OutputStyle, 121 | enabled: false, 122 | icon: IconConfig { 123 | plain: "🎯".to_string(), 124 | nerd_font: "\u{f12f5}".to_string(), 125 | }, 126 | colors: ColorConfig { 127 | icon: Some(AnsiColor::Color16 { c16: 6 }), 128 | text: Some(AnsiColor::Color16 { c16: 6 }), 129 | background: None, 130 | }, 131 | styles: TextStyleConfig { text_bold: true }, 132 | options: HashMap::new(), 133 | } 134 | } 135 | 136 | pub fn usage_segment() -> SegmentConfig { 137 | SegmentConfig { 138 | id: SegmentId::Usage, 139 | enabled: false, 140 | icon: IconConfig { 141 | plain: "📊".to_string(), 142 | nerd_font: "\u{f0a9e}".to_string(), 143 | }, 144 | colors: ColorConfig { 145 | icon: Some(AnsiColor::Color16 { c16: 14 }), 146 | text: Some(AnsiColor::Color16 { c16: 14 }), 147 | background: None, 148 | }, 149 | styles: TextStyleConfig::default(), 150 | options: { 151 | let mut opts = HashMap::new(); 152 | opts.insert( 153 | "api_base_url".to_string(), 154 | serde_json::Value::String("https://api.anthropic.com".to_string()), 155 | ); 156 | opts.insert( 157 | "cache_duration".to_string(), 158 | serde_json::Value::Number(180.into()), 159 | ); 160 | opts.insert("timeout".to_string(), serde_json::Value::Number(2.into())); 161 | opts 162 | }, 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/ui/themes/theme_default.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{ 2 | AnsiColor, ColorConfig, IconConfig, SegmentConfig, SegmentId, TextStyleConfig, 3 | }; 4 | use std::collections::HashMap; 5 | 6 | pub fn model_segment() -> SegmentConfig { 7 | SegmentConfig { 8 | id: SegmentId::Model, 9 | enabled: true, 10 | icon: IconConfig { 11 | plain: "🤖".to_string(), 12 | nerd_font: "\u{e26d}".to_string(), 13 | }, 14 | colors: ColorConfig { 15 | icon: Some(AnsiColor::Color16 { c16: 14 }), // Cyan 16 | text: Some(AnsiColor::Color16 { c16: 14 }), 17 | background: None, 18 | }, 19 | styles: TextStyleConfig::default(), 20 | options: HashMap::new(), 21 | } 22 | } 23 | 24 | pub fn directory_segment() -> SegmentConfig { 25 | SegmentConfig { 26 | id: SegmentId::Directory, 27 | enabled: true, 28 | icon: IconConfig { 29 | plain: "📁".to_string(), 30 | nerd_font: "\u{f024b}".to_string(), 31 | }, 32 | colors: ColorConfig { 33 | icon: Some(AnsiColor::Color16 { c16: 11 }), // Yellow 34 | text: Some(AnsiColor::Color16 { c16: 10 }), // Green 35 | background: None, 36 | }, 37 | styles: TextStyleConfig::default(), 38 | options: HashMap::new(), 39 | } 40 | } 41 | 42 | pub fn git_segment() -> SegmentConfig { 43 | SegmentConfig { 44 | id: SegmentId::Git, 45 | enabled: true, 46 | icon: IconConfig { 47 | plain: "🌿".to_string(), 48 | nerd_font: "\u{f02a2}".to_string(), 49 | }, 50 | colors: ColorConfig { 51 | icon: Some(AnsiColor::Color16 { c16: 12 }), // Blue 52 | text: Some(AnsiColor::Color16 { c16: 12 }), 53 | background: None, 54 | }, 55 | styles: TextStyleConfig::default(), 56 | options: { 57 | let mut opts = HashMap::new(); 58 | opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); 59 | opts 60 | }, 61 | } 62 | } 63 | 64 | pub fn context_window_segment() -> SegmentConfig { 65 | SegmentConfig { 66 | id: SegmentId::ContextWindow, 67 | enabled: true, 68 | icon: IconConfig { 69 | plain: "⚡️".to_string(), 70 | nerd_font: "\u{f49b}".to_string(), 71 | }, 72 | colors: ColorConfig { 73 | icon: Some(AnsiColor::Color16 { c16: 13 }), // Magenta 74 | text: Some(AnsiColor::Color16 { c16: 13 }), 75 | background: None, 76 | }, 77 | styles: TextStyleConfig::default(), 78 | options: HashMap::new(), 79 | } 80 | } 81 | 82 | pub fn usage_segment() -> SegmentConfig { 83 | SegmentConfig { 84 | id: SegmentId::Usage, 85 | enabled: false, 86 | icon: IconConfig { 87 | plain: "📊".to_string(), 88 | nerd_font: "\u{f0a9e}".to_string(), // circle_slice_1 89 | }, 90 | colors: ColorConfig { 91 | icon: Some(AnsiColor::Color16 { c16: 14 }), // Cyan 92 | text: Some(AnsiColor::Color16 { c16: 14 }), 93 | background: None, 94 | }, 95 | styles: TextStyleConfig::default(), 96 | options: { 97 | let mut opts = HashMap::new(); 98 | opts.insert( 99 | "api_base_url".to_string(), 100 | serde_json::Value::String("https://api.anthropic.com".to_string()), 101 | ); 102 | opts.insert( 103 | "cache_duration".to_string(), 104 | serde_json::Value::Number(180.into()), 105 | ); 106 | opts.insert("timeout".to_string(), serde_json::Value::Number(2.into())); 107 | opts 108 | }, 109 | } 110 | } 111 | 112 | pub fn cost_segment() -> SegmentConfig { 113 | SegmentConfig { 114 | id: SegmentId::Cost, 115 | enabled: false, 116 | icon: IconConfig { 117 | plain: "💰".to_string(), 118 | nerd_font: "\u{eec1}".to_string(), 119 | }, 120 | colors: ColorConfig { 121 | icon: Some(AnsiColor::Color16 { c16: 3 }), // Yellow 122 | text: Some(AnsiColor::Color16 { c16: 3 }), 123 | background: None, 124 | }, 125 | styles: TextStyleConfig::default(), 126 | options: HashMap::new(), 127 | } 128 | } 129 | 130 | pub fn session_segment() -> SegmentConfig { 131 | SegmentConfig { 132 | id: SegmentId::Session, 133 | enabled: false, 134 | icon: IconConfig { 135 | plain: "⏱️".to_string(), 136 | nerd_font: "\u{f19bb}".to_string(), 137 | }, 138 | colors: ColorConfig { 139 | icon: Some(AnsiColor::Color16 { c16: 2 }), // Green 140 | text: Some(AnsiColor::Color16 { c16: 2 }), 141 | background: None, 142 | }, 143 | styles: TextStyleConfig::default(), 144 | options: HashMap::new(), 145 | } 146 | } 147 | 148 | pub fn output_style_segment() -> SegmentConfig { 149 | SegmentConfig { 150 | id: SegmentId::OutputStyle, 151 | enabled: false, 152 | icon: IconConfig { 153 | plain: "🎯".to_string(), 154 | nerd_font: "\u{f12f5}".to_string(), 155 | }, 156 | colors: ColorConfig { 157 | icon: Some(AnsiColor::Color16 { c16: 6 }), // Cyan 158 | text: Some(AnsiColor::Color16 { c16: 6 }), 159 | background: None, 160 | }, 161 | styles: TextStyleConfig::default(), 162 | options: HashMap::new(), 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/ui/themes/theme_gruvbox.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{ 2 | AnsiColor, ColorConfig, IconConfig, SegmentConfig, SegmentId, TextStyleConfig, 3 | }; 4 | use std::collections::HashMap; 5 | 6 | pub fn model_segment() -> SegmentConfig { 7 | SegmentConfig { 8 | id: SegmentId::Model, 9 | enabled: true, 10 | icon: IconConfig { 11 | plain: "🤖".to_string(), 12 | nerd_font: "\u{e26d}".to_string(), 13 | }, 14 | colors: ColorConfig { 15 | icon: Some(AnsiColor::Color256 { c256: 208 }), // Gruvbox orange 16 | text: Some(AnsiColor::Color256 { c256: 208 }), 17 | background: None, 18 | }, 19 | styles: TextStyleConfig { text_bold: true }, 20 | options: HashMap::new(), 21 | } 22 | } 23 | 24 | pub fn directory_segment() -> SegmentConfig { 25 | SegmentConfig { 26 | id: SegmentId::Directory, 27 | enabled: true, 28 | icon: IconConfig { 29 | plain: "📁".to_string(), 30 | nerd_font: "\u{f024b}".to_string(), 31 | }, 32 | colors: ColorConfig { 33 | icon: Some(AnsiColor::Color256 { c256: 142 }), // Gruvbox green 34 | text: Some(AnsiColor::Color256 { c256: 142 }), 35 | background: None, 36 | }, 37 | styles: TextStyleConfig { text_bold: true }, 38 | options: HashMap::new(), 39 | } 40 | } 41 | 42 | pub fn git_segment() -> SegmentConfig { 43 | SegmentConfig { 44 | id: SegmentId::Git, 45 | enabled: true, 46 | icon: IconConfig { 47 | plain: "🌿".to_string(), 48 | nerd_font: "\u{f02a2}".to_string(), 49 | }, 50 | colors: ColorConfig { 51 | icon: Some(AnsiColor::Color256 { c256: 109 }), // Gruvbox cyan 52 | text: Some(AnsiColor::Color256 { c256: 109 }), 53 | background: None, 54 | }, 55 | styles: TextStyleConfig { text_bold: true }, 56 | options: { 57 | let mut opts = HashMap::new(); 58 | opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); 59 | opts 60 | }, 61 | } 62 | } 63 | 64 | pub fn context_window_segment() -> SegmentConfig { 65 | SegmentConfig { 66 | id: SegmentId::ContextWindow, 67 | enabled: true, 68 | icon: IconConfig { 69 | plain: "⚡️".to_string(), 70 | nerd_font: "\u{f49b}".to_string(), 71 | }, 72 | colors: ColorConfig { 73 | icon: Some(AnsiColor::Color16 { c16: 5 }), 74 | text: Some(AnsiColor::Color16 { c16: 5 }), 75 | background: None, 76 | }, 77 | styles: TextStyleConfig { text_bold: true }, 78 | options: HashMap::new(), 79 | } 80 | } 81 | 82 | pub fn cost_segment() -> SegmentConfig { 83 | SegmentConfig { 84 | id: SegmentId::Cost, 85 | enabled: false, 86 | icon: IconConfig { 87 | plain: "💰".to_string(), 88 | nerd_font: "\u{eec1}".to_string(), 89 | }, 90 | colors: ColorConfig { 91 | icon: Some(AnsiColor::Color256 { c256: 214 }), // Gruvbox yellow 92 | text: Some(AnsiColor::Color256 { c256: 214 }), 93 | background: None, 94 | }, 95 | styles: TextStyleConfig { text_bold: true }, 96 | options: HashMap::new(), 97 | } 98 | } 99 | 100 | pub fn session_segment() -> SegmentConfig { 101 | SegmentConfig { 102 | id: SegmentId::Session, 103 | enabled: false, 104 | icon: IconConfig { 105 | plain: "⏱️".to_string(), 106 | nerd_font: "\u{f19bb}".to_string(), 107 | }, 108 | colors: ColorConfig { 109 | icon: Some(AnsiColor::Color256 { c256: 142 }), // Gruvbox green 110 | text: Some(AnsiColor::Color256 { c256: 142 }), 111 | background: None, 112 | }, 113 | styles: TextStyleConfig { text_bold: true }, 114 | options: HashMap::new(), 115 | } 116 | } 117 | 118 | pub fn output_style_segment() -> SegmentConfig { 119 | SegmentConfig { 120 | id: SegmentId::OutputStyle, 121 | enabled: false, 122 | icon: IconConfig { 123 | plain: "🎯".to_string(), 124 | nerd_font: "\u{f12f5}".to_string(), 125 | }, 126 | colors: ColorConfig { 127 | icon: Some(AnsiColor::Color256 { c256: 109 }), // Gruvbox cyan 128 | text: Some(AnsiColor::Color256 { c256: 109 }), 129 | background: None, 130 | }, 131 | styles: TextStyleConfig { text_bold: true }, 132 | options: HashMap::new(), 133 | } 134 | } 135 | 136 | pub fn usage_segment() -> SegmentConfig { 137 | SegmentConfig { 138 | id: SegmentId::Usage, 139 | enabled: false, 140 | icon: IconConfig { 141 | plain: "📊".to_string(), 142 | nerd_font: "\u{f0a9e}".to_string(), 143 | }, 144 | colors: ColorConfig { 145 | icon: Some(AnsiColor::Color16 { c16: 14 }), 146 | text: Some(AnsiColor::Color16 { c16: 14 }), 147 | background: None, 148 | }, 149 | styles: TextStyleConfig::default(), 150 | options: { 151 | let mut opts = HashMap::new(); 152 | opts.insert( 153 | "api_base_url".to_string(), 154 | serde_json::Value::String("https://api.anthropic.com".to_string()), 155 | ); 156 | opts.insert( 157 | "cache_duration".to_string(), 158 | serde_json::Value::Number(180.into()), 159 | ); 160 | opts.insert("timeout".to_string(), serde_json::Value::Number(2.into())); 161 | opts 162 | }, 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /npm/main/scripts/postinstall.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const os = require('os'); 4 | 5 | // Silent mode detection 6 | const silent = process.env.npm_config_loglevel === 'silent' || 7 | process.env.CCLINE_SKIP_POSTINSTALL === '1'; 8 | 9 | if (!silent) { 10 | console.log('🚀 Setting up CCometixLine for Claude Code...'); 11 | } 12 | 13 | try { 14 | const platform = process.platform; 15 | const arch = process.arch; 16 | const homeDir = os.homedir(); 17 | const claudeDir = path.join(homeDir, '.claude', 'ccline'); 18 | 19 | // Create directory 20 | fs.mkdirSync(claudeDir, { recursive: true }); 21 | 22 | // Determine platform key 23 | let platformKey = `${platform}-${arch}`; 24 | if (platform === 'linux') { 25 | // Detect if static linking is needed based on glibc version 26 | function shouldUseStaticBinary() { 27 | try { 28 | const { execSync } = require('child_process'); 29 | const lddOutput = execSync('ldd --version 2>/dev/null || echo ""', { 30 | encoding: 'utf8', 31 | timeout: 1000 32 | }); 33 | 34 | // Parse "ldd (GNU libc) 2.35" format 35 | const match = lddOutput.match(/(?:GNU libc|GLIBC).*?(\d+)\.(\d+)/); 36 | if (match) { 37 | const major = parseInt(match[1]); 38 | const minor = parseInt(match[2]); 39 | // Use static binary if glibc < 2.35 40 | return major < 2 || (major === 2 && minor < 35); 41 | } 42 | } catch (e) { 43 | // If detection fails, default to dynamic binary 44 | return false; 45 | } 46 | 47 | return false; 48 | } 49 | 50 | if (shouldUseStaticBinary()) { 51 | platformKey = 'linux-x64-musl'; 52 | } 53 | } 54 | 55 | const packageMap = { 56 | 'darwin-x64': '@cometix/ccline-darwin-x64', 57 | 'darwin-arm64': '@cometix/ccline-darwin-arm64', 58 | 'linux-x64': '@cometix/ccline-linux-x64', 59 | 'linux-x64-musl': '@cometix/ccline-linux-x64-musl', 60 | 'win32-x64': '@cometix/ccline-win32-x64', 61 | 'win32-ia32': '@cometix/ccline-win32-x64', // Use 64-bit for 32-bit 62 | }; 63 | 64 | const packageName = packageMap[platformKey]; 65 | if (!packageName) { 66 | if (!silent) { 67 | console.log(`Platform ${platformKey} not supported for auto-setup`); 68 | } 69 | process.exit(0); 70 | } 71 | 72 | const binaryName = platform === 'win32' ? 'ccline.exe' : 'ccline'; 73 | const targetPath = path.join(claudeDir, binaryName); 74 | 75 | // Multiple path search strategies for different package managers 76 | const findBinaryPath = () => { 77 | const possiblePaths = [ 78 | // npm/yarn: nested in node_modules 79 | path.join(__dirname, '..', 'node_modules', packageName, binaryName), 80 | // pnpm: try require.resolve first 81 | (() => { 82 | try { 83 | const packagePath = require.resolve(packageName + '/package.json'); 84 | return path.join(path.dirname(packagePath), binaryName); 85 | } catch { 86 | return null; 87 | } 88 | })(), 89 | // pnpm: flat structure fallback with version detection 90 | (() => { 91 | const currentPath = __dirname; 92 | const pnpmMatch = currentPath.match(/(.+\.pnpm)[\\/]([^\\//]+)[\\/]/); 93 | if (pnpmMatch) { 94 | const pnpmRoot = pnpmMatch[1]; 95 | const packageNameEncoded = packageName.replace('/', '+'); 96 | 97 | try { 98 | // Try to find any version of the package 99 | const pnpmContents = fs.readdirSync(pnpmRoot); 100 | const packagePattern = new RegExp(`^${packageNameEncoded.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}@`); 101 | const matchingPackage = pnpmContents.find(dir => packagePattern.test(dir)); 102 | 103 | if (matchingPackage) { 104 | return path.join(pnpmRoot, matchingPackage, 'node_modules', packageName, binaryName); 105 | } 106 | } catch { 107 | // Fallback to current behavior if directory reading fails 108 | } 109 | } 110 | return null; 111 | })() 112 | ].filter(p => p !== null); 113 | 114 | for (const testPath of possiblePaths) { 115 | if (fs.existsSync(testPath)) { 116 | return testPath; 117 | } 118 | } 119 | return null; 120 | }; 121 | 122 | const sourcePath = findBinaryPath(); 123 | if (!sourcePath) { 124 | if (!silent) { 125 | console.log('Binary package not installed, skipping Claude Code setup'); 126 | console.log('The global ccline command will still work via npm'); 127 | } 128 | process.exit(0); 129 | } 130 | 131 | // Copy or link the binary 132 | if (platform === 'win32') { 133 | // Windows: Copy file 134 | fs.copyFileSync(sourcePath, targetPath); 135 | } else { 136 | // Unix: Try hard link first, fallback to copy 137 | try { 138 | if (fs.existsSync(targetPath)) { 139 | fs.unlinkSync(targetPath); 140 | } 141 | fs.linkSync(sourcePath, targetPath); 142 | } catch { 143 | fs.copyFileSync(sourcePath, targetPath); 144 | } 145 | fs.chmodSync(targetPath, '755'); 146 | } 147 | 148 | if (!silent) { 149 | console.log('✨ CCometixLine is ready for Claude Code!'); 150 | console.log(`📍 Location: ${targetPath}`); 151 | console.log('🎉 You can now use: ccline --help'); 152 | } 153 | } catch (error) { 154 | // Silent failure - don't break installation 155 | if (!silent) { 156 | console.log('Note: Could not auto-configure for Claude Code'); 157 | console.log('The global ccline command will still work.'); 158 | console.log('You can manually copy ccline to ~/.claude/ccline/ if needed'); 159 | } 160 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | build: 14 | name: Build for ${{ matrix.target }} 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | include: 19 | - target: x86_64-unknown-linux-gnu 20 | os: ubuntu-22.04 21 | name: ccline-linux-x64.tar.gz 22 | - target: x86_64-unknown-linux-musl 23 | os: ubuntu-latest 24 | name: ccline-linux-x64-static.tar.gz 25 | - target: x86_64-pc-windows-gnu 26 | os: ubuntu-latest 27 | name: ccline-windows-x64.zip 28 | - target: x86_64-apple-darwin 29 | os: macos-latest 30 | name: ccline-macos-x64.tar.gz 31 | - target: aarch64-apple-darwin 32 | os: macos-latest 33 | name: ccline-macos-arm64.tar.gz 34 | 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v4 38 | 39 | - name: Install Rust 40 | uses: dtolnay/rust-toolchain@stable 41 | with: 42 | targets: ${{ matrix.target }} 43 | 44 | - name: Install cross-compilation tools 45 | if: matrix.target == 'x86_64-pc-windows-gnu' 46 | run: | 47 | sudo apt-get update 48 | sudo apt-get install -y mingw-w64 49 | 50 | - name: Install musl tools 51 | if: matrix.target == 'x86_64-unknown-linux-musl' 52 | run: | 53 | sudo apt-get update 54 | sudo apt-get install -y musl-tools 55 | 56 | - name: Build binary 57 | run: cargo build --release --target ${{ matrix.target }} 58 | 59 | - name: Package Linux/macOS 60 | if: matrix.os != 'windows-latest' && matrix.target != 'x86_64-pc-windows-gnu' 61 | run: | 62 | mkdir -p dist 63 | cp target/${{ matrix.target }}/release/ccometixline dist/ccline 64 | cd dist 65 | tar czf ../${{ matrix.name }} ccline 66 | 67 | - name: Package Windows 68 | if: matrix.target == 'x86_64-pc-windows-gnu' 69 | run: | 70 | mkdir -p dist 71 | cp target/${{ matrix.target }}/release/ccometixline.exe dist/ccline.exe 72 | cd dist 73 | zip ../${{ matrix.name }} ccline.exe 74 | 75 | - name: Upload artifact 76 | uses: actions/upload-artifact@v4 77 | with: 78 | name: ${{ matrix.name }} 79 | path: ${{ matrix.name }} 80 | 81 | release: 82 | name: Create Release 83 | runs-on: ubuntu-latest 84 | needs: build 85 | if: startsWith(github.ref, 'refs/tags/') 86 | steps: 87 | - name: Checkout 88 | uses: actions/checkout@v4 89 | 90 | - name: Download artifacts 91 | uses: actions/download-artifact@v4 92 | with: 93 | path: artifacts 94 | 95 | - name: Create Release 96 | uses: softprops/action-gh-release@v2 97 | with: 98 | files: artifacts/*/* 99 | generate_release_notes: true 100 | draft: false 101 | prerelease: false 102 | 103 | - name: Setup Node.js for NPM 104 | uses: actions/setup-node@v4 105 | with: 106 | node-version: '18' 107 | registry-url: 'https://registry.npmjs.org' 108 | 109 | - name: Extract binaries from archives 110 | run: | 111 | mkdir -p extracted 112 | 113 | # macOS x64 114 | tar -xzf artifacts/ccline-macos-x64.tar.gz/ccline-macos-x64.tar.gz -C extracted 115 | mv extracted/ccline extracted/ccline-darwin-x64 116 | 117 | # macOS ARM64 118 | tar -xzf artifacts/ccline-macos-arm64.tar.gz/ccline-macos-arm64.tar.gz -C extracted 119 | mv extracted/ccline extracted/ccline-darwin-arm64 120 | 121 | # Linux x64 122 | tar -xzf artifacts/ccline-linux-x64.tar.gz/ccline-linux-x64.tar.gz -C extracted 123 | mv extracted/ccline extracted/ccline-linux-x64 124 | 125 | # Linux musl (static) 126 | tar -xzf artifacts/ccline-linux-x64-static.tar.gz/ccline-linux-x64-static.tar.gz -C extracted 127 | mv extracted/ccline extracted/ccline-linux-x64-musl 128 | 129 | # Windows 130 | unzip artifacts/ccline-windows-x64.zip/ccline-windows-x64.zip -d extracted 131 | mv extracted/ccline.exe extracted/ccline-win32-x64.exe 132 | 133 | # List extracted files 134 | ls -la extracted/ 135 | 136 | - name: Prepare NPM packages 137 | run: | 138 | # Prepare packages with version management 139 | node npm/scripts/prepare-packages.js 140 | 141 | # Copy binaries to platform directories 142 | cp extracted/ccline-darwin-x64 npm-publish/darwin-x64/ccline 143 | cp extracted/ccline-darwin-arm64 npm-publish/darwin-arm64/ccline 144 | cp extracted/ccline-linux-x64 npm-publish/linux-x64/ccline 145 | cp extracted/ccline-linux-x64-musl npm-publish/linux-x64-musl/ccline 146 | cp extracted/ccline-win32-x64.exe npm-publish/win32-x64/ccline.exe 147 | 148 | # Set executable permissions for Unix binaries 149 | chmod +x npm-publish/darwin-x64/ccline 150 | chmod +x npm-publish/darwin-arm64/ccline 151 | chmod +x npm-publish/linux-x64/ccline 152 | chmod +x npm-publish/linux-x64-musl/ccline 153 | 154 | # Verify packages 155 | echo "Package structure:" 156 | find npm-publish -name "package.json" -exec echo "=== {} ===" \; -exec head -5 {} \; 157 | 158 | - name: Publish platform packages to NPM 159 | env: 160 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 161 | run: | 162 | # Publish platform packages first 163 | for platform in darwin-x64 darwin-arm64 linux-x64 linux-x64-musl win32-x64; do 164 | echo "📦 Publishing @cometix/ccline-$platform" 165 | cd npm-publish/$platform 166 | npm publish --access public 167 | cd ../.. 168 | echo "✅ Published @cometix/ccline-$platform" 169 | done 170 | 171 | - name: Wait for NPM registry 172 | run: | 173 | echo "⏳ Waiting for platform packages to be available on NPM..." 174 | sleep 30 175 | 176 | - name: Publish main package to NPM 177 | env: 178 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 179 | run: | 180 | cd npm-publish/main 181 | echo "📦 Publishing @cometix/ccline" 182 | npm publish --access public 183 | echo "✅ Published @cometix/ccline" 184 | echo "" 185 | echo "🎉 NPM packages published successfully!" 186 | echo "Install with: npm install -g @cometix/ccline" -------------------------------------------------------------------------------- /src/ui/components/separator_editor.rs: -------------------------------------------------------------------------------- 1 | use ratatui::{ 2 | layout::{Constraint, Direction, Layout, Rect}, 3 | style::{Color, Style}, 4 | widgets::{Block, Borders, Clear, Paragraph}, 5 | Frame, 6 | }; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct SeparatorEditorComponent { 10 | pub is_open: bool, 11 | pub input: String, 12 | pub presets: Vec, 13 | pub selected_preset: Option, 14 | } 15 | 16 | #[derive(Debug, Clone)] 17 | pub struct SeparatorPreset { 18 | pub name: String, 19 | pub value: String, 20 | pub description: String, 21 | } 22 | 23 | impl Default for SeparatorEditorComponent { 24 | fn default() -> Self { 25 | Self::new() 26 | } 27 | } 28 | 29 | impl SeparatorEditorComponent { 30 | pub fn new() -> Self { 31 | Self { 32 | is_open: false, 33 | input: String::new(), 34 | presets: Self::default_presets(), 35 | selected_preset: None, 36 | } 37 | } 38 | 39 | fn default_presets() -> Vec { 40 | vec![ 41 | SeparatorPreset { 42 | name: "Pipe".to_string(), 43 | value: " | ".to_string(), 44 | description: "Classic pipe separator".to_string(), 45 | }, 46 | SeparatorPreset { 47 | name: "Thin".to_string(), 48 | value: " │ ".to_string(), 49 | description: "Thin vertical line".to_string(), 50 | }, 51 | SeparatorPreset { 52 | name: "Arrow".to_string(), 53 | value: "\u{e0b0}".to_string(), 54 | description: "Powerline arrow (seamless transition)".to_string(), 55 | }, 56 | SeparatorPreset { 57 | name: "Space".to_string(), 58 | value: " ".to_string(), 59 | description: "Double space".to_string(), 60 | }, 61 | SeparatorPreset { 62 | name: "Dot".to_string(), 63 | value: " • ".to_string(), 64 | description: "Middle dot".to_string(), 65 | }, 66 | ] 67 | } 68 | 69 | pub fn open(&mut self, current_separator: &str) { 70 | self.is_open = true; 71 | self.input = current_separator.to_string(); 72 | self.selected_preset = None; 73 | 74 | // Check if current separator matches a preset 75 | for (i, preset) in self.presets.iter().enumerate() { 76 | if preset.value == current_separator { 77 | self.selected_preset = Some(i); 78 | break; 79 | } 80 | } 81 | } 82 | 83 | pub fn close(&mut self) { 84 | self.is_open = false; 85 | self.input.clear(); 86 | self.selected_preset = None; 87 | } 88 | 89 | pub fn input_char(&mut self, c: char) { 90 | // Allow most characters for separator 91 | if !c.is_control() { 92 | self.input.push(c); 93 | self.selected_preset = None; // Clear preset selection when manually editing 94 | } 95 | } 96 | 97 | pub fn backspace(&mut self) { 98 | self.input.pop(); 99 | self.selected_preset = None; // Clear preset selection when manually editing 100 | } 101 | 102 | pub fn move_preset_selection(&mut self, delta: i32) { 103 | let new_selection = if let Some(current) = self.selected_preset { 104 | let new_idx = (current as i32 + delta).clamp(0, self.presets.len() as i32 - 1) as usize; 105 | Some(new_idx) 106 | } else if delta > 0 { 107 | Some(0) 108 | } else { 109 | Some(self.presets.len() - 1) 110 | }; 111 | 112 | self.selected_preset = new_selection; 113 | if let Some(idx) = new_selection { 114 | self.input = self.presets[idx].value.clone(); 115 | } 116 | } 117 | 118 | pub fn get_separator(&self) -> String { 119 | self.input.clone() 120 | } 121 | 122 | pub fn render(&self, f: &mut Frame, area: Rect) { 123 | if !self.is_open { 124 | return; 125 | } 126 | 127 | // Calculate exact size needed 128 | let popup_height = 15; 129 | let popup_width = 60; 130 | let popup_area = Rect { 131 | x: (area.width.saturating_sub(popup_width)) / 2, 132 | y: (area.height.saturating_sub(popup_height)) / 2, 133 | width: popup_width, 134 | height: popup_height, 135 | }; 136 | 137 | // Clear the popup area first 138 | f.render_widget(Clear, popup_area); 139 | 140 | let popup_block = Block::default() 141 | .borders(Borders::ALL) 142 | .title("Separator Editor"); 143 | let inner = popup_block.inner(popup_area); 144 | f.render_widget(popup_block, popup_area); 145 | 146 | let chunks = Layout::default() 147 | .direction(Direction::Vertical) 148 | .constraints([ 149 | Constraint::Length(3), // Current input 150 | Constraint::Min(5), // Presets list 151 | Constraint::Length(3), // Actions 152 | ]) 153 | .split(inner); 154 | 155 | // Current input field 156 | f.render_widget( 157 | Paragraph::new(format!("> {} <", self.input)) 158 | .style(Style::default().fg(Color::Yellow)) 159 | .block( 160 | Block::default() 161 | .borders(Borders::ALL) 162 | .title("Current Separator"), 163 | ), 164 | chunks[0], 165 | ); 166 | 167 | // Presets list 168 | let preset_text = self 169 | .presets 170 | .iter() 171 | .enumerate() 172 | .map(|(i, preset)| { 173 | let marker = if Some(i) == self.selected_preset { 174 | "[•]" 175 | } else { 176 | "[ ]" 177 | }; 178 | format!("{} {} - {}", marker, preset.name, preset.description) 179 | }) 180 | .collect::>() 181 | .join("\n"); 182 | 183 | f.render_widget( 184 | Paragraph::new(preset_text).block( 185 | Block::default() 186 | .borders(Borders::ALL) 187 | .title("Presets (↑↓ to select)"), 188 | ), 189 | chunks[1], 190 | ); 191 | 192 | // Actions 193 | f.render_widget( 194 | Paragraph::new("[Enter] Confirm [Esc] Cancel [Tab] Clear") 195 | .block(Block::default().borders(Borders::ALL)), 196 | chunks[2], 197 | ); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # CCometixLine 2 | 3 | [English](README.md) | [中文](README.zh.md) 4 | 5 | 基于 Rust 的高性能 Claude Code 状态栏工具,集成 Git 信息、使用量跟踪、交互式 TUI 配置和 Claude Code 补丁工具。 6 | 7 | ![Language:Rust](https://img.shields.io/static/v1?label=Language&message=Rust&color=orange&style=flat-square) 8 | ![License:MIT](https://img.shields.io/static/v1?label=License&message=MIT&color=blue&style=flat-square) 9 | 10 | ## 截图 11 | 12 | ![CCometixLine](assets/img1.png) 13 | 14 | 状态栏显示:模型 | 目录 | Git 分支状态 | 上下文窗口信息 15 | 16 | ## 特性 17 | 18 | ### 核心功能 19 | - **Git 集成** 显示分支、状态和跟踪信息 20 | - **模型显示** 简化的 Claude 模型名称 21 | - **使用量跟踪** 基于转录文件分析 22 | - **目录显示** 显示当前工作空间 23 | - **简洁设计** 使用 Nerd Font 图标 24 | 25 | ### 交互式 TUI 功能 26 | - **交互式主菜单** 无输入时直接执行显示菜单 27 | - **TUI 配置界面** 实时预览配置效果 28 | - **主题系统** 多种内置预设主题 29 | - **段落自定义** 精细化控制各段落 30 | - **配置管理** 初始化、检查、编辑配置 31 | 32 | ### Claude Code 增强 33 | - **禁用上下文警告** 移除烦人的"Context low"消息 34 | - **启用详细模式** 增强输出详细信息 35 | - **稳定补丁器** 适应 Claude Code 版本更新 36 | - **自动备份** 安全修改,支持轻松恢复 37 | 38 | ## 安装 39 | 40 | ### 快速安装(推荐) 41 | 42 | 通过 npm 安装(适用于所有平台): 43 | 44 | ```bash 45 | # 全局安装 46 | npm install -g @cometix/ccline 47 | 48 | # 或使用 yarn 49 | yarn global add @cometix/ccline 50 | 51 | # 或使用 pnpm 52 | pnpm add -g @cometix/ccline 53 | ``` 54 | 55 | 使用镜像源加速下载: 56 | ```bash 57 | npm install -g @cometix/ccline --registry https://registry.npmmirror.com 58 | ``` 59 | 60 | 安装后: 61 | - ✅ 全局命令 `ccline` 可在任何地方使用 62 | - ⚙️ 按照下方提示进行配置以集成到 Claude Code 63 | - 🎨 运行 `ccline -c` 打开配置面板进行主题选择 64 | 65 | ### Claude Code 配置 66 | 67 | 添加到 Claude Code `settings.json`: 68 | 69 | **Linux/macOS:** 70 | ```json 71 | { 72 | "statusLine": { 73 | "type": "command", 74 | "command": "~/.claude/ccline/ccline", 75 | "padding": 0 76 | } 77 | } 78 | ``` 79 | 80 | **Windows:** 81 | ```json 82 | { 83 | "statusLine": { 84 | "type": "command", 85 | "command": "%USERPROFILE%\\.claude\\ccline\\ccline.exe", 86 | "padding": 0 87 | } 88 | } 89 | ``` 90 | 91 | **后备方案 (npm 安装):** 92 | ```json 93 | { 94 | "statusLine": { 95 | "type": "command", 96 | "command": "ccline", 97 | "padding": 0 98 | } 99 | } 100 | ``` 101 | *如果 npm 全局安装已在 PATH 中可用,则使用此配置* 102 | 103 | ### 更新 104 | 105 | ```bash 106 | npm update -g @cometix/ccline 107 | ``` 108 | 109 |
110 | 手动安装(点击展开) 111 | 112 | 或者从 [Releases](https://github.com/Haleclipse/CCometixLine/releases) 手动下载: 113 | 114 | #### Linux 115 | 116 | #### 选项 1: 动态链接版本(推荐) 117 | ```bash 118 | mkdir -p ~/.claude/ccline 119 | wget https://github.com/Haleclipse/CCometixLine/releases/latest/download/ccline-linux-x64.tar.gz 120 | tar -xzf ccline-linux-x64.tar.gz 121 | cp ccline ~/.claude/ccline/ 122 | chmod +x ~/.claude/ccline/ccline 123 | ``` 124 | *系统要求: Ubuntu 22.04+, CentOS 9+, Debian 11+, RHEL 9+ (glibc 2.35+)* 125 | 126 | #### 选项 2: 静态链接版本(通用兼容) 127 | ```bash 128 | mkdir -p ~/.claude/ccline 129 | wget https://github.com/Haleclipse/CCometixLine/releases/latest/download/ccline-linux-x64-static.tar.gz 130 | tar -xzf ccline-linux-x64-static.tar.gz 131 | cp ccline ~/.claude/ccline/ 132 | chmod +x ~/.claude/ccline/ccline 133 | ``` 134 | *适用于任何 Linux 发行版(静态链接,无依赖)* 135 | 136 | #### macOS (Intel) 137 | 138 | ```bash 139 | mkdir -p ~/.claude/ccline 140 | wget https://github.com/Haleclipse/CCometixLine/releases/latest/download/ccline-macos-x64.tar.gz 141 | tar -xzf ccline-macos-x64.tar.gz 142 | cp ccline ~/.claude/ccline/ 143 | chmod +x ~/.claude/ccline/ccline 144 | ``` 145 | 146 | #### macOS (Apple Silicon) 147 | 148 | ```bash 149 | mkdir -p ~/.claude/ccline 150 | wget https://github.com/Haleclipse/CCometixLine/releases/latest/download/ccline-macos-arm64.tar.gz 151 | tar -xzf ccline-macos-arm64.tar.gz 152 | cp ccline ~/.claude/ccline/ 153 | chmod +x ~/.claude/ccline/ccline 154 | ``` 155 | 156 | #### Windows 157 | 158 | ```powershell 159 | # 创建目录并下载 160 | New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.claude\ccline" 161 | Invoke-WebRequest -Uri "https://github.com/Haleclipse/CCometixLine/releases/latest/download/ccline-windows-x64.zip" -OutFile "ccline-windows-x64.zip" 162 | Expand-Archive -Path "ccline-windows-x64.zip" -DestinationPath "." 163 | Move-Item "ccline.exe" "$env:USERPROFILE\.claude\ccline\" 164 | ``` 165 | 166 |
167 | 168 | ### 从源码构建 169 | 170 | ```bash 171 | git clone https://github.com/Haleclipse/CCometixLine.git 172 | cd CCometixLine 173 | cargo build --release 174 | cp target/release/ccometixline ~/.claude/ccline/ccline 175 | ``` 176 | 177 | ## 使用 178 | 179 | ### 配置管理 180 | 181 | ```bash 182 | # 初始化配置文件 183 | ccline --init 184 | 185 | # 检查配置有效性 186 | ccline --check 187 | 188 | # 打印当前配置 189 | ccline --print 190 | 191 | # 进入 TUI 配置模式 192 | ccline --config 193 | ``` 194 | 195 | ### 主题覆盖 196 | 197 | ```bash 198 | # 临时使用指定主题(覆盖配置文件设置) 199 | ccline --theme cometix 200 | ccline --theme minimal 201 | ccline --theme gruvbox 202 | ccline --theme nord 203 | ccline --theme powerline-dark 204 | 205 | # 或使用 ~/.claude/ccline/themes/ 目录下的自定义主题 206 | ccline --theme my-custom-theme 207 | ``` 208 | 209 | ### Claude Code 增强 210 | 211 | ```bash 212 | # 禁用上下文警告并启用详细模式 213 | ccline --patch /path/to/claude-code/cli.js 214 | 215 | # 常见安装路径示例 216 | ccline --patch ~/.local/share/fnm/node-versions/v24.4.1/installation/lib/node_modules/@anthropic-ai/claude-code/cli.js 217 | ``` 218 | 219 | ## 默认段落 220 | 221 | 显示:`目录 | Git 分支状态 | 模型 | 上下文窗口` 222 | 223 | ### Git 状态指示器 224 | 225 | - 带 Nerd Font 图标的分支名 226 | - 状态:`✓` 清洁,`●` 有更改,`⚠` 冲突 227 | - 远程跟踪:`↑n` 领先,`↓n` 落后 228 | 229 | ### 模型显示 230 | 231 | 显示简化的 Claude 模型名称: 232 | - `claude-3-5-sonnet` → `Sonnet 3.5` 233 | - `claude-4-sonnet` → `Sonnet 4` 234 | 235 | ### 上下文窗口显示 236 | 237 | 基于转录文件分析的令牌使用百分比,包含上下文限制跟踪。 238 | 239 | ## 配置 240 | 241 | CCometixLine 支持通过 TOML 文件和交互式 TUI 进行完整配置: 242 | 243 | - **配置文件**: `~/.claude/ccline/config.toml` 244 | - **交互式 TUI**: `ccline --config` 实时编辑配置并预览效果 245 | - **主题文件**: `~/.claude/ccline/themes/*.toml` 自定义主题文件 246 | - **自动初始化**: `ccline --init` 创建默认配置 247 | 248 | ### 可用段落 249 | 250 | 所有段落都支持配置: 251 | - 启用/禁用切换 252 | - 自定义分隔符和图标 253 | - 颜色自定义 254 | - 格式选项 255 | 256 | 支持的段落:目录、Git、模型、使用量、时间、成本、输出样式 257 | 258 | 259 | ## 系统要求 260 | 261 | - **Git**: 版本 1.5+ (推荐 Git 2.22+ 以获得更好的分支检测) 262 | - **终端**: 必须支持 Nerd Font 图标正常显示 263 | - 安装 [Nerd Font](https://www.nerdfonts.com/) 字体 264 | - 中文用户推荐: [Maple Font](https://github.com/subframe7536/maple-font) (支持中文的 Nerd Font) 265 | - 在终端中配置使用该字体 266 | - **Claude Code**: 用于状态栏集成 267 | 268 | ## 开发 269 | 270 | ```bash 271 | # 构建开发版本 272 | cargo build 273 | 274 | # 运行测试 275 | cargo test 276 | 277 | # 构建优化版本 278 | cargo build --release 279 | ``` 280 | 281 | ## 路线图 282 | 283 | - [x] TOML 配置文件支持 284 | - [x] TUI 配置界面 285 | - [x] 自定义主题 286 | - [x] 交互式主菜单 287 | - [x] Claude Code 增强工具 288 | 289 | ## 贡献 290 | 291 | 欢迎贡献!请随时提交 issue 或 pull request。 292 | 293 | ## 许可证 294 | 295 | 本项目采用 [MIT 许可证](LICENSE)。 296 | 297 | ## Star History 298 | 299 | [![Star History Chart](https://api.star-history.com/svg?repos=Haleclipse/CCometixLine&type=Date)](https://star-history.com/#Haleclipse/CCometixLine&Date) -------------------------------------------------------------------------------- /src/config/loader.rs: -------------------------------------------------------------------------------- 1 | use super::types::Config; 2 | use std::fs; 3 | use std::path::{Path, PathBuf}; 4 | 5 | /// Result of config initialization 6 | #[derive(Debug)] 7 | pub enum InitResult { 8 | /// Config was created at the given path 9 | Created(PathBuf), 10 | /// Config already existed at the given path 11 | AlreadyExists(PathBuf), 12 | } 13 | 14 | pub struct ConfigLoader; 15 | 16 | impl ConfigLoader { 17 | pub fn load() -> Config { 18 | Config::load().unwrap_or_else(|_| Config::default()) 19 | } 20 | 21 | pub fn load_from_path>(path: P) -> Result> { 22 | let content = fs::read_to_string(path)?; 23 | let config: Config = toml::from_str(&content)?; 24 | Ok(config) 25 | } 26 | 27 | /// Initialize themes directory and create built-in theme files 28 | pub fn init_themes() -> Result<(), Box> { 29 | let themes_dir = Self::get_themes_path(); 30 | 31 | // Create themes directory 32 | fs::create_dir_all(&themes_dir)?; 33 | 34 | let builtin_themes = [ 35 | "cometix", 36 | "default", 37 | "minimal", 38 | "gruvbox", 39 | "nord", 40 | "powerline-dark", 41 | "powerline-light", 42 | "powerline-rose-pine", 43 | "powerline-tokyo-night", 44 | ]; 45 | let mut created_any = false; 46 | 47 | for theme_name in &builtin_themes { 48 | let theme_path = themes_dir.join(format!("{}.toml", theme_name)); 49 | 50 | if !theme_path.exists() { 51 | let theme_config = crate::ui::themes::ThemePresets::get_theme(theme_name); 52 | let content = toml::to_string_pretty(&theme_config)?; 53 | fs::write(&theme_path, content)?; 54 | println!("Created theme file: {}", theme_path.display()); 55 | created_any = true; 56 | } 57 | } 58 | 59 | if !created_any { 60 | // println!("All built-in theme files already exist"); 61 | } 62 | 63 | Ok(()) 64 | } 65 | 66 | /// Get the themes directory path (~/.claude/ccline/themes/) 67 | pub fn get_themes_path() -> PathBuf { 68 | if let Some(home) = dirs::home_dir() { 69 | home.join(".claude").join("ccline").join("themes") 70 | } else { 71 | PathBuf::from(".claude/ccline/themes") 72 | } 73 | } 74 | 75 | /// Ensure themes directory exists and has built-in themes (silent mode) 76 | pub fn ensure_themes_exist() { 77 | // Silently ensure themes exist without printing output 78 | let _ = Self::init_themes_silent(); 79 | } 80 | 81 | /// Initialize themes directory and create built-in theme files (silent mode) 82 | fn init_themes_silent() -> Result<(), Box> { 83 | let themes_dir = Self::get_themes_path(); 84 | 85 | // Create themes directory 86 | fs::create_dir_all(&themes_dir)?; 87 | 88 | let builtin_themes = [ 89 | "default", 90 | "minimal", 91 | "gruvbox", 92 | "nord", 93 | "cometix", 94 | "powerline-dark", 95 | "powerline-light", 96 | "powerline-rose-pine", 97 | "powerline-tokyo-night", 98 | ]; 99 | 100 | for theme_name in &builtin_themes { 101 | let theme_path = themes_dir.join(format!("{}.toml", theme_name)); 102 | 103 | if !theme_path.exists() { 104 | let theme_config = crate::ui::themes::ThemePresets::get_theme(theme_name); 105 | let content = toml::to_string_pretty(&theme_config)?; 106 | fs::write(&theme_path, content)?; 107 | } 108 | } 109 | 110 | Ok(()) 111 | } 112 | } 113 | 114 | impl Config { 115 | /// Load configuration from default location 116 | pub fn load() -> Result> { 117 | // Ensure themes directory exists and has built-in themes 118 | ConfigLoader::ensure_themes_exist(); 119 | 120 | let config_path = Self::get_config_path(); 121 | 122 | if !config_path.exists() { 123 | return Ok(Config::default()); 124 | } 125 | 126 | let content = fs::read_to_string(config_path)?; 127 | let config: Config = toml::from_str(&content)?; 128 | Ok(config) 129 | } 130 | 131 | /// Save configuration to default location 132 | pub fn save(&self) -> Result<(), Box> { 133 | let config_path = Self::get_config_path(); 134 | 135 | // Ensure config directory exists 136 | if let Some(parent) = config_path.parent() { 137 | fs::create_dir_all(parent)?; 138 | } 139 | 140 | let content = toml::to_string_pretty(self)?; 141 | fs::write(config_path, content)?; 142 | Ok(()) 143 | } 144 | 145 | /// Get the default config file path (~/.claude/ccline/config.toml) 146 | fn get_config_path() -> PathBuf { 147 | if let Some(home) = dirs::home_dir() { 148 | home.join(".claude").join("ccline").join("config.toml") 149 | } else { 150 | PathBuf::from(".claude/ccline/config.toml") 151 | } 152 | } 153 | 154 | /// Initialize config directory and create default config 155 | pub fn init() -> Result> { 156 | let config_path = Self::get_config_path(); 157 | 158 | // Create directory 159 | if let Some(parent) = config_path.parent() { 160 | fs::create_dir_all(parent)?; 161 | } 162 | 163 | // Initialize themes directory and built-in themes 164 | ConfigLoader::init_themes()?; 165 | 166 | // Create default config if it doesn't exist 167 | if !config_path.exists() { 168 | let default_config = Config::default(); 169 | default_config.save()?; 170 | Ok(InitResult::Created(config_path)) 171 | } else { 172 | Ok(InitResult::AlreadyExists(config_path)) 173 | } 174 | } 175 | 176 | /// Validate configuration 177 | pub fn check(&self) -> Result<(), Box> { 178 | // Basic validation 179 | if self.segments.is_empty() { 180 | return Err("No segments configured".into()); 181 | } 182 | 183 | // Validate segment IDs are unique 184 | let mut seen_ids = std::collections::HashSet::new(); 185 | for segment in &self.segments { 186 | if !seen_ids.insert(segment.id) { 187 | return Err(format!("Duplicate segment ID: {:?}", segment.id).into()); 188 | } 189 | } 190 | 191 | Ok(()) 192 | } 193 | 194 | /// Print configuration as TOML 195 | pub fn print(&self) -> Result<(), Box> { 196 | let content = toml::to_string_pretty(self)?; 197 | println!("{}", content); 198 | Ok(()) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/core/segments/git.rs: -------------------------------------------------------------------------------- 1 | use super::{Segment, SegmentData}; 2 | use crate::config::{InputData, SegmentId}; 3 | use std::collections::HashMap; 4 | use std::process::Command; 5 | 6 | #[derive(Debug)] 7 | pub struct GitInfo { 8 | pub branch: String, 9 | pub status: GitStatus, 10 | pub ahead: u32, 11 | pub behind: u32, 12 | pub sha: Option, 13 | } 14 | 15 | #[derive(Debug, PartialEq)] 16 | pub enum GitStatus { 17 | Clean, 18 | Dirty, 19 | Conflicts, 20 | } 21 | 22 | pub struct GitSegment { 23 | show_sha: bool, 24 | } 25 | 26 | impl Default for GitSegment { 27 | fn default() -> Self { 28 | Self::new() 29 | } 30 | } 31 | 32 | impl GitSegment { 33 | pub fn new() -> Self { 34 | Self { show_sha: false } 35 | } 36 | 37 | pub fn with_sha(mut self, show_sha: bool) -> Self { 38 | self.show_sha = show_sha; 39 | self 40 | } 41 | 42 | fn get_git_info(&self, working_dir: &str) -> Option { 43 | if !self.is_git_repository(working_dir) { 44 | return None; 45 | } 46 | 47 | let branch = self 48 | .get_branch(working_dir) 49 | .unwrap_or_else(|| "detached".to_string()); 50 | let status = self.get_status(working_dir); 51 | let (ahead, behind) = self.get_ahead_behind(working_dir); 52 | let sha = if self.show_sha { 53 | self.get_sha(working_dir) 54 | } else { 55 | None 56 | }; 57 | 58 | Some(GitInfo { 59 | branch, 60 | status, 61 | ahead, 62 | behind, 63 | sha, 64 | }) 65 | } 66 | 67 | fn is_git_repository(&self, working_dir: &str) -> bool { 68 | Command::new("git") 69 | .args(["--no-optional-locks", "rev-parse", "--git-dir"]) 70 | .current_dir(working_dir) 71 | .output() 72 | .map(|output| output.status.success()) 73 | .unwrap_or(false) 74 | } 75 | 76 | fn get_branch(&self, working_dir: &str) -> Option { 77 | if let Ok(output) = Command::new("git") 78 | .args(["--no-optional-locks", "branch", "--show-current"]) 79 | .current_dir(working_dir) 80 | .output() 81 | { 82 | if output.status.success() { 83 | let branch = String::from_utf8(output.stdout).ok()?.trim().to_string(); 84 | if !branch.is_empty() { 85 | return Some(branch); 86 | } 87 | } 88 | } 89 | 90 | if let Ok(output) = Command::new("git") 91 | .args(["--no-optional-locks", "symbolic-ref", "--short", "HEAD"]) 92 | .current_dir(working_dir) 93 | .output() 94 | { 95 | if output.status.success() { 96 | let branch = String::from_utf8(output.stdout).ok()?.trim().to_string(); 97 | if !branch.is_empty() { 98 | return Some(branch); 99 | } 100 | } 101 | } 102 | 103 | None 104 | } 105 | 106 | fn get_status(&self, working_dir: &str) -> GitStatus { 107 | let output = Command::new("git") 108 | .args(["--no-optional-locks", "status", "--porcelain"]) 109 | .current_dir(working_dir) 110 | .output(); 111 | 112 | match output { 113 | Ok(output) if output.status.success() => { 114 | let status_text = String::from_utf8(output.stdout).unwrap_or_default(); 115 | 116 | if status_text.trim().is_empty() { 117 | return GitStatus::Clean; 118 | } 119 | 120 | if status_text.contains("UU") 121 | || status_text.contains("AA") 122 | || status_text.contains("DD") 123 | { 124 | GitStatus::Conflicts 125 | } else { 126 | GitStatus::Dirty 127 | } 128 | } 129 | _ => GitStatus::Clean, 130 | } 131 | } 132 | 133 | fn get_ahead_behind(&self, working_dir: &str) -> (u32, u32) { 134 | let ahead = self.get_commit_count(working_dir, "@{u}..HEAD"); 135 | let behind = self.get_commit_count(working_dir, "HEAD..@{u}"); 136 | (ahead, behind) 137 | } 138 | 139 | fn get_commit_count(&self, working_dir: &str, range: &str) -> u32 { 140 | let output = Command::new("git") 141 | .args(["--no-optional-locks", "rev-list", "--count", range]) 142 | .current_dir(working_dir) 143 | .output(); 144 | 145 | match output { 146 | Ok(output) if output.status.success() => String::from_utf8(output.stdout) 147 | .ok() 148 | .and_then(|s| s.trim().parse().ok()) 149 | .unwrap_or(0), 150 | _ => 0, 151 | } 152 | } 153 | 154 | fn get_sha(&self, working_dir: &str) -> Option { 155 | let output = Command::new("git") 156 | .args(["--no-optional-locks", "rev-parse", "--short=7", "HEAD"]) 157 | .current_dir(working_dir) 158 | .output() 159 | .ok()?; 160 | 161 | if output.status.success() { 162 | let sha = String::from_utf8(output.stdout).ok()?.trim().to_string(); 163 | if sha.is_empty() { 164 | None 165 | } else { 166 | Some(sha) 167 | } 168 | } else { 169 | None 170 | } 171 | } 172 | } 173 | 174 | impl Segment for GitSegment { 175 | fn collect(&self, input: &InputData) -> Option { 176 | let git_info = self.get_git_info(&input.workspace.current_dir)?; 177 | 178 | let mut metadata = HashMap::new(); 179 | metadata.insert("branch".to_string(), git_info.branch.clone()); 180 | metadata.insert("status".to_string(), format!("{:?}", git_info.status)); 181 | metadata.insert("ahead".to_string(), git_info.ahead.to_string()); 182 | metadata.insert("behind".to_string(), git_info.behind.to_string()); 183 | 184 | if let Some(ref sha) = git_info.sha { 185 | metadata.insert("sha".to_string(), sha.clone()); 186 | } 187 | 188 | let primary = git_info.branch; 189 | let mut status_parts = Vec::new(); 190 | 191 | match git_info.status { 192 | GitStatus::Clean => status_parts.push("✓".to_string()), 193 | GitStatus::Dirty => status_parts.push("●".to_string()), 194 | GitStatus::Conflicts => status_parts.push("⚠".to_string()), 195 | } 196 | 197 | if git_info.ahead > 0 { 198 | status_parts.push(format!("↑{}", git_info.ahead)); 199 | } 200 | if git_info.behind > 0 { 201 | status_parts.push(format!("↓{}", git_info.behind)); 202 | } 203 | 204 | if let Some(ref sha) = git_info.sha { 205 | status_parts.push(sha.clone()); 206 | } 207 | 208 | Some(SegmentData { 209 | primary, 210 | secondary: status_parts.join(" "), 211 | metadata, 212 | }) 213 | } 214 | 215 | fn id(&self) -> SegmentId { 216 | SegmentId::Git 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/ui/themes/theme_powerline_light.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{ 2 | AnsiColor, ColorConfig, IconConfig, SegmentConfig, SegmentId, TextStyleConfig, 3 | }; 4 | use std::collections::HashMap; 5 | 6 | pub fn model_segment() -> SegmentConfig { 7 | SegmentConfig { 8 | id: SegmentId::Model, 9 | enabled: true, 10 | icon: IconConfig { 11 | plain: "🤖".to_string(), 12 | nerd_font: "\u{e26d}".to_string(), 13 | }, 14 | colors: ColorConfig { 15 | icon: Some(AnsiColor::Rgb { r: 0, g: 0, b: 0 }), 16 | text: Some(AnsiColor::Rgb { r: 0, g: 0, b: 0 }), 17 | background: Some(AnsiColor::Rgb { 18 | r: 135, 19 | g: 206, 20 | b: 235, 21 | }), 22 | }, 23 | styles: TextStyleConfig::default(), 24 | options: HashMap::new(), 25 | } 26 | } 27 | 28 | pub fn directory_segment() -> SegmentConfig { 29 | SegmentConfig { 30 | id: SegmentId::Directory, 31 | enabled: true, 32 | icon: IconConfig { 33 | plain: "📁".to_string(), 34 | nerd_font: "\u{f024b}".to_string(), 35 | }, 36 | colors: ColorConfig { 37 | icon: Some(AnsiColor::Rgb { 38 | r: 255, 39 | g: 255, 40 | b: 255, 41 | }), 42 | text: Some(AnsiColor::Rgb { 43 | r: 255, 44 | g: 255, 45 | b: 255, 46 | }), 47 | background: Some(AnsiColor::Rgb { 48 | r: 255, 49 | g: 107, 50 | b: 71, 51 | }), 52 | }, 53 | styles: TextStyleConfig::default(), 54 | options: HashMap::new(), 55 | } 56 | } 57 | 58 | pub fn git_segment() -> SegmentConfig { 59 | SegmentConfig { 60 | id: SegmentId::Git, 61 | enabled: true, 62 | icon: IconConfig { 63 | plain: "🌿".to_string(), 64 | nerd_font: "\u{f02a2}".to_string(), 65 | }, 66 | colors: ColorConfig { 67 | icon: Some(AnsiColor::Rgb { 68 | r: 255, 69 | g: 255, 70 | b: 255, 71 | }), 72 | text: Some(AnsiColor::Rgb { 73 | r: 255, 74 | g: 255, 75 | b: 255, 76 | }), 77 | background: Some(AnsiColor::Rgb { 78 | r: 79, 79 | g: 179, 80 | b: 217, 81 | }), 82 | }, 83 | styles: TextStyleConfig::default(), 84 | options: { 85 | let mut opts = HashMap::new(); 86 | opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); 87 | opts 88 | }, 89 | } 90 | } 91 | 92 | pub fn context_window_segment() -> SegmentConfig { 93 | SegmentConfig { 94 | id: SegmentId::ContextWindow, 95 | enabled: true, 96 | icon: IconConfig { 97 | plain: "⚡️".to_string(), 98 | nerd_font: "\u{f49b}".to_string(), 99 | }, 100 | colors: ColorConfig { 101 | icon: Some(AnsiColor::Rgb { 102 | r: 255, 103 | g: 255, 104 | b: 255, 105 | }), 106 | text: Some(AnsiColor::Rgb { 107 | r: 255, 108 | g: 255, 109 | b: 255, 110 | }), 111 | background: Some(AnsiColor::Rgb { 112 | r: 107, 113 | g: 114, 114 | b: 128, 115 | }), 116 | }, 117 | styles: TextStyleConfig::default(), 118 | options: HashMap::new(), 119 | } 120 | } 121 | 122 | pub fn cost_segment() -> SegmentConfig { 123 | SegmentConfig { 124 | id: SegmentId::Cost, 125 | enabled: false, 126 | icon: IconConfig { 127 | plain: "💰".to_string(), 128 | nerd_font: "\u{eec1}".to_string(), 129 | }, 130 | colors: ColorConfig { 131 | icon: Some(AnsiColor::Rgb { 132 | r: 255, 133 | g: 255, 134 | b: 255, 135 | }), 136 | text: Some(AnsiColor::Rgb { 137 | r: 255, 138 | g: 255, 139 | b: 255, 140 | }), 141 | background: Some(AnsiColor::Rgb { 142 | r: 255, 143 | g: 193, 144 | b: 7, 145 | }), 146 | }, 147 | styles: TextStyleConfig::default(), 148 | options: HashMap::new(), 149 | } 150 | } 151 | 152 | pub fn session_segment() -> SegmentConfig { 153 | SegmentConfig { 154 | id: SegmentId::Session, 155 | enabled: false, 156 | icon: IconConfig { 157 | plain: "⏱️".to_string(), 158 | nerd_font: "\u{f19bb}".to_string(), 159 | }, 160 | colors: ColorConfig { 161 | icon: Some(AnsiColor::Rgb { 162 | r: 255, 163 | g: 255, 164 | b: 255, 165 | }), 166 | text: Some(AnsiColor::Rgb { 167 | r: 255, 168 | g: 255, 169 | b: 255, 170 | }), 171 | background: Some(AnsiColor::Rgb { 172 | r: 40, 173 | g: 167, 174 | b: 69, 175 | }), 176 | }, 177 | styles: TextStyleConfig::default(), 178 | options: HashMap::new(), 179 | } 180 | } 181 | 182 | pub fn output_style_segment() -> SegmentConfig { 183 | SegmentConfig { 184 | id: SegmentId::OutputStyle, 185 | enabled: false, 186 | icon: IconConfig { 187 | plain: "🎯".to_string(), 188 | nerd_font: "\u{f12f5}".to_string(), 189 | }, 190 | colors: ColorConfig { 191 | icon: Some(AnsiColor::Rgb { 192 | r: 255, 193 | g: 255, 194 | b: 255, 195 | }), 196 | text: Some(AnsiColor::Rgb { 197 | r: 255, 198 | g: 255, 199 | b: 255, 200 | }), 201 | background: Some(AnsiColor::Rgb { 202 | r: 32, 203 | g: 201, 204 | b: 151, 205 | }), 206 | }, 207 | styles: TextStyleConfig::default(), 208 | options: HashMap::new(), 209 | } 210 | } 211 | 212 | pub fn usage_segment() -> SegmentConfig { 213 | SegmentConfig { 214 | id: SegmentId::Usage, 215 | enabled: false, 216 | icon: IconConfig { 217 | plain: "📊".to_string(), 218 | nerd_font: "\u{f0a9e}".to_string(), 219 | }, 220 | colors: ColorConfig { 221 | icon: Some(AnsiColor::Color16 { c16: 14 }), 222 | text: Some(AnsiColor::Color16 { c16: 14 }), 223 | background: None, 224 | }, 225 | styles: TextStyleConfig::default(), 226 | options: { 227 | let mut opts = HashMap::new(); 228 | opts.insert( 229 | "api_base_url".to_string(), 230 | serde_json::Value::String("https://api.anthropic.com".to_string()), 231 | ); 232 | opts.insert( 233 | "cache_duration".to_string(), 234 | serde_json::Value::Number(180.into()), 235 | ); 236 | opts.insert("timeout".to_string(), serde_json::Value::Number(2.into())); 237 | opts 238 | }, 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/ui/themes/theme_nord.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{ 2 | AnsiColor, ColorConfig, IconConfig, SegmentConfig, SegmentId, TextStyleConfig, 3 | }; 4 | use std::collections::HashMap; 5 | 6 | pub fn model_segment() -> SegmentConfig { 7 | SegmentConfig { 8 | id: SegmentId::Model, 9 | enabled: true, 10 | icon: IconConfig { 11 | plain: "🤖".to_string(), 12 | nerd_font: "\u{e26d}".to_string(), 13 | }, 14 | colors: ColorConfig { 15 | icon: Some(AnsiColor::Rgb { 16 | r: 46, 17 | g: 52, 18 | b: 64, 19 | }), 20 | text: Some(AnsiColor::Rgb { 21 | r: 46, 22 | g: 52, 23 | b: 64, 24 | }), 25 | background: Some(AnsiColor::Rgb { 26 | r: 136, 27 | g: 192, 28 | b: 208, 29 | }), 30 | }, 31 | styles: TextStyleConfig::default(), 32 | options: HashMap::new(), 33 | } 34 | } 35 | 36 | pub fn directory_segment() -> SegmentConfig { 37 | SegmentConfig { 38 | id: SegmentId::Directory, 39 | enabled: true, 40 | icon: IconConfig { 41 | plain: "📁".to_string(), 42 | nerd_font: "\u{f024b}".to_string(), 43 | }, 44 | colors: ColorConfig { 45 | icon: Some(AnsiColor::Rgb { 46 | r: 46, 47 | g: 52, 48 | b: 64, 49 | }), 50 | text: Some(AnsiColor::Rgb { 51 | r: 46, 52 | g: 52, 53 | b: 64, 54 | }), 55 | background: Some(AnsiColor::Rgb { 56 | r: 163, 57 | g: 190, 58 | b: 140, 59 | }), 60 | }, 61 | styles: TextStyleConfig::default(), 62 | options: HashMap::new(), 63 | } 64 | } 65 | 66 | pub fn git_segment() -> SegmentConfig { 67 | SegmentConfig { 68 | id: SegmentId::Git, 69 | enabled: true, 70 | icon: IconConfig { 71 | plain: "🌿".to_string(), 72 | nerd_font: "\u{f02a2}".to_string(), 73 | }, 74 | colors: ColorConfig { 75 | icon: Some(AnsiColor::Rgb { 76 | r: 46, 77 | g: 52, 78 | b: 64, 79 | }), 80 | text: Some(AnsiColor::Rgb { 81 | r: 46, 82 | g: 52, 83 | b: 64, 84 | }), 85 | background: Some(AnsiColor::Rgb { 86 | r: 129, 87 | g: 161, 88 | b: 193, 89 | }), 90 | }, 91 | styles: TextStyleConfig::default(), 92 | options: { 93 | let mut opts = HashMap::new(); 94 | opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); 95 | opts 96 | }, 97 | } 98 | } 99 | 100 | pub fn context_window_segment() -> SegmentConfig { 101 | SegmentConfig { 102 | id: SegmentId::ContextWindow, 103 | enabled: true, 104 | icon: IconConfig { 105 | plain: "⚡️".to_string(), 106 | nerd_font: "\u{f49b}".to_string(), 107 | }, 108 | colors: ColorConfig { 109 | icon: Some(AnsiColor::Rgb { 110 | r: 46, 111 | g: 52, 112 | b: 64, 113 | }), 114 | text: Some(AnsiColor::Rgb { 115 | r: 46, 116 | g: 52, 117 | b: 64, 118 | }), 119 | background: Some(AnsiColor::Rgb { 120 | r: 180, 121 | g: 142, 122 | b: 173, 123 | }), 124 | }, 125 | styles: TextStyleConfig::default(), 126 | options: HashMap::new(), 127 | } 128 | } 129 | 130 | pub fn cost_segment() -> SegmentConfig { 131 | SegmentConfig { 132 | id: SegmentId::Cost, 133 | enabled: false, 134 | icon: IconConfig { 135 | plain: "💰".to_string(), 136 | nerd_font: "\u{eec1}".to_string(), 137 | }, 138 | colors: ColorConfig { 139 | icon: Some(AnsiColor::Rgb { 140 | r: 46, 141 | g: 52, 142 | b: 64, 143 | }), 144 | text: Some(AnsiColor::Rgb { 145 | r: 46, 146 | g: 52, 147 | b: 64, 148 | }), 149 | background: Some(AnsiColor::Rgb { 150 | r: 235, 151 | g: 203, 152 | b: 139, 153 | }), // Nord yellow background 154 | }, 155 | styles: TextStyleConfig::default(), 156 | options: HashMap::new(), 157 | } 158 | } 159 | 160 | pub fn session_segment() -> SegmentConfig { 161 | SegmentConfig { 162 | id: SegmentId::Session, 163 | enabled: false, 164 | icon: IconConfig { 165 | plain: "⏱️".to_string(), 166 | nerd_font: "\u{f19bb}".to_string(), 167 | }, 168 | colors: ColorConfig { 169 | icon: Some(AnsiColor::Rgb { 170 | r: 46, 171 | g: 52, 172 | b: 64, 173 | }), 174 | text: Some(AnsiColor::Rgb { 175 | r: 46, 176 | g: 52, 177 | b: 64, 178 | }), 179 | background: Some(AnsiColor::Rgb { 180 | r: 163, 181 | g: 190, 182 | b: 140, 183 | }), // Nord green background 184 | }, 185 | styles: TextStyleConfig::default(), 186 | options: HashMap::new(), 187 | } 188 | } 189 | 190 | pub fn output_style_segment() -> SegmentConfig { 191 | SegmentConfig { 192 | id: SegmentId::OutputStyle, 193 | enabled: false, 194 | icon: IconConfig { 195 | plain: "🎯".to_string(), 196 | nerd_font: "\u{f12f5}".to_string(), 197 | }, 198 | colors: ColorConfig { 199 | icon: Some(AnsiColor::Rgb { 200 | r: 46, 201 | g: 52, 202 | b: 64, 203 | }), 204 | text: Some(AnsiColor::Rgb { 205 | r: 46, 206 | g: 52, 207 | b: 64, 208 | }), 209 | background: Some(AnsiColor::Rgb { 210 | r: 136, 211 | g: 192, 212 | b: 208, 213 | }), // Nord cyan background 214 | }, 215 | styles: TextStyleConfig::default(), 216 | options: HashMap::new(), 217 | } 218 | } 219 | 220 | pub fn usage_segment() -> SegmentConfig { 221 | SegmentConfig { 222 | id: SegmentId::Usage, 223 | enabled: false, 224 | icon: IconConfig { 225 | plain: "📊".to_string(), 226 | nerd_font: "\u{f0a9e}".to_string(), 227 | }, 228 | colors: ColorConfig { 229 | icon: Some(AnsiColor::Color16 { c16: 14 }), 230 | text: Some(AnsiColor::Color16 { c16: 14 }), 231 | background: None, 232 | }, 233 | styles: TextStyleConfig::default(), 234 | options: { 235 | let mut opts = HashMap::new(); 236 | opts.insert( 237 | "api_base_url".to_string(), 238 | serde_json::Value::String("https://api.anthropic.com".to_string()), 239 | ); 240 | opts.insert( 241 | "cache_duration".to_string(), 242 | serde_json::Value::Number(180.into()), 243 | ); 244 | opts.insert("timeout".to_string(), serde_json::Value::Number(2.into())); 245 | opts 246 | }, 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/ui/themes/theme_powerline_dark.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{ 2 | AnsiColor, ColorConfig, IconConfig, SegmentConfig, SegmentId, TextStyleConfig, 3 | }; 4 | use std::collections::HashMap; 5 | 6 | pub fn model_segment() -> SegmentConfig { 7 | SegmentConfig { 8 | id: SegmentId::Model, 9 | enabled: true, 10 | icon: IconConfig { 11 | plain: "🤖".to_string(), 12 | nerd_font: "\u{e26d}".to_string(), 13 | }, 14 | colors: ColorConfig { 15 | icon: Some(AnsiColor::Rgb { 16 | r: 255, 17 | g: 255, 18 | b: 255, 19 | }), 20 | text: Some(AnsiColor::Rgb { 21 | r: 255, 22 | g: 255, 23 | b: 255, 24 | }), 25 | background: Some(AnsiColor::Rgb { 26 | r: 45, 27 | g: 45, 28 | b: 45, 29 | }), 30 | }, 31 | styles: TextStyleConfig::default(), 32 | options: HashMap::new(), 33 | } 34 | } 35 | 36 | pub fn directory_segment() -> SegmentConfig { 37 | SegmentConfig { 38 | id: SegmentId::Directory, 39 | enabled: true, 40 | icon: IconConfig { 41 | plain: "📁".to_string(), 42 | nerd_font: "\u{f024b}".to_string(), 43 | }, 44 | colors: ColorConfig { 45 | icon: Some(AnsiColor::Rgb { 46 | r: 255, 47 | g: 255, 48 | b: 255, 49 | }), 50 | text: Some(AnsiColor::Rgb { 51 | r: 255, 52 | g: 255, 53 | b: 255, 54 | }), 55 | background: Some(AnsiColor::Rgb { 56 | r: 139, 57 | g: 69, 58 | b: 19, 59 | }), 60 | }, 61 | styles: TextStyleConfig::default(), 62 | options: HashMap::new(), 63 | } 64 | } 65 | 66 | pub fn git_segment() -> SegmentConfig { 67 | SegmentConfig { 68 | id: SegmentId::Git, 69 | enabled: true, 70 | icon: IconConfig { 71 | plain: "🌿".to_string(), 72 | nerd_font: "\u{f02a2}".to_string(), 73 | }, 74 | colors: ColorConfig { 75 | icon: Some(AnsiColor::Rgb { 76 | r: 255, 77 | g: 255, 78 | b: 255, 79 | }), 80 | text: Some(AnsiColor::Rgb { 81 | r: 255, 82 | g: 255, 83 | b: 255, 84 | }), 85 | background: Some(AnsiColor::Rgb { 86 | r: 64, 87 | g: 64, 88 | b: 64, 89 | }), 90 | }, 91 | styles: TextStyleConfig::default(), 92 | options: { 93 | let mut opts = HashMap::new(); 94 | opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); 95 | opts 96 | }, 97 | } 98 | } 99 | 100 | pub fn context_window_segment() -> SegmentConfig { 101 | SegmentConfig { 102 | id: SegmentId::ContextWindow, 103 | enabled: true, 104 | icon: IconConfig { 105 | plain: "⚡️".to_string(), 106 | nerd_font: "\u{f49b}".to_string(), 107 | }, 108 | colors: ColorConfig { 109 | icon: Some(AnsiColor::Rgb { 110 | r: 209, 111 | g: 213, 112 | b: 219, 113 | }), 114 | text: Some(AnsiColor::Rgb { 115 | r: 209, 116 | g: 213, 117 | b: 219, 118 | }), 119 | background: Some(AnsiColor::Rgb { 120 | r: 55, 121 | g: 65, 122 | b: 81, 123 | }), 124 | }, 125 | styles: TextStyleConfig::default(), 126 | options: HashMap::new(), 127 | } 128 | } 129 | 130 | pub fn cost_segment() -> SegmentConfig { 131 | SegmentConfig { 132 | id: SegmentId::Cost, 133 | enabled: false, 134 | icon: IconConfig { 135 | plain: "💰".to_string(), 136 | nerd_font: "\u{eec1}".to_string(), 137 | }, 138 | colors: ColorConfig { 139 | icon: Some(AnsiColor::Rgb { 140 | r: 229, 141 | g: 192, 142 | b: 123, 143 | }), 144 | text: Some(AnsiColor::Rgb { 145 | r: 229, 146 | g: 192, 147 | b: 123, 148 | }), 149 | background: Some(AnsiColor::Rgb { 150 | r: 40, 151 | g: 44, 152 | b: 52, 153 | }), // Powerline dark background 154 | }, 155 | styles: TextStyleConfig::default(), 156 | options: HashMap::new(), 157 | } 158 | } 159 | 160 | pub fn session_segment() -> SegmentConfig { 161 | SegmentConfig { 162 | id: SegmentId::Session, 163 | enabled: false, 164 | icon: IconConfig { 165 | plain: "⏱️".to_string(), 166 | nerd_font: "\u{f19bb}".to_string(), 167 | }, 168 | colors: ColorConfig { 169 | icon: Some(AnsiColor::Rgb { 170 | r: 163, 171 | g: 190, 172 | b: 140, 173 | }), 174 | text: Some(AnsiColor::Rgb { 175 | r: 163, 176 | g: 190, 177 | b: 140, 178 | }), 179 | background: Some(AnsiColor::Rgb { 180 | r: 45, 181 | g: 50, 182 | b: 59, 183 | }), // Powerline darker background 184 | }, 185 | styles: TextStyleConfig::default(), 186 | options: HashMap::new(), 187 | } 188 | } 189 | 190 | pub fn output_style_segment() -> SegmentConfig { 191 | SegmentConfig { 192 | id: SegmentId::OutputStyle, 193 | enabled: false, 194 | icon: IconConfig { 195 | plain: "🎯".to_string(), 196 | nerd_font: "\u{f12f5}".to_string(), 197 | }, 198 | colors: ColorConfig { 199 | icon: Some(AnsiColor::Rgb { 200 | r: 129, 201 | g: 161, 202 | b: 193, 203 | }), 204 | text: Some(AnsiColor::Rgb { 205 | r: 129, 206 | g: 161, 207 | b: 193, 208 | }), 209 | background: Some(AnsiColor::Rgb { 210 | r: 50, 211 | g: 56, 212 | b: 66, 213 | }), // Powerline darkest background 214 | }, 215 | styles: TextStyleConfig::default(), 216 | options: HashMap::new(), 217 | } 218 | } 219 | 220 | pub fn usage_segment() -> SegmentConfig { 221 | SegmentConfig { 222 | id: SegmentId::Usage, 223 | enabled: false, 224 | icon: IconConfig { 225 | plain: "📊".to_string(), 226 | nerd_font: "\u{f0a9e}".to_string(), 227 | }, 228 | colors: ColorConfig { 229 | icon: Some(AnsiColor::Color16 { c16: 14 }), 230 | text: Some(AnsiColor::Color16 { c16: 14 }), 231 | background: None, 232 | }, 233 | styles: TextStyleConfig::default(), 234 | options: { 235 | let mut opts = HashMap::new(); 236 | opts.insert( 237 | "api_base_url".to_string(), 238 | serde_json::Value::String("https://api.anthropic.com".to_string()), 239 | ); 240 | opts.insert( 241 | "cache_duration".to_string(), 242 | serde_json::Value::Number(180.into()), 243 | ); 244 | opts.insert("timeout".to_string(), serde_json::Value::Number(2.into())); 245 | opts 246 | }, 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/ui/themes/theme_powerline_rose_pine.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{ 2 | AnsiColor, ColorConfig, IconConfig, SegmentConfig, SegmentId, TextStyleConfig, 3 | }; 4 | use std::collections::HashMap; 5 | 6 | pub fn model_segment() -> SegmentConfig { 7 | SegmentConfig { 8 | id: SegmentId::Model, 9 | enabled: true, 10 | icon: IconConfig { 11 | plain: "🤖".to_string(), 12 | nerd_font: "\u{e26d}".to_string(), 13 | }, 14 | colors: ColorConfig { 15 | icon: Some(AnsiColor::Rgb { 16 | r: 235, 17 | g: 188, 18 | b: 186, 19 | }), 20 | text: Some(AnsiColor::Rgb { 21 | r: 235, 22 | g: 188, 23 | b: 186, 24 | }), 25 | background: Some(AnsiColor::Rgb { 26 | r: 25, 27 | g: 23, 28 | b: 36, 29 | }), 30 | }, 31 | styles: TextStyleConfig::default(), 32 | options: HashMap::new(), 33 | } 34 | } 35 | 36 | pub fn directory_segment() -> SegmentConfig { 37 | SegmentConfig { 38 | id: SegmentId::Directory, 39 | enabled: true, 40 | icon: IconConfig { 41 | plain: "📁".to_string(), 42 | nerd_font: "\u{f024b}".to_string(), 43 | }, 44 | colors: ColorConfig { 45 | icon: Some(AnsiColor::Rgb { 46 | r: 196, 47 | g: 167, 48 | b: 231, 49 | }), 50 | text: Some(AnsiColor::Rgb { 51 | r: 196, 52 | g: 167, 53 | b: 231, 54 | }), 55 | background: Some(AnsiColor::Rgb { 56 | r: 38, 57 | g: 35, 58 | b: 58, 59 | }), 60 | }, 61 | styles: TextStyleConfig::default(), 62 | options: HashMap::new(), 63 | } 64 | } 65 | 66 | pub fn git_segment() -> SegmentConfig { 67 | SegmentConfig { 68 | id: SegmentId::Git, 69 | enabled: true, 70 | icon: IconConfig { 71 | plain: "🌿".to_string(), 72 | nerd_font: "\u{f02a2}".to_string(), 73 | }, 74 | colors: ColorConfig { 75 | icon: Some(AnsiColor::Rgb { 76 | r: 156, 77 | g: 207, 78 | b: 216, 79 | }), 80 | text: Some(AnsiColor::Rgb { 81 | r: 156, 82 | g: 207, 83 | b: 216, 84 | }), 85 | background: Some(AnsiColor::Rgb { 86 | r: 31, 87 | g: 29, 88 | b: 46, 89 | }), 90 | }, 91 | styles: TextStyleConfig::default(), 92 | options: { 93 | let mut opts = HashMap::new(); 94 | opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); 95 | opts 96 | }, 97 | } 98 | } 99 | 100 | pub fn context_window_segment() -> SegmentConfig { 101 | SegmentConfig { 102 | id: SegmentId::ContextWindow, 103 | enabled: true, 104 | icon: IconConfig { 105 | plain: "⚡️".to_string(), 106 | nerd_font: "\u{f49b}".to_string(), 107 | }, 108 | colors: ColorConfig { 109 | icon: Some(AnsiColor::Rgb { 110 | r: 224, 111 | g: 222, 112 | b: 244, 113 | }), 114 | text: Some(AnsiColor::Rgb { 115 | r: 224, 116 | g: 222, 117 | b: 244, 118 | }), 119 | background: Some(AnsiColor::Rgb { 120 | r: 82, 121 | g: 79, 122 | b: 103, 123 | }), 124 | }, 125 | styles: TextStyleConfig::default(), 126 | options: HashMap::new(), 127 | } 128 | } 129 | 130 | pub fn cost_segment() -> SegmentConfig { 131 | SegmentConfig { 132 | id: SegmentId::Cost, 133 | enabled: false, 134 | icon: IconConfig { 135 | plain: "💰".to_string(), 136 | nerd_font: "\u{eec1}".to_string(), 137 | }, 138 | colors: ColorConfig { 139 | icon: Some(AnsiColor::Rgb { 140 | r: 246, 141 | g: 193, 142 | b: 119, 143 | }), 144 | text: Some(AnsiColor::Rgb { 145 | r: 246, 146 | g: 193, 147 | b: 119, 148 | }), 149 | background: Some(AnsiColor::Rgb { 150 | r: 35, 151 | g: 33, 152 | b: 54, 153 | }), // Rose Pine dark background 154 | }, 155 | styles: TextStyleConfig::default(), 156 | options: HashMap::new(), 157 | } 158 | } 159 | 160 | pub fn session_segment() -> SegmentConfig { 161 | SegmentConfig { 162 | id: SegmentId::Session, 163 | enabled: false, 164 | icon: IconConfig { 165 | plain: "⏱️".to_string(), 166 | nerd_font: "\u{f19bb}".to_string(), 167 | }, 168 | colors: ColorConfig { 169 | icon: Some(AnsiColor::Rgb { 170 | r: 156, 171 | g: 207, 172 | b: 216, 173 | }), 174 | text: Some(AnsiColor::Rgb { 175 | r: 156, 176 | g: 207, 177 | b: 216, 178 | }), 179 | background: Some(AnsiColor::Rgb { 180 | r: 42, 181 | g: 39, 182 | b: 63, 183 | }), // Rose Pine darker background 184 | }, 185 | styles: TextStyleConfig::default(), 186 | options: HashMap::new(), 187 | } 188 | } 189 | 190 | pub fn output_style_segment() -> SegmentConfig { 191 | SegmentConfig { 192 | id: SegmentId::OutputStyle, 193 | enabled: false, 194 | icon: IconConfig { 195 | plain: "🎯".to_string(), 196 | nerd_font: "\u{f12f5}".to_string(), 197 | }, 198 | colors: ColorConfig { 199 | icon: Some(AnsiColor::Rgb { 200 | r: 49, 201 | g: 116, 202 | b: 143, 203 | }), 204 | text: Some(AnsiColor::Rgb { 205 | r: 49, 206 | g: 116, 207 | b: 143, 208 | }), 209 | background: Some(AnsiColor::Rgb { 210 | r: 38, 211 | g: 35, 212 | b: 58, 213 | }), // Rose Pine darkest background 214 | }, 215 | styles: TextStyleConfig::default(), 216 | options: HashMap::new(), 217 | } 218 | } 219 | 220 | pub fn usage_segment() -> SegmentConfig { 221 | SegmentConfig { 222 | id: SegmentId::Usage, 223 | enabled: false, 224 | icon: IconConfig { 225 | plain: "📊".to_string(), 226 | nerd_font: "\u{f0a9e}".to_string(), 227 | }, 228 | colors: ColorConfig { 229 | icon: Some(AnsiColor::Color16 { c16: 14 }), 230 | text: Some(AnsiColor::Color16 { c16: 14 }), 231 | background: None, 232 | }, 233 | styles: TextStyleConfig::default(), 234 | options: { 235 | let mut opts = HashMap::new(); 236 | opts.insert( 237 | "api_base_url".to_string(), 238 | serde_json::Value::String("https://api.anthropic.com".to_string()), 239 | ); 240 | opts.insert( 241 | "cache_duration".to_string(), 242 | serde_json::Value::Number(180.into()), 243 | ); 244 | opts.insert("timeout".to_string(), serde_json::Value::Number(2.into())); 245 | opts 246 | }, 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/ui/themes/theme_powerline_tokyo_night.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{ 2 | AnsiColor, ColorConfig, IconConfig, SegmentConfig, SegmentId, TextStyleConfig, 3 | }; 4 | use std::collections::HashMap; 5 | 6 | pub fn model_segment() -> SegmentConfig { 7 | SegmentConfig { 8 | id: SegmentId::Model, 9 | enabled: true, 10 | icon: IconConfig { 11 | plain: "🤖".to_string(), 12 | nerd_font: "\u{e26d}".to_string(), 13 | }, 14 | colors: ColorConfig { 15 | icon: Some(AnsiColor::Rgb { 16 | r: 252, 17 | g: 167, 18 | b: 234, 19 | }), 20 | text: Some(AnsiColor::Rgb { 21 | r: 252, 22 | g: 167, 23 | b: 234, 24 | }), 25 | background: Some(AnsiColor::Rgb { 26 | r: 25, 27 | g: 27, 28 | b: 41, 29 | }), 30 | }, 31 | styles: TextStyleConfig::default(), 32 | options: HashMap::new(), 33 | } 34 | } 35 | 36 | pub fn directory_segment() -> SegmentConfig { 37 | SegmentConfig { 38 | id: SegmentId::Directory, 39 | enabled: true, 40 | icon: IconConfig { 41 | plain: "📁".to_string(), 42 | nerd_font: "\u{f024b}".to_string(), 43 | }, 44 | colors: ColorConfig { 45 | icon: Some(AnsiColor::Rgb { 46 | r: 130, 47 | g: 170, 48 | b: 255, 49 | }), 50 | text: Some(AnsiColor::Rgb { 51 | r: 130, 52 | g: 170, 53 | b: 255, 54 | }), 55 | background: Some(AnsiColor::Rgb { 56 | r: 47, 57 | g: 51, 58 | b: 77, 59 | }), 60 | }, 61 | styles: TextStyleConfig::default(), 62 | options: HashMap::new(), 63 | } 64 | } 65 | 66 | pub fn git_segment() -> SegmentConfig { 67 | SegmentConfig { 68 | id: SegmentId::Git, 69 | enabled: true, 70 | icon: IconConfig { 71 | plain: "🌿".to_string(), 72 | nerd_font: "\u{f02a2}".to_string(), 73 | }, 74 | colors: ColorConfig { 75 | icon: Some(AnsiColor::Rgb { 76 | r: 195, 77 | g: 232, 78 | b: 141, 79 | }), 80 | text: Some(AnsiColor::Rgb { 81 | r: 195, 82 | g: 232, 83 | b: 141, 84 | }), 85 | background: Some(AnsiColor::Rgb { 86 | r: 30, 87 | g: 32, 88 | b: 48, 89 | }), 90 | }, 91 | styles: TextStyleConfig::default(), 92 | options: { 93 | let mut opts = HashMap::new(); 94 | opts.insert("show_sha".to_string(), serde_json::Value::Bool(false)); 95 | opts 96 | }, 97 | } 98 | } 99 | 100 | pub fn context_window_segment() -> SegmentConfig { 101 | SegmentConfig { 102 | id: SegmentId::ContextWindow, 103 | enabled: true, 104 | icon: IconConfig { 105 | plain: "⚡️️".to_string(), 106 | nerd_font: "\u{f49b}".to_string(), 107 | }, 108 | colors: ColorConfig { 109 | icon: Some(AnsiColor::Rgb { 110 | r: 192, 111 | g: 202, 112 | b: 245, 113 | }), 114 | text: Some(AnsiColor::Rgb { 115 | r: 192, 116 | g: 202, 117 | b: 245, 118 | }), 119 | background: Some(AnsiColor::Rgb { 120 | r: 61, 121 | g: 89, 122 | b: 161, 123 | }), 124 | }, 125 | styles: TextStyleConfig::default(), 126 | options: HashMap::new(), 127 | } 128 | } 129 | 130 | pub fn cost_segment() -> SegmentConfig { 131 | SegmentConfig { 132 | id: SegmentId::Cost, 133 | enabled: false, 134 | icon: IconConfig { 135 | plain: "💰".to_string(), 136 | nerd_font: "\u{eec1}".to_string(), 137 | }, 138 | colors: ColorConfig { 139 | icon: Some(AnsiColor::Rgb { 140 | r: 224, 141 | g: 175, 142 | b: 104, 143 | }), 144 | text: Some(AnsiColor::Rgb { 145 | r: 224, 146 | g: 175, 147 | b: 104, 148 | }), 149 | background: Some(AnsiColor::Rgb { 150 | r: 36, 151 | g: 40, 152 | b: 59, 153 | }), // Tokyo Night dark background 154 | }, 155 | styles: TextStyleConfig::default(), 156 | options: HashMap::new(), 157 | } 158 | } 159 | 160 | pub fn session_segment() -> SegmentConfig { 161 | SegmentConfig { 162 | id: SegmentId::Session, 163 | enabled: false, 164 | icon: IconConfig { 165 | plain: "⏱️".to_string(), 166 | nerd_font: "\u{f1ad3}".to_string(), 167 | }, 168 | colors: ColorConfig { 169 | icon: Some(AnsiColor::Rgb { 170 | r: 158, 171 | g: 206, 172 | b: 106, 173 | }), 174 | text: Some(AnsiColor::Rgb { 175 | r: 158, 176 | g: 206, 177 | b: 106, 178 | }), 179 | background: Some(AnsiColor::Rgb { 180 | r: 41, 181 | g: 46, 182 | b: 66, 183 | }), // Tokyo Night darker background 184 | }, 185 | styles: TextStyleConfig::default(), 186 | options: HashMap::new(), 187 | } 188 | } 189 | 190 | pub fn output_style_segment() -> SegmentConfig { 191 | SegmentConfig { 192 | id: SegmentId::OutputStyle, 193 | enabled: false, 194 | icon: IconConfig { 195 | plain: "🎯".to_string(), 196 | nerd_font: "\u{f12f5}".to_string(), 197 | }, 198 | colors: ColorConfig { 199 | icon: Some(AnsiColor::Rgb { 200 | r: 125, 201 | g: 207, 202 | b: 255, 203 | }), 204 | text: Some(AnsiColor::Rgb { 205 | r: 125, 206 | g: 207, 207 | b: 255, 208 | }), 209 | background: Some(AnsiColor::Rgb { 210 | r: 32, 211 | g: 35, 212 | b: 52, 213 | }), // Tokyo Night darkest background 214 | }, 215 | styles: TextStyleConfig::default(), 216 | options: HashMap::new(), 217 | } 218 | } 219 | 220 | pub fn usage_segment() -> SegmentConfig { 221 | SegmentConfig { 222 | id: SegmentId::Usage, 223 | enabled: false, 224 | icon: IconConfig { 225 | plain: "📊".to_string(), 226 | nerd_font: "\u{f0a9e}".to_string(), 227 | }, 228 | colors: ColorConfig { 229 | icon: Some(AnsiColor::Color16 { c16: 14 }), 230 | text: Some(AnsiColor::Color16 { c16: 14 }), 231 | background: None, 232 | }, 233 | styles: TextStyleConfig::default(), 234 | options: { 235 | let mut opts = HashMap::new(); 236 | opts.insert( 237 | "api_base_url".to_string(), 238 | serde_json::Value::String("https://api.anthropic.com".to_string()), 239 | ); 240 | opts.insert( 241 | "cache_duration".to_string(), 242 | serde_json::Value::Number(180.into()), 243 | ); 244 | opts.insert("timeout".to_string(), serde_json::Value::Number(2.into())); 245 | opts 246 | }, 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/config/models.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::fs; 3 | use std::path::Path; 4 | 5 | #[derive(Debug, Clone, Serialize, Deserialize)] 6 | pub struct ModelConfig { 7 | #[serde(rename = "models")] 8 | pub model_entries: Vec, 9 | } 10 | 11 | #[derive(Debug, Clone, Serialize, Deserialize)] 12 | pub struct ModelEntry { 13 | pub pattern: String, 14 | pub display_name: String, 15 | pub context_limit: u32, 16 | } 17 | 18 | impl ModelConfig { 19 | /// Load model configuration from TOML file 20 | pub fn load_from_file>(path: P) -> Result> { 21 | let content = fs::read_to_string(path)?; 22 | let config: ModelConfig = toml::from_str(&content)?; 23 | Ok(config) 24 | } 25 | 26 | /// Load model configuration with fallback locations 27 | pub fn load() -> Self { 28 | let mut model_config = Self::default(); 29 | 30 | // First, try to create default models.toml if it doesn't exist 31 | if let Some(home_dir) = dirs::home_dir() { 32 | let user_models_path = home_dir.join(".claude").join("ccline").join("models.toml"); 33 | if !user_models_path.exists() { 34 | let _ = Self::create_default_file(&user_models_path); 35 | } 36 | } 37 | 38 | // Try loading from user config directory first, then local 39 | let config_paths = [ 40 | dirs::home_dir().map(|d| d.join(".claude").join("ccline").join("models.toml")), 41 | Some(Path::new("models.toml").to_path_buf()), 42 | ]; 43 | 44 | for path in config_paths.iter().flatten() { 45 | if path.exists() { 46 | if let Ok(config) = Self::load_from_file(path) { 47 | // Prepend external models to built-in ones for priority 48 | let mut merged_entries = config.model_entries; 49 | merged_entries.extend(model_config.model_entries); 50 | model_config.model_entries = merged_entries; 51 | return model_config; 52 | } 53 | } 54 | } 55 | 56 | // Fallback to default configuration if no file found 57 | model_config 58 | } 59 | 60 | /// Get context limit for a model based on ID pattern matching 61 | /// Checks external config first, then falls back to built-in config 62 | pub fn get_context_limit(&self, model_id: &str) -> u32 { 63 | let model_lower = model_id.to_lowercase(); 64 | 65 | // Check model entries 66 | for entry in &self.model_entries { 67 | if model_lower.contains(&entry.pattern.to_lowercase()) { 68 | return entry.context_limit; 69 | } 70 | } 71 | 72 | 200_000 73 | } 74 | 75 | /// Get display name for a model based on ID pattern matching 76 | /// Checks external config first, then falls back to built-in config 77 | /// Returns None if no match found (should use fallback display_name) 78 | pub fn get_display_name(&self, model_id: &str) -> Option { 79 | let model_lower = model_id.to_lowercase(); 80 | 81 | // Check model entries 82 | for entry in &self.model_entries { 83 | if model_lower.contains(&entry.pattern.to_lowercase()) { 84 | return Some(entry.display_name.clone()); 85 | } 86 | } 87 | 88 | None 89 | } 90 | 91 | /// Create default model configuration file with minimal template 92 | pub fn create_default_file>(path: P) -> Result<(), Box> { 93 | // Create a minimal template config (not the full fallback config) 94 | let template_config = Self { 95 | model_entries: vec![], // Empty - just provide the structure 96 | }; 97 | 98 | let toml_content = toml::to_string_pretty(&template_config)?; 99 | 100 | // Add comments and examples to the template 101 | let template_content = format!( 102 | "# CCometixLine Model Configuration\n\ 103 | # This file defines model display names and context limits for different LLM models\n\ 104 | # File location: ~/.claude/ccline/models.toml\n\ 105 | \n\ 106 | {}\n\ 107 | \n\ 108 | # Model configurations\n\ 109 | # Each [[models]] section defines a model pattern and its properties\n\ 110 | # Order matters: first match wins, so put more specific patterns first\n\ 111 | \n\ 112 | # Example of how to add new models:\n\ 113 | # [[models]]\n\ 114 | # pattern = \"glm-4.5\"\n\ 115 | # display_name = \"GLM-4.5\"\n\ 116 | # context_limit = 128000\n", 117 | toml_content.trim() 118 | ); 119 | 120 | // Create parent directory if it doesn't exist 121 | if let Some(parent) = path.as_ref().parent() { 122 | fs::create_dir_all(parent)?; 123 | } 124 | 125 | fs::write(path, template_content)?; 126 | Ok(()) 127 | } 128 | } 129 | 130 | impl Default for ModelConfig { 131 | fn default() -> Self { 132 | Self { 133 | model_entries: vec![ 134 | // 1M context models (put first for priority matching) 135 | ModelEntry { 136 | pattern: "[1m]".to_string(), 137 | display_name: "Sonnet 4.5 1M".to_string(), 138 | context_limit: 1_000_000, 139 | }, 140 | // ModelEntry { 141 | // pattern: "claude-sonnet-4-5".to_string(), 142 | // display_name: "Sonnet 4.5".to_string(), 143 | // context_limit: 200_000, 144 | // }, 145 | // ModelEntry { 146 | // pattern: "claude-sonnet-4".to_string(), 147 | // display_name: "Sonnet 4".to_string(), 148 | // context_limit: 200_000, 149 | // }, 150 | // ModelEntry { 151 | // pattern: "claude-4-sonnet".to_string(), 152 | // display_name: "Sonnet 4".to_string(), 153 | // context_limit: 200_000, 154 | // }, 155 | // ModelEntry { 156 | // pattern: "claude-4-opus".to_string(), 157 | // display_name: "Opus 4".to_string(), 158 | // context_limit: 200_000, 159 | // }, 160 | // ModelEntry { 161 | // pattern: "sonnet-4".to_string(), 162 | // display_name: "Sonnet 4".to_string(), 163 | // context_limit: 200_000, 164 | // }, 165 | ModelEntry { 166 | pattern: "claude-3-7-sonnet".to_string(), 167 | display_name: "Sonnet 3.7".to_string(), 168 | context_limit: 200_000, 169 | }, 170 | // Third-party models 171 | ModelEntry { 172 | pattern: "glm-4.5".to_string(), 173 | display_name: "GLM-4.5".to_string(), 174 | context_limit: 128_000, 175 | }, 176 | ModelEntry { 177 | pattern: "kimi-k2-turbo".to_string(), 178 | display_name: "Kimi K2 Turbo".to_string(), 179 | context_limit: 128_000, 180 | }, 181 | ModelEntry { 182 | pattern: "kimi-k2".to_string(), 183 | display_name: "Kimi K2".to_string(), 184 | context_limit: 128_000, 185 | }, 186 | ModelEntry { 187 | pattern: "qwen3-coder".to_string(), 188 | display_name: "Qwen Coder".to_string(), 189 | context_limit: 256_000, 190 | }, 191 | ], 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/ui/components/preview.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{Config, SegmentId}; 2 | use crate::core::segments::SegmentData; 3 | use crate::core::StatusLineGenerator; 4 | use ratatui::{ 5 | layout::Rect, 6 | text::{Line, Text}, 7 | widgets::{Block, Borders, Paragraph}, 8 | Frame, 9 | }; 10 | use std::collections::HashMap; 11 | 12 | pub struct PreviewComponent { 13 | preview_cache: String, 14 | preview_text: Text<'static>, 15 | } 16 | 17 | impl Default for PreviewComponent { 18 | fn default() -> Self { 19 | Self::new() 20 | } 21 | } 22 | 23 | impl PreviewComponent { 24 | pub fn new() -> Self { 25 | Self { 26 | preview_cache: String::new(), 27 | preview_text: Text::default(), 28 | } 29 | } 30 | 31 | pub fn update_preview(&mut self, config: &Config) { 32 | self.update_preview_with_width(config, 80); // Default width 33 | } 34 | 35 | pub fn update_preview_with_width(&mut self, config: &Config, width: u16) { 36 | // Generate mock segments data directly for preview 37 | let segments_data = self.generate_mock_segments_data(config); 38 | 39 | // Generate both string and TUI text versions 40 | let renderer = StatusLineGenerator::new(config.clone()); 41 | 42 | // Keep string version for compatibility (if needed elsewhere) 43 | self.preview_cache = renderer.generate(segments_data.clone()); 44 | 45 | // Generate TUI-optimized text with smart segment wrapping for preview display 46 | // Use actual available width minus borders 47 | let content_width = width.saturating_sub(2); 48 | let preview_result = renderer.generate_for_tui_preview(segments_data, content_width); 49 | 50 | // Convert to owned text by cloning the spans 51 | let owned_lines: Vec> = preview_result 52 | .lines 53 | .into_iter() 54 | .map(|line| { 55 | let owned_spans: Vec> = line 56 | .spans 57 | .into_iter() 58 | .map(|span| ratatui::text::Span::styled(span.content.to_string(), span.style)) 59 | .collect(); 60 | Line::from(owned_spans) 61 | }) 62 | .collect(); 63 | 64 | self.preview_text = Text::from(owned_lines); 65 | } 66 | 67 | pub fn calculate_height(&self) -> u16 { 68 | let line_count = self.preview_text.lines.len().max(1); 69 | // Min 3 (1 line + 2 borders), max 8 to prevent taking too much space 70 | ((line_count + 2).max(3) as u16).min(8) 71 | } 72 | 73 | pub fn render(&self, f: &mut Frame, area: Rect) { 74 | let preview = Paragraph::new(self.preview_text.clone()) 75 | .block(Block::default().borders(Borders::ALL).title("Preview")) 76 | .wrap(ratatui::widgets::Wrap { trim: false }); 77 | f.render_widget(preview, area); 78 | } 79 | 80 | pub fn get_preview_cache(&self) -> &str { 81 | &self.preview_cache 82 | } 83 | 84 | /// Generate mock segments data for preview display 85 | /// This creates perfect preview data without depending on real environment 86 | fn generate_mock_segments_data( 87 | &self, 88 | config: &Config, 89 | ) -> Vec<(crate::config::SegmentConfig, SegmentData)> { 90 | let mut segments_data = Vec::new(); 91 | 92 | for segment_config in &config.segments { 93 | if !segment_config.enabled { 94 | continue; 95 | } 96 | 97 | let mock_data = match segment_config.id { 98 | SegmentId::Model => SegmentData { 99 | primary: "Sonnet 4".to_string(), 100 | secondary: "".to_string(), 101 | metadata: { 102 | let mut map = HashMap::new(); 103 | map.insert("model".to_string(), "claude-4-sonnet-20250512".to_string()); 104 | map 105 | }, 106 | }, 107 | SegmentId::Directory => SegmentData { 108 | primary: "CCometixLine".to_string(), 109 | secondary: "".to_string(), 110 | metadata: { 111 | let mut map = HashMap::new(); 112 | map.insert("current_dir".to_string(), "~/CCometixLine".to_string()); 113 | map 114 | }, 115 | }, 116 | SegmentId::Git => SegmentData { 117 | primary: "master".to_string(), 118 | secondary: "✓".to_string(), 119 | metadata: { 120 | let mut map = HashMap::new(); 121 | map.insert("branch".to_string(), "master".to_string()); 122 | map.insert("status".to_string(), "Clean".to_string()); 123 | map.insert("ahead".to_string(), "0".to_string()); 124 | map.insert("behind".to_string(), "0".to_string()); 125 | map 126 | }, 127 | }, 128 | SegmentId::ContextWindow => SegmentData { 129 | primary: "78.2%".to_string(), 130 | secondary: "· 156.4k".to_string(), 131 | metadata: { 132 | let mut map = HashMap::new(); 133 | map.insert("total_tokens".to_string(), "156400".to_string()); 134 | map.insert("percentage".to_string(), "78.2".to_string()); 135 | map.insert("session_tokens".to_string(), "48200".to_string()); 136 | map 137 | }, 138 | }, 139 | SegmentId::Usage => SegmentData { 140 | primary: "24%".to_string(), 141 | secondary: "· 10-7-2".to_string(), 142 | metadata: HashMap::new(), 143 | }, 144 | SegmentId::Cost => SegmentData { 145 | primary: "$0.02".to_string(), 146 | secondary: "".to_string(), 147 | metadata: { 148 | let mut map = HashMap::new(); 149 | map.insert("cost".to_string(), "0.01234".to_string()); 150 | map 151 | }, 152 | }, 153 | SegmentId::Session => SegmentData { 154 | primary: "3m45s".to_string(), 155 | secondary: "+156 -23".to_string(), 156 | metadata: { 157 | let mut map = HashMap::new(); 158 | map.insert("duration_ms".to_string(), "225000".to_string()); 159 | map.insert("lines_added".to_string(), "156".to_string()); 160 | map.insert("lines_removed".to_string(), "23".to_string()); 161 | map 162 | }, 163 | }, 164 | SegmentId::OutputStyle => SegmentData { 165 | primary: "default".to_string(), 166 | secondary: "".to_string(), 167 | metadata: { 168 | let mut map = HashMap::new(); 169 | map.insert("style_name".to_string(), "default".to_string()); 170 | map 171 | }, 172 | }, 173 | SegmentId::Update => SegmentData { 174 | primary: format!("v{}", env!("CARGO_PKG_VERSION")), 175 | secondary: "".to_string(), 176 | metadata: { 177 | let mut map = HashMap::new(); 178 | map.insert( 179 | "current_version".to_string(), 180 | env!("CARGO_PKG_VERSION").to_string(), 181 | ); 182 | map.insert("update_available".to_string(), "false".to_string()); 183 | map 184 | }, 185 | }, 186 | }; 187 | 188 | segments_data.push((segment_config.clone(), mock_data)); 189 | } 190 | 191 | segments_data 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CCometixLine 2 | 3 | [English](README.md) | [中文](README.zh.md) 4 | 5 | A high-performance Claude Code statusline tool written in Rust with Git integration, usage tracking, interactive TUI configuration, and Claude Code enhancement utilities. 6 | 7 | ![Language:Rust](https://img.shields.io/static/v1?label=Language&message=Rust&color=orange&style=flat-square) 8 | ![License:MIT](https://img.shields.io/static/v1?label=License&message=MIT&color=blue&style=flat-square) 9 | 10 | ## Screenshots 11 | 12 | ![CCometixLine](assets/img1.png) 13 | 14 | The statusline shows: Model | Directory | Git Branch Status | Context Window Information 15 | 16 | ## Features 17 | 18 | ### Core Functionality 19 | - **Git integration** with branch, status, and tracking info 20 | - **Model display** with simplified Claude model names 21 | - **Usage tracking** based on transcript analysis 22 | - **Directory display** showing current workspace 23 | - **Minimal design** using Nerd Font icons 24 | 25 | ### Interactive TUI Features 26 | - **Interactive main menu** when executed without input 27 | - **TUI configuration interface** with real-time preview 28 | - **Theme system** with multiple built-in presets 29 | - **Segment customization** with granular control 30 | - **Configuration management** (init, check, edit) 31 | 32 | ### Claude Code Enhancement 33 | - **Context warning disabler** - Remove annoying "Context low" messages 34 | - **Verbose mode enabler** - Enhanced output detail 35 | - **Robust patcher** - Survives Claude Code version updates 36 | - **Automatic backups** - Safe modification with easy recovery 37 | 38 | ## Installation 39 | 40 | ### Quick Install (Recommended) 41 | 42 | Install via npm (works on all platforms): 43 | 44 | ```bash 45 | # Install globally 46 | npm install -g @cometix/ccline 47 | 48 | # Or using yarn 49 | yarn global add @cometix/ccline 50 | 51 | # Or using pnpm 52 | pnpm add -g @cometix/ccline 53 | ``` 54 | 55 | Use npm mirror for faster download: 56 | ```bash 57 | npm install -g @cometix/ccline --registry https://registry.npmmirror.com 58 | ``` 59 | 60 | After installation: 61 | - ✅ Global command `ccline` is available everywhere 62 | - ⚙️ Follow the configuration steps below to integrate with Claude Code 63 | - 🎨 Run `ccline -c` to open configuration panel for theme selection 64 | 65 | ### Claude Code Configuration 66 | 67 | Add to your Claude Code `settings.json`: 68 | 69 | **Linux/macOS:** 70 | ```json 71 | { 72 | "statusLine": { 73 | "type": "command", 74 | "command": "~/.claude/ccline/ccline", 75 | "padding": 0 76 | } 77 | } 78 | ``` 79 | 80 | **Windows:** 81 | ```json 82 | { 83 | "statusLine": { 84 | "type": "command", 85 | "command": "%USERPROFILE%\\.claude\\ccline\\ccline.exe", 86 | "padding": 0 87 | } 88 | } 89 | ``` 90 | 91 | **Fallback (npm installation):** 92 | ```json 93 | { 94 | "statusLine": { 95 | "type": "command", 96 | "command": "ccline", 97 | "padding": 0 98 | } 99 | } 100 | ``` 101 | *Use this if npm global installation is available in PATH* 102 | 103 | ### Update 104 | 105 | ```bash 106 | npm update -g @cometix/ccline 107 | ``` 108 | 109 |
110 | Manual Installation (Click to expand) 111 | 112 | Alternatively, download from [Releases](https://github.com/Haleclipse/CCometixLine/releases): 113 | 114 | #### Linux 115 | 116 | #### Option 1: Dynamic Binary (Recommended) 117 | ```bash 118 | mkdir -p ~/.claude/ccline 119 | wget https://github.com/Haleclipse/CCometixLine/releases/latest/download/ccline-linux-x64.tar.gz 120 | tar -xzf ccline-linux-x64.tar.gz 121 | cp ccline ~/.claude/ccline/ 122 | chmod +x ~/.claude/ccline/ccline 123 | ``` 124 | *Requires: Ubuntu 22.04+, CentOS 9+, Debian 11+, RHEL 9+ (glibc 2.35+)* 125 | 126 | #### Option 2: Static Binary (Universal Compatibility) 127 | ```bash 128 | mkdir -p ~/.claude/ccline 129 | wget https://github.com/Haleclipse/CCometixLine/releases/latest/download/ccline-linux-x64-static.tar.gz 130 | tar -xzf ccline-linux-x64-static.tar.gz 131 | cp ccline ~/.claude/ccline/ 132 | chmod +x ~/.claude/ccline/ccline 133 | ``` 134 | *Works on any Linux distribution (static, no dependencies)* 135 | 136 | #### macOS (Intel) 137 | 138 | ```bash 139 | mkdir -p ~/.claude/ccline 140 | wget https://github.com/Haleclipse/CCometixLine/releases/latest/download/ccline-macos-x64.tar.gz 141 | tar -xzf ccline-macos-x64.tar.gz 142 | cp ccline ~/.claude/ccline/ 143 | chmod +x ~/.claude/ccline/ccline 144 | ``` 145 | 146 | #### macOS (Apple Silicon) 147 | 148 | ```bash 149 | mkdir -p ~/.claude/ccline 150 | wget https://github.com/Haleclipse/CCometixLine/releases/latest/download/ccline-macos-arm64.tar.gz 151 | tar -xzf ccline-macos-arm64.tar.gz 152 | cp ccline ~/.claude/ccline/ 153 | chmod +x ~/.claude/ccline/ccline 154 | ``` 155 | 156 | #### Windows 157 | 158 | ```powershell 159 | # Create directory and download 160 | New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.claude\ccline" 161 | Invoke-WebRequest -Uri "https://github.com/Haleclipse/CCometixLine/releases/latest/download/ccline-windows-x64.zip" -OutFile "ccline-windows-x64.zip" 162 | Expand-Archive -Path "ccline-windows-x64.zip" -DestinationPath "." 163 | Move-Item "ccline.exe" "$env:USERPROFILE\.claude\ccline\" 164 | ``` 165 | 166 |
167 | 168 | ### Build from Source 169 | 170 | ```bash 171 | git clone https://github.com/Haleclipse/CCometixLine.git 172 | cd CCometixLine 173 | cargo build --release 174 | 175 | # Linux/macOS 176 | mkdir -p ~/.claude/ccline 177 | cp target/release/ccometixline ~/.claude/ccline/ccline 178 | chmod +x ~/.claude/ccline/ccline 179 | 180 | # Windows (PowerShell) 181 | New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.claude\ccline" 182 | copy target\release\ccometixline.exe "$env:USERPROFILE\.claude\ccline\ccline.exe" 183 | ``` 184 | 185 | ## Usage 186 | 187 | ### Configuration Management 188 | 189 | ```bash 190 | # Initialize configuration file 191 | ccline --init 192 | 193 | # Check configuration validity 194 | ccline --check 195 | 196 | # Print current configuration 197 | ccline --print 198 | 199 | # Enter TUI configuration mode 200 | ccline --config 201 | ``` 202 | 203 | ### Theme Override 204 | 205 | ```bash 206 | # Temporarily use specific theme (overrides config file) 207 | ccline --theme cometix 208 | ccline --theme minimal 209 | ccline --theme gruvbox 210 | ccline --theme nord 211 | ccline --theme powerline-dark 212 | 213 | # Or use custom theme files from ~/.claude/ccline/themes/ 214 | ccline --theme my-custom-theme 215 | ``` 216 | 217 | ### Claude Code Enhancement 218 | 219 | ```bash 220 | # Disable context warnings and enable verbose mode 221 | ccline --patch /path/to/claude-code/cli.js 222 | 223 | # Example for common installation 224 | ccline --patch ~/.local/share/fnm/node-versions/v24.4.1/installation/lib/node_modules/@anthropic-ai/claude-code/cli.js 225 | ``` 226 | 227 | ## Default Segments 228 | 229 | Displays: `Directory | Git Branch Status | Model | Context Window` 230 | 231 | ### Git Status Indicators 232 | 233 | - Branch name with Nerd Font icon 234 | - Status: `✓` Clean, `●` Dirty, `⚠` Conflicts 235 | - Remote tracking: `↑n` Ahead, `↓n` Behind 236 | 237 | ### Model Display 238 | 239 | Shows simplified Claude model names: 240 | - `claude-3-5-sonnet` → `Sonnet 3.5` 241 | - `claude-4-sonnet` → `Sonnet 4` 242 | 243 | ### Context Window Display 244 | 245 | Token usage percentage based on transcript analysis with context limit tracking. 246 | 247 | ## Configuration 248 | 249 | CCometixLine supports full configuration via TOML files and interactive TUI: 250 | 251 | - **Configuration file**: `~/.claude/ccline/config.toml` 252 | - **Interactive TUI**: `ccline --config` for real-time editing with preview 253 | - **Theme files**: `~/.claude/ccline/themes/*.toml` for custom themes 254 | - **Automatic initialization**: `ccline --init` creates default configuration 255 | 256 | ### Available Segments 257 | 258 | All segments are configurable with: 259 | - Enable/disable toggle 260 | - Custom separators and icons 261 | - Color customization 262 | - Format options 263 | 264 | Supported segments: Directory, Git, Model, Usage, Time, Cost, OutputStyle 265 | 266 | 267 | ## Requirements 268 | 269 | - **Git**: Version 1.5+ (Git 2.22+ recommended for better branch detection) 270 | - **Terminal**: Must support Nerd Fonts for proper icon display 271 | - Install a [Nerd Font](https://www.nerdfonts.com/) (e.g., FiraCode Nerd Font, JetBrains Mono Nerd Font) 272 | - Configure your terminal to use the Nerd Font 273 | - **Claude Code**: For statusline integration 274 | 275 | ## Development 276 | 277 | ```bash 278 | # Build development version 279 | cargo build 280 | 281 | # Run tests 282 | cargo test 283 | 284 | # Build optimized release 285 | cargo build --release 286 | ``` 287 | 288 | ## Roadmap 289 | 290 | - [x] TOML configuration file support 291 | - [x] TUI configuration interface 292 | - [x] Custom themes 293 | - [x] Interactive main menu 294 | - [x] Claude Code enhancement tools 295 | 296 | ## Contributing 297 | 298 | Contributions are welcome! Please feel free to submit issues or pull requests. 299 | 300 | ## Related Projects 301 | 302 | - [tweakcc](https://github.com/Piebald-AI/tweakcc) - Command-line tool to customize your Claude Code themes, thinking verbs, and more. 303 | 304 | ## License 305 | 306 | This project is licensed under the [MIT License](LICENSE). 307 | 308 | ## Star History 309 | 310 | [![Star History Chart](https://api.star-history.com/svg?repos=Haleclipse/CCometixLine&type=Date)](https://star-history.com/#Haleclipse/CCometixLine&Date) 311 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.0.4] - 2025-08-28 9 | 10 | ### Added 11 | - **Interactive Main Menu**: Direct execution now shows TUI menu instead of hanging 12 | - **Claude Code Patcher**: `--patch` command to disable context warnings and enable verbose mode 13 | - **Three New Segments**: Extended statusline with additional information 14 | - **Cost Segment**: Shows monetary cost with intelligent zero-cost handling 15 | - **Session Segment**: Displays session duration and line changes 16 | - **OutputStyle Segment**: Shows current output style name 17 | - **Enhanced Theme System**: Comprehensive theme architecture with 9 built-in themes 18 | - Modular theme organization with individual theme modules 19 | - 4 new Powerline theme variants (dark, light, rose pine, tokyo night) 20 | - Enhanced existing themes (cometix, default, minimal, gruvbox, nord) 21 | - **Model Management System**: Intelligent model recognition and configuration 22 | 23 | ### Fixed 24 | - **Direct Execution Hanging**: No longer hangs when executed without stdin input 25 | - **Help Component Styling**: Consistent key highlighting across all TUI help displays 26 | - **Cross-platform Path Support**: Enhanced Windows %USERPROFILE% and Unix ~/ path handling 27 | 28 | 29 | ## [1.0.3] - 2025-08-17 30 | 31 | ### Fixed 32 | - **TUI Preview Display**: Complete redesign of preview system for cross-platform reliability 33 | - Replaced environment-dependent segment collection with pure mock data generation 34 | - Fixed Git segment not showing in preview on Windows and Linux systems 35 | - Ensures consistent preview display across all supported platforms 36 | - **Documentation Accuracy**: Corrected CLI parameter reference from `--interactive` to `--config` 37 | - Fixed changelog and documentation to reflect actual CLI parameters 38 | - **Preview Data Quality**: Enhanced mock data to better represent actual usage 39 | - Usage segment now displays proper format: "78.2% · 156.4k" 40 | - Update segment displays dynamic version number from Cargo.toml 41 | - All segments show realistic and informative preview data 42 | 43 | ### Changed 44 | - **Preview Architecture**: Complete rewrite of preview component for better maintainability 45 | - Removed dependency on real file system and Git repository detection 46 | - Implemented `generate_mock_segments_data()` for environment-independent previews 47 | - Simplified code structure and improved performance 48 | - Preview now works reliably in any environment without external dependencies 49 | 50 | ### Technical Details 51 | - Environment-independent mock data generation for all segment types 52 | - Dynamic version display using `env!("CARGO_PKG_VERSION")` 53 | - Optimized preview rendering without file system calls or Git operations 54 | - Consistent cross-platform display: "Sonnet 4 | CCometixLine | main ✓ | 78.2% · 156.4k" 55 | 56 | ## [1.0.2] - 2025-08-17 57 | 58 | ### Fixed 59 | - **Windows PowerShell Compatibility**: Fixed double key event triggering in TUI interface 60 | - Resolved issue #18 where keystrokes were registered twice on Windows PowerShell 61 | - Added proper KeyEventKind filtering to only process key press events 62 | - Maintained cross-platform compatibility with Unix/Linux/macOS systems 63 | 64 | ### Technical Details 65 | - Import KeyEventKind from crossterm::event module 66 | - Filter out KeyUp events to prevent double triggering on Windows Console API 67 | - Uses efficient continue statement to skip non-press events 68 | - No impact on existing behavior on Unix-based systems 69 | 70 | ## [1.0.1] - 2025-08-17 71 | 72 | ### Fixed 73 | - NPM package publishing workflow compatibility issues 74 | - Cargo.lock version synchronization with package version 75 | - GitHub Actions release pipeline for NPM distribution 76 | 77 | ### Changed 78 | - Enhanced npm postinstall script with improved binary lookup for different package managers 79 | - Better error handling and user feedback in installation process 80 | - Improved cross-platform compatibility for npm package installation 81 | 82 | ### Technical 83 | - Updated dependency versions (bitflags, proc-macro2) 84 | - Resolved NPM version conflict preventing 1.0.0 re-publication 85 | - Ensured proper version alignment across all distribution channels 86 | 87 | ## [1.0.0] - 2025-08-16 88 | 89 | ### Added 90 | - **Interactive TUI Mode**: Full-featured terminal user interface with ratatui 91 | - Real-time statusline preview while editing configuration 92 | - Live theme switching with instant visual feedback 93 | - Intuitive keyboard navigation (Tab, Escape, Enter, Arrow keys) 94 | - Comprehensive help system with context-sensitive guidance 95 | - **Comprehensive Theme System**: Modular theme architecture with multiple presets 96 | - Default, Minimal, Powerline, Compact themes included 97 | - Custom color schemes and icon sets 98 | - Theme validation and error reporting 99 | - Powerline theme importer for external theme compatibility 100 | - **Enhanced Configuration System**: Robust config management with validation 101 | - TOML-based configuration with schema validation 102 | - Dynamic config loading with intelligent defaults 103 | - Interactive mode support and theme selection 104 | - Configuration error handling and user feedback 105 | - **Advanced Segment System**: Modular statusline segments with improved functionality 106 | - Enhanced Git segment with stash detection and conflict status 107 | - Model segment with simplified display names for Claude models 108 | - Directory segment with customizable display options 109 | - Usage segment with better token calculation accuracy 110 | - Update segment for version management and notifications 111 | - **CLI Interface Enhancements**: Improved command-line experience 112 | - `--config` flag for launching TUI configuration mode 113 | - Enhanced argument parsing with better error messages 114 | - Theme selection via command line options 115 | - Comprehensive help and version information 116 | 117 | ### Changed 118 | - **Architecture**: Complete modularization of codebase for better maintainability 119 | - Separated core logic from presentation layer 120 | - Improved error handling throughout all modules 121 | - Better separation of concerns between data and UI 122 | - **Dependencies**: Added TUI and terminal handling capabilities 123 | - ratatui for terminal user interface components 124 | - crossterm for cross-platform terminal manipulation 125 | - ansi_term and ansi-to-tui for color processing 126 | - **Configuration**: Enhanced config structure for theme and TUI mode support 127 | - Expanded config types to support new features 128 | - Improved validation and default value handling 129 | - Better error messages for configuration issues 130 | 131 | ### Technical Improvements 132 | - **Performance**: Optimized statusline generation and rendering 133 | - **Code Quality**: Comprehensive refactoring with improved error handling 134 | - **User Experience**: Intuitive interface design with immediate visual feedback 135 | - **Extensibility**: Modular architecture allows easy addition of new themes and segments 136 | 137 | ### Breaking Changes 138 | - Configuration file format has been extended (backward compatible for basic usage) 139 | - Some internal APIs have been restructured for better modularity 140 | - Minimum supported features now include optional TUI dependencies 141 | 142 | ## [0.1.1] - 2025-08-12 143 | 144 | ### Added 145 | - Support for `total_tokens` field in token calculation for better accuracy with GLM-4.5 and similar providers 146 | - Proper Git repository detection using `git rev-parse --git-dir` 147 | - Cross-platform compatibility improvements for Windows path handling 148 | - Pre-commit hooks for automatic code formatting 149 | - **Static Linux binary**: Added musl-based static binary for universal Linux compatibility without glibc dependencies 150 | 151 | ### Changed 152 | - **Token calculation priority**: `total_tokens` → Claude format → OpenAI format → fallback 153 | - **Display formatting**: Removed redundant ".0" from integer percentages and token counts 154 | - `0.0%` → `0%`, `25.0%` → `25%`, `50.0k` → `50k` 155 | - **CI/CD**: Updated GitHub Actions to use Ubuntu 22.04 for Linux builds and ubuntu-latest for Windows cross-compilation 156 | - **Binary distribution**: Now provides two Linux options - dynamic (glibc) and static (musl) binaries 157 | - **Version management**: Unified version number using `env!("CARGO_PKG_VERSION")` 158 | 159 | ### Fixed 160 | - Git segment now properly hides for non-Git directories instead of showing misleading "detached" status 161 | - Windows Git repository path handling issues by removing overly aggressive path sanitization 162 | - GitHub Actions runner compatibility issues (updated to supported versions: ubuntu-22.04 for Linux, ubuntu-latest for Windows) 163 | - **Git version compatibility**: Added fallback to `git symbolic-ref` for Git versions < 2.22 when `--show-current` is not available 164 | 165 | ### Removed 166 | - Path sanitization function that could break Windows paths in Git operations 167 | 168 | ## [0.1.0] - 2025-08-11 169 | 170 | ### Added 171 | - Initial release of CCometixLine 172 | - High-performance Rust-based statusline tool for Claude Code 173 | - Git integration with branch, status, and tracking info 174 | - Model display with simplified Claude model names 175 | - Usage tracking based on transcript analysis 176 | - Directory display showing current workspace 177 | - Minimal design using Nerd Font icons 178 | - Cross-platform support (Linux, macOS, Windows) 179 | - Command-line configuration options 180 | - GitHub Actions CI/CD pipeline 181 | 182 | ### Technical Details 183 | - Context limit: 200,000 tokens 184 | - Startup time: < 50ms 185 | - Memory usage: < 10MB 186 | - Binary size: ~2MB optimized release build 187 | 188 | -------------------------------------------------------------------------------- /src/core/segments/context_window.rs: -------------------------------------------------------------------------------- 1 | use super::{Segment, SegmentData}; 2 | use crate::config::{InputData, ModelConfig, SegmentId, TranscriptEntry}; 3 | use std::collections::HashMap; 4 | use std::fs; 5 | use std::io::{BufRead, BufReader}; 6 | use std::path::{Path, PathBuf}; 7 | 8 | #[derive(Default)] 9 | pub struct ContextWindowSegment; 10 | 11 | impl ContextWindowSegment { 12 | pub fn new() -> Self { 13 | Self 14 | } 15 | 16 | /// Get context limit for the specified model 17 | fn get_context_limit_for_model(model_id: &str) -> u32 { 18 | let model_config = ModelConfig::load(); 19 | model_config.get_context_limit(model_id) 20 | } 21 | } 22 | 23 | impl Segment for ContextWindowSegment { 24 | fn collect(&self, input: &InputData) -> Option { 25 | // Dynamically determine context limit based on current model ID 26 | let context_limit = Self::get_context_limit_for_model(&input.model.id); 27 | 28 | let context_used_token_opt = parse_transcript_usage(&input.transcript_path); 29 | 30 | let (percentage_display, tokens_display) = match context_used_token_opt { 31 | Some(context_used_token) => { 32 | let context_used_rate = (context_used_token as f64 / context_limit as f64) * 100.0; 33 | 34 | let percentage = if context_used_rate.fract() == 0.0 { 35 | format!("{:.0}%", context_used_rate) 36 | } else { 37 | format!("{:.1}%", context_used_rate) 38 | }; 39 | 40 | let tokens = if context_used_token >= 1000 { 41 | let k_value = context_used_token as f64 / 1000.0; 42 | if k_value.fract() == 0.0 { 43 | format!("{}k", k_value as u32) 44 | } else { 45 | format!("{:.1}k", k_value) 46 | } 47 | } else { 48 | context_used_token.to_string() 49 | }; 50 | 51 | (percentage, tokens) 52 | } 53 | None => { 54 | // No usage data available 55 | ("-".to_string(), "-".to_string()) 56 | } 57 | }; 58 | 59 | let mut metadata = HashMap::new(); 60 | match context_used_token_opt { 61 | Some(context_used_token) => { 62 | let context_used_rate = (context_used_token as f64 / context_limit as f64) * 100.0; 63 | metadata.insert("tokens".to_string(), context_used_token.to_string()); 64 | metadata.insert("percentage".to_string(), context_used_rate.to_string()); 65 | } 66 | None => { 67 | metadata.insert("tokens".to_string(), "-".to_string()); 68 | metadata.insert("percentage".to_string(), "-".to_string()); 69 | } 70 | } 71 | metadata.insert("limit".to_string(), context_limit.to_string()); 72 | metadata.insert("model".to_string(), input.model.id.clone()); 73 | 74 | Some(SegmentData { 75 | primary: format!("{} · {} tokens", percentage_display, tokens_display), 76 | secondary: String::new(), 77 | metadata, 78 | }) 79 | } 80 | 81 | fn id(&self) -> SegmentId { 82 | SegmentId::ContextWindow 83 | } 84 | } 85 | 86 | fn parse_transcript_usage>(transcript_path: P) -> Option { 87 | let path = transcript_path.as_ref(); 88 | 89 | // Try to parse from current transcript file 90 | if let Some(usage) = try_parse_transcript_file(path) { 91 | return Some(usage); 92 | } 93 | 94 | // If file doesn't exist, try to find usage from project history 95 | if !path.exists() { 96 | if let Some(usage) = try_find_usage_from_project_history(path) { 97 | return Some(usage); 98 | } 99 | } 100 | 101 | None 102 | } 103 | 104 | fn try_parse_transcript_file(path: &Path) -> Option { 105 | let file = fs::File::open(path).ok()?; 106 | let reader = BufReader::new(file); 107 | let lines: Vec = reader 108 | .lines() 109 | .collect::, _>>() 110 | .unwrap_or_default(); 111 | 112 | if lines.is_empty() { 113 | return None; 114 | } 115 | 116 | // Check if the last line is a summary 117 | let last_line = lines.last()?.trim(); 118 | if let Ok(entry) = serde_json::from_str::(last_line) { 119 | if entry.r#type.as_deref() == Some("summary") { 120 | // Handle summary case: find usage by leafUuid 121 | if let Some(leaf_uuid) = &entry.leaf_uuid { 122 | let project_dir = path.parent()?; 123 | return find_usage_by_leaf_uuid(leaf_uuid, project_dir); 124 | } 125 | } 126 | } 127 | 128 | // Normal case: find the last assistant message in current file 129 | for line in lines.iter().rev() { 130 | let line = line.trim(); 131 | if line.is_empty() { 132 | continue; 133 | } 134 | 135 | if let Ok(entry) = serde_json::from_str::(line) { 136 | if entry.r#type.as_deref() == Some("assistant") { 137 | if let Some(message) = &entry.message { 138 | if let Some(raw_usage) = &message.usage { 139 | let normalized = raw_usage.clone().normalize(); 140 | return Some(normalized.display_tokens()); 141 | } 142 | } 143 | } 144 | } 145 | } 146 | 147 | None 148 | } 149 | 150 | fn find_usage_by_leaf_uuid(leaf_uuid: &str, project_dir: &Path) -> Option { 151 | // Search for the leafUuid across all session files in the project directory 152 | let entries = fs::read_dir(project_dir).ok()?; 153 | 154 | for entry in entries { 155 | let entry = entry.ok()?; 156 | let path = entry.path(); 157 | 158 | if path.extension().and_then(|s| s.to_str()) != Some("jsonl") { 159 | continue; 160 | } 161 | 162 | if let Some(usage) = search_uuid_in_file(&path, leaf_uuid) { 163 | return Some(usage); 164 | } 165 | } 166 | 167 | None 168 | } 169 | 170 | fn search_uuid_in_file(path: &Path, target_uuid: &str) -> Option { 171 | let file = fs::File::open(path).ok()?; 172 | let reader = BufReader::new(file); 173 | let lines: Vec = reader 174 | .lines() 175 | .collect::, _>>() 176 | .unwrap_or_default(); 177 | 178 | // Find the message with target_uuid 179 | for line in &lines { 180 | let line = line.trim(); 181 | if line.is_empty() { 182 | continue; 183 | } 184 | 185 | if let Ok(entry) = serde_json::from_str::(line) { 186 | if let Some(uuid) = &entry.uuid { 187 | if uuid == target_uuid { 188 | // Found the target message, check its type 189 | if entry.r#type.as_deref() == Some("assistant") { 190 | // Direct assistant message with usage 191 | if let Some(message) = &entry.message { 192 | if let Some(raw_usage) = &message.usage { 193 | let normalized = raw_usage.clone().normalize(); 194 | return Some(normalized.display_tokens()); 195 | } 196 | } 197 | } else if entry.r#type.as_deref() == Some("user") { 198 | // User message, need to find the parent assistant message 199 | if let Some(parent_uuid) = &entry.parent_uuid { 200 | return find_assistant_message_by_uuid(&lines, parent_uuid); 201 | } 202 | } 203 | break; 204 | } 205 | } 206 | } 207 | } 208 | 209 | None 210 | } 211 | 212 | fn find_assistant_message_by_uuid(lines: &[String], target_uuid: &str) -> Option { 213 | for line in lines { 214 | let line = line.trim(); 215 | if line.is_empty() { 216 | continue; 217 | } 218 | 219 | if let Ok(entry) = serde_json::from_str::(line) { 220 | if let Some(uuid) = &entry.uuid { 221 | if uuid == target_uuid && entry.r#type.as_deref() == Some("assistant") { 222 | if let Some(message) = &entry.message { 223 | if let Some(raw_usage) = &message.usage { 224 | let normalized = raw_usage.clone().normalize(); 225 | return Some(normalized.display_tokens()); 226 | } 227 | } 228 | } 229 | } 230 | } 231 | } 232 | 233 | None 234 | } 235 | 236 | fn try_find_usage_from_project_history(transcript_path: &Path) -> Option { 237 | let project_dir = transcript_path.parent()?; 238 | 239 | // Find the most recent session file in the project directory 240 | let mut session_files: Vec = Vec::new(); 241 | let entries = fs::read_dir(project_dir).ok()?; 242 | 243 | for entry in entries { 244 | let entry = entry.ok()?; 245 | let path = entry.path(); 246 | 247 | if path.extension().and_then(|s| s.to_str()) == Some("jsonl") { 248 | session_files.push(path); 249 | } 250 | } 251 | 252 | if session_files.is_empty() { 253 | return None; 254 | } 255 | 256 | // Sort by modification time (most recent first) 257 | session_files.sort_by_key(|path| { 258 | fs::metadata(path) 259 | .and_then(|m| m.modified()) 260 | .unwrap_or(std::time::UNIX_EPOCH) 261 | }); 262 | session_files.reverse(); 263 | 264 | // Try to find usage from the most recent session 265 | for session_path in &session_files { 266 | if let Some(usage) = try_parse_transcript_file(session_path) { 267 | return Some(usage); 268 | } 269 | } 270 | 271 | None 272 | } 273 | -------------------------------------------------------------------------------- /src/core/segments/usage.rs: -------------------------------------------------------------------------------- 1 | use super::{Segment, SegmentData}; 2 | use crate::config::{InputData, SegmentId}; 3 | use crate::utils::credentials; 4 | use chrono::{DateTime, Datelike, Duration, Local, Timelike, Utc}; 5 | use serde::{Deserialize, Serialize}; 6 | use std::collections::HashMap; 7 | 8 | #[derive(Debug, Deserialize)] 9 | struct ApiUsageResponse { 10 | five_hour: UsagePeriod, 11 | seven_day: UsagePeriod, 12 | } 13 | 14 | #[derive(Debug, Deserialize)] 15 | struct UsagePeriod { 16 | utilization: f64, 17 | resets_at: Option, 18 | } 19 | 20 | #[derive(Debug, Serialize, Deserialize)] 21 | struct ApiUsageCache { 22 | five_hour_utilization: f64, 23 | seven_day_utilization: f64, 24 | resets_at: Option, 25 | cached_at: String, 26 | } 27 | 28 | #[derive(Default)] 29 | pub struct UsageSegment; 30 | 31 | impl UsageSegment { 32 | pub fn new() -> Self { 33 | Self 34 | } 35 | 36 | fn get_circle_icon(utilization: f64) -> String { 37 | let percent = (utilization * 100.0) as u8; 38 | match percent { 39 | 0..=12 => "\u{f0a9e}".to_string(), // circle_slice_1 40 | 13..=25 => "\u{f0a9f}".to_string(), // circle_slice_2 41 | 26..=37 => "\u{f0aa0}".to_string(), // circle_slice_3 42 | 38..=50 => "\u{f0aa1}".to_string(), // circle_slice_4 43 | 51..=62 => "\u{f0aa2}".to_string(), // circle_slice_5 44 | 63..=75 => "\u{f0aa3}".to_string(), // circle_slice_6 45 | 76..=87 => "\u{f0aa4}".to_string(), // circle_slice_7 46 | _ => "\u{f0aa5}".to_string(), // circle_slice_8 47 | } 48 | } 49 | 50 | fn format_reset_time(reset_time_str: Option<&str>) -> String { 51 | if let Some(time_str) = reset_time_str { 52 | if let Ok(dt) = DateTime::parse_from_rfc3339(time_str) { 53 | let mut local_dt = dt.with_timezone(&Local); 54 | if local_dt.minute() > 45 { 55 | local_dt += Duration::hours(1); 56 | } 57 | return format!( 58 | "{}-{}-{}", 59 | local_dt.month(), 60 | local_dt.day(), 61 | local_dt.hour() 62 | ); 63 | } 64 | } 65 | "?".to_string() 66 | } 67 | 68 | fn get_cache_path() -> Option { 69 | let home = dirs::home_dir()?; 70 | Some( 71 | home.join(".claude") 72 | .join("ccline") 73 | .join(".api_usage_cache.json"), 74 | ) 75 | } 76 | 77 | fn load_cache(&self) -> Option { 78 | let cache_path = Self::get_cache_path()?; 79 | if !cache_path.exists() { 80 | return None; 81 | } 82 | 83 | let content = std::fs::read_to_string(&cache_path).ok()?; 84 | serde_json::from_str(&content).ok() 85 | } 86 | 87 | fn save_cache(&self, cache: &ApiUsageCache) { 88 | if let Some(cache_path) = Self::get_cache_path() { 89 | if let Some(parent) = cache_path.parent() { 90 | let _ = std::fs::create_dir_all(parent); 91 | } 92 | if let Ok(json) = serde_json::to_string_pretty(cache) { 93 | let _ = std::fs::write(&cache_path, json); 94 | } 95 | } 96 | } 97 | 98 | fn is_cache_valid(&self, cache: &ApiUsageCache, cache_duration: u64) -> bool { 99 | if let Ok(cached_at) = DateTime::parse_from_rfc3339(&cache.cached_at) { 100 | let now = Utc::now(); 101 | let elapsed = now.signed_duration_since(cached_at.with_timezone(&Utc)); 102 | elapsed.num_seconds() < cache_duration as i64 103 | } else { 104 | false 105 | } 106 | } 107 | 108 | fn get_claude_code_version() -> String { 109 | use std::process::Command; 110 | 111 | let output = Command::new("npm") 112 | .args(["view", "@anthropic-ai/claude-code", "version"]) 113 | .output(); 114 | 115 | match output { 116 | Ok(output) if output.status.success() => { 117 | let version = String::from_utf8_lossy(&output.stdout).trim().to_string(); 118 | if !version.is_empty() { 119 | return format!("claude-code/{}", version); 120 | } 121 | } 122 | _ => {} 123 | } 124 | 125 | "claude-code".to_string() 126 | } 127 | 128 | fn get_proxy_from_settings() -> Option { 129 | let home = std::env::var("HOME") 130 | .or_else(|_| std::env::var("USERPROFILE")) 131 | .ok()?; 132 | let settings_path = format!("{}/.claude/settings.json", home); 133 | 134 | let content = std::fs::read_to_string(&settings_path).ok()?; 135 | let settings: serde_json::Value = serde_json::from_str(&content).ok()?; 136 | 137 | // Try HTTPS_PROXY first, then HTTP_PROXY 138 | settings 139 | .get("env")? 140 | .get("HTTPS_PROXY") 141 | .or_else(|| settings.get("env")?.get("HTTP_PROXY")) 142 | .and_then(|v| v.as_str()) 143 | .map(|s| s.to_string()) 144 | } 145 | 146 | fn fetch_api_usage( 147 | &self, 148 | api_base_url: &str, 149 | token: &str, 150 | timeout_secs: u64, 151 | ) -> Option { 152 | let url = format!("{}/api/oauth/usage", api_base_url); 153 | let user_agent = Self::get_claude_code_version(); 154 | 155 | let mut agent_builder = ureq::AgentBuilder::new(); 156 | 157 | // Configure proxy from Claude settings if available 158 | if let Some(proxy_url) = Self::get_proxy_from_settings() { 159 | if let Ok(proxy) = ureq::Proxy::new(&proxy_url) { 160 | agent_builder = agent_builder.proxy(proxy); 161 | } 162 | } 163 | 164 | let agent = agent_builder.build(); 165 | 166 | let response = agent 167 | .get(&url) 168 | .set("Authorization", &format!("Bearer {}", token)) 169 | .set("anthropic-beta", "oauth-2025-04-20") 170 | .set("User-Agent", &user_agent) 171 | .timeout(std::time::Duration::from_secs(timeout_secs)) 172 | .call() 173 | .ok()?; 174 | 175 | if response.status() == 200 { 176 | response.into_json().ok() 177 | } else { 178 | None 179 | } 180 | } 181 | } 182 | 183 | impl Segment for UsageSegment { 184 | fn collect(&self, _input: &InputData) -> Option { 185 | let token = credentials::get_oauth_token()?; 186 | 187 | // Load config from file to get segment options 188 | let config = crate::config::Config::load().ok()?; 189 | let segment_config = config.segments.iter().find(|s| s.id == SegmentId::Usage); 190 | 191 | let api_base_url = segment_config 192 | .and_then(|sc| sc.options.get("api_base_url")) 193 | .and_then(|v| v.as_str()) 194 | .unwrap_or("https://api.anthropic.com"); 195 | 196 | let cache_duration = segment_config 197 | .and_then(|sc| sc.options.get("cache_duration")) 198 | .and_then(|v| v.as_u64()) 199 | .unwrap_or(300); 200 | 201 | let timeout = segment_config 202 | .and_then(|sc| sc.options.get("timeout")) 203 | .and_then(|v| v.as_u64()) 204 | .unwrap_or(2); 205 | 206 | let cached_data = self.load_cache(); 207 | let use_cached = cached_data 208 | .as_ref() 209 | .map(|cache| self.is_cache_valid(cache, cache_duration)) 210 | .unwrap_or(false); 211 | 212 | let (five_hour_util, seven_day_util, resets_at) = if use_cached { 213 | let cache = cached_data.unwrap(); 214 | ( 215 | cache.five_hour_utilization, 216 | cache.seven_day_utilization, 217 | cache.resets_at, 218 | ) 219 | } else { 220 | match self.fetch_api_usage(api_base_url, &token, timeout) { 221 | Some(response) => { 222 | let cache = ApiUsageCache { 223 | five_hour_utilization: response.five_hour.utilization, 224 | seven_day_utilization: response.seven_day.utilization, 225 | resets_at: response.seven_day.resets_at.clone(), 226 | cached_at: Utc::now().to_rfc3339(), 227 | }; 228 | self.save_cache(&cache); 229 | ( 230 | response.five_hour.utilization, 231 | response.seven_day.utilization, 232 | response.seven_day.resets_at, 233 | ) 234 | } 235 | None => { 236 | if let Some(cache) = cached_data { 237 | ( 238 | cache.five_hour_utilization, 239 | cache.seven_day_utilization, 240 | cache.resets_at, 241 | ) 242 | } else { 243 | return None; 244 | } 245 | } 246 | } 247 | }; 248 | 249 | let dynamic_icon = Self::get_circle_icon(seven_day_util / 100.0); 250 | let five_hour_percent = five_hour_util.round() as u8; 251 | let primary = format!("{}%", five_hour_percent); 252 | let secondary = format!("· {}", Self::format_reset_time(resets_at.as_deref())); 253 | 254 | let mut metadata = HashMap::new(); 255 | metadata.insert("dynamic_icon".to_string(), dynamic_icon); 256 | metadata.insert( 257 | "five_hour_utilization".to_string(), 258 | five_hour_util.to_string(), 259 | ); 260 | metadata.insert( 261 | "seven_day_utilization".to_string(), 262 | seven_day_util.to_string(), 263 | ); 264 | 265 | Some(SegmentData { 266 | primary, 267 | secondary, 268 | metadata, 269 | }) 270 | } 271 | 272 | fn id(&self) -> SegmentId { 273 | SegmentId::Usage 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/ui/themes/presets.rs: -------------------------------------------------------------------------------- 1 | // Theme presets for TUI configuration 2 | 3 | use crate::config::{Config, StyleConfig, StyleMode}; 4 | 5 | // Import all theme modules 6 | use super::{ 7 | theme_cometix, theme_default, theme_gruvbox, theme_minimal, theme_nord, theme_powerline_dark, 8 | theme_powerline_light, theme_powerline_rose_pine, theme_powerline_tokyo_night, 9 | }; 10 | 11 | pub struct ThemePresets; 12 | 13 | impl ThemePresets { 14 | pub fn get_theme(theme_name: &str) -> Config { 15 | // First try to load from file 16 | if let Ok(config) = Self::load_theme_from_file(theme_name) { 17 | return config; 18 | } 19 | 20 | // Fallback to built-in themes 21 | match theme_name { 22 | "cometix" => Self::get_cometix(), 23 | "default" => Self::get_default(), 24 | "gruvbox" => Self::get_gruvbox(), 25 | "minimal" => Self::get_minimal(), 26 | "nord" => Self::get_nord(), 27 | "powerline-dark" => Self::get_powerline_dark(), 28 | "powerline-light" => Self::get_powerline_light(), 29 | "powerline-rose-pine" => Self::get_powerline_rose_pine(), 30 | "powerline-tokyo-night" => Self::get_powerline_tokyo_night(), 31 | _ => Self::get_default(), 32 | } 33 | } 34 | 35 | /// Load theme from file system 36 | pub fn load_theme_from_file(theme_name: &str) -> Result> { 37 | let themes_dir = Self::get_themes_path(); 38 | let theme_path = themes_dir.join(format!("{}.toml", theme_name)); 39 | 40 | if !theme_path.exists() { 41 | return Err(format!("Theme file not found: {}", theme_path.display()).into()); 42 | } 43 | 44 | let content = std::fs::read_to_string(&theme_path)?; 45 | let mut config: Config = toml::from_str(&content)?; 46 | 47 | // Ensure the theme field matches the requested theme 48 | config.theme = theme_name.to_string(); 49 | 50 | Ok(config) 51 | } 52 | 53 | /// Get the themes directory path (~/.claude/ccline/themes/) 54 | fn get_themes_path() -> std::path::PathBuf { 55 | if let Some(home) = dirs::home_dir() { 56 | home.join(".claude").join("ccline").join("themes") 57 | } else { 58 | std::path::PathBuf::from(".claude/ccline/themes") 59 | } 60 | } 61 | 62 | /// Save current config as a new theme 63 | pub fn save_theme(theme_name: &str, config: &Config) -> Result<(), Box> { 64 | let themes_dir = Self::get_themes_path(); 65 | let theme_path = themes_dir.join(format!("{}.toml", theme_name)); 66 | 67 | // Create themes directory if it doesn't exist 68 | std::fs::create_dir_all(&themes_dir)?; 69 | 70 | // Create a copy of config with the correct theme name 71 | let mut theme_config = config.clone(); 72 | theme_config.theme = theme_name.to_string(); 73 | 74 | let content = toml::to_string_pretty(&theme_config)?; 75 | std::fs::write(&theme_path, content)?; 76 | 77 | Ok(()) 78 | } 79 | 80 | /// List all available themes (built-in + custom) 81 | pub fn list_available_themes() -> Vec { 82 | let mut themes = vec![ 83 | "cometix".to_string(), 84 | "default".to_string(), 85 | "minimal".to_string(), 86 | "gruvbox".to_string(), 87 | "nord".to_string(), 88 | "powerline-dark".to_string(), 89 | "powerline-light".to_string(), 90 | "powerline-rose-pine".to_string(), 91 | "powerline-tokyo-night".to_string(), 92 | ]; 93 | 94 | // Add custom themes from file system 95 | if let Ok(themes_dir) = std::fs::read_dir(Self::get_themes_path()) { 96 | for entry in themes_dir.flatten() { 97 | if let Some(name) = entry.file_name().to_str() { 98 | if name.ends_with(".toml") { 99 | let theme_name = name.trim_end_matches(".toml").to_string(); 100 | if !themes.contains(&theme_name) { 101 | themes.push(theme_name); 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | themes 109 | } 110 | 111 | pub fn get_available_themes() -> Vec<(&'static str, &'static str)> { 112 | vec![ 113 | ("cometix", "Cometix theme"), 114 | ("default", "Default theme with emoji icons"), 115 | ("minimal", "Minimal theme with reduced colors"), 116 | ("gruvbox", "Gruvbox color scheme"), 117 | ("nord", "Nord color scheme"), 118 | ("powerline-dark", "Dark powerline theme"), 119 | ("powerline-light", "Light powerline theme"), 120 | ("powerline-rose-pine", "Rose Pine powerline theme"), 121 | ("powerline-tokyo-night", "Tokyo Night powerline theme"), 122 | ] 123 | } 124 | 125 | pub fn get_cometix() -> Config { 126 | Config { 127 | style: StyleConfig { 128 | mode: StyleMode::NerdFont, 129 | separator: " | ".to_string(), 130 | }, 131 | segments: vec![ 132 | theme_cometix::model_segment(), 133 | theme_cometix::directory_segment(), 134 | theme_cometix::git_segment(), 135 | theme_cometix::context_window_segment(), 136 | theme_cometix::usage_segment(), 137 | theme_cometix::cost_segment(), 138 | theme_cometix::session_segment(), 139 | theme_cometix::output_style_segment(), 140 | ], 141 | theme: "cometix".to_string(), 142 | } 143 | } 144 | 145 | pub fn get_default() -> Config { 146 | Config { 147 | style: StyleConfig { 148 | mode: StyleMode::Plain, 149 | separator: " | ".to_string(), 150 | }, 151 | segments: vec![ 152 | theme_default::model_segment(), 153 | theme_default::directory_segment(), 154 | theme_default::git_segment(), 155 | theme_default::context_window_segment(), 156 | theme_default::usage_segment(), 157 | theme_default::cost_segment(), 158 | theme_default::session_segment(), 159 | theme_default::output_style_segment(), 160 | ], 161 | theme: "default".to_string(), 162 | } 163 | } 164 | 165 | pub fn get_minimal() -> Config { 166 | Config { 167 | style: StyleConfig { 168 | mode: StyleMode::Plain, 169 | separator: " │ ".to_string(), 170 | }, 171 | segments: vec![ 172 | theme_minimal::model_segment(), 173 | theme_minimal::directory_segment(), 174 | theme_minimal::git_segment(), 175 | theme_minimal::context_window_segment(), 176 | theme_minimal::usage_segment(), 177 | theme_minimal::cost_segment(), 178 | theme_minimal::session_segment(), 179 | theme_minimal::output_style_segment(), 180 | ], 181 | theme: "minimal".to_string(), 182 | } 183 | } 184 | 185 | pub fn get_gruvbox() -> Config { 186 | Config { 187 | style: StyleConfig { 188 | mode: StyleMode::NerdFont, 189 | separator: " | ".to_string(), 190 | }, 191 | segments: vec![ 192 | theme_gruvbox::model_segment(), 193 | theme_gruvbox::directory_segment(), 194 | theme_gruvbox::git_segment(), 195 | theme_gruvbox::context_window_segment(), 196 | theme_gruvbox::usage_segment(), 197 | theme_gruvbox::cost_segment(), 198 | theme_gruvbox::session_segment(), 199 | theme_gruvbox::output_style_segment(), 200 | ], 201 | theme: "gruvbox".to_string(), 202 | } 203 | } 204 | 205 | pub fn get_nord() -> Config { 206 | Config { 207 | style: StyleConfig { 208 | mode: StyleMode::NerdFont, 209 | separator: "".to_string(), 210 | }, 211 | segments: vec![ 212 | theme_nord::model_segment(), 213 | theme_nord::directory_segment(), 214 | theme_nord::git_segment(), 215 | theme_nord::context_window_segment(), 216 | theme_nord::usage_segment(), 217 | theme_nord::cost_segment(), 218 | theme_nord::session_segment(), 219 | theme_nord::output_style_segment(), 220 | ], 221 | theme: "nord".to_string(), 222 | } 223 | } 224 | 225 | pub fn get_powerline_dark() -> Config { 226 | Config { 227 | style: StyleConfig { 228 | mode: StyleMode::NerdFont, 229 | separator: "".to_string(), 230 | }, 231 | segments: vec![ 232 | theme_powerline_dark::model_segment(), 233 | theme_powerline_dark::directory_segment(), 234 | theme_powerline_dark::git_segment(), 235 | theme_powerline_dark::context_window_segment(), 236 | theme_powerline_dark::usage_segment(), 237 | theme_powerline_dark::cost_segment(), 238 | theme_powerline_dark::session_segment(), 239 | theme_powerline_dark::output_style_segment(), 240 | ], 241 | theme: "powerline-dark".to_string(), 242 | } 243 | } 244 | 245 | pub fn get_powerline_light() -> Config { 246 | Config { 247 | style: StyleConfig { 248 | mode: StyleMode::NerdFont, 249 | separator: "".to_string(), 250 | }, 251 | segments: vec![ 252 | theme_powerline_light::model_segment(), 253 | theme_powerline_light::directory_segment(), 254 | theme_powerline_light::git_segment(), 255 | theme_powerline_light::context_window_segment(), 256 | theme_powerline_light::usage_segment(), 257 | theme_powerline_light::cost_segment(), 258 | theme_powerline_light::session_segment(), 259 | theme_powerline_light::output_style_segment(), 260 | ], 261 | theme: "powerline-light".to_string(), 262 | } 263 | } 264 | 265 | pub fn get_powerline_rose_pine() -> Config { 266 | Config { 267 | style: StyleConfig { 268 | mode: StyleMode::NerdFont, 269 | separator: "".to_string(), 270 | }, 271 | segments: vec![ 272 | theme_powerline_rose_pine::model_segment(), 273 | theme_powerline_rose_pine::directory_segment(), 274 | theme_powerline_rose_pine::git_segment(), 275 | theme_powerline_rose_pine::context_window_segment(), 276 | theme_powerline_rose_pine::usage_segment(), 277 | theme_powerline_rose_pine::cost_segment(), 278 | theme_powerline_rose_pine::session_segment(), 279 | theme_powerline_rose_pine::output_style_segment(), 280 | ], 281 | theme: "powerline-rose-pine".to_string(), 282 | } 283 | } 284 | 285 | pub fn get_powerline_tokyo_night() -> Config { 286 | Config { 287 | style: StyleConfig { 288 | mode: StyleMode::NerdFont, 289 | separator: "".to_string(), 290 | }, 291 | segments: vec![ 292 | theme_powerline_tokyo_night::model_segment(), 293 | theme_powerline_tokyo_night::directory_segment(), 294 | theme_powerline_tokyo_night::git_segment(), 295 | theme_powerline_tokyo_night::context_window_segment(), 296 | theme_powerline_tokyo_night::usage_segment(), 297 | theme_powerline_tokyo_night::cost_segment(), 298 | theme_powerline_tokyo_night::session_segment(), 299 | theme_powerline_tokyo_night::output_style_segment(), 300 | ], 301 | theme: "powerline-tokyo-night".to_string(), 302 | } 303 | } 304 | } 305 | --------------------------------------------------------------------------------