rm_lisa/display/renderers/
text.rs1use crate::{
5 display::{
6 renderers::{ConsoleOutputFeatures, ConsoleRenderer, get_ansi_escape_code_regex},
7 tracing::SuperConsoleLogMessage,
8 },
9 errors::LisaError,
10 input::{InputProvider, TerminalInputEvent},
11 tasks::{GloballyUniqueTaskId, LisaTaskStatus, TaskEvent},
12};
13use chrono::prelude::*;
14use fnv::FnvHashMap;
15use parking_lot::RwLock;
16use regex::Regex;
17use std::{
18 borrow::Cow,
19 env::var as env_var,
20 fmt::Write,
21 sync::atomic::{AtomicBool, Ordering},
22};
23
24#[derive(Debug)]
26pub struct TextConsoleRenderer {
27 ansi_escapes: Regex,
28 force_pause: AtomicBool,
29 ps1: RwLock<String>,
30}
31
32impl TextConsoleRenderer {
33 #[must_use]
35 pub fn new() -> Self {
36 Self {
37 ansi_escapes: get_ansi_escape_code_regex(),
38 force_pause: AtomicBool::new(false),
39 ps1: RwLock::new("> ".to_owned()),
40 }
41 }
42}
43
44impl Default for TextConsoleRenderer {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50impl ConsoleRenderer for TextConsoleRenderer {
51 fn should_use_renderer(
52 &self,
53 _features: &dyn ConsoleOutputFeatures,
54 environment_prefix: &str,
55 ) -> bool {
56 if let Ok(explicit_renderer) = env_var(format!("{environment_prefix}_LOG_FORMAT")) {
58 return explicit_renderer.trim().eq_ignore_ascii_case("text");
59 }
60
61 for no_color_var in ["NO_COLOR", "NOCOLOR"] {
63 if env_var(no_color_var).as_deref() == Ok("1") {
64 return true;
65 }
66 }
67 for color_var in ["CLICOLOR", "CLI_COLOR", "CLICOLOR_FORCE"] {
68 let env = env_var(color_var);
69 if env.as_deref() == Ok("0") {
70 return true;
71 }
72 if env.as_deref() == Ok("1") {
73 return false;
74 }
75 }
76
77 true
80 }
81
82 fn render_message(
83 &self,
84 app_name: &'static str,
85 log: SuperConsoleLogMessage,
86 _term_width: u16,
87 ) -> Result<String, LisaError> {
88 let mut line = String::new();
89
90 if log.should_decorate() {
91 write!(
92 &mut line,
93 "{}/{}|",
94 log.subsytem().unwrap_or(app_name),
95 log.level(),
96 )?;
97 }
98
99 if let Some(msg) = log.message() {
100 line += &msg.replace('\n', " ").replace('\r', "");
101 } else {
102 line += "<no message>";
103 }
104 if log.should_decorate() {
105 write!(&mut line, "|")?;
106 }
107 if !log.metadata().is_empty() && !log.should_hide_fields_for_humans() {
108 let mut has_written = false;
109 for (key, value) in log.metadata() {
110 if has_written {
111 line.push(',');
112 }
113 write!(&mut line, "{key}={value}")?;
114 has_written = true;
115 }
116 }
117 write!(
118 &mut line,
119 "|{:04}/{:02}/{:02} {:02}:{:02}:{:02}.{:04}",
120 log.at().year(),
121 log.at().month0(),
122 log.at().day0(),
123 log.at().hour(),
124 log.at().minute(),
125 log.at().second(),
126 log.at().timestamp_subsec_millis(),
127 )?;
128 writeln!(&mut line)?;
129
130 Ok(match self.ansi_escapes.replace_all(&line, "") {
131 Cow::Borrowed(_) => line,
132 Cow::Owned(owned) => owned,
133 })
134 }
135
136 fn default_ps1(&self) -> String {
137 "> ".to_owned()
138 }
139
140 fn update_ps1(&self, new_ps1: String) {
141 let mut guarded = self.ps1.write();
142 *guarded = new_ps1;
143 }
144
145 fn supports_ansi(&self) -> bool {
146 false
147 }
148
149 fn clear_input(&self, _term_width: u16) -> String {
151 String::with_capacity(0)
152 }
153
154 fn render_input(
167 &self,
168 _app_name: &'static str,
169 _provider: &dyn InputProvider,
170 _term_width: u16,
171 ) -> Result<String, LisaError> {
172 Ok(String::with_capacity(0))
173 }
174
175 fn clear_task_list(&self, _task_list_size: usize) -> String {
177 String::with_capacity(0)
178 }
179
180 fn rerender_tasks(
185 &self,
186 new_task_events: &[TaskEvent],
187 _current_task_states: &FnvHashMap<
188 GloballyUniqueTaskId,
189 (DateTime<Utc>, String, LisaTaskStatus),
190 >,
191 _running_since: Option<DateTime<Utc>>,
192 _term_height: u16,
193 ) -> Result<String, LisaError> {
194 let mut result = String::new();
195
196 for event in new_task_events {
197 match event {
198 TaskEvent::TaskStart(thread, task, name, status) => {
199 write!(
200 &mut result,
201 "{thread}/{task}|task started with name: [{name}]|status={status}",
202 )?;
203 }
204 TaskEvent::TaskStatusUpdate(thread, task, new_status) => {
205 write!(
206 &mut result,
207 "{thread}/{task}|task has a new status|status={new_status}",
208 )?;
209 }
210 TaskEvent::TaskEnd(thread, task) => {
211 write!(&mut result, "{thread}/{task}|task ended")?;
212 }
213 }
214 result.push('\n');
215 }
216
217 Ok(result)
218 }
219
220 fn on_input(
229 &self,
230 event: TerminalInputEvent,
231 provider: &dyn InputProvider,
232 ) -> Result<String, LisaError> {
233 match event {
234 TerminalInputEvent::InputStarted => {
235 let ps1_read = self.ps1.read();
236 Ok(ps1_read.clone())
237 }
238 TerminalInputEvent::InputFinished => Ok("\n".to_owned()),
239 TerminalInputEvent::InputAppend(character) => {
240 let mut new = String::with_capacity(1);
241 new.push(character);
242 Ok(new)
243 }
244 TerminalInputEvent::InputMassAppend(data) => Ok(data),
245 TerminalInputEvent::InputChanged(_) => {
246 let ps1_read = self.ps1.read();
247 let mut data = String::with_capacity(1 + ps1_read.len());
248 data.push('\n');
249 data.push_str(ps1_read.as_str());
250 data.push_str(&provider.current_input());
251 Ok(data)
252 }
253 TerminalInputEvent::InputCancelled => Ok("<CANCELLED>\n".to_owned()),
254 TerminalInputEvent::ClearScreen => Ok(String::with_capacity(0)),
255 TerminalInputEvent::CursorMoveLeft(_) | TerminalInputEvent::CursorMoveRight(_) => {
256 Ok(String::with_capacity(0))
257 }
258 TerminalInputEvent::ToggleOutputPause => {
259 self.force_pause.fetch_not(Ordering::Release);
260 Ok(String::with_capacity(0))
261 }
262 }
263 }
264
265 fn should_pause_log_events(&self, provider: &dyn InputProvider) -> bool {
266 provider.input_in_progress() || self.force_pause.load(Ordering::Acquire)
267 }
268}