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 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 writeln!(&mut buffer)?;
147
148 buffer.set_color(&main_spec)?;
150 write!(&mut buffer, "{}", parts[0])?;
151
152 buffer.set_color(&time_spec)?;
154 write!(&mut buffer, "{}", parts[1])?;
155
156 buffer.set_color(&main_spec)?;
158 write!(&mut buffer, "{}", parts[2])?;
159
160 writeln!(&mut buffer)?;
162 writeln!(&mut buffer)?;
163
164 buffer.set_color(&author_spec)?;
166 write!(&mut buffer, "{footer}")?;
167
168 writeln!(&mut buffer)?;
170
171 Ok(String::from_utf8(buffer.into_inner())?)
172 }
173}
174
175#[cfg(test)]
176mod test {
177 use super::*;
178 #[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}