From ebda7dd7e2444f8b384b001a9807828028e30292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=9E=E5=BB=BA=E9=BE=99?= <300116@nd.com> Date: Mon, 9 Feb 2026 10:05:25 +0800 Subject: [PATCH 1/2] feat: add multi-subdirectory Git support and project-level configuration - Add sub_dirs option to Git segment for monitoring multiple subdirectories - Add project-level .ccline.toml configuration support - Update README with new features documentation Co-Authored-By: Claude --- README.md | 52 +++++++++++++++++++++++++++++++++-- README.zh.md | 50 ++++++++++++++++++++++++++++++++-- src/config/loader.rs | 51 ++++++++++++++++++++++++++++++++++ src/core/segments/git.rs | 59 ++++++++++++++++++++++++++++++++++++++-- src/core/statusline.rs | 17 +++++++++++- src/main.rs | 17 ++++++------ 6 files changed, 230 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 91512b7..32c50a0 100644 --- a/README.md +++ b/README.md @@ -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 (ddline fork feature) +- **Project-level configuration** override global settings per project - **Model display** with simplified Claude model names - **Usage tracking** based on transcript analysis - **Directory display** showing current workspace @@ -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** (`/.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: @@ -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**: `/.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: diff --git a/README.zh.md b/README.zh.md index 88656db..6433aed 100644 --- a/README.zh.md +++ b/README.zh.md @@ -17,8 +17,10 @@ ### 核心功能 - **Git 集成** 显示分支、状态和跟踪信息 +- **多子目录 Git 支持** 适用于 monorepo 项目(ddline fork 新增功能) +- **项目级配置** 支持按项目覆盖全局设置 - **模型显示** 简化的 Claude 模型名称 -- **使用量跟踪** 基于转录文件分析 +- **使用量跟踪** 基于转录文件分析 - **目录显示** 显示当前工作空间 - **简洁设计** 使用 Nerd Font 图标 @@ -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 模型名称: @@ -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 特定选项 +``` + ### 可用段落 所有段落都支持配置: diff --git a/src/config/loader.rs b/src/config/loader.rs index 8d68211..78c780d 100644 --- a/src/config/loader.rs +++ b/src/config/loader.rs @@ -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>>, +} + /// Result of config initialization #[derive(Debug)] pub enum InitResult { @@ -128,6 +139,46 @@ impl Config { Ok(config) } + /// Load configuration with project-level override + /// Project config path: /.ccline.toml + pub fn load_with_project(project_dir: &str) -> Result> { + // 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() { + let content = fs::read_to_string(&project_config_path)?; + let project_config: ProjectConfig = toml::from_str(&content)?; + + // Merge project config into global config + config.merge_project_config(project_config); + } + + 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 + for segment in &mut self.segments { + let segment_id_str = format!("{:?}", segment.id).to_lowercase(); + if segment_id_str == 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> { let config_path = Self::get_config_path(); diff --git a/src/core/segments/git.rs b/src/core/segments/git.rs index 6bc9c84..14a7eb2 100644 --- a/src/core/segments/git.rs +++ b/src/core/segments/git.rs @@ -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)] @@ -21,6 +22,7 @@ pub enum GitStatus { pub struct GitSegment { show_sha: bool, + sub_dirs: Vec, } impl Default for GitSegment { @@ -31,7 +33,10 @@ 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 { @@ -39,6 +44,11 @@ impl GitSegment { self } + pub fn with_sub_dirs(mut self, sub_dirs: Vec) -> Self { + self.sub_dirs = sub_dirs; + self + } + fn get_git_info(&self, working_dir: &str) -> Option { if !self.is_git_repository(working_dir) { return None; @@ -169,11 +179,56 @@ impl GitSegment { None } } + + /// 多子目录模式:获取多个子目录的 git 分支信息 + fn collect_multi_dirs(&self, working_dir: &str) -> Option { + // 文件夹索引 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 { - 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()); diff --git a/src/core/statusline.rs b/src/core/statusline.rs index 5f84bf1..8271063 100644 --- a/src/core/statusline.rs +++ b/src/core/statusline.rs @@ -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 => { diff --git a/src/main.rs b/src/main.rs index 3a3a170..5712051 100644 --- a/src/main.rs +++ b/src/main.rs @@ -88,14 +88,6 @@ fn main() -> Result<(), Box> { return Ok(()); } - // Load configuration - let mut config = Config::load().unwrap_or_else(|_| Config::default()); - - // Apply theme override if provided - if let Some(theme) = cli.theme { - config = ccometixline::ui::themes::ThemePresets::get_theme(&theme); - } - // Check if stdin has data if io::stdin().is_terminal() { // No input data available, show main menu @@ -131,6 +123,15 @@ fn main() -> Result<(), Box> { let stdin = io::stdin(); let input: InputData = serde_json::from_reader(stdin.lock())?; + // Load configuration with project-level override + let mut config = Config::load_with_project(&input.workspace.current_dir) + .unwrap_or_else(|_| Config::default()); + + // Apply theme override if provided + if let Some(theme) = cli.theme { + config = ccometixline::ui::themes::ThemePresets::get_theme(&theme); + } + // Collect segment data let segments_data = collect_all_segments(&config, &input); From e7b31fca4028d04513a5bf15152ee548e4b04cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=9E=E5=BB=BA=E9=BE=99?= <300116@nd.com> Date: Mon, 9 Feb 2026 10:26:07 +0800 Subject: [PATCH 2/2] fix: address code review feedback - Add SegmentId::config_key() for stable config matching instead of using brittle Debug formatting - Improve error handling in load_with_project(): project config errors now log warnings but preserve global config instead of discarding it - Fix grammar: "override" -> "overrides" in README - Remove "ddline fork feature" references for consistent naming Co-Authored-By: Claude Opus 4.5 --- README.md | 4 ++-- README.zh.md | 2 +- src/config/loader.rs | 36 +++++++++++++++++++++++++++++------- src/config/types.rs | 18 ++++++++++++++++++ 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 32c50a0..6489991 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ The statusline shows: Model | Directory | Git Branch Status | Context Window Inf ### Core Functionality - **Git integration** with branch, status, and tracking info -- **Multi-subdirectory Git support** for monorepo projects (ddline fork feature) -- **Project-level configuration** override global settings per project +- **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 diff --git a/README.zh.md b/README.zh.md index 6433aed..c344409 100644 --- a/README.zh.md +++ b/README.zh.md @@ -17,7 +17,7 @@ ### 核心功能 - **Git 集成** 显示分支、状态和跟踪信息 -- **多子目录 Git 支持** 适用于 monorepo 项目(ddline fork 新增功能) +- **多子目录 Git 支持** 适用于 monorepo 项目 - **项目级配置** 支持按项目覆盖全局设置 - **模型显示** 简化的 Claude 模型名称 - **使用量跟踪** 基于转录文件分析 diff --git a/src/config/loader.rs b/src/config/loader.rs index 78c780d..6883fdb 100644 --- a/src/config/loader.rs +++ b/src/config/loader.rs @@ -141,6 +141,10 @@ impl Config { /// Load configuration with project-level override /// Project config path: /.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> { // Load global config first let mut config = Self::load()?; @@ -149,11 +153,29 @@ impl Config { let project_config_path = Path::new(project_dir).join(".ccline.toml"); if project_config_path.exists() { - let content = fs::read_to_string(&project_config_path)?; - let project_config: ProjectConfig = toml::from_str(&content)?; - - // Merge project config into global config - config.merge_project_config(project_config); + match fs::read_to_string(&project_config_path) { + Ok(content) => match toml::from_str::(&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) @@ -165,9 +187,9 @@ impl Config { 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 { - let segment_id_str = format!("{:?}", segment.id).to_lowercase(); - if segment_id_str == segment_id { + if segment.id.config_key() == segment_id { // Merge options for (key, value) in options { segment.options.insert(key, value); diff --git a/src/config/types.rs b/src/config/types.rs index e5a78dc..0347937 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -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 {