1use crate::{
6 display::{
7 SuperConsole,
8 renderers::{ConsoleOutputFeatures, ConsoleRenderer},
9 tracing::SuperConsoleLogMessage,
10 },
11 errors::LisaError,
12 input::{InputProvider, TerminalInputEvent},
13 tasks::{GloballyUniqueTaskId, LisaTaskStatus, TaskEvent, TaskEventLogProvider},
14};
15use chrono::{DateTime, Utc};
16use fnv::FnvHashMap;
17use std::{env::var as env_var, hash::BuildHasherDefault, io::Write as IoWrite};
18use tokio::{
19 runtime::Handle,
20 sync::mpsc::{Receiver as BoundedReceiver, Sender as BoundedSender},
21};
22use tracing::warn;
23
24type TrackedTask = (DateTime<Utc>, String, LisaTaskStatus);
30
31pub struct UnlockedRendererState<
33 StdoutTy: ConsoleOutputFeatures + IoWrite + Send,
34 StderrTy: ConsoleOutputFeatures + IoWrite + Send,
35> {
36 app_name: &'static str,
38 cached_log_events: Vec<SuperConsoleLogMessage>,
40 completed_input_senders: Option<BoundedSender<String>>,
42 force_term_height: Option<u16>,
44 force_term_width: Option<u16>,
46 has_warned_about_terminal_size: bool,
48 input_provider: Option<Box<dyn InputProvider>>,
50 log_messages: BoundedReceiver<SuperConsoleLogMessage>,
52 stderr: StderrTy,
54 stderr_renderer: Box<dyn ConsoleRenderer>,
56 stdout: StdoutTy,
58 stdout_renderer: Box<dyn ConsoleRenderer>,
60 task_provider: Option<Box<dyn TaskEventLogProvider>>,
62 task_statuses: FnvHashMap<GloballyUniqueTaskId, TrackedTask>,
64 tasks_running_since: Option<DateTime<Utc>>,
66}
67
68impl<
69 StdoutTy: ConsoleOutputFeatures + IoWrite + Send,
70 StderrTy: ConsoleOutputFeatures + IoWrite + Send,
71> UnlockedRendererState<StdoutTy, StderrTy>
72{
73 #[must_use]
75 pub fn new(
76 app_name: &'static str,
77 environment_prefix: &str,
78 (stdout, stderr): (StdoutTy, StderrTy),
79 (stdout_renderer, stderr_renderer): (Box<dyn ConsoleRenderer>, Box<dyn ConsoleRenderer>),
80 log_messages: BoundedReceiver<SuperConsoleLogMessage>,
81 ) -> Self {
82 Self {
83 app_name,
84 cached_log_events: Vec::with_capacity(0),
85 completed_input_senders: None,
86 force_term_height: env_var(format!("{environment_prefix}_FORCE_TERM_HEIGHT"))
87 .ok()
88 .and_then(|item| item.parse::<u16>().ok()),
89 force_term_width: env_var(format!("{environment_prefix}_FORCE_TERM_WIDTH"))
90 .ok()
91 .and_then(|item| item.parse::<u16>().ok()),
92 has_warned_about_terminal_size: false,
93 input_provider: None,
94 log_messages,
95 stderr,
96 stderr_renderer,
97 stdout,
98 stdout_renderer,
99 task_provider: None,
100 task_statuses: FnvHashMap::with_capacity_and_hasher(0, BuildHasherDefault::default()),
101 tasks_running_since: None,
102 }
103 }
104
105 #[must_use]
107 pub fn get_input_provider(&mut self) -> &mut Option<Box<dyn InputProvider>> {
108 &mut self.input_provider
109 }
110
111 pub fn set_input_provider(&mut self, iprovider: Box<dyn InputProvider>) {
112 self.input_provider = Some(iprovider);
113 }
114
115 pub fn set_task_provider(&mut self, tprovider: Box<dyn TaskEventLogProvider>) {
116 self.task_provider = Some(tprovider);
117 }
118
119 pub fn set_completed_input_channel(&mut self, channel: BoundedSender<String>) {
120 self.completed_input_senders = Some(channel);
121 }
122
123 pub fn get_unprocessed_inputs(&mut self) -> Vec<String> {
124 if let Some(iprovider) = self.input_provider.as_mut() {
125 iprovider.inputs()
126 } else {
127 Vec::with_capacity(0)
128 }
129 }
130
131 pub fn set_input_active(&mut self, active: bool) -> Result<(), LisaError> {
137 if let Some(prov) = self.input_provider.as_mut() {
138 prov.set_active(active)?;
139 }
140
141 Ok(())
142 }
143
144 pub fn render_if_needed(&mut self) -> Result<(), LisaError> {
150 let mut pause_log_events = false;
151 if let Some(iprovider) = self.input_provider.as_ref()
152 && iprovider.is_active()
153 && iprovider.is_stdin()
154 && self.stderr_renderer.should_pause_log_events(&**iprovider)
155 {
156 pause_log_events = true;
157 }
158
159 let mut new_logs = self.cached_log_events.drain(..).collect::<Vec<_>>();
160 while let Ok(value) = self.log_messages.try_recv() {
161 new_logs.push(value);
162 }
163 let mut input_events = Vec::with_capacity(0);
164 if let Some(iprovider) = self.input_provider.as_mut()
165 && iprovider.is_active()
166 && iprovider.is_stdin()
167 {
168 input_events = iprovider.poll_for_input(self.stderr_renderer.supports_ansi());
169 }
170 let mut task_events = Vec::with_capacity(0);
171 if let Some(tprovider) = self.task_provider.as_ref() {
172 task_events = tprovider.new_events();
173 }
174
175 if new_logs.is_empty() && input_events.is_empty() && task_events.is_empty() {
176 return Ok(());
177 }
178 if pause_log_events && input_events.is_empty() {
179 return Ok(());
180 }
181 self.render(pause_log_events, new_logs, input_events, &task_events)
182 }
183
184 fn render(
186 &mut self,
187 pause_logs: bool,
188 new_logs: Vec<SuperConsoleLogMessage>,
189 new_input_events: Vec<TerminalInputEvent>,
190 new_task_events: &[TaskEvent],
191 ) -> Result<(), LisaError> {
192 let mut term_width = self
193 .force_term_width
194 .or_else(SuperConsole::<std::io::Stdout, std::io::Stderr>::terminal_width)
195 .unwrap_or(40_u16);
196 if term_width < 40 {
197 if !self.has_warned_about_terminal_size {
198 warn!(actual_terminal_width = %term_width, "Your terminal is a very small size! Please bump it up to at least 40 characters for the best experience!");
199 self.has_warned_about_terminal_size = true;
200 }
201 term_width = 40;
202 }
203
204 let had_input_events = !new_input_events.is_empty();
205 if let Some(iprovider) = self.input_provider.as_mut() {
206 for ievent in new_input_events {
207 let rendered = self.stderr_renderer.on_input(ievent, &**iprovider)?;
208 if !rendered.is_empty() {
209 self.stderr.write_all(rendered.as_bytes())?;
210 }
211 }
212
213 if let Some(channel) = self.completed_input_senders.as_ref() {
214 for input in iprovider.inputs() {
215 if let Ok(h) = Handle::try_current() {
217 tokio::task::block_in_place(move || {
218 h.block_on(async {
219 _ = channel.send(input).await;
220 });
221 });
222 } else {
223 _ = channel.blocking_send(input);
224 }
225 }
226 }
227 }
228 if had_input_events && (new_logs.is_empty() && new_task_events.is_empty()) {
230 return Ok(());
231 }
232
233 self.clear_input(term_width)?;
234 self.clear_tasks()?;
235
236 if pause_logs {
237 self.cached_log_events = new_logs;
238 } else {
239 for log_line in new_logs {
240 if log_line.towards_stdout() {
241 let rendered =
242 self.stdout_renderer
243 .render_message(self.app_name, log_line, term_width)?;
244
245 self.stdout.write_all(rendered.as_bytes())?;
246 } else {
247 let rendered =
248 self.stderr_renderer
249 .render_message(self.app_name, log_line, term_width)?;
250
251 if !rendered.is_empty() {
252 self.stderr.write_all(rendered.as_bytes())?;
253 }
254 }
255 }
256 }
257
258 self.render_tasks(new_task_events)?;
259 self.render_input(term_width)?;
260
261 Ok(())
262 }
263
264 fn clear_input(&mut self, term_width: u16) -> Result<(), LisaError> {
270 if let Some(iprovider) = self.input_provider.as_ref()
271 && iprovider.is_stdin()
272 && iprovider.is_active()
273 {
274 let clear_line = self.stderr_renderer.clear_input(term_width);
275 if !clear_line.is_empty() {
276 self.stderr.write_all(clear_line.as_bytes())?;
277 }
278 }
279
280 Ok(())
281 }
282
283 fn clear_tasks(&mut self) -> Result<(), LisaError> {
289 if self.task_provider.is_some() {
290 let clear_line = self
291 .stderr_renderer
292 .clear_task_list(self.task_statuses.len());
293 if !clear_line.is_empty() {
294 self.stderr.write_all(clear_line.as_bytes())?;
295 }
296 }
297
298 Ok(())
299 }
300
301 fn render_tasks(&mut self, new_task_events: &[TaskEvent]) -> Result<(), LisaError> {
303 let my_date = Utc::now();
304 let mut was_empty = self.task_statuses.is_empty();
305
306 for task_event in new_task_events {
307 match task_event {
308 TaskEvent::TaskStart(thread_id, lisa_task_id, name, status) => {
309 if was_empty {
310 _ = self.tasks_running_since.insert(my_date);
311 was_empty = false;
312 }
313 self.task_statuses.insert(
314 (*thread_id, *lisa_task_id),
315 (my_date, name.clone(), status.clone()),
316 );
317 }
318 TaskEvent::TaskStatusUpdate(thread_id, lisa_task_id, new_status) => {
319 if let Some(mutable_data) =
320 self.task_statuses.get_mut(&(*thread_id, *lisa_task_id))
321 {
322 mutable_data.2 = new_status.clone();
323 }
324 }
325 TaskEvent::TaskEnd(thread_id, lisa_task_id) => {
326 self.task_statuses.remove(&(*thread_id, *lisa_task_id));
327 }
328 }
329 }
330
331 if !was_empty && self.task_statuses.is_empty() {
332 self.tasks_running_since = None;
333 }
334
335 let rendered = self.stderr_renderer.rerender_tasks(
336 new_task_events,
337 &self.task_statuses,
338 self.tasks_running_since,
339 self.force_term_height
340 .and_then(|_| SuperConsole::<std::io::Stdout, std::io::Stderr>::terminal_height())
341 .unwrap_or_default(),
342 )?;
343
344 if !rendered.is_empty() {
345 self.stderr.write_all(rendered.as_bytes())?;
346 }
347
348 Ok(())
349 }
350
351 fn render_input(&mut self, normalized_term_width: u16) -> Result<(), LisaError> {
352 if let Some(iprovider) = self.input_provider.as_ref()
353 && iprovider.is_stdin()
354 && iprovider.is_active()
355 {
356 let input_renderer = self.stderr_renderer.render_input(
357 self.app_name,
358 &**iprovider,
359 normalized_term_width,
360 )?;
361 if !input_renderer.is_empty() {
362 self.stderr.write_all(input_renderer.as_bytes())?;
363 }
364 }
365
366 Ok(())
367 }
368}