use std::cmp;
use std::env;
use std::io;
use crossterm::terminal::Clear;
use crossterm::terminal::ClearType;
use crossterm::tty::IsTty;
use crossterm::QueueableCommand;
use crate::ansi_support::enable_ansi_support;
use crate::components::Canvas;
use crate::components::Component;
use crate::components::DrawMode;
use crate::content::Line;
use crate::output::BlockingSuperConsoleOutput;
use crate::output::SuperConsoleOutput;
use crate::Dimensions;
use crate::Direction;
use crate::Lines;
const MINIMUM_EMIT: usize = 5;
const MAX_GRAPHEME_BUFFER: usize = 1000000;
pub struct SuperConsole {
root: Canvas,
to_emit: Lines,
fallback_size: Option<Dimensions>,
pub(crate) output: Box<dyn SuperConsoleOutput>,
}
impl SuperConsole {
pub fn new() -> Option<Self> {
Self::compatible().then(|| {
Self::new_internal(
None,
Box::new(BlockingSuperConsoleOutput::new(Box::new(io::stderr()))),
)
})
}
pub fn forced_new(fallback_size: Dimensions) -> Self {
Self::new_internal(
Some(fallback_size),
Box::new(BlockingSuperConsoleOutput::new(Box::new(io::stderr()))),
)
}
pub(crate) fn new_internal(
fallback_size: Option<Dimensions>,
output: Box<dyn SuperConsoleOutput>,
) -> Self {
Self {
root: Canvas::new(),
to_emit: Lines::new(),
fallback_size,
output,
}
}
pub fn compatible() -> bool {
io::stderr().is_tty() && !Self::is_term_dumb() && enable_ansi_support().is_ok()
}
fn is_term_dumb() -> bool {
matches!(env::var("TERM"), Ok(kind) if kind == "dumb")
}
pub fn render(&mut self, root: &dyn Component) -> anyhow::Result<()> {
let mut anything_emitted = true;
let mut has_rendered = false;
while !has_rendered || (anything_emitted && !self.to_emit.is_empty()) {
if !self.output.should_render() {
break;
}
let last_len = self.to_emit.len();
self.render_with_mode(root, DrawMode::Normal)?;
anything_emitted = last_len == self.to_emit.len();
has_rendered = true;
}
Ok(())
}
pub fn finalize(self, root: &dyn Component) -> anyhow::Result<()> {
self.finalize_with_mode(root, DrawMode::Final)
}
pub fn finalize_with_mode(
mut self,
root: &dyn Component,
mode: DrawMode,
) -> anyhow::Result<()> {
self.render_with_mode(root, mode)?;
self.output.finalize()
}
pub fn emit_now(&mut self, lines: Lines, root: &dyn Component) -> anyhow::Result<()> {
self.emit(lines);
self.render(root)
}
pub fn emit(&mut self, mut lines: Lines) {
self.to_emit.0.append(&mut lines.0);
}
fn size(&self) -> anyhow::Result<Dimensions> {
match (self.output.terminal_size(), self.fallback_size) {
(Ok(size), Some(fallback)) if size.width == 0 || size.height == 0 => Ok(fallback),
(Ok(size), _) => Ok(size),
(Err(_), Some(fallback)) => Ok(fallback),
(Err(e), None) => Err(e),
}
}
pub fn clear(&mut self) -> anyhow::Result<()> {
let mut buffer = vec![];
self.root.clear(&mut buffer)?;
self.output.output(buffer)
}
fn render_with_mode(&mut self, root: &dyn Component, mode: DrawMode) -> anyhow::Result<()> {
let size = self.size()?.saturating_sub(1, Direction::Vertical);
let mut buffer = Vec::new();
self.render_general(&mut buffer, root, mode, size)?;
self.output.output(buffer)
}
fn render_general(
&mut self,
buffer: &mut Vec<u8>,
root: &dyn Component,
mode: DrawMode,
size: Dimensions,
) -> anyhow::Result<()> {
#[allow(clippy::ptr_arg)]
fn is_big(buf: &Lines) -> bool {
let len: usize = buf.iter().map(Line::len).sum();
len > MAX_GRAPHEME_BUFFER
}
self.root.move_up(buffer)?;
let mut frame = self.root.draw(root, size, mode)?;
let limit = match mode {
DrawMode::Normal if !is_big(&self.to_emit) => {
let limit = size.height.saturating_sub(frame.len());
Some(cmp::max(limit, MINIMUM_EMIT))
}
_ => None,
};
self.to_emit.render(buffer, limit)?;
frame.render(buffer, None)?;
buffer.queue(Clear(ClearType::FromCursorDown))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use anyhow::Context as _;
use derive_more::AsRef;
use super::*;
use crate::components::echo::Echo;
use crate::testing::frame_contains;
use crate::testing::test_console;
use crate::testing::SuperConsoleTestingExt;
use crate::Lines;
#[derive(AsRef, Debug)]
struct Msg(Lines);
#[test]
fn test_small_buffer() -> anyhow::Result<()> {
let mut console = test_console();
let msg_count = MINIMUM_EMIT + 5;
console.emit(Lines(vec![vec!["line 1"].try_into()?; msg_count]));
let msg = Lines(vec![vec!["line"].try_into()?; msg_count]);
let root = Echo(msg);
let mut buffer = Vec::new();
console.render_general(
&mut buffer,
&root,
DrawMode::Normal,
Dimensions::new(100, 2),
)?;
assert_eq!(console.to_emit.len(), msg_count - MINIMUM_EMIT);
Ok(())
}
#[test]
fn test_huge_buffer() -> anyhow::Result<()> {
let mut console = test_console();
console.emit(Lines(vec![
vec!["line 1"].try_into()?;
MAX_GRAPHEME_BUFFER * 2
]));
let root = Echo(Lines(vec![vec!["line"].try_into()?; 1]));
let mut buffer = Vec::new();
console.render_general(
&mut buffer,
&root,
DrawMode::Normal,
Dimensions::new(100, 20),
)?;
assert!(console.to_emit.is_empty());
Ok(())
}
#[test]
fn test_block_render() -> anyhow::Result<()> {
let mut console = test_console();
let root = Echo(Lines(vec![vec!["state"].try_into()?; 1]));
console.render(&root)?;
assert_eq!(console.test_output()?.frames.len(), 1);
console.test_output_mut()?.should_render = false;
console.render(&root)?;
assert_eq!(console.test_output()?.frames.len(), 1);
console.emit(Lines(vec![vec!["line 1"].try_into()?]));
console.render(&root)?;
assert_eq!(console.test_output()?.frames.len(), 1);
Ok(())
}
#[test]
fn test_block_lines() -> anyhow::Result<()> {
let mut console = test_console();
let root = Echo(Lines(vec![vec!["state"].try_into()?; 1]));
console.test_output_mut()?.should_render = false;
console.emit(Lines(vec![vec!["line 1"].try_into()?]));
console.render(&root)?;
assert_eq!(console.test_output()?.frames.len(), 0);
console.test_output_mut()?.should_render = true;
console.emit(Lines(vec![vec!["line 2"].try_into()?]));
console.render(&root)?;
let frame = console
.test_output_mut()?
.frames
.pop()
.context("No frame was emitted")?;
assert!(frame_contains(&frame, "state"));
assert!(frame_contains(&frame, "line 1"));
assert!(frame_contains(&frame, "line 2"));
Ok(())
}
#[test]
fn test_block_finalize() -> anyhow::Result<()> {
let mut console = test_console();
let root = Echo(Lines(vec![vec!["state"].try_into()?; 1]));
console.test_output_mut()?.should_render = false;
console.emit(Lines(vec![vec!["line 1"].try_into()?]));
console.emit(Lines(vec![vec!["line 2"].try_into()?]));
console.render_with_mode(&root, DrawMode::Final)?;
let frame = console
.test_output_mut()?
.frames
.pop()
.context("No frame was emitted")?;
assert!(frame_contains(&frame, "state"));
assert!(frame_contains(&frame, "line 1"));
assert!(frame_contains(&frame, "line 2"));
Ok(())
}
}