Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 49 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ The statusline shows: Model | Directory | Git Branch Status | Context Window Inf
## Features

### Core Functionality
- **Git integration** with branch, status, and tracking info
- **Git integration** with branch, status, and tracking info
- **Multi-subdirectory Git support** for monorepo projects
- **Project-level configuration** overrides global settings per project
- **Model display** with simplified Claude model names
- **Usage tracking** based on transcript analysis
- **Directory display** showing current workspace
Expand Down Expand Up @@ -231,9 +233,37 @@ Displays: `Directory | Git Branch Status | Model | Context Window`
### Git Status Indicators

- Branch name with Nerd Font icon
- Status: `✓` Clean, `●` Dirty, `⚠` Conflicts
- Status: `✓` Clean, `●` Dirty, `⚠` Conflicts
- Remote tracking: `↑n` Ahead, `↓n` Behind

### Multi-Subdirectory Git Support (ddline fork)

For monorepo projects with multiple Git repositories in subdirectories, you can configure ddline to show branch and status for each:

**Output example:**
```
🗂️ api:master ✓ | 🗂️ web:feature/my-feature ●
```

**Global configuration** (`~/.claude/ccline/config.toml`):
```toml
[[segments]]
id = "git"
enabled = true

[segments.options]
sub_dirs = ["api", "web"] # List of subdirectories to monitor
```

**Project-level configuration** (`<project-dir>/.ccline.toml`):
```toml
# Project-level config overrides global config
[segments.git]
sub_dirs = ["api", "web", "shared"]
```

Project-level configuration takes precedence over global configuration, allowing different subdirectory settings per project.

### Model Display

Shows simplified Claude model names:
Expand All @@ -248,11 +278,27 @@ Token usage percentage based on transcript analysis with context limit tracking.

CCometixLine supports full configuration via TOML files and interactive TUI:

- **Configuration file**: `~/.claude/ccline/config.toml`
- **Global configuration file**: `~/.claude/ccline/config.toml`
- **Project-level configuration**: `<project-dir>/.ccline.toml` (overrides global)
- **Interactive TUI**: `ccline --config` for real-time editing with preview
- **Theme files**: `~/.claude/ccline/themes/*.toml` for custom themes
- **Automatic initialization**: `ccline --init` creates default configuration

### Project-Level Configuration

Create a `.ccline.toml` file in your project root to override global settings:

```toml
# .ccline.toml - Project-level configuration
# Only segment options can be overridden

[segments.git]
sub_dirs = ["api", "web"] # Override git subdirectories for this project

[segments.directory]
# Add other segment-specific options here
```

### Available Segments

All segments are configurable with:
Expand Down
50 changes: 48 additions & 2 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@

### 核心功能
- **Git 集成** 显示分支、状态和跟踪信息
- **多子目录 Git 支持** 适用于 monorepo 项目
- **项目级配置** 支持按项目覆盖全局设置
- **模型显示** 简化的 Claude 模型名称
- **使用量跟踪** 基于转录文件分析
- **使用量跟踪** 基于转录文件分析
- **目录显示** 显示当前工作空间
- **简洁设计** 使用 Nerd Font 图标

Expand Down Expand Up @@ -226,6 +228,34 @@ ccline --patch ~/.local/share/fnm/node-versions/v24.4.1/installation/lib/node_mo
- 状态:`✓` 清洁,`●` 有更改,`⚠` 冲突
- 远程跟踪:`↑n` 领先,`↓n` 落后

### 多子目录 Git 支持(ddline fork 新增)

对于包含多个 Git 仓库子目录的 monorepo 项目,可以配置 ddline 显示每个子目录的分支和状态:

**输出示例:**
```
🗂️ api:master ✓ | 🗂️ web:feature/my-feature ●
```

**全局配置** (`~/.claude/ccline/config.toml`):
```toml
[[segments]]
id = "git"
enabled = true

[segments.options]
sub_dirs = ["api", "web"] # 要监控的子目录列表
```

