sparrow/tui/formatters/
diff.rs1#[derive(Debug, Clone)]
7pub enum DiffLineKind {
8 Context,
9 Plus,
10 Minus,
11 Hunk,
12 FileHeader,
13}
14
15#[derive(Debug, Clone)]
17pub struct DiffLine {
18 pub kind: DiffLineKind,
19 pub text: String,
20}
21
22#[derive(Debug, Clone)]
24pub struct ParsedDiff {
25 pub file_header: Option<String>,
26 pub lines: Vec<DiffLine>,
27 pub plus_count: u32,
28 pub minus_count: u32,
29}
30
31pub struct DiffColors {
33 pub plus: &'static str,
34 pub minus: &'static str,
35 pub hunk: &'static str,
36 pub file: &'static str,
37 pub context: &'static str,
38 pub reset: &'static str,
39}
40
41impl Default for DiffColors {
42 fn default() -> Self {
43 Self {
44 plus: "\x1b[38;2;116;194;88m", minus: "\x1b[38;2;217;106;99m", hunk: "\x1b[38;2;86;182;194m", file: "\x1b[1;38;2;242;169;60m", context: "\x1b[38;2;137;125;108m", reset: "\x1b[0m",
50 }
51 }
52}
53
54pub fn parse(diff: &str) -> ParsedDiff {
67 let mut lines = Vec::new();
68 let mut file_header: Option<String> = None;
69 let mut plus_count: u32 = 0;
70 let mut minus_count: u32 = 0;
71
72 for raw in diff.lines() {
73 let line = raw.to_string();
74
75 if raw.starts_with("diff --git") || raw.starts_with("--- ") || raw.starts_with("+++ ") {
76 if file_header.is_none() && (raw.starts_with("diff --git") || raw.starts_with("--- ")) {
77 let fname = if raw.starts_with("diff --git") {
79 raw.strip_prefix("diff --git a/")
80 .and_then(|s| s.split_whitespace().next())
81 .unwrap_or(raw)
82 } else if raw.starts_with("--- ") {
83 raw.strip_prefix("--- a/")
84 .or_else(|| raw.strip_prefix("--- "))
85 .unwrap_or(raw)
86 } else {
87 raw
88 };
89 file_header = Some(fname.to_string());
90 }
91 lines.push(DiffLine {
92 kind: DiffLineKind::FileHeader,
93 text: line,
94 });
95 } else if raw.starts_with("@@") {
96 lines.push(DiffLine {
97 kind: DiffLineKind::Hunk,
98 text: line,
99 });
100 } else if raw.starts_with('+') && !raw.starts_with("+++") {
101 plus_count += 1;
102 lines.push(DiffLine {
103 kind: DiffLineKind::Plus,
104 text: line,
105 });
106 } else if raw.starts_with('-') && !raw.starts_with("---") {
107 minus_count += 1;
108 lines.push(DiffLine {
109 kind: DiffLineKind::Minus,
110 text: line,
111 });
112 } else {
113 lines.push(DiffLine {
114 kind: DiffLineKind::Context,
115 text: line,
116 });
117 }
118 }
119
120 ParsedDiff {
121 file_header,
122 lines,
123 plus_count,
124 minus_count,
125 }
126}
127
128pub fn render(parsed: &ParsedDiff, colors: &DiffColors) -> String {
130 let mut out = String::new();
131
132 if let Some(ref file) = parsed.file_header {
134 out.push_str(&format!(
135 "{file}📄 {file}{reset} ",
136 file = colors.file,
137 reset = colors.reset,
138 ));
139 }
140 out.push_str(&format!(
141 "{plus}+{}{reset} {minus}-{}{reset}\n",
142 parsed.plus_count,
143 parsed.minus_count,
144 plus = colors.plus,
145 minus = colors.minus,
146 reset = colors.reset
147 ));
148
149 out.push_str(&format!(
151 "{dim}─────────────────────────────────────────────{reset}\n",
152 dim = colors.context,
153 reset = colors.reset
154 ));
155
156 for entry in &parsed.lines {
157 let (prefix, reset) = match entry.kind {
158 DiffLineKind::FileHeader => (colors.file, colors.reset),
159 DiffLineKind::Hunk => (colors.hunk, colors.reset),
160 DiffLineKind::Plus => (colors.plus, colors.reset),
161 DiffLineKind::Minus => (colors.minus, colors.reset),
162 DiffLineKind::Context => (colors.context, colors.reset),
163 };
164 out.push_str(&format!("{prefix}{text}{reset}\n", text = entry.text));
165 }
166
167 out
168}
169
170pub fn format_diff(diff: &str) -> String {
172 let colors = DiffColors::default();
173 let parsed = parse(diff);
174 render(&parsed, &colors)
175}