pub mod render_state;
pub mod renderers;
pub mod tracing;
use crate::{
app_name_to_prefix,
display::{
render_state::UnlockedRendererState,
renderers::{
ColorConsoleRenderer, ConsoleOutputFeatures, ConsoleRenderer, JSONConsoleRenderer,
TextConsoleRenderer,
},
tracing::SuperConsoleLogMessage,
},
errors::LisaError,
input::InputProvider,
tasks::TaskEventLogProvider,
};
use parking_lot::{Mutex, RawMutex, lock_api::MutexGuard};
use std::{
io::{Stderr, Stdout, Write as IoWrite, stderr as get_stderr, stdout as get_stdout},
sync::Arc,
time::Duration,
};
use tokio::{
signal::ctrl_c,
sync::{
Mutex as AsyncMutex,
mpsc::{Receiver as BoundedReceiver, Sender as BoundedSender, channel as bounded_channel},
},
task::Builder as TaskBuilder,
time::sleep,
};
const LOG_CHANNEL_SIZE: usize = 64_usize;
pub struct SuperConsole<
StdoutTy: ConsoleOutputFeatures + IoWrite + Send + 'static,
StderrTy: ConsoleOutputFeatures + IoWrite + Send + 'static,
> {
flush_sender: BoundedSender<()>,
did_do_flush: AsyncMutex<BoundedReceiver<()>>,
log_messages: BoundedSender<SuperConsoleLogMessage>,
state: Arc<Mutex<UnlockedRendererState<StdoutTy, StderrTy>>>,
stop_tick_task: BoundedSender<()>,
}
impl SuperConsole<Stdout, Stderr> {
pub fn new(app_name: &'static str) -> Result<Self, LisaError> {
let stdout_sink = get_stdout();
let stderr_sink = get_stderr();
let environment_prefix = app_name_to_prefix(app_name);
let stdout_renderer = Self::choose_default_renderer(&stdout_sink, &environment_prefix)
.ok_or(LisaError::NoRendererFound)?;
let stderr_renderer = Self::choose_default_renderer(&stderr_sink, &environment_prefix)
.ok_or(LisaError::NoRendererFound)?;
let (log_messages, log_receiver) = bounded_channel(LOG_CHANNEL_SIZE);
let (flush_sender, flush_receiver) = bounded_channel(1);
let (flush_completed_sender, flush_completed_receiver) = bounded_channel(1);
let (stop_tick_sender, stop_tick_receiver) = bounded_channel(1);
let state = Arc::new(Mutex::new(UnlockedRendererState::new(
app_name,
&environment_prefix,
(stdout_sink, stderr_sink),
(stdout_renderer, stderr_renderer),
log_receiver,
)));
Self::spawn_tick_task(
flush_completed_sender,
flush_receiver,
stop_tick_receiver,
state.clone(),
)?;
Ok(Self {
did_do_flush: AsyncMutex::new(flush_completed_receiver),
flush_sender,
log_messages,
state,
stop_tick_task: stop_tick_sender,
})
}
pub fn new_preselected_renderers(
app_name: &'static str,
stdout_renderer: Box<dyn ConsoleRenderer>,
stderr_renderer: Box<dyn ConsoleRenderer>,
) -> Result<Self, LisaError> {
let stdout_sink = get_stdout();
let stderr_sink = get_stderr();
let environment_prefix = app_name_to_prefix(app_name);
let (log_messages, log_receiver) = bounded_channel(LOG_CHANNEL_SIZE);
let (flush_sender, flush_receiver) = bounded_channel(1);
let (flush_completed_sender, flush_completed_receiver) = bounded_channel(1);
let (stop_tick_sender, stop_tick_receiver) = bounded_channel(1);
let state = Arc::new(Mutex::new(UnlockedRendererState::new(
app_name,
&environment_prefix,
(stdout_sink, stderr_sink),
(stdout_renderer, stderr_renderer),
log_receiver,
)));
Self::spawn_tick_task(
flush_completed_sender,
flush_receiver,
stop_tick_receiver,
state.clone(),
)?;
Ok(Self {
did_do_flush: AsyncMutex::new(flush_completed_receiver),
flush_sender,
log_messages,
state,
stop_tick_task: stop_tick_sender,
})
}
}
impl<
StdoutTy: ConsoleOutputFeatures + IoWrite + Send + 'static,
StderrTy: ConsoleOutputFeatures + IoWrite + Send + 'static,
> SuperConsole<StdoutTy, StderrTy>
{
pub fn new_with_outputs(
app_name: &'static str,
stdout: StdoutTy,
stderr: StderrTy,
) -> Result<Self, LisaError> {
let environment_prefix = app_name_to_prefix(app_name);
let stdout_renderer = Self::choose_default_renderer(&stdout, &environment_prefix)
.ok_or(LisaError::NoRendererFound)?;
let stderr_renderer = Self::choose_default_renderer(&stderr, &environment_prefix)
.ok_or(LisaError::NoRendererFound)?;
let (log_messages, log_receiver) = bounded_channel(LOG_CHANNEL_SIZE);
let (flush_sender, flush_receiver) = bounded_channel(1);
let (flush_completed_sender, flush_completed_receiver) = bounded_channel(1);
let (stop_tick_sender, stop_tick_receiver) = bounded_channel(1);
let state = Arc::new(Mutex::new(UnlockedRendererState::new(
app_name,
&environment_prefix,
(stdout, stderr),
(stdout_renderer, stderr_renderer),
log_receiver,
)));
Self::spawn_tick_task(
flush_completed_sender,
flush_receiver,
stop_tick_receiver,
state.clone(),
)?;
Ok(Self {
did_do_flush: AsyncMutex::new(flush_completed_receiver),
flush_sender,
log_messages,
state,
stop_tick_task: stop_tick_sender,
})
}
pub fn new_with_outputs_and_preselected_renderers(
app_name: &'static str,
stdout: StdoutTy,
stderr: StderrTy,
stdout_renderer: Box<dyn ConsoleRenderer>,
stderr_renderer: Box<dyn ConsoleRenderer>,
) -> Result<Self, LisaError> {
let environment_prefix = app_name_to_prefix(app_name);
let (log_messages, log_receiver) = bounded_channel(LOG_CHANNEL_SIZE);
let (flush_sender, flush_receiver) = bounded_channel(1);
let (flush_completed_sender, flush_completed_receiver) = bounded_channel(1);
let (stop_tick_sender, stop_tick_receiver) = bounded_channel(1);
let state = Arc::new(Mutex::new(UnlockedRendererState::new(
app_name,
&environment_prefix,
(stdout, stderr),
(stdout_renderer, stderr_renderer),
log_receiver,
)));
Self::spawn_tick_task(
flush_completed_sender,
flush_receiver,
stop_tick_receiver,
state.clone(),
)?;
Ok(Self {
did_do_flush: AsyncMutex::new(flush_completed_receiver),
flush_sender,
log_messages,
state,
stop_tick_task: stop_tick_sender,
})
}
pub async fn flush(&self) {
let mut did_do_flush_lock = self.did_do_flush.lock().await;
_ = self.flush_sender.send(()).await;
_ = did_do_flush_lock.recv().await;
}
pub fn get_render_state(
&self,
) -> MutexGuard<'_, RawMutex, UnlockedRendererState<StdoutTy, StderrTy>> {
self.state.lock()
}
pub fn set_input_provider<Ty: InputProvider + Sized + 'static>(&self, provider: Ty) {
self.state.lock().set_input_provider(Box::new(provider));
}
pub fn set_task_provider<Ty: TaskEventLogProvider + Sized + 'static>(&self, provider: Ty) {
self.state.lock().set_task_provider(Box::new(provider));
}
pub fn set_input_channel(&self, channel: BoundedSender<String>) {
self.state.lock().set_completed_input_channel(channel);
}
pub fn set_input_active(&self, active: bool) -> Result<(), LisaError> {
self.state.lock().set_input_active(active)
}
pub fn get_unprocessed_inputs(&self) -> Vec<String> {
self.state.lock().get_unprocessed_inputs()
}
#[must_use]
pub fn choose_default_renderer(
features: &dyn ConsoleOutputFeatures,
environment_prefix: &str,
) -> Option<Box<dyn ConsoleRenderer>> {
let color = ColorConsoleRenderer::new();
if color.should_use_renderer(features, environment_prefix) {
return Some(Box::new(color));
}
std::mem::drop(color);
let json = JSONConsoleRenderer::new();
if json.should_use_renderer(features, environment_prefix) {
return Some(Box::new(json));
}
std::mem::drop(json);
let text = TextConsoleRenderer::new();
if text.should_use_renderer(features, environment_prefix) {
return Some(Box::new(text));
}
std::mem::drop(text);
None
}
#[allow(
// Dependent on OS...
unreachable_code,
)]
#[must_use]
pub fn terminal_height_and_width() -> Option<(u16, u16)> {
#[cfg(any(
target_os = "aix",
target_os = "linux",
target_os = "android",
target_os = "macos",
target_os = "ios",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
target_os = "solaris",
target_os = "illumos",
target_os = "haiku",
))]
{
use crate::termios::tcgetwinsize;
let winsize = tcgetwinsize(0).ok()?;
return Some((winsize.ws_row, winsize.ws_col));
}
#[cfg(target_os = "windows")]
{
use windows::Win32::System::Console::{
CONSOLE_SCREEN_BUFFER_INFO, GetConsoleScreenBufferInfo, GetStdHandle,
STD_INPUT_HANDLE,
};
let mut buffer_info = CONSOLE_SCREEN_BUFFER_INFO::default();
let handle = unsafe { GetStdHandle(STD_INPUT_HANDLE) }.ok()?;
unsafe { GetConsoleScreenBufferInfo(handle, &raw mut buffer_info).ok()? };
return Some((
u16::try_from(buffer_info.dwSize.Y).unwrap_or(u16::MIN),
u16::try_from(buffer_info.dwSize.X).unwrap_or(u16::MIN),
));
}
None
}
#[must_use]
pub fn terminal_height() -> Option<u16> {
Self::terminal_height_and_width().map(|tuple| tuple.0)
}
#[must_use]
pub fn terminal_width() -> Option<u16> {
Self::terminal_height_and_width().map(|tuple| tuple.1)
}
pub(crate) async fn log(&self, log_message: SuperConsoleLogMessage) {
_ = self.log_messages.send(log_message).await;
}
pub(crate) async fn log_sync(&self, log_message: SuperConsoleLogMessage) {
_ = self.log_messages.send(log_message).await;
}
fn spawn_tick_task(
flush_completed_sender: BoundedSender<()>,
mut flush_recveiver: BoundedReceiver<()>,
mut receiver: BoundedReceiver<()>,
state: Arc<Mutex<UnlockedRendererState<StdoutTy, StderrTy>>>,
) -> Result<(), LisaError> {
TaskBuilder::new()
.name("lisa::display::SuperConsole::tick")
.spawn(async move {
loop {
let mut flush_queued = false;
tokio::select! {
() = sleep(Duration::from_millis(12)) => {}
_ = flush_recveiver.recv() => {
flush_queued = true;
}
_ = receiver.recv() => {
break;
}
_ = ctrl_c() => {
break;
}
}
_ = state.lock().render_if_needed();
if flush_queued {
_ = flush_completed_sender.send(()).await;
}
}
})?;
Ok(())
}
}
impl<
StdoutTy: ConsoleOutputFeatures + IoWrite + Send,
StderrTy: ConsoleOutputFeatures + IoWrite + Send,
> Drop for SuperConsole<StdoutTy, StderrTy>
{
fn drop(&mut self) {
let cloned_flush_sender = self.flush_sender.clone();
let cloned_stop_tick_task = self.stop_tick_task.clone();
_ = TaskBuilder::new()
.name("lisa::display::SuperConsole::flush_drop")
.spawn(async move {
_ = cloned_flush_sender.send(()).await;
_ = cloned_stop_tick_task.send(()).await;
});
}
}
#[cfg(test)]
mod unit_tests {
use super::*;
#[test]
pub fn is_send_sync() {
fn accepts_send_sync<Ty: Send + Sync>() {}
accepts_send_sync::<SuperConsole<Stdout, Stderr>>();
}
#[test]
pub fn term_size_matches_terminal_size_crate() {
assert_eq!(
SuperConsole::<Stdout, Stderr>::terminal_width(),
terminal_size::terminal_size().map(|item| item.0.0),
"{:?} != {:?}, (us == term_size) [WIDTH]",
SuperConsole::<Stdout, Stderr>::terminal_width()
.expect("Failed to query terminal width"),
terminal_size::terminal_size().map(|item| item.0.0)
);
assert_eq!(
SuperConsole::<Stdout, Stderr>::terminal_height(),
terminal_size::terminal_size().map(|item| item.1.0),
"{:?} != {:?}, (us == term_size) [HEIGHT]",
SuperConsole::<Stdout, Stderr>::terminal_width()
.expect("Failed to query terminal height"),
terminal_size::terminal_size().map(|item| item.1.0)
);
}
}