use crate::{
display::{
SuperConsole,
renderers::{ConsoleOutputFeatures, ConsoleRenderer},
tracing::SuperConsoleLogMessage,
},
errors::LisaError,
input::{InputProvider, TerminalInputEvent},
tasks::{GloballyUniqueTaskId, LisaTaskStatus, TaskEvent, TaskEventLogProvider},
};
use chrono::{DateTime, Utc};
use fnv::FnvHashMap;
use std::{env::var as env_var, hash::BuildHasherDefault, io::Write as IoWrite};
use tokio::{
runtime::Handle,
sync::mpsc::{Receiver as BoundedReceiver, Sender as BoundedSender},
};
use tracing::warn;
type TrackedTask = (DateTime<Utc>, String, LisaTaskStatus);
pub struct UnlockedRendererState<
StdoutTy: ConsoleOutputFeatures + IoWrite + Send,
StderrTy: ConsoleOutputFeatures + IoWrite + Send,
> {
app_name: &'static str,
cached_log_events: Vec<SuperConsoleLogMessage>,
completed_input_senders: Option<BoundedSender<String>>,
force_term_height: Option<u16>,
force_term_width: Option<u16>,
has_warned_about_terminal_size: bool,
input_provider: Option<Box<dyn InputProvider>>,
log_messages: BoundedReceiver<SuperConsoleLogMessage>,
stderr: StderrTy,
stderr_renderer: Box<dyn ConsoleRenderer>,
stdout: StdoutTy,
stdout_renderer: Box<dyn ConsoleRenderer>,
task_provider: Option<Box<dyn TaskEventLogProvider>>,
task_statuses: FnvHashMap<GloballyUniqueTaskId, TrackedTask>,
tasks_running_since: Option<DateTime<Utc>>,
}
impl<
StdoutTy: ConsoleOutputFeatures + IoWrite + Send,
StderrTy: ConsoleOutputFeatures + IoWrite + Send,
> UnlockedRendererState<StdoutTy, StderrTy>
{
#[must_use]
pub fn new(
app_name: &'static str,
environment_prefix: &str,
(stdout, stderr): (StdoutTy, StderrTy),
(stdout_renderer, stderr_renderer): (Box<dyn ConsoleRenderer>, Box<dyn ConsoleRenderer>),
log_messages: BoundedReceiver<SuperConsoleLogMessage>,
) -> Self {
Self {
app_name,
cached_log_events: Vec::with_capacity(0),
completed_input_senders: None,
force_term_height: env_var(format!("{environment_prefix}_FORCE_TERM_HEIGHT"))
.ok()
.and_then(|item| item.parse::<u16>().ok()),
force_term_width: env_var(format!("{environment_prefix}_FORCE_TERM_WIDTH"))
.ok()
.and_then(|item| item.parse::<u16>().ok()),
has_warned_about_terminal_size: false,
input_provider: None,
log_messages,
stderr,
stderr_renderer,
stdout,
stdout_renderer,
task_provider: None,
task_statuses: FnvHashMap::with_capacity_and_hasher(0, BuildHasherDefault::default()),
tasks_running_since: None,
}
}
#[must_use]
pub fn get_input_provider(&mut self) -> &mut Option<Box<dyn InputProvider>> {
&mut self.input_provider
}
pub fn set_input_provider(&mut self, iprovider: Box<dyn InputProvider>) {
self.input_provider = Some(iprovider);
}
pub fn set_task_provider(&mut self, tprovider: Box<dyn TaskEventLogProvider>) {
self.task_provider = Some(tprovider);
}
pub fn set_completed_input_channel(&mut self, channel: BoundedSender<String>) {
self.completed_input_senders = Some(channel);
}
pub fn get_unprocessed_inputs(&mut self) -> Vec<String> {
if let Some(iprovider) = self.input_provider.as_mut() {
iprovider.inputs()
} else {
Vec::with_capacity(0)
}
}
pub fn set_input_active(&mut self, active: bool) -> Result<(), LisaError> {
if let Some(prov) = self.input_provider.as_mut() {
prov.set_active(active)?;
}
Ok(())
}
pub fn render_if_needed(&mut self) -> Result<(), LisaError> {
let mut pause_log_events = false;
if let Some(iprovider) = self.input_provider.as_ref()
&& iprovider.is_active()
&& iprovider.is_stdin()
&& self.stderr_renderer.should_pause_log_events(&**iprovider)
{
pause_log_events = true;
}
let mut new_logs = self.cached_log_events.drain(..).collect::<Vec<_>>();
while let Ok(value) = self.log_messages.try_recv() {
new_logs.push(value);
}
let mut input_events = Vec::with_capacity(0);
if let Some(iprovider) = self.input_provider.as_mut()
&& iprovider.is_active()
&& iprovider.is_stdin()
{
input_events = iprovider.poll_for_input(self.stderr_renderer.supports_ansi());
}
let mut task_events = Vec::with_capacity(0);
if let Some(tprovider) = self.task_provider.as_ref() {
task_events = tprovider.new_events();
}
if new_logs.is_empty() && input_events.is_empty() && task_events.is_empty() {
return Ok(());
}
if pause_log_events && input_events.is_empty() {
return Ok(());
}
self.render(pause_log_events, new_logs, input_events, &task_events)
}
fn render(
&mut self,
pause_logs: bool,
new_logs: Vec<SuperConsoleLogMessage>,
new_input_events: Vec<TerminalInputEvent>,
new_task_events: &[TaskEvent],
) -> Result<(), LisaError> {
let mut term_width = self
.force_term_width
.or_else(SuperConsole::<std::io::Stdout, std::io::Stderr>::terminal_width)
.unwrap_or(40_u16);
if term_width < 40 {
if !self.has_warned_about_terminal_size {
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!");
self.has_warned_about_terminal_size = true;
}
term_width = 40;
}
let had_input_events = !new_input_events.is_empty();
if let Some(iprovider) = self.input_provider.as_mut() {
for ievent in new_input_events {
let rendered = self.stderr_renderer.on_input(ievent, &**iprovider)?;
if !rendered.is_empty() {
self.stderr.write_all(rendered.as_bytes())?;
}
}
if let Some(channel) = self.completed_input_senders.as_ref() {
for input in iprovider.inputs() {
if let Ok(h) = Handle::try_current() {
tokio::task::block_in_place(move || {
h.block_on(async {
_ = channel.send(input).await;
});
});
} else {
_ = channel.blocking_send(input);
}
}
}
}
if had_input_events && (new_logs.is_empty() && new_task_events.is_empty()) {
return Ok(());
}
self.clear_input(term_width)?;
self.clear_tasks()?;
if pause_logs {
self.cached_log_events = new_logs;
} else {
for log_line in new_logs {
if log_line.towards_stdout() {
let rendered =
self.stdout_renderer
.render_message(self.app_name, log_line, term_width)?;
self.stdout.write_all(rendered.as_bytes())?;
} else {
let rendered =
self.stderr_renderer
.render_message(self.app_name, log_line, term_width)?;
if !rendered.is_empty() {
self.stderr.write_all(rendered.as_bytes())?;
}
}
}
}
self.render_tasks(new_task_events)?;
self.render_input(term_width)?;
Ok(())
}
fn clear_input(&mut self, term_width: u16) -> Result<(), LisaError> {
if let Some(iprovider) = self.input_provider.as_ref()
&& iprovider.is_stdin()
&& iprovider.is_active()
{
let clear_line = self.stderr_renderer.clear_input(term_width);
if !clear_line.is_empty() {
self.stderr.write_all(clear_line.as_bytes())?;
}
}
Ok(())
}
fn clear_tasks(&mut self) -> Result<(), LisaError> {
if self.task_provider.is_some() {
let clear_line = self
.stderr_renderer
.clear_task_list(self.task_statuses.len());
if !clear_line.is_empty() {
self.stderr.write_all(clear_line.as_bytes())?;
}
}
Ok(())
}
fn render_tasks(&mut self, new_task_events: &[TaskEvent]) -> Result<(), LisaError> {
let my_date = Utc::now();
let mut was_empty = self.task_statuses.is_empty();
for task_event in new_task_events {
match task_event {
TaskEvent::TaskStart(thread_id, lisa_task_id, name, status) => {
if was_empty {
_ = self.tasks_running_since.insert(my_date);
was_empty = false;
}
self.task_statuses.insert(
(*thread_id, *lisa_task_id),
(my_date, name.clone(), status.clone()),
);
}
TaskEvent::TaskStatusUpdate(thread_id, lisa_task_id, new_status) => {
if let Some(mutable_data) =
self.task_statuses.get_mut(&(*thread_id, *lisa_task_id))
{
mutable_data.2 = new_status.clone();
}
}
TaskEvent::TaskEnd(thread_id, lisa_task_id) => {
self.task_statuses.remove(&(*thread_id, *lisa_task_id));
}
}
}
if !was_empty && self.task_statuses.is_empty() {
self.tasks_running_since = None;
}
let rendered = self.stderr_renderer.rerender_tasks(
new_task_events,
&self.task_statuses,
self.tasks_running_since,
self.force_term_height
.and_then(|_| SuperConsole::<std::io::Stdout, std::io::Stderr>::terminal_height())
.unwrap_or_default(),
)?;
if !rendered.is_empty() {
self.stderr.write_all(rendered.as_bytes())?;
}
Ok(())
}
fn render_input(&mut self, normalized_term_width: u16) -> Result<(), LisaError> {
if let Some(iprovider) = self.input_provider.as_ref()
&& iprovider.is_stdin()
&& iprovider.is_active()
{
let input_renderer = self.stderr_renderer.render_input(
self.app_name,
&**iprovider,
normalized_term_width,
)?;
if !input_renderer.is_empty() {
self.stderr.write_all(input_renderer.as_bytes())?;
}
}
Ok(())
}
}