Skip to main content

gw_render/
views.rs

1//! Static views: `gw branch`, `gw commit` summary, `gw stash list`.
2
3use gw_parse::{Branch, Stash};
4use unicode_width::UnicodeWidthStr;
5
6use crate::style::{BRANCH, BRANCH_BOLD, DIM, GREEN, RED, UPSTREAM, YELLOW, paint, sanitize, truncate};
7
8fn pad(s: &str, w: usize) -> String {
9    format!("{s}{}", " ".repeat(w.saturating_sub(s.width())))
10}
11
12fn counts(a: u32, b: u32, color: bool) -> String {
13    format!(
14        "{} {}",
15        paint(&format!("↑{a}"), if a > 0 { GREEN } else { DIM }, color),
16        paint(&format!("↓{b}"), if b > 0 { YELLOW } else { DIM }, color)
17    )
18}
19
20/// `+N -M` with zero counts dim.
21pub fn lines_stat(add: u64, del: u64, color: bool) -> String {
22    format!(
23        "{} {}",
24        paint(&format!("+{add}"), if add > 0 { GREEN } else { DIM }, color),
25        paint(&format!("-{del}"), if del > 0 { RED } else { DIM }, color)
26    )
27}
28
29fn files_word(n: usize) -> String {
30    format!("{n} {}", if n == 1 { "file" } else { "files" })
31}
32
33/// Current branch first, then by recency (input is already sorted by committer date).
34pub fn render_branches(branches: &[Branch], width: usize, color: bool) -> String {
35    let mut order: Vec<&Branch> = branches.iter().filter(|b| b.current).collect();
36    order.extend(branches.iter().filter(|b| !b.current));
37    let name_w = order.iter().map(|b| sanitize(&b.name).width()).max().unwrap_or(0);
38    let track = |b: &Branch| match &b.upstream {
39        Some(u) => format!("-> {}", sanitize(u)),
40        None => "no upstream".into(),
41    };
42    let track_w = order.iter().map(|b| track(b).width()).max().unwrap_or(0);
43    let age_w = order.iter().map(|b| b.age.width()).max().unwrap_or(0);
44    let state = |b: &Branch| {
45        if b.gone {
46            paint("gone", RED, color)
47        } else if b.upstream.is_some() {
48            counts(b.ahead, b.behind, color)
49        } else {
50            String::new()
51        }
52    };
53    let state_w = order.iter().map(|b| crate::style::visible_width(&state(b))).max().unwrap_or(0);
54    let mut out = String::new();
55    for b in order {
56        let name = sanitize(&b.name);
57        let mark = if b.current { paint("*", GREEN, color) } else { " ".into() };
58        let name_s = paint(&pad(&name, name_w), if b.current { BRANCH_BOLD } else { BRANCH }, color);
59        let t = track(b);
60        let track_s = match &b.upstream {
61            Some(u) => format!("-> {}{}", paint(&sanitize(u), UPSTREAM, color), " ".repeat(track_w - t.width())),
62            None => paint(&pad(&t, track_w), DIM, color),
63        };
64        let st = state(b);
65        let state = format!("{st}{}", " ".repeat(state_w - crate::style::visible_width(&st)));
66        // Visible width so far: mark + space + name + 2 + track + 2 + state + 2 + age + 2
67        let used = 2 + name_w + 2 + track_w + 2 + state_w + 2 + age_w + 2;
68        let subject = truncate(&sanitize(&b.subject), width.saturating_sub(used).max(10));
69        out += &format!(
70            "{mark} {name_s}  {track_s}  {state}  {}  {subject}\n",
71            paint(&pad(&b.age, age_w), DIM, color)
72        );
73    }
74    out
75}
76
77/// After `git commit`: `✓ <sha> <branch>  <subject>` then `<K> files  +N -M`.
78pub fn render_commit_summary(short: &str, branch: &str, subject: &str, files: usize, add: u64, del: u64, color: bool) -> String {
79    format!(
80        "{} {} {}  {}\n{}  {}\n",
81        paint("✓", GREEN, color),
82        paint(&sanitize(short), YELLOW, color),
83        paint(&sanitize(branch), BRANCH_BOLD, color),
84        sanitize(subject),
85        files_word(files),
86        lines_stat(add, del, color)
87    )
88}
89
90/// One line per stash: index, age, branch, message, `<K> files +N -M`.
91pub fn render_stash_list(stashes: &[(Stash, usize, u64, u64)], color: bool) -> String {
92    if stashes.is_empty() {
93        return format!("{}\n", paint("no stashes", DIM, color));
94    }
95    let age_w = stashes.iter().map(|s| s.0.age.width()).max().unwrap_or(0);
96    let br_w = stashes.iter().map(|s| s.0.branch.as_deref().map_or(0, |b| sanitize(b).width())).max().unwrap_or(0);
97    let mut out = String::new();
98    for (s, files, add, del) in stashes {
99        let br = s.branch.as_deref().map(sanitize).unwrap_or_default();
100        out += &format!(
101            "{}  {}  {}  {}  {}\n",
102            paint(&s.index.to_string(), YELLOW, color),
103            paint(&pad(&s.age, age_w), DIM, color),
104            paint(&pad(&br, br_w), BRANCH, color),
105            sanitize(&s.message),
106            paint(&format!("{} {}", files_word(*files), lines_stat(*add, *del, color)), DIM, false)
107        );
108    }
109    out
110}