1use 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
20pub 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
33pub 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 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
77pub 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
90pub 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}