1mod json;
12mod unified;
13
14use self::json::Json;
15use crate::diff::RichHunks;
16use anyhow::anyhow;
17use console::{Color, Style, Term};
18use enum_dispatch::enum_dispatch;
19use serde::{Deserialize, Serialize};
20use std::io::Write;
21use strum::{self, Display, EnumIter, EnumString, IntoEnumIterator};
22use unified::Unified;
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
26pub struct DocumentDiffData<'a> {
27 pub filename: &'a str,
29 pub text: &'a str,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
35pub struct DisplayData<'a> {
36 pub hunks: RichHunks<'a>,
38 pub old: DocumentDiffData<'a>,
40 pub new: DocumentDiffData<'a>,
42}
43
44#[enum_dispatch]
45#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize, Display, EnumIter, EnumString)]
46#[strum(serialize_all = "snake_case")]
47#[serde(tag = "type", rename_all = "snake_case")]
48pub enum Renderers {
49 Unified,
50 Json,
51}
52
53impl Default for Renderers {
54 fn default() -> Self {
55 Renderers::Unified(Unified::default())
56 }
57}
58
59#[enum_dispatch(Renderers)]
61pub trait Renderer {
62 fn render(
72 &self,
73 writer: &mut dyn Write,
74 data: &DisplayData,
75 term_info: Option<&Term>,
76 ) -> anyhow::Result<()>;
77}
78
79#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
82#[serde(remote = "Color", rename_all = "snake_case")]
83#[derive(Default)]
84enum ColorDef {
85 Color256(u8),
86 #[default]
87 Black,
88 Red,
89 Green,
90 Yellow,
91 Blue,
92 Magenta,
93 Cyan,
94 White,
95}
96
97impl From<ColorDef> for Color {
98 fn from(c: ColorDef) -> Self {
99 match c {
100 ColorDef::Black => Color::Black,
101 ColorDef::White => Color::White,
102 ColorDef::Red => Color::Red,
103 ColorDef::Green => Color::Green,
104 ColorDef::Yellow => Color::Yellow,
105 ColorDef::Blue => Color::Blue,
106 ColorDef::Magenta => Color::Magenta,
107 ColorDef::Cyan => Color::Cyan,
108 ColorDef::Color256(c) => Color::Color256(c),
109 }
110 }
111}
112
113mod opt_color_def {
115 use super::{Color, ColorDef};
116 use serde::{Deserialize, Deserializer, Serialize, Serializer};
117
118 #[allow(clippy::trivially_copy_pass_by_ref)]
119 pub fn serialize<S>(value: &Option<Color>, serializer: S) -> Result<S::Ok, S::Error>
120 where
121 S: Serializer,
122 {
123 #[derive(Serialize)]
124 struct Helper<'a>(#[serde(with = "ColorDef")] &'a Color);
125
126 value.as_ref().map(Helper).serialize(serializer)
127 }
128
129 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Color>, D::Error>
130 where
131 D: Deserializer<'de>,
132 {
133 #[derive(Deserialize)]
134 struct Helper(#[serde(with = "ColorDef")] Color);
135
136 let helper = Option::deserialize(deserializer)?;
137 Ok(helper.map(|Helper(external)| external))
138 }
139}
140
141fn default_option<T>() -> Option<T> {
146 None
147}
148
149#[derive(Clone, Debug, PartialEq, Eq)]
151struct RegularStyle(Style);
152
153#[derive(Clone, Debug, PartialEq, Eq)]
155struct EmphasizedStyle(Style);
156
157#[derive(Debug, PartialEq, EnumString, Serialize, Deserialize, Eq)]
161#[strum(serialize_all = "snake_case")]
162#[derive(Default)]
163pub enum Emphasis {
164 None,
169 #[default]
171 Bold,
172 Underline,
174 Highlight(HighlightColors),
176}
177
178#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
180pub struct HighlightColors {
181 #[serde(with = "ColorDef")]
183 pub addition: Color,
184 #[serde(with = "ColorDef")]
186 pub deletion: Color,
187}
188
189impl Default for HighlightColors {
190 fn default() -> Self {
191 HighlightColors {
192 addition: Color::Color256(0),
193 deletion: Color::Color256(0),
194 }
195 }
196}
197
198#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
203#[serde(rename_all = "snake_case", default)]
204pub struct RenderConfig {
205 default: String,
209
210 unified: unified::Unified,
211 json: json::Json,
212}
213
214impl Default for RenderConfig {
215 fn default() -> Self {
216 let default_renderer = Renderers::default();
217 RenderConfig {
218 default: default_renderer.to_string(),
219 unified: Unified::default(),
220 json: Json::default(),
221 }
222 }
223}
224
225impl RenderConfig {
226 pub fn get_renderer(self, tag: Option<String>) -> anyhow::Result<Renderers> {
231 if let Some(t) = tag {
232 let cand_enum = Renderers::iter().find(|e| e.to_string() == t);
233 match cand_enum {
234 None => Err(anyhow!("'{}' is not a valid renderer", &t)),
235 Some(renderer) => Ok(renderer),
236 }
237 } else {
238 Ok(Renderers::default())
239 }
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use test_case::test_case;
247
248 #[test_case("unified")]
249 #[test_case("json")]
250 fn test_get_renderer_custom_tag(tag: &str) {
251 let cfg = RenderConfig::default();
252 let res = cfg.get_renderer(Some(tag.into()));
253 assert!(res.is_ok());
254 }
255
256 #[test]
257 fn test_render_config_default_tag() {
258 let cfg = RenderConfig::default();
259 let res = cfg.get_renderer(None);
260 assert_eq!(res.unwrap(), Renderers::default());
261 }
262}