mod input;
use std::panic;
use std::{
io::{self, Write},
time::{Duration, Instant},
};
use crossterm::{
cursor,
event::{self, Event as CtEvent},
execute, style, terminal,
};
use input::InputQueue;
use crate::grid::Grid;
use crate::output::StyledChar;
use crate::{Event, Frame, pos::Size};
use super::Backend;
pub struct Term<W: io::Write> {
output: W,
prev: Instant,
pending: InputQueue,
title: Option<String>,
chars: Grid<StyledChar>,
need_render: bool,
}
impl Term<io::StdoutLock<'_>> {
pub fn excl() -> io::Result<Self> {
let old = panic::take_hook();
panic::set_hook(Box::new(move |phi| {
let _ = Self::cleanup(&mut io::stderr());
old(phi)
}));
Self::new(io::stdout().lock())
}
}
impl<W: io::Write> Term<W> {
const FRAME_PD: Duration = Duration::from_nanos(1_000_000_000 / 60);
pub fn new(o: W) -> io::Result<Self> {
let (x, y) = terminal::size()?;
Self {
output: o,
prev: Instant::now(),
pending: InputQueue::default(),
title: None,
chars: Grid::<StyledChar>::new(Size::rnew(x as _, y as _)),
need_render: false,
}
.setup()
}
fn setup(mut self) -> io::Result<Self> {
terminal::enable_raw_mode()?;
execute!(
&mut self.output,
terminal::EnterAlternateScreen,
event::EnableBracketedPaste,
event::EnableMouseCapture,
event::EnableFocusChange,
event::PushKeyboardEnhancementFlags(
event::KeyboardEnhancementFlags::REPORT_EVENT_TYPES
| event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
| event::KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES
),
cursor::Hide,
)?;
Ok(self)
}
fn cleanup(mut w: impl Write) -> io::Result<()> {
crossterm::execute!(
w,
cursor::Show,
event::PopKeyboardEnhancementFlags,
event::DisableMouseCapture,
event::DisableFocusChange,
event::DisableBracketedPaste,
terminal::LeaveAlternateScreen,
)?;
terminal::disable_raw_mode()?;
Ok(())
}
}
impl<W: io::Write> Drop for Term<W> {
fn drop(&mut self) {
let _ = Self::cleanup(&mut self.output);
}
}
impl<W: io::Write> Term<W> {
fn raster(&mut self, buf: &mut Vec<u8>) {
buf.clear();
let _ = crossterm::queue!(buf, terminal::Clear(terminal::ClearType::All),);
if self.chars.is_empty() {
return;
}
if let Some(title) = self.title.take() {
let _ = crossterm::queue!(buf, terminal::SetTitle(title));
}
for (i, line) in self.chars.rows().enumerate() {
let _ = crossterm::queue!(buf, cursor::MoveTo(0, i as u16), style::ResetColor,);
let (head, rest) = line.split_first().unwrap();
let _ = write!(buf, "{}{}", head.style.prefix_formatter(), head.char);
let mut style = head.style;
for ch in rest {
if ch.style != style {
let _ = write!(buf, "{}", ch.style.prefix_formatter());
style = ch.style;
}
let _ = write!(buf, "{}", ch.char);
}
}
}
fn render(&mut self) -> io::Result<()> {
let buf = &mut vec![];
self.raster(buf);
self.output.write_all(&*buf)?;
buf.clear();
Ok(())
}
}
impl<W: io::Write> Backend for Term<W> {
fn step(&mut self) -> io::Result<Frame<'_, Event>> {
if self.need_render {
self.render()?;
self.need_render = false;
}
let deadline = self.prev + Self::FRAME_PD;
let (input, redraw) = loop {
let rem = deadline.checked_duration_since(Instant::now());
if let Some(p) = self.pending.pop() {
break (p, rem.is_none());
};
let Some(rem) = rem else {
break (Event::Redraw, true);
};
if event::poll(rem)? {
self.pending.push_ct(event::read()?);
} else {
break (Event::Redraw, true);
}
};
if redraw {
self.need_render = true;
self.prev = Instant::now();
}
if let Some(sz) = self.pending.new_sz {
self.chars.resize(sz);
}
Ok(Frame::new(input, &mut self.chars))
}
fn title(&mut self, title: &str) -> io::Result<()> {
self.title = Some(title.to_owned());
Ok(())
}
}