litime/
formatter.rs

1use anyhow::{bail, Result};
2use std::fmt;
3use std::io::Write;
4use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
5use textwrap::{fill, Options};
6
7use crate::minute::Minute;
8
9static INITIAL_INDENT: &str = "  \" ";
10static SUBSEQUENT_INDENT: &str = "    ";
11static FOOTER_INDENT: &str = "        ";
12
13pub static FORMATTING_HELP: &str = r"Formatting in the form of '<style> <colour>' or just '<colour>', such as 'bold red' or 'blue'.
14
15Available colours are: black, white, blue, cyan, green, magenta, red and yellow
16Available styles are: italic, bold, strikethrough, underline, intense and dimmed
17";
18
19#[derive(Debug, Clone)]
20pub enum Style {
21    Bold,
22    Dimmed,
23    Intense,
24    Italic,
25    Plain,
26    Strikethrough,
27    Underline,
28}
29
30impl Style {
31    fn name(&self) -> &str {
32        match self {
33            Self::Bold => "bold",
34            Self::Dimmed => "dimmed",
35            Self::Intense => "intense",
36            Self::Italic => "italic",
37            Self::Plain => "plain",
38            Self::Strikethrough => "strikethrough",
39            Self::Underline => "underline",
40        }
41    }
42}
43
44impl TryFrom<&str> for Style {
45    type Error = anyhow::Error;
46
47    fn try_from(value: &str) -> Result<Self> {
48        match value {
49            "bold" => Ok(Self::Bold),
50            "dimmed" => Ok(Self::Dimmed),
51            "intense" => Ok(Self::Intense),
52            "italic" => Ok(Self::Italic),
53            "plain" => Ok(Self::Plain),
54            "strikethrough" => Ok(Self::Strikethrough),
55            "underline" => Ok(Self::Underline),
56            _ => bail!("Unknown style: {}\n\n{}", value, FORMATTING_HELP),
57        }
58    }
59}
60
61#[derive(Debug, Clone)]
62pub struct Formatting {
63    pub colour: Color,
64    pub style: Style,
65}
66
67impl From<Color> for Formatting {
68    fn from(color: Color) -> Self {
69        Self {
70            style: Style::Plain,
71            colour: color,
72        }
73    }
74}
75
76impl From<Formatting> for ColorSpec {
77    fn from(formatting: Formatting) -> Self {
78        let mut spec = ColorSpec::new();
79        spec.set_fg(Some(formatting.colour));
80        match formatting.style {
81            Style::Bold => spec.set_bold(true),
82            Style::Dimmed => spec.set_dimmed(true),
83            Style::Intense => spec.set_intense(true),
84            Style::Italic => spec.set_italic(true),
85            Style::Plain => &spec,
86            Style::Strikethrough => spec.set_strikethrough(true),
87            Style::Underline => spec.set_underline(true),
88        };
89
90        spec
91    }
92}
93
94impl fmt::Display for Formatting {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        let colour_name = match self.colour {
97            Color::Black => "black",
98            Color::Blue => "blue",
99            Color::Cyan => "cyan",
100            Color::Green => "green",
101            Color::Magenta => "magenta",
102            Color::Red => "red",
103            Color::White => "white",
104            Color::Yellow => "yellow",
105            _ => panic!("Unsupported colour"),
106        };
107        match self.style {
108            Style::Plain => write!(f, "{colour_name}"),
109            _ => write!(f, "{} {}", self.style.name(), colour_name),
110        }
111    }
112}
113
114impl Minute<'_> {
115    pub fn formatted(
116        &self,
117        width: u16,
118        main: &Formatting,
119        time: &Formatting,
120        author: &Formatting,
121    ) -> Result<String> {
122        let quote = format!("{}\x00{}\x00{}", self.start, self.time, self.end);
123        let footer = format!("{} – {}", self.author, self.title);
124
125        let quote_options = Options::new(width.into())
126            .initial_indent(INITIAL_INDENT)
127            .subsequent_indent(SUBSEQUENT_INDENT);
128        let footer_options = Options::new(width.into())
129            .initial_indent(FOOTER_INDENT)
130            .subsequent_indent(FOOTER_INDENT);
131
132        let quote = fill(quote.as_str(), quote_options);
133        let footer = fill(footer.as_str(), footer_options);
134
135        // Split the quote into three sections, which will be the start, time and end
136        let parts: Vec<_> = quote.split('\x00').collect();
137
138        let main_spec: ColorSpec = main.clone().into();
139        let time_spec: ColorSpec = time.clone().into();
140        let author_spec: ColorSpec = author.clone().into();
141
142        let buffer_writer = BufferWriter::stdout(ColorChoice::Auto);
143        let mut buffer = buffer_writer.buffer();
144
145        // Initial line
146        writeln!(&mut buffer)?;
147
148        // First part of main colour
149        buffer.set_color(&main_spec)?;
150        write!(&mut buffer, "{}", parts[0])?;
151
152        // The time itself
153        buffer.set_color(&time_spec)?;
154        write!(&mut buffer, "{}", parts[1])?;
155
156        // Rest of the main colour
157        buffer.set_color(&main_spec)?;
158        write!(&mut buffer, "{}", parts[2])?;
159
160        // Two lines between quote and author
161        writeln!(&mut buffer)?;
162        writeln!(&mut buffer)?;
163
164        // Author
165        buffer.set_color(&author_spec)?;
166        write!(&mut buffer, "{footer}")?;
167
168        // End with new line
169        writeln!(&mut buffer)?;
170
171        Ok(String::from_utf8(buffer.into_inner())?)
172    }
173}
174
175#[cfg(test)]
176mod test {
177    use super::*;
178    //use pretty_assertions::assert_eq;
179
180    #[test]
181    fn wrapped_quote() {
182        let minute = Minute {
183            start: "black black black ",
184            time: "red red red red",
185            end: " black black black",
186            author: "author",
187            title: "title",
188        };
189
190        let formatted = minute
191            .formatted(
192                20,
193                &Color::Black.into(),
194                &Color::Red.into(),
195                &Color::White.into(),
196            )
197            .unwrap();
198        let expected = [
199            "\n\u{1b}[0m\u{1b}[30m  \" black black".to_string(),
200            "    black \u{1b}[0m\u{1b}[31mred red".to_string(),
201            "    red red\u{1b}[0m\u{1b}[30m black".to_string(),
202            "    black black\n".to_string(),
203            "\u{1b}[0m\u{1b}[37m        author –".to_string(),
204            "        title\n".to_string(),
205        ]
206        .join("\n");
207
208        assert_eq!(formatted, expected);
209    }
210
211    #[test]
212    fn short_quote() {
213        let minute = Minute {
214            start: "foo ",
215            time: "bar",
216            end: " baz",
217            author: "author",
218            title: "title",
219        };
220
221        let formatted = minute
222            .formatted(
223                50,
224                &Color::Black.into(),
225                &Color::Red.into(),
226                &Color::White.into(),
227            )
228            .unwrap();
229        let expected = [
230            "\n\u{1b}[0m\u{1b}[30m  \" foo \u{1b}[0m\u{1b}[31mbar\u{1b}[0m\u{1b}[30m baz"
231                .to_string(),
232            "\n\u{1b}[0m\u{1b}[37m        author – title\n".to_string(),
233        ]
234        .join("\n");
235
236        assert_eq!(formatted, expected);
237    }
238
239    #[test]
240    fn no_start() {
241        let minute = Minute {
242            start: "",
243            time: "bar",
244            end: " baz",
245            author: "author",
246            title: "title",
247        };
248
249        let formatted = minute
250            .formatted(
251                50,
252                &Color::Black.into(),
253                &Color::Red.into(),
254                &Color::White.into(),
255            )
256            .unwrap();
257        let expected = [
258            "\n\u{1b}[0m\u{1b}[30m  \" \u{1b}[0m\u{1b}[31mbar\u{1b}[0m\u{1b}[30m baz".to_string(),
259            "\n\u{1b}[0m\u{1b}[37m        author – title\n".to_string(),
260        ]
261        .join("\n");
262
263        assert_eq!(formatted, expected);
264    }
265
266    #[test]
267    fn no_end() {
268        let minute = Minute {
269            start: "foo ",
270            time: "bar",
271            end: "",
272            author: "author",
273            title: "title",
274        };
275
276        let formatted = minute
277            .formatted(
278                50,
279                &Color::Black.into(),
280                &Color::Red.into(),
281                &Color::White.into(),
282            )
283            .unwrap();
284        let expected = [
285            "\n\u{1b}[0m\u{1b}[30m  \" foo \u{1b}[0m\u{1b}[31mbar\u{1b}[0m\u{1b}[30m".to_string(),
286            "\n\u{1b}[0m\u{1b}[37m        author – title\n".to_string(),
287        ]
288        .join("\n");
289
290        assert_eq!(formatted, expected);
291    }
292
293    #[test]
294    fn issue_4() {
295        let minute = Minute {
296            start: "At 10.15 Arlena departed from her rondezvous, a minute or two later Patrick Redfern came down and registered surprise, annoyance, etc. Christine's task was easy enough. Keeping her own watch concealed she asked Linda at twenty-five past eleven what time it was. Linda looked at her watch and replied that it was a ",
297            time: "quarter to twelve",
298            end: ".",
299            author: "Agatha Christie",
300            title: "Evil under the Sun",
301        };
302
303        let formatted = minute
304            .formatted(
305                50,
306                &Color::Black.into(),
307                &Color::Red.into(),
308                &Color::White.into(),
309            )
310            .unwrap();
311        let expected = [
312            "\n\u{1b}[0m\u{1b}[30m  \" At 10.15 Arlena departed from her rondezvous,".to_string(),
313            "    a minute or two later Patrick Redfern came".to_string(),
314            "    down and registered surprise, annoyance, etc.".to_string(),
315            "    Christine's task was easy enough. Keeping her".to_string(),
316            "    own watch concealed she asked Linda at twenty-".to_string(),
317            "    five past eleven what time it was. Linda".to_string(),
318            "    looked at her watch and replied that it was a".to_string(),
319            "    \u{1b}[0m\u{1b}[31mquarter to twelve\u{1b}[0m\u{1b}[30m.".to_string(),
320            "\n\u{1b}[0m\u{1b}[37m        Agatha Christie – Evil under the Sun\n".to_string(),
321        ]
322        .join("\n");
323
324        assert_eq!(formatted, expected);
325    }
326
327    #[test]
328    fn issue_6() {
329        let minute = Minute {
330            start: "And the first stop had been at ",
331            time: "1.16pm",
332            end: " which was 17 minutes later.",
333            author: "Mark Haddon",
334            title: "The Curious Incident of the Dog in the Night-Time",
335        };
336
337        let formatted = minute
338            .formatted(
339                30,
340                &Color::Black.into(),
341                &Color::Red.into(),
342                &Color::White.into(),
343            )
344            .unwrap();
345        let expected = [
346            "\n\u{1b}[0m\u{1b}[30m  \" And the first stop had".to_string(),
347            "    been at \u{1b}[0m\u{1b}[31m1.16pm\u{1b}[0m\u{1b}[30m which was".to_string(),
348            "    17 minutes later.".to_string(),
349            "\n\u{1b}[0m\u{1b}[37m        Mark Haddon – The".to_string(),
350            "        Curious Incident of".to_string(),
351            "        the Dog in the Night-".to_string(),
352            "        Time\n".to_string(),
353        ]
354        .join("\n");
355
356        assert_eq!(formatted, expected);
357    }
358}