Skip to main content

gpg_tui/app/
style.rs

1use clap::ValueEnum;
2use ratatui::style::{Color, Style as TuiStyle};
3use ratatui::text::{Line, Span, Text};
4use std::fmt::{Display, Formatter, Result as FmtResult};
5
6/// Application style.
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
8pub enum Style {
9	/// Plain style with basic colors.
10	#[default]
11	Plain,
12	/// More rich style with highlighted widgets and more colors.
13	Colored,
14}
15
16impl Display for Style {
17	fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
18		write!(f, "{}", format!("{self:?}").to_lowercase())
19	}
20}
21
22impl Style {
23	/// Returns `true` if the style is [`Colored`].
24	///
25	/// [`Colored`]: Self::Colored
26	pub fn is_colored(&self) -> bool {
27		self == &Self::Colored
28	}
29
30	/// Returns the next style.
31	pub fn next(&self) -> Self {
32		match self {
33			Self::Plain => Self::Colored,
34			_ => Self::Plain,
35		}
36	}
37}
38
39/// Converts the given multi-line row value to colored [`Text`] widget.
40///
41/// It adds colors to:
42/// * flags in bracket characters. (e.g. `[?]`)
43/// * parts separated by slash character. (e.g. `rsa2048/abc123`)
44/// * values in arrow characters (e.g. `<test@example.com>`)
45pub fn get_colored_table_row<'a>(
46	row_data: &[String],
47	highlighted: bool,
48) -> Text<'a> {
49	let highlight_style = if highlighted {
50		TuiStyle::default().fg(Color::Reset)
51	} else {
52		TuiStyle::default()
53	};
54	let mut row = Vec::new();
55	for line in row_data.iter() {
56		let (first_bracket, second_bracket) = (
57			line.find('[').unwrap_or_default(),
58			line.find(']').unwrap_or_default(),
59		);
60		row.push(
61			// Colorize inside the brackets to start.
62			if second_bracket > first_bracket + 1 {
63				let data = line[first_bracket + 1..second_bracket].to_string();
64				let mut colored_line = vec![Span::styled(
65					line[..first_bracket + 1].to_string(),
66					highlight_style,
67				)];
68				if [
69					// expired
70					String::from("exp"),
71					// revoked
72					String::from("rev"),
73					// disabled
74					String::from("d"),
75					// invalid
76					String::from("i"),
77				]
78				.contains(&data)
79				{
80					colored_line.push(Span::styled(
81						data,
82						TuiStyle::default().fg(Color::Red),
83					))
84				} else if data.len() == 2 {
85					let style = match data.as_ref() {
86						// 0x10: no indication
87						"10" => TuiStyle::default().fg(Color::Yellow),
88						// 0x11: personal belief but no verification
89						"11" => TuiStyle::default().fg(Color::Magenta),
90						// 0x12: casual verification
91						"12" => TuiStyle::default().fg(Color::Blue),
92						// 0x13: extensive verification
93						"13" => TuiStyle::default().fg(Color::Green),
94						_ => TuiStyle::default().fg(Color::Red),
95					};
96					colored_line.push(Span::styled(data, style))
97				} else {
98					for c in data.chars().map(String::from) {
99						let style = match c.as_ref() {
100							// GPGME_VALIDITY_UNKNOWN | GPGME_VALIDITY_UNDEFINED | 0
101							"?" | "q" | "-" => {
102								TuiStyle::default().fg(Color::DarkGray)
103							}
104							// GPGME_VALIDITY_NEVER
105							"n" => TuiStyle::default().fg(Color::Red),
106							// GPGME_VALIDITY_MARGINAL
107							"m" => TuiStyle::default().fg(Color::Blue),
108							// GPGME_VALIDITY_FULL
109							"f" => TuiStyle::default().fg(Color::Magenta),
110							// GPGME_VALIDITY_ULTIMATE | GPGME_SIG_NOTATION_HUMAN_READABLE
111							"u" | "h" => TuiStyle::default().fg(Color::Green),
112							// can_sign
113							"s" => TuiStyle::default().fg(Color::LightGreen),
114							// can_certify
115							"c" => TuiStyle::default().fg(Color::LightBlue),
116							// can_encrypt
117							"e" => TuiStyle::default().fg(Color::Yellow),
118							// can_authenticate | GPGME_SIG_NOTATION_CRITICAL
119							"a" | "!" => {
120								TuiStyle::default().fg(Color::LightRed)
121							}
122							_ => TuiStyle::default(),
123						};
124						colored_line.push(Span::styled(c, style))
125					}
126				}
127				let data = line[second_bracket..].to_string();
128				// Colorize the separate parts using slash character.
129				if data.find('/') == Some(9) {
130					colored_line.push(Span::styled(
131						data.chars().next().unwrap_or_default().to_string(),
132						highlight_style,
133					));
134					colored_line.push(Span::styled(
135						data[1..9].to_string(),
136						TuiStyle::default().fg(Color::Cyan),
137					));
138					colored_line.push(Span::styled(
139						"/",
140						TuiStyle::default().fg(Color::DarkGray),
141					));
142					colored_line.push(Span::styled(
143						data[10..].to_string(),
144						highlight_style,
145					));
146				// Colorize inside the arrows.
147				} else if let (Some(first_arrow), Some(second_arrow)) =
148					(data.rfind('<'), data.rfind('>'))
149				{
150					colored_line.push(Span::styled(
151						data[..first_arrow].to_string(),
152						highlight_style,
153					));
154					colored_line.push(Span::styled(
155						"<",
156						TuiStyle::default().fg(Color::DarkGray),
157					));
158					colored_line.push(Span::styled(
159						data[first_arrow + 1..second_arrow].to_string(),
160						TuiStyle::default().fg(Color::Cyan),
161					));
162					colored_line.push(Span::styled(
163						">",
164						TuiStyle::default().fg(Color::DarkGray),
165					));
166					colored_line.push(Span::styled(
167						data[second_arrow + 1..].to_string(),
168						highlight_style,
169					));
170				// Use the rest of the data as raw.
171				} else {
172					colored_line.push(Span::styled(data, highlight_style));
173				}
174				Line::from(colored_line)
175			// Use the unfit data as is.
176			} else {
177				Line::from(vec![Span::styled(
178					line.to_string(),
179					highlight_style,
180				)])
181			},
182		)
183	}
184	Text::from(row)
185}
186
187/// Converts the given information text to colored [`Text`] widget.
188///
189/// It adds colors to:
190/// * parts separated by ':' character. (e.g. `version: 2`)
191///
192/// Skips the lines that starts with ' '.
193pub fn get_colored_info(info: &str, color: Color) -> Text<'_> {
194	Text::from(
195		info.lines()
196			.map(|v| {
197				let mut values = v.split(':').collect::<Vec<&str>>();
198				Line::from(if values.len() >= 2 && !v.starts_with(' ') {
199					vec![
200						Span::styled(
201							values[0],
202							TuiStyle::default().fg(Color::Reset),
203						),
204						Span::styled(
205							":",
206							TuiStyle::default().fg(Color::DarkGray),
207						),
208						Span::styled(
209							values.drain(1..).collect::<Vec<&str>>().join(":"),
210							TuiStyle::default().fg(color),
211						),
212					]
213				} else {
214					vec![Span::styled(v, TuiStyle::default().fg(Color::Reset))]
215				})
216			})
217			.collect::<Vec<Line>>(),
218	)
219}
220
221#[cfg(test)]
222mod tests {
223	use super::*;
224	use pretty_assertions::assert_eq;
225	use std::borrow::Cow::Borrowed;
226	#[test]
227	fn test_app_style() {
228		let row_data = r#"
229[sc--] rsa2048/C4B2D24CF87CD188C79D00BB485B7C52E9EC0DC6
230       └─(2020-07-29)
231		"#
232		.to_string()
233		.lines()
234		.map(String::from)
235		.collect::<Vec<String>>();
236		assert_eq!(
237			Text {
238				lines: vec![
239					Line {
240						spans: vec![Span {
241							content: Borrowed(""),
242							style: TuiStyle::default(),
243						}],
244						..Default::default()
245					},
246					Line {
247						spans: vec![
248							Span {
249								content: Borrowed("["),
250								style: TuiStyle::default(),
251							},
252							Span {
253								content: Borrowed("s"),
254								style: TuiStyle {
255									fg: Some(Color::LightGreen),
256									..TuiStyle::default()
257								},
258							},
259							Span {
260								content: Borrowed("c"),
261								style: TuiStyle {
262									fg: Some(Color::LightBlue),
263									..TuiStyle::default()
264								},
265							},
266							Span {
267								content: Borrowed("-"),
268								style: TuiStyle {
269									fg: Some(Color::DarkGray),
270									..TuiStyle::default()
271								},
272							},
273							Span {
274								content: Borrowed("-"),
275								style: TuiStyle {
276									fg: Some(Color::DarkGray),
277									..TuiStyle::default()
278								},
279							},
280							Span {
281								content: Borrowed("]"),
282								style: TuiStyle::default(),
283							},
284							Span {
285								content: Borrowed(" rsa2048"),
286								style: TuiStyle {
287									fg: Some(Color::Cyan),
288									..TuiStyle::default()
289								},
290							},
291							Span {
292								content: Borrowed("/"),
293								style: TuiStyle {
294									fg: Some(Color::DarkGray),
295									..TuiStyle::default()
296								},
297							},
298							Span {
299								content: Borrowed(
300									"C4B2D24CF87CD188C79D00BB485B7C52E9EC0DC6"
301								),
302								style: TuiStyle::default(),
303							},
304						],
305						..Default::default()
306					},
307					Line {
308						spans: vec![Span {
309							content: Borrowed("       └─(2020-07-29)"),
310							style: TuiStyle::default(),
311						}],
312						..Default::default()
313					},
314					Line {
315						spans: vec![Span {
316							content: Borrowed("\t\t"),
317							style: TuiStyle::default(),
318						}],
319						..Default::default()
320					},
321				],
322				..Default::default()
323			},
324			get_colored_table_row(&row_data, false)
325		);
326		let row_data = r#"
327[u] kmon releases <kmonlinux@protonmail.com>
328	├─[13] selfsig (2020-07-29)
329	├─][ test
330	└─[10] B928720AEC532117 orhun <orhunparmaksiz@gmail.com> (2020-07-29)
331				"#
332		.to_string()
333		.lines()
334		.map(String::from)
335		.collect::<Vec<String>>();
336		assert_eq!(
337			Text {
338				lines: vec![
339					Line {
340						spans: vec![Span {
341							content: Borrowed(""),
342							style: TuiStyle::default(),
343						}],
344						..Default::default()
345					},
346					Line {
347						spans: vec![
348							Span {
349								content: Borrowed("["),
350								style: TuiStyle::default(),
351							},
352							Span {
353								content: Borrowed("u"),
354								style: TuiStyle {
355									fg: Some(Color::Green),
356									..TuiStyle::default()
357								},
358							},
359							Span {
360								content: Borrowed("] kmon releases "),
361								style: TuiStyle::default(),
362							},
363							Span {
364								content: Borrowed("<"),
365								style: TuiStyle {
366									fg: Some(Color::DarkGray),
367									..TuiStyle::default()
368								},
369							},
370							Span {
371								content: Borrowed("kmonlinux@protonmail.com"),
372								style: TuiStyle {
373									fg: Some(Color::Cyan),
374									..TuiStyle::default()
375								},
376							},
377							Span {
378								content: Borrowed(">"),
379								style: TuiStyle {
380									fg: Some(Color::DarkGray),
381									..TuiStyle::default()
382								},
383							},
384							Span {
385								content: Borrowed(""),
386								style: TuiStyle::default(),
387							},
388						],
389						..Default::default()
390					},
391					Line {
392						spans: vec![
393							Span {
394								content: Borrowed("\t├─["),
395								style: TuiStyle::default(),
396							},
397							Span {
398								content: Borrowed("13"),
399								style: TuiStyle {
400									fg: Some(Color::Green),
401									..TuiStyle::default()
402								},
403							},
404							Span {
405								content: Borrowed("] selfsig (2020-07-29)"),
406								style: TuiStyle::default(),
407							},
408						],
409						..Default::default()
410					},
411					Line {
412						spans: vec![Span {
413							content: Borrowed("\t├─][ test"),
414							style: TuiStyle::default(),
415						}],
416						..Default::default()
417					},
418					Line {
419						spans: vec![
420							Span {
421								content: Borrowed("\t└─["),
422								style: TuiStyle::default(),
423							},
424							Span {
425								content: Borrowed("10"),
426								style: TuiStyle {
427									fg: Some(Color::Yellow),
428									..TuiStyle::default()
429								},
430							},
431							Span {
432								content: Borrowed("] B928720AEC532117 orhun "),
433								style: TuiStyle::default(),
434							},
435							Span {
436								content: Borrowed("<"),
437								style: TuiStyle {
438									fg: Some(Color::DarkGray),
439									..TuiStyle::default()
440								},
441							},
442							Span {
443								content: Borrowed("orhunparmaksiz@gmail.com"),
444								style: TuiStyle {
445									fg: Some(Color::Cyan),
446									..TuiStyle::default()
447								},
448							},
449							Span {
450								content: Borrowed(">"),
451								style: TuiStyle {
452									fg: Some(Color::DarkGray),
453									..TuiStyle::default()
454								},
455							},
456							Span {
457								content: Borrowed(" (2020-07-29)"),
458								style: TuiStyle::default(),
459							},
460						],
461						..Default::default()
462					},
463					Line {
464						spans: vec![Span {
465							content: Borrowed("\t\t\t\t"),
466							style: TuiStyle::default(),
467						}],
468						..Default::default()
469					},
470				],
471				..Default::default()
472			},
473			get_colored_table_row(&row_data, false)
474		);
475		assert_eq!(
476			Text {
477				lines: vec![
478					Line {
479						spans: vec![
480							Span {
481								content: Borrowed("test"),
482								style: TuiStyle {
483									fg: Some(Color::Reset),
484									..TuiStyle::default()
485								},
486							},
487							Span {
488								content: Borrowed(":"),
489								style: TuiStyle {
490									fg: Some(Color::DarkGray),
491									..TuiStyle::default()
492								},
493							},
494							Span {
495								content: Borrowed(" xyz "),
496								style: TuiStyle {
497									fg: Some(Color::LightRed),
498									..TuiStyle::default()
499								},
500							},
501						],
502						..Default::default()
503					},
504					Line {
505						spans: vec![
506							Span {
507								content: Borrowed("test2"),
508								style: TuiStyle {
509									fg: Some(Color::Reset),
510									..TuiStyle::default()
511								},
512							},
513							Span {
514								content: Borrowed(":"),
515								style: TuiStyle {
516									fg: Some(Color::DarkGray),
517									..TuiStyle::default()
518								},
519							},
520							Span {
521								content: Borrowed(" abc"),
522								style: TuiStyle {
523									fg: Some(Color::LightRed),
524									..TuiStyle::default()
525								},
526							},
527						],
528						..Default::default()
529					},
530					Line {
531						spans: vec![Span {
532							content: Borrowed(" skip this line"),
533							style: TuiStyle {
534								fg: Some(Color::Reset),
535								..TuiStyle::default()
536							},
537						}],
538						..Default::default()
539					},
540					Line {
541						spans: vec![Span {
542							content: Borrowed("reset"),
543							style: TuiStyle {
544								fg: Some(Color::Reset),
545								..TuiStyle::default()
546							},
547						}],
548						..Default::default()
549					},
550				],
551				..Default::default()
552			},
553			get_colored_info(
554				"test: xyz \n\
555				test2: abc\n \
556				skip this line\n\
557				reset",
558				Color::LightRed
559			)
560		)
561	}
562}