**项目级配置** (`<项目目录>/.ccline.toml`):
```toml
# 项目级配置覆盖全局配置
[segments.git]
sub_dirs = ["api", "web", "shared"]
```

项目级配置优先于全局配置,允许每个项目使用不同的子目录设置。

### 模型显示

显示简化的 Claude 模型名称:
Expand All @@ -240,11 +270,27 @@ ccline --patch ~/.local/share/fnm/node-versions/v24.4.1/installation/lib/node_mo

CCometixLine 支持通过 TOML 文件和交互式 TUI 进行完整配置:

- **配置文件**: `~/.claude/ccline/config.toml`
- **全局配置文件**: `~/.claude/ccline/config.toml`
- **项目级配置**: `<项目目录>/.ccline.toml`(覆盖全局配置)
- **交互式 TUI**: `ccline --config` 实时编辑配置并预览效果
- **主题文件**: `~/.claude/ccline/themes/*.toml` 自定义主题文件
- **自动初始化**: `ccline --init` 创建默认配置

### 项目级配置

在项目根目录创建 `.ccline.toml` 文件来覆盖全局设置:

```toml
# .ccline.toml - 项目级配置
# 仅支持覆盖 segment options

[segments.git]
sub_dirs = ["api", "web"] # 为此项目覆盖 git 子目录设置

[segments.directory]
# 在此添加其他 segment 特定选项
```

### 可用段落

所有段落都支持配置:
Expand Down
73 changes: 73 additions & 0 deletions src/config/loader.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
use super::types::Config;
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

/// Project-level configuration (partial override)
/// Only contains options that can be overridden per-project
#[derive(Debug, Deserialize)]
pub struct ProjectConfig {
/// Segment-specific options override
/// Key is segment id (e.g., "git"), value is options map
pub segments: Option<HashMap<String, HashMap<String, serde_json::Value>>>,
}

/// Result of config initialization
#[derive(Debug)]
pub enum InitResult {
Expand Down Expand Up @@ -128,6 +139,68 @@ impl Config {
Ok(config)
}

/// Load configuration with project-level override
/// Project config path: <project_dir>/.ccline.toml
///
/// Error handling:
/// - Global config failure: falls back to default config
/// - Project config failure: logs warning but keeps global config
pub fn load_with_project(project_dir: &str) -> Result<Config, Box<dyn std::error::Error>> {
// Load global config first
let mut config = Self::load()?;

// Check for project-level config
let project_config_path = Path::new(project_dir).join(".ccline.toml");

if project_config_path.exists() {
match fs::read_to_string(&project_config_path) {
Ok(content) => match toml::from_str::<ProjectConfig>(&content) {
Ok(project_config) => {
config.merge_project_config(project_config);
}
Err(e) => {
eprintln!(
"Warning: Failed to parse project config at {}: {}",
project_config_path.display(),
e
);
// Continue with global config only
}
},
Err(e) => {
eprintln!(
"Warning: Failed to read project config at {}: {}",
project_config_path.display(),
e
);
// Continue with global config only
}
}
}

Ok(config)
}

/// Merge project-level configuration into this config
fn merge_project_config(&mut self, project_config: ProjectConfig) {
// Merge segment options
if let Some(segment_options) = project_config.segments {
for (segment_id, options) in segment_options {
// Find the matching segment in config
// Use stable config_key() instead of Debug formatting
for segment in &mut self.segments {
if segment.id.config_key() == segment_id {
// Merge options
for (key, value) in options {
segment.options.insert(key, value);
}
break;
}
}
}
}
}

/// Save configuration to default location
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
let config_path = Self::get_config_path();
Expand Down
18 changes: 18 additions & 0 deletions src/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ pub enum SegmentId {
Update,
}

impl SegmentId {
/// Returns a stable config key for matching in project-level configuration.
/// This matches the serde snake_case serialization format.
pub fn config_key(&self) -> &'static str {
match self {
SegmentId::Model => "model",
SegmentId::Directory => "directory",
SegmentId::Git => "git",
SegmentId::ContextWindow => "context_window",
SegmentId::Usage => "usage",
SegmentId::Cost => "cost",
SegmentId::Session => "session",
SegmentId::OutputStyle => "output_style",
SegmentId::Update => "update",
}
}
}

