1use urushi::{Align, BlockStyle, Border, TextStyle, VerticalAlign, View};
4
5use crate::CliRole;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct Warning {
9 title: String,
10 message: String,
11}
12
13impl Warning {
14 pub fn new(title: impl Into<String>, message: impl Into<String>) -> Self {
16 Self {
17 title: title.into(),
18 message: message.into(),
19 }
20 }
21
22 pub fn title(&self) -> &str {
23 &self.title
24 }
25
26 pub fn message(&self) -> &str {
27 &self.message
28 }
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct WarningPresentation {
34 muted: TextStyle,
35 warning: TextStyle,
36 body: TextStyle,
37}
38
39impl WarningPresentation {
40 pub fn new(muted: TextStyle, warning: TextStyle, body: TextStyle) -> Self {
42 Self {
43 muted,
44 warning,
45 body,
46 }
47 }
48
49 pub fn compose(&self, warning: &Warning) -> View {
51 let rail = Border {
52 left: '│',
53 ..Border::HIDDEN
54 };
55 let body = View::block(
56 BlockStyle::new()
57 .border(rail)
58 .border_top(false)
59 .border_right(false)
60 .border_bottom(false)
61 .border_text_style(self.muted.clone()),
62 View::row(
63 VerticalAlign::Top,
64 [
65 View::block(
66 BlockStyle::from_text_style(self.muted.clone()).width(2),
67 View::text(" ", self.muted.clone()),
68 ),
69 View::text(warning.message.clone(), self.body.clone()),
70 ],
71 ),
72 );
73
74 View::column(
75 Align::Left,
76 [
77 View::text("│", self.muted.clone()),
78 View::row(
79 VerticalAlign::Top,
80 [
81 View::text("▲", self.warning.clone()),
82 View::text(" ", self.muted.clone()),
83 View::text(warning.title.clone(), self.warning.clone()),
84 ],
85 ),
86 body,
87 ],
88 )
89 }
90
91 pub(crate) fn set_style(&mut self, role: CliRole, style: TextStyle) {
92 match role {
93 CliRole::Muted => self.muted = style,
94 CliRole::Warning => self.warning = style,
95 CliRole::Body => self.body = style,
96 CliRole::Accent => {}
97 }
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use crate::test_support::{plain_at, style_at_width};
105 use urushi::{Available, Color, SemanticTokens, Theme, resolve};
106
107 fn cli_theme() -> crate::CliTheme {
108 crate::CliTheme::from_theme(&Theme::from_tokens(SemanticTokens {
109 text: Color::Ansi(7),
110 text_muted: Color::Ansi(8),
111 background: Color::Ansi(0),
112 surface: Color::Ansi(0),
113 accent: Color::Ansi(6),
114 accent_text: Color::Ansi(0),
115 success: Color::Ansi(2),
116 warning: Color::Ansi(3),
117 error: Color::Ansi(1),
118 border: Color::Ansi(8),
119 }))
120 }
121
122 #[test]
123 fn preserves_the_canonical_appearance() {
124 let view = cli_theme().warning(&Warning::new("Caution", "Careful"));
125
126 assert_eq!(plain_at(&view, 80), "│\n▲ Caution\n│ Careful");
127 }
128
129 #[test]
130 fn the_same_view_reflows_and_extends_its_border() {
131 let view = cli_theme().warning(&Warning::new(
132 "Caution",
133 "the quick brown fox jumps over the lazy dog",
134 ));
135
136 assert_eq!(
137 plain_at(&view, 24),
138 "│\n▲ Caution\n│ the quick brown fox\n│ jumps over the lazy\n│ dog"
139 );
140 assert_eq!(
141 plain_at(&view, 14),
142 "│\n▲ Caution\n│ the quick\n│ brown fox\n│ jumps over\n│ the lazy\n│ dog"
143 );
144 }
145
146 #[test]
147 fn border_title_and_body_keep_their_complete_roles() {
148 let muted = TextStyle::new().foreground(Color::BLUE).dim();
149 let warning = TextStyle::new().foreground(Color::YELLOW).bold();
150 let body = TextStyle::new().foreground(Color::WHITE);
151 let view = WarningPresentation::new(muted.clone(), warning.clone(), body.clone())
152 .compose(&Warning::new("Caution", "Careful"));
153 let resolved = resolve(&view, Available::columns(20)).unwrap();
154
155 assert_eq!(resolved.rows()[0][0].style(), &muted);
156 assert_eq!(resolved.rows()[1][0].style(), &warning);
157 assert_eq!(resolved.rows()[2][0].style(), &muted);
158 assert_eq!(resolved.rows()[2][1].style(), &muted);
159 assert_eq!(resolved.rows()[2][2].style(), &muted);
160 assert_eq!(style_at_width(&view, 20, 2, 3), body);
161 }
162}