1use std::fmt::Write;
8
9use owo_colors::OwoColorize;
10
11use crate::DiffTheme;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum SemanticColor {
16 Deleted,
19 DeletedHighlight,
21 Inserted,
23 InsertedHighlight,
25 Moved,
27 MovedHighlight,
29
30 DeletedKey,
33 InsertedKey,
35 Key,
37
38 DeletedStructure,
40 InsertedStructure,
42 Structure,
44
45 DeletedComment,
47 InsertedComment,
49 Comment,
51
52 DeletedString,
55 InsertedString,
57 String,
59
60 DeletedNumber,
62 InsertedNumber,
64 Number,
66
67 DeletedBoolean,
69 InsertedBoolean,
71 Boolean,
73
74 DeletedNull,
76 InsertedNull,
78 Null,
80
81 Whitespace,
84
85 Unchanged,
88}
89
90pub trait ColorBackend {
92 fn write_styled<W: Write>(
94 &self,
95 w: &mut W,
96 text: &str,
97 color: SemanticColor,
98 ) -> std::fmt::Result;
99
100 fn write_prefix<W: Write>(
102 &self,
103 w: &mut W,
104 prefix: char,
105 color: SemanticColor,
106 ) -> std::fmt::Result {
107 self.write_styled(w, &prefix.to_string(), color)
108 }
109}
110
111#[derive(Debug, Clone, Copy, Default)]
115pub struct PlainBackend;
116
117impl ColorBackend for PlainBackend {
118 fn write_styled<W: Write>(
119 &self,
120 w: &mut W,
121 text: &str,
122 _color: SemanticColor,
123 ) -> std::fmt::Result {
124 write!(w, "{}", text)
125 }
126}
127
128#[derive(Debug, Clone)]
132pub struct AnsiBackend {
133 theme: DiffTheme,
134}
135
136impl AnsiBackend {
137 pub fn new(theme: DiffTheme) -> Self {
139 Self { theme }
140 }
141
142 pub fn with_default_theme() -> Self {
144 Self::new(DiffTheme::default())
145 }
146}
147
148impl Default for AnsiBackend {
149 fn default() -> Self {
150 Self::with_default_theme()
151 }
152}
153
154impl ColorBackend for AnsiBackend {
155 fn write_styled<W: Write>(
156 &self,
157 w: &mut W,
158 text: &str,
159 color: SemanticColor,
160 ) -> std::fmt::Result {
161 let (fg, bg) = match color {
162 SemanticColor::Deleted => {
164 (self.theme.deleted, self.theme.desaturated_deleted_line_bg())
165 }
166 SemanticColor::DeletedHighlight => (
167 self.theme.deleted,
168 self.theme.desaturated_deleted_highlight_bg(),
169 ),
170 SemanticColor::Inserted => (
171 self.theme.inserted,
172 self.theme.desaturated_inserted_line_bg(),
173 ),
174 SemanticColor::InsertedHighlight => (
175 self.theme.inserted,
176 self.theme.desaturated_inserted_highlight_bg(),
177 ),
178 SemanticColor::Moved => (self.theme.moved, self.theme.desaturated_moved_line_bg()),
179 SemanticColor::MovedHighlight => (
180 self.theme.moved,
181 self.theme.desaturated_moved_highlight_bg(),
182 ),
183
184 SemanticColor::DeletedKey => (
186 self.theme.deleted_highlight_key(),
187 self.theme.desaturated_deleted_highlight_bg(),
188 ),
189 SemanticColor::InsertedKey => (
190 self.theme.inserted_highlight_key(),
191 self.theme.desaturated_inserted_highlight_bg(),
192 ),
193 SemanticColor::Key => (self.theme.key, None),
194
195 SemanticColor::DeletedStructure => (
196 self.theme.deleted_structure(),
197 self.theme.desaturated_deleted_line_bg(),
198 ),
199 SemanticColor::InsertedStructure => (
200 self.theme.inserted_structure(),
201 self.theme.desaturated_inserted_line_bg(),
202 ),
203 SemanticColor::Structure => (self.theme.structure, None),
204
205 SemanticColor::DeletedComment => (
206 self.theme.deleted_highlight_comment(),
207 self.theme.desaturated_deleted_highlight_bg(),
208 ),
209 SemanticColor::InsertedComment => (
210 self.theme.inserted_highlight_comment(),
211 self.theme.desaturated_inserted_highlight_bg(),
212 ),
213 SemanticColor::Comment => (self.theme.comment, None),
214
215 SemanticColor::DeletedString => (
217 self.theme.deleted_highlight_string(),
218 self.theme.desaturated_deleted_highlight_bg(),
219 ),
220 SemanticColor::InsertedString => (
221 self.theme.inserted_highlight_string(),
222 self.theme.desaturated_inserted_highlight_bg(),
223 ),
224 SemanticColor::String => (self.theme.string, None),
225
226 SemanticColor::DeletedNumber => (
227 self.theme.deleted_highlight_number(),
228 self.theme.desaturated_deleted_highlight_bg(),
229 ),
230 SemanticColor::InsertedNumber => (
231 self.theme.inserted_highlight_number(),
232 self.theme.desaturated_inserted_highlight_bg(),
233 ),
234 SemanticColor::Number => (self.theme.number, None),
235
236 SemanticColor::DeletedBoolean => (
237 self.theme.deleted_highlight_boolean(),
238 self.theme.desaturated_deleted_highlight_bg(),
239 ),
240 SemanticColor::InsertedBoolean => (
241 self.theme.inserted_highlight_boolean(),
242 self.theme.desaturated_inserted_highlight_bg(),
243 ),
244 SemanticColor::Boolean => (self.theme.boolean, None),
245
246 SemanticColor::DeletedNull => (
247 self.theme.deleted_highlight_null(),
248 self.theme.desaturated_deleted_highlight_bg(),
249 ),
250 SemanticColor::InsertedNull => (
251 self.theme.inserted_highlight_null(),
252 self.theme.desaturated_inserted_highlight_bg(),
253 ),
254 SemanticColor::Null => (self.theme.null, None),
255
256 SemanticColor::Whitespace => (self.theme.comment, None),
258
259 SemanticColor::Unchanged => (self.theme.unchanged, None),
261 };
262 if let Some(bg) = bg {
263 write!(w, "{}", text.color(fg).on_color(bg))
264 } else {
265 write!(w, "{}", text.color(fg))
266 }
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn test_plain_backend() {
276 let backend = PlainBackend;
277 let mut out = String::new();
278
279 backend
280 .write_styled(&mut out, "hello", SemanticColor::Deleted)
281 .unwrap();
282 assert_eq!(out, "hello");
283
284 out.clear();
285 backend
286 .write_styled(&mut out, "world", SemanticColor::Inserted)
287 .unwrap();
288 assert_eq!(out, "world");
289 }
290
291 #[test]
292 fn test_ansi_backend() {
293 let backend = AnsiBackend::default();
294 let mut out = String::new();
295
296 backend
297 .write_styled(&mut out, "deleted", SemanticColor::Deleted)
298 .unwrap();
299 assert!(out.contains("\x1b["));
301 assert!(out.contains("deleted"));
302 }
303
304 #[test]
305 fn test_prefix() {
306 let backend = PlainBackend;
307 let mut out = String::new();
308
309 backend
310 .write_prefix(&mut out, '-', SemanticColor::Deleted)
311 .unwrap();
312 assert_eq!(out, "-");
313 }
314}