// Legacy compatibility structure
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SegmentsConfig {
Expand Down
59 changes: 57 additions & 2 deletions src/core/segments/git.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::{Segment, SegmentData};
use crate::config::{InputData, SegmentId};
use std::collections::HashMap;
use std::path::Path;
use std::process::Command;

#[derive(Debug)]
Expand All @@ -21,6 +22,7 @@ pub enum GitStatus {

pub struct GitSegment {
show_sha: bool,
sub_dirs: Vec<String>,
}

impl Default for GitSegment {
Expand All @@ -31,14 +33,22 @@ impl Default for GitSegment {

impl GitSegment {
pub fn new() -> Self {
Self { show_sha: false }
Self {
show_sha: false,
sub_dirs: Vec::new(),
}
}

pub fn with_sha(mut self, show_sha: bool) -> Self {
self.show_sha = show_sha;
self
}

pub fn with_sub_dirs(mut self, sub_dirs: Vec<String>) -> Self {
self.sub_dirs = sub_dirs;
self
}

fn get_git_info(&self, working_dir: &str) -> Option<GitInfo> {
if !self.is_git_repository(working_dir) {
return None;
Expand Down Expand Up @@ -169,11 +179,56 @@ impl GitSegment {
None
}
}

/// 多子目录模式:获取多个子目录的 git 分支信息
fn collect_multi_dirs(&self, working_dir: &str) -> Option<SegmentData> {
// 文件夹索引 emoji: 🗂️
let folder_icon = "🗂️";
let mut branch_parts = Vec::new();
let mut metadata = HashMap::new();

for sub_dir in &self.sub_dirs {
let full_path = Path::new(working_dir).join(sub_dir);
let full_path_str = full_path.to_string_lossy();

if let Some(branch) = self.get_branch(&full_path_str) {
// 获取 git status
let status = self.get_status(&full_path_str);
let status_icon = match status {
GitStatus::Clean => "✓",
GitStatus::Dirty => "●",
GitStatus::Conflicts => "⚠",
};

branch_parts.push(format!("{} {}:{} {}", folder_icon, sub_dir, branch, status_icon));
metadata.insert(format!("{}_branch", sub_dir), branch);
metadata.insert(format!("{}_status", sub_dir), format!("{:?}", status));
}
}

if branch_parts.is_empty() {
return None;
}

Some(SegmentData {
primary: branch_parts.join(" | "),
secondary: String::new(),
metadata,
})
}
}

impl Segment for GitSegment {
fn collect(&self, input: &InputData) -> Option<SegmentData> {
let git_info = self.get_git_info(&input.workspace.current_dir)?;
let working_dir = &input.workspace.current_dir;

// 多子目录模式
if !self.sub_dirs.is_empty() {
return self.collect_multi_dirs(working_dir);
}

// 原有逻辑:单目录模式
let git_info = self.get_git_info(working_dir)?;

let mut metadata = HashMap::new();
metadata.insert("branch".to_string(), git_info.branch.clone());
Expand Down
17 changes: 16 additions & 1 deletion src/core/statusline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,22 @@ pub fn collect_all_segments(
.get("show_sha")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let segment = GitSegment::new().with_sha(show_sha);

// 读取 sub_dirs 配置
let sub_dirs = segment_config
.options
.get("sub_dirs")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();

let segment = GitSegment::new()
.with_sha(show_sha)
.with_sub_dirs(sub_dirs);
segment.collect(input)
}
crate::config::SegmentId::ContextWindow => {
Expand Down
Loading