Skip to content

Commit 5531b52

Browse files
Release 1.5.0: add configurable picker colors.
Add the display crate for per-field branch listing colors from ~/.git-b/config.toml, with --no-color and --color CLI overrides. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f902a17 commit 5531b52

11 files changed

Lines changed: 538 additions & 10 deletions

File tree

Cargo.lock

Lines changed: 70 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
[workspace]
2-
members = [".", "crates/cli", "crates/parse", "crates/algo"]
2+
members = [".", "crates/cli", "crates/parse", "crates/algo", "crates/display"]
33
resolver = "2"
44

55
[workspace.package]
6-
version = "1.4.0"
6+
version = "1.5.0"
77
edition = "2021"
88

99
[package]
@@ -17,6 +17,7 @@ build = "build.rs"
1717
[dependencies]
1818
algo = { path = "crates/algo" }
1919
cli = { path = "crates/cli" }
20+
display = { path = "crates/display" }
2021
parse = { path = "crates/parse" }
2122
skim = "4.6.3"
2223

DEVELOPMENT.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
| `cli` | [`crates/cli`](crates/cli) | Clap CLI definition |
88
| `parse` | [`crates/parse`](crates/parse) | Load branches via `git for-each-ref` (name, hash, subject) |
99
| `algo` | [`crates/algo`](crates/algo) | Weighted fuzzy match (name preferred over commit subject); `BranchItem` for Skim |
10+
| `display` | [`crates/display`](crates/display) | Picker colors from `~/.git-b/config.toml`; CLI `--no-color` / `--color` overrides |
1011

1112
Tune name vs subject ranking in [`crates/algo/src/lib.rs`](crates/algo/src/lib.rs): `NAME_WEIGHT`, `EXACT_NAME_BONUS`, `PREFIX_NAME_BONUS`.
1213

14+
User-facing color config lives in `~/.git-b/config.toml` under `[colors]` (see README). Precedence: defaults → config file → CLI flags.
15+
1316
## Man page
1417

1518
The man page is generated at build time with [clap_mangen](https://crates.io/crates/clap_mangen) and written to `man/git-b.1`.

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,27 @@ git-b # open the fuzzy branch picker
3838
git-b feature # checkout the first branch matching "feature"
3939
git-b -b new-name # git checkout -b new-name
4040
man git-b # after installing the man page (see below)
41+
git-b --no-color # disable picker colors for this run
4142
```
4243

44+
### Colors
45+
46+
Picker lines show the branch name, short hash, and commit subject in separate colors. Defaults are cyan, yellow, and white.
47+
48+
Create `~/.git-b/config.toml`:
49+
50+
```toml
51+
[colors]
52+
enabled = true
53+
name = "cyan"
54+
hash = "yellow"
55+
subject = "white"
56+
```
57+
58+
Set `enabled = false` to turn colors off globally. Supported color names include `red`, `green`, `blue`, `magenta`, `cyan`, `gray`, and `bright-*` variants (for example `bright-green`). Use `default` or `none` to leave a field unstyled.
59+
60+
CLI flags override the config file for a single run: `--no-color`, or `--color NAME:HASH:SUBJECT` (for example `cyan:yellow:white`).
61+
4362
### Man page
4463

4564
Building from source generates `man/git-b.1` via [clap_mangen](https://crates.io/crates/clap_mangen). Install it with:

crates/algo/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ description = "Weighted branch matching for git-b"
66
publish = false
77

88
[dependencies]
9+
display = { path = "../display" }
910
parse = { path = "../parse" }
1011
skim = "4.6.3"

crates/algo/src/lib.rs

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
//! Weighted fuzzy matching for branches (name preferred over commit subject).
22
33
use std::borrow::Cow;
4+
use std::collections::HashSet;
45

6+
use display::{colored_line, field_ranges, DisplayColors, FieldRanges, Line};
57
use parse::Branch;
68
use skim::prelude::*;
7-
use skim::{MatchEngine, MatchEngineFactory, SkimItem};
9+
use skim::{DisplayContext, MatchEngine, MatchEngineFactory, Matches, SkimItem};
810

911
/// Multiply name-field fuzzy scores so name matches outrank subject-only matches.
1012
pub const NAME_WEIGHT: i32 = 1_000;
@@ -21,18 +23,23 @@ pub struct BranchItem {
2123
branch: Branch,
2224
display: String,
2325
matching_ranges: [(usize, usize); 2],
26+
field_ranges: FieldRanges,
27+
colors: DisplayColors,
2428
}
2529

2630
impl BranchItem {
27-
pub fn new(branch: Branch) -> Self {
31+
pub fn new(branch: Branch, colors: DisplayColors) -> Self {
2832
let display = branch.display_line();
2933
let name_end = branch.name.len();
3034
let subject_start = name_end + 1 + branch.short_hash.len() + 1;
3135
let subject_end = display.len();
36+
let field = field_ranges(&branch.name, &branch.short_hash, &branch.subject);
3237
Self {
3338
branch,
3439
display,
35-
matching_ranges: [(0, name_end), (subject_start, subject_end)],
40+
matching_ranges: [(0, name_end), (subject_start, subject_end)], // byte ranges for skim
41+
field_ranges: field, // char ranges for display colors
42+
colors,
3643
}
3744
}
3845

@@ -43,7 +50,26 @@ impl BranchItem {
4350

4451
impl From<Branch> for BranchItem {
4552
fn from(branch: Branch) -> Self {
46-
Self::new(branch)
53+
Self::new(branch, DisplayColors::default())
54+
}
55+
}
56+
57+
fn highlight_chars(text: &str, matches: &Matches) -> HashSet<usize> {
58+
match matches {
59+
Matches::None => HashSet::new(),
60+
Matches::CharIndices(indices) => indices.iter().copied().collect(),
61+
Matches::CharRange(start, end) => (*start..*end).collect(),
62+
Matches::ByteRange(start, end) => {
63+
let mut set = HashSet::new();
64+
let mut ci = 0;
65+
for (bi, _) in text.char_indices() {
66+
if bi >= *start && bi < *end {
67+
set.insert(ci);
68+
}
69+
ci += 1;
70+
}
71+
set
72+
}
4773
}
4874
}
4975

@@ -59,6 +85,21 @@ impl SkimItem for BranchItem {
5985
fn get_matching_ranges(&self) -> Option<&[(usize, usize)]> {
6086
Some(&self.matching_ranges)
6187
}
88+
89+
fn display(&self, context: DisplayContext) -> Line<'_> {
90+
if !self.colors.enabled {
91+
return context.to_line(Cow::Borrowed(&self.display));
92+
}
93+
let highlight = highlight_chars(&self.display, &context.matches);
94+
colored_line(
95+
&self.display,
96+
self.field_ranges,
97+
self.colors,
98+
context.base_style,
99+
context.matched_style,
100+
&highlight,
101+
)
102+
}
62103
}
63104

64105
struct Scored<'a> {
@@ -132,6 +173,7 @@ pub fn find_first_matching<'a>(query: &str, branches: &'a [Branch]) -> Option<&'
132173
#[cfg(test)]
133174
mod tests {
134175
use super::*;
176+
use display::DisplayColors;
135177
use parse::Branch;
136178

137179
fn branch(name: &str, subject: &str) -> Branch {
@@ -164,16 +206,24 @@ mod tests {
164206

165207
#[test]
166208
fn branch_item_output_is_checkout_name() {
167-
let item = BranchItem::new(branch("foo/bar", "subject"));
209+
let item = BranchItem::new(branch("foo/bar", "subject"), DisplayColors::disabled());
168210
assert_eq!(item.output(), "foo/bar");
169211
assert_eq!(item.text(), "foo/bar abc1234 subject");
170212
}
171213

172214
#[test]
173215
fn branch_item_matching_ranges_name_before_subject() {
174-
let item = BranchItem::new(branch("my-branch", "my subject"));
216+
let item = BranchItem::new(branch("my-branch", "my subject"), DisplayColors::disabled());
175217
let ranges = item.get_matching_ranges().unwrap();
176218
assert_eq!(ranges[0], (0, "my-branch".len()));
177219
assert!(ranges[1].0 > ranges[0].1);
178220
}
221+
222+
#[test]
223+
fn colored_display_when_enabled() {
224+
let item = BranchItem::new(branch("main", "init"), DisplayColors::default());
225+
assert!(item.colors.enabled);
226+
let line = item.display(DisplayContext::default());
227+
assert!(!line.spans.is_empty());
228+
}
179229
}

crates/cli/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,12 @@ pub struct Args {
1818
/// Branch name to fuzzy-match and checkout directly (skips the picker).
1919
#[arg()]
2020
pub branch: Option<String>,
21+
22+
/// Disable colored branch listing in the picker.
23+
#[arg(long = "no-color")]
24+
pub no_color: bool,
25+
26+
/// Set field colors as NAME:HASH:SUBJECT (e.g. cyan:yellow:white).
27+
#[arg(long = "color", value_name = "NAME:HASH:SUBJECT")]
28+
pub color: Option<String>,
2129
}

crates/display/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "display"
3+
version.workspace = true
4+
edition.workspace = true
5+
description = "Display colors for git-b branch picker"
6+
publish = false
7+
8+
[dependencies]
9+
ratatui = "0.30"
10+
serde = { version = "1.0", features = ["derive"] }
11+
toml = "0.8"

0 commit comments

Comments
 (0)