rm_lisa/display/renderers/color/
mod.rs1use crate::{
4 display::{
5 renderers::{
6 ConsoleOutputFeatures, ConsoleRenderer,
7 color::{
8 fields::{create_combined_message, create_field_tailers},
9 helpers::{
10 EMPTY_HEADER, calculate_message_width, calculate_tailer_width,
11 chunk_string_into_width, create_header, erase_line, move_cursor, pad_to_width,
12 },
13 terminal::TerminalState,
14 },
15 },
16 tracing::{FlattenedTracingField, SuperConsoleLogMessage},
17 },
18 errors::LisaError,
19 input::{InputProvider, TerminalInputEvent},
20 tasks::{GloballyUniqueTaskId, LisaTaskStatus, TaskEvent},
21};
22use chrono::{DateTime, Utc};
23use fnv::FnvHashMap;
24use owo_colors::OwoColorize;
25use parking_lot::Mutex;
26use std::{
27 env::var as env_var,
28 fmt::Write,
29 hash::BuildHasherDefault,
30 sync::atomic::{AtomicBool, Ordering},
31};
32use tracing::Level;
33
34mod fields;
35mod helpers;
36mod terminal;
37
38#[derive(Debug)]
41pub struct ColorConsoleRenderer {
42 force_pause: AtomicBool,
44 new_ps1: Mutex<Option<String>>,
46 state: Mutex<TerminalState>,
48 task_lines_rendered: Mutex<u16>,
50}
51
52impl ColorConsoleRenderer {
53 #[must_use]
55 pub fn new() -> Self {
56 Self {
57 force_pause: AtomicBool::new(false),
58 new_ps1: Mutex::new(None),
59 state: Mutex::new(TerminalState::new(Self::default_ps1_impl())),
60 task_lines_rendered: Mutex::new(0),
61 }
62 }
63
64 #[must_use]
68 fn default_ps1_impl() -> String {
69 "$ ".to_owned()
70 }
71}
72
73impl Default for ColorConsoleRenderer {
74 fn default() -> Self {
75 Self::new()
76 }
77}
78
79impl ConsoleRenderer for ColorConsoleRenderer {
80 fn should_use_renderer(
81 &self,
82 stream_features: &dyn ConsoleOutputFeatures,
83 environment_prefix: &str,
84 ) -> bool {
85 if let Ok(explicit_renderer) = env_var(format!("{environment_prefix}_LOG_FORMAT")) {
87 return explicit_renderer.trim().eq_ignore_ascii_case("color");
88 }
89
90 for no_color_var in ["NO_COLOR", "NOCOLOR"] {
92 if env_var(no_color_var).as_deref() == Ok("1") {
93 return false;
94 }
95 }
96 for color_var in ["CLICOLOR", "CLI_COLOR", "CLICOLOR_FORCE"] {
97 let env = env_var(color_var);
98 if env.as_deref() == Ok("0") {
99 return false;
100 }
101 if env.as_deref() == Ok("1") {
102 return true;
103 }
104 }
105
106 if !stream_features.is_atty() {
107 return false;
108 }
109 if !stream_features.enable_ansi() {
110 return false;
111 }
112
113 true
114 }
115
116 fn render_message(
117 &self,
118 app_name: &'static str,
119 log: SuperConsoleLogMessage,
120 term_width: u16,
121 ) -> Result<String, LisaError> {
122 let mut data = String::new();
123
124 let header = create_header(app_name, &log)?;
125 let tailer_width = calculate_tailer_width(term_width);
126 let msg_width = calculate_message_width(term_width);
127
128 let (real_msg, skip_cause) =
129 if *log.level() == Level::ERROR && log.metadata().contains_key("cause") {
130 let mut new_message = log
131 .message()
132 .map_or("<no message>".to_owned(), ToOwned::to_owned);
133 new_message.push('\n');
134 write!(
135 &mut new_message,
136 "{}",
137 log.metadata()
138 .get("cause")
139 .unwrap_or_else(|| unreachable!())
140 )?;
141 (Some(new_message), true)
142 } else {
143 (log.message().map(ToOwned::to_owned).clone(), false)
144 };
145
146 let excessive_field_count = if skip_cause { 4 } else { 3 };
150 let actual_field_count: usize = log
151 .metadata()
152 .values()
153 .map(FlattenedTracingField::field_count)
154 .sum();
155 if actual_field_count > excessive_field_count || log.force_combine() {
156 let messages = create_combined_message(
157 msg_width + tailer_width,
158 log.metadata(),
159 real_msg.unwrap_or("<no message>".to_owned()),
160 log.should_hide_fields_for_humans(),
161 skip_cause,
162 );
163
164 for (idx, msg) in messages.into_iter().enumerate() {
165 write!(
166 &mut data,
167 "{}{}",
168 if idx == 0 {
169 header.as_str()
170 } else {
171 EMPTY_HEADER
172 },
173 msg,
174 )?;
175 writeln!(&mut data)?;
176 }
177 } else {
178 let empty_map = FnvHashMap::with_capacity_and_hasher(0, BuildHasherDefault::default());
179 let mut fields = create_field_tailers(
180 tailer_width,
181 if log.should_hide_fields_for_humans() {
182 &empty_map
183 } else {
184 log.metadata()
185 },
186 skip_cause,
187 false,
188 );
189 let mut messages =
190 chunk_string_into_width(msg_width, &real_msg.unwrap_or("<no message>".to_owned()));
191
192 while fields.len() < messages.len() {
193 fields.push(pad_to_width("|".to_owned(), tailer_width));
194 }
195 while messages.len() < fields.len() {
196 messages.push(pad_to_width(String::new(), msg_width));
197 }
198
199 for idx in 0..fields.len() {
200 write!(
201 &mut data,
202 "{}{}{}",
203 if idx == 0 {
204 header.as_str()
205 } else {
206 EMPTY_HEADER
207 },
208 messages[idx],
209 fields[idx],
210 )?;
211 writeln!(&mut data)?;
212 }
213 }
214
215 Ok(data)
216 }
217
218 fn default_ps1(&self) -> String {
219 Self::default_ps1_impl()
220 }
221
222 fn supports_ansi(&self) -> bool {
223 true
224 }
225
226 fn should_pause_log_events(&self, _provider: &dyn InputProvider) -> bool {
227 self.force_pause.load(Ordering::Acquire)
228 }
229
230 fn render_input(
231 &self,
232 _app_name: &'static str,
233 provider: &dyn InputProvider,
234 term_width: u16,
235 ) -> Result<String, LisaError> {
236 let msg_width = calculate_message_width(term_width);
237 let tailer_width = calculate_tailer_width(term_width);
238
239 let mut state_lock = self.state.lock();
240 let new_ps1_lock = self.new_ps1.lock();
241 Ok(state_lock.render_current_standalone(
242 new_ps1_lock.as_deref(),
243 msg_width,
244 tailer_width,
245 &provider.current_input(),
246 ))
247 }
248
249 fn clear_input(&self, _term_width: u16) -> String {
250 let state_lock = self.state.lock();
251 state_lock.clear_current_render()
252 }
253
254 fn clear_task_list(&self, _task_list_size: usize) -> String {
255 let mut data = String::new();
256
257 let task_lines_lock = self.task_lines_rendered.lock();
258 for _ in 0..*task_lines_lock {
259 if data.is_empty() {
260 data = move_cursor(helpers::CursorDirection::Left, 9999);
261 } else {
262 data.push_str(&move_cursor(helpers::CursorDirection::Up, 1));
263 }
264 data.push_str(&erase_line(helpers::ClearLine::EntireLine));
265 }
266 std::mem::drop(task_lines_lock);
267
268 data
269 }
270
271 fn rerender_tasks(
272 &self,
273 _new_task_events: &[TaskEvent],
274 current_task_states: &FnvHashMap<
275 GloballyUniqueTaskId,
276 (DateTime<Utc>, String, LisaTaskStatus),
277 >,
278 tasks_running_since: Option<DateTime<Utc>>,
279 term_height: u16,
280 ) -> Result<String, LisaError> {
281 let Some(running_since) = tasks_running_since else {
282 return Ok(String::with_capacity(0));
283 };
284 if current_task_states.is_empty() {
285 return Ok(String::with_capacity(0));
286 }
287
288 let mut task_lines_lock = self.task_lines_rendered.lock();
289 let max_lines_rendered = term_height / 10;
290 *task_lines_lock = 2;
292 let my_time = Utc::now();
293 let mut data = String::new();
294 data.push('[');
295 write!(&mut data, "{}", '+'.bright_green())?;
296 data.push_str("] Running...");
297
298 let duration_since_start_time_delta = my_time.signed_duration_since(running_since);
299 writeln!(
300 &mut data,
301 "{}.{}s",
302 std::cmp::min(0, duration_since_start_time_delta.num_seconds()),
303 std::cmp::min(0, duration_since_start_time_delta.subsec_micros()),
304 )?;
305
306 let mut keys_to_sort = current_task_states.keys().collect::<Vec<_>>();
307 keys_to_sort.sort_by(|one, two| {
308 let first_comp = one.0.cmp(&two.0);
309 let second_comp = one.1.cmp(&two.1);
310
311 if second_comp == std::cmp::Ordering::Equal {
312 first_comp
313 } else {
314 second_comp
315 }
316 });
317
318 let mut did_break_early = false;
319 for (tasks_rendered, key) in keys_to_sort.into_iter().enumerate() {
320 if u16::try_from(tasks_rendered).unwrap_or(u16::MAX) > max_lines_rendered {
322 did_break_early = true;
323 break;
324 }
325 *task_lines_lock += 1;
326 let (task_start_time, task_name, status) =
327 current_task_states.get(key).expect("Guaranteed to exist!");
328 let time_delta_since_task_start = my_time.signed_duration_since(task_start_time);
329
330 match status {
331 LisaTaskStatus::Inactive => {
332 writeln!(
333 &mut data,
334 "{}",
335 format!(
336 " | => {}/{}|{task_name}: {status} [{}.{:04}s...]",
337 key.0,
338 key.1,
339 std::cmp::min(0, time_delta_since_task_start.num_seconds()),
340 std::cmp::min(0, time_delta_since_task_start.subsec_micros()),
341 )
342 .white()
343 .bold()
344 )?;
345 }
346 LisaTaskStatus::Running(_) => {
347 writeln!(
348 &mut data,
349 "{}",
350 format!(
351 " | => {}/{}|{task_name}: {status} [{}.{:04}s...]",
352 key.0,
353 key.1,
354 std::cmp::min(0, time_delta_since_task_start.num_seconds()),
355 std::cmp::min(0, time_delta_since_task_start.subsec_micros()),
356 )
357 .cyan()
358 .italic()
359 )?;
360 }
361 LisaTaskStatus::Waiting(_) => {
362 writeln!(
363 &mut data,
364 "{}",
365 format!(
366 " | => {}/{}|{task_name}: {status} [{}.{:04}s...]",
367 key.0,
368 key.1,
369 std::cmp::min(0, duration_since_start_time_delta.num_seconds()),
370 std::cmp::min(0, duration_since_start_time_delta.subsec_micros()),
371 )
372 .yellow()
373 .italic()
374 )?;
375 }
376 }
377 }
378 if did_break_early {
379 writeln!(
380 &mut data,
381 " | => {} tasks also running...",
382 current_task_states.len() - usize::from(*task_lines_lock),
383 )?;
384 *task_lines_lock += 1;
385 }
386 std::mem::drop(task_lines_lock);
387
388 Ok(data)
389 }
390
391 fn on_input(
392 &self,
393 event: TerminalInputEvent,
394 provider: &dyn InputProvider,
395 ) -> Result<String, LisaError> {
396 let mut state_lock = self.state.lock();
397 Ok(state_lock.on_input_event(provider, event, &self.force_pause))
398 }
399
400 fn update_ps1(&self, new_ps1: String) {
401 let mut new_ps1_lock = self.new_ps1.lock();
402 _ = new_ps1_lock.insert(new_ps1);
403 }
404}