1mod render;
4#[cfg(test)]
5mod sink;
6
7use super::text::visible_inline_text;
8use crate::config::ResolvedConfig;
9use crate::ui::section_chrome::RuledSectionPolicy;
10use crate::ui::section_chrome::SectionFrameStyle;
11use crate::ui::settings::{RenderProfile, RenderSettings, resolve_settings};
12use crate::ui::style::{StyleOverrides, StyleToken, ThemeStyler};
13use crate::ui::theme::ThemeDefinition;
14
15pub(crate) use render::render_messages_with_styler_and_chrome;
16pub(crate) use render::{
17 MessageChrome, MessageFrameStyle, MessageRenderOptions, MessageRulePolicy,
18 render_messages_with_styler_from_config,
19};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
23pub enum MessageLevel {
24 Error,
25 Warning,
26 Success,
27 Info,
28 Trace,
29}
30
31impl MessageLevel {
32 pub fn parse(value: &str) -> Option<Self> {
33 match value.trim().to_ascii_lowercase().as_str() {
34 "error" => Some(MessageLevel::Error),
35 "warning" | "warn" => Some(MessageLevel::Warning),
36 "success" => Some(MessageLevel::Success),
37 "info" => Some(MessageLevel::Info),
38 "trace" => Some(MessageLevel::Trace),
39 _ => None,
40 }
41 }
42
43 pub fn title(self) -> &'static str {
44 match self {
45 MessageLevel::Error => "Errors",
46 MessageLevel::Warning => "Warnings",
47 MessageLevel::Success => "Success",
48 MessageLevel::Info => "Info",
49 MessageLevel::Trace => "Trace",
50 }
51 }
52
53 fn as_rank(self) -> i8 {
54 match self {
55 MessageLevel::Error => 0,
56 MessageLevel::Warning => 1,
57 MessageLevel::Success => 2,
58 MessageLevel::Info => 3,
59 MessageLevel::Trace => 4,
60 }
61 }
62
63 pub fn as_env_str(self) -> &'static str {
64 match self {
65 MessageLevel::Error => "error",
66 MessageLevel::Warning => "warning",
67 MessageLevel::Success => "success",
68 MessageLevel::Info => "info",
69 MessageLevel::Trace => "trace",
70 }
71 }
72
73 fn from_rank(rank: i8) -> Self {
74 match rank {
75 i8::MIN..=0 => MessageLevel::Error,
76 1 => MessageLevel::Warning,
77 2 => MessageLevel::Success,
78 3 => MessageLevel::Info,
79 _ => MessageLevel::Trace,
80 }
81 }
82
83 pub(crate) fn ordered() -> impl Iterator<Item = Self> {
84 [
85 MessageLevel::Error,
86 MessageLevel::Warning,
87 MessageLevel::Success,
88 MessageLevel::Info,
89 MessageLevel::Trace,
90 ]
91 .into_iter()
92 }
93
94 pub(crate) fn style_token(self) -> StyleToken {
95 match self {
96 MessageLevel::Error => StyleToken::Error,
97 MessageLevel::Warning => StyleToken::Warning,
98 MessageLevel::Success => StyleToken::Success,
99 MessageLevel::Info => StyleToken::Info,
100 MessageLevel::Trace => StyleToken::Trace,
101 }
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum MessageLayout {
108 Minimal,
109 Plain,
110 Compact,
111 Grouped,
112}
113
114impl MessageLayout {
115 pub fn parse(value: &str) -> Option<Self> {
116 match value.trim().to_ascii_lowercase().as_str() {
117 "minimal" | "austere" => Some(Self::Minimal),
118 "plain" | "none" => Some(Self::Plain),
119 "compact" => Some(Self::Compact),
120 "grouped" | "full" => Some(Self::Grouped),
121 _ => None,
122 }
123 }
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct UiMessage {
129 pub level: MessageLevel,
130 pub text: String,
131}
132
133#[derive(Debug, Clone, Default, PartialEq, Eq)]
135pub struct MessageBuffer {
136 entries: Vec<UiMessage>,
137}
138
139#[derive(Debug, Clone)]
141pub struct GroupedRenderOptions<'a> {
142 pub max_level: MessageLevel,
143 pub color: bool,
144 pub unicode: bool,
145 pub width: Option<usize>,
146 pub theme: &'a ThemeDefinition,
147 pub layout: MessageLayout,
148 pub chrome_frame: SectionFrameStyle,
149 pub style_overrides: StyleOverrides,
150}
151
152impl MessageBuffer {
153 pub fn new() -> Self {
154 Self::default()
155 }
156
157 pub fn push<T: Into<String>>(&mut self, level: MessageLevel, text: T) {
158 self.push_message(UiMessage::new(level, text));
159 }
160
161 pub(crate) fn push_message(&mut self, message: UiMessage) {
162 self.entries.push(message);
163 }
164
165 pub fn error<T: Into<String>>(&mut self, text: T) {
166 self.push(MessageLevel::Error, text);
167 }
168
169 pub fn warning<T: Into<String>>(&mut self, text: T) {
170 self.push(MessageLevel::Warning, text);
171 }
172
173 pub fn success<T: Into<String>>(&mut self, text: T) {
174 self.push(MessageLevel::Success, text);
175 }
176
177 pub fn info<T: Into<String>>(&mut self, text: T) {
178 self.push(MessageLevel::Info, text);
179 }
180
181 pub fn trace<T: Into<String>>(&mut self, text: T) {
182 self.push(MessageLevel::Trace, text);
183 }
184
185 pub fn is_empty(&self) -> bool {
186 self.entries.is_empty()
187 }
188
189 pub fn entries(&self) -> &[UiMessage] {
190 &self.entries
191 }
192
193 pub(crate) fn entries_for_level(
194 &self,
195 level: MessageLevel,
196 ) -> impl Iterator<Item = &UiMessage> {
197 self.entries
198 .iter()
199 .filter(move |entry| entry.level == level)
200 }
201
202 pub fn render_grouped(&self, max_level: MessageLevel) -> String {
203 let theme = crate::ui::theme::resolve_theme("plain");
204 self.render_grouped_styled(
205 max_level,
206 false,
207 false,
208 None,
209 &theme,
210 MessageLayout::Grouped,
211 )
212 }
213
214 pub fn render_grouped_styled(
215 &self,
216 max_level: MessageLevel,
217 color: bool,
218 unicode: bool,
219 width: Option<usize>,
220 theme: &ThemeDefinition,
221 layout: MessageLayout,
222 ) -> String {
223 self.render_grouped_with_options(GroupedRenderOptions {
224 max_level,
225 color,
226 unicode,
227 width,
228 theme,
229 layout,
230 chrome_frame: default_message_chrome_frame(layout),
231 style_overrides: StyleOverrides::default(),
232 })
233 }
234
235 pub fn render_grouped_with_options(&self, options: GroupedRenderOptions<'_>) -> String {
236 if self.is_empty() {
237 return String::new();
238 }
239
240 let styler = ThemeStyler::new(options.color, options.theme, &options.style_overrides);
241 render::render_messages_with_styler_and_chrome(
242 self,
243 render::MessageRenderOptions {
244 max_level: options.max_level,
245 layout: options.layout,
246 },
247 &styler,
248 render::MessageChrome {
249 frame_style: message_frame_style(options.chrome_frame),
250 ruled_policy: render::MessageRulePolicy::PerSection,
251 unicode: options.unicode,
252 width: options.width,
253 },
254 )
255 }
256}
257
258pub(crate) fn message_layout_from_config(config: &ResolvedConfig) -> MessageLayout {
259 config
260 .get_string("ui.messages.layout")
261 .and_then(MessageLayout::parse)
262 .unwrap_or(MessageLayout::Grouped)
263}
264
265pub(crate) fn render_messages_from_settings(
266 config: &ResolvedConfig,
267 settings: &RenderSettings,
268 messages: &MessageBuffer,
269 verbosity: MessageLevel,
270) -> String {
271 let resolved = resolve_settings(settings, RenderProfile::Normal);
272 let styler = ThemeStyler::new(resolved.color, &resolved.theme, &resolved.style_overrides);
273 render_messages_with_styler_from_config(
274 messages,
275 config,
276 verbosity,
277 &styler,
278 MessageChrome {
279 frame_style: match settings.chrome_frame {
280 SectionFrameStyle::None => MessageFrameStyle::None,
281 SectionFrameStyle::Top => MessageFrameStyle::Top,
282 SectionFrameStyle::Bottom => MessageFrameStyle::Bottom,
283 SectionFrameStyle::TopBottom => MessageFrameStyle::TopBottom,
284 SectionFrameStyle::Square => MessageFrameStyle::Square,
285 SectionFrameStyle::Round => MessageFrameStyle::Round,
286 },
287 ruled_policy: match settings.ruled_section_policy {
288 RuledSectionPolicy::PerSection => MessageRulePolicy::PerSection,
289 RuledSectionPolicy::Shared => MessageRulePolicy::Shared,
290 },
291 unicode: resolved.unicode,
292 width: resolved.width,
293 },
294 )
295}
296
297pub(crate) fn render_messages_without_config(
298 settings: &RenderSettings,
299 messages: &MessageBuffer,
300 verbosity: MessageLevel,
301) -> String {
302 let resolved = resolve_settings(settings, RenderProfile::Normal);
303 let styler = ThemeStyler::new(resolved.color, &resolved.theme, &resolved.style_overrides);
304 render_messages_with_styler_and_chrome(
305 &visible_message_buffer(messages),
306 MessageRenderOptions::full(verbosity),
307 &styler,
308 MessageChrome {
309 frame_style: MessageFrameStyle::TopBottom,
310 ruled_policy: MessageRulePolicy::Shared,
311 unicode: resolved.unicode,
312 width: resolved.width.or(Some(12)),
313 },
314 )
315}
316
317fn visible_message_buffer(messages: &MessageBuffer) -> MessageBuffer {
318 let mut out = MessageBuffer::default();
319 for entry in messages.entries() {
320 out.push(entry.level, visible_inline_text(&entry.text));
321 }
322 out
323}
324
325fn default_message_chrome_frame(layout: MessageLayout) -> SectionFrameStyle {
326 match layout {
327 MessageLayout::Minimal | MessageLayout::Plain | MessageLayout::Compact => {
328 SectionFrameStyle::None
329 }
330 MessageLayout::Grouped => SectionFrameStyle::TopBottom,
331 }
332}
333
334fn message_frame_style(frame: SectionFrameStyle) -> render::MessageFrameStyle {
335 match frame {
336 SectionFrameStyle::None => render::MessageFrameStyle::None,
337 SectionFrameStyle::Top => render::MessageFrameStyle::Top,
338 SectionFrameStyle::Bottom => render::MessageFrameStyle::Bottom,
339 SectionFrameStyle::TopBottom => render::MessageFrameStyle::TopBottom,
340 SectionFrameStyle::Square => render::MessageFrameStyle::Square,
341 SectionFrameStyle::Round => render::MessageFrameStyle::Round,
342 }
343}
344
345pub fn adjust_verbosity(base: MessageLevel, verbose: u8, quiet: u8) -> MessageLevel {
346 let rank = base.as_rank() + verbose as i8 - quiet as i8;
347 MessageLevel::from_rank(rank)
348}
349
350impl UiMessage {
351 pub(crate) fn new(level: MessageLevel, text: impl Into<String>) -> Self {
352 Self {
353 level,
354 text: text.into(),
355 }
356 }
357}