use std::io::{self, Write};
use std::time::Duration;
use crate::app::{App, Ctx};
use crate::element::Element;
use crate::input::InputEvent;
use crate::task::Effect;
use crate::timeline::Timeline;
pub struct Runtime<A: App> {
app: A,
timeline: Timeline,
animate: Option<Duration>,
effects: Vec<Effect<A::Msg>>,
pending: Vec<u8>,
init_exit: Option<A::Output>,
}
impl<A: App> Runtime<A>
where
A::Msg: Clone,
{
pub fn new(mut app: A, width: u16, terminal_height: u16) -> Self {
let mut timeline = Timeline::new(width, terminal_height);
let mut pending = Vec::new();
let mut effects = Vec::new();
let mut ctx = Ctx {
timeline: &mut timeline,
output: &mut pending,
effects: &mut effects,
exit: None,
};
app.init(&mut ctx);
let init_exit = ctx.exit;
Self {
app,
timeline,
animate: None,
effects,
pending,
init_exit,
}
}
pub fn startup(&mut self) -> (Vec<u8>, Option<A::Output>) {
(self.present(), self.init_exit.take())
}
pub fn handle(&mut self, event: InputEvent) -> (Vec<u8>, Option<A::Output>) {
match self.app.keymap().dispatch(&event) {
Some(msg) => self.process(msg),
None => (Vec::new(), None),
}
}
pub fn process(&mut self, msg: A::Msg) -> (Vec<u8>, Option<A::Output>) {
self.process_batch(std::iter::once(msg))
}
pub fn process_batch(
&mut self,
msgs: impl IntoIterator<Item = A::Msg>,
) -> (Vec<u8>, Option<A::Output>) {
let mut bytes = std::mem::take(&mut self.pending);
let mut exit = None;
for msg in msgs {
let mut ctx = Ctx {
timeline: &mut self.timeline,
output: &mut bytes,
effects: &mut self.effects,
exit: None,
};
self.app.update(msg, &mut ctx);
if let Some(output) = ctx.exit {
exit = Some(output);
break;
}
}
bytes.extend_from_slice(&self.present());
if exit.is_some() {
bytes.extend_from_slice(&self.timeline.finalize());
}
(bytes, exit)
}
pub fn take_effects(&mut self) -> Vec<Effect<A::Msg>> {
std::mem::take(&mut self.effects)
}
pub fn present(&mut self) -> Vec<u8> {
let tail = self.app.tail();
self.animate = tail.animated();
let mut bytes = std::mem::take(&mut self.pending);
bytes.extend(self.timeline.present(&tail));
bytes
}
pub fn animation_interval(&self) -> Option<Duration> {
self.animate
}
pub fn resize(&mut self, width: u16, terminal_height: u16) -> Vec<u8> {
self.timeline.set_terminal_height(terminal_height);
let mut bytes = self.timeline.resize(width);
bytes.extend_from_slice(&self.present());
bytes
}
pub fn app(&self) -> &A {
&self.app
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum KeyboardProtocol {
#[default]
Legacy,
Enhanced,
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct RunOptions {
pub keyboard: KeyboardProtocol,
}
impl RunOptions {
pub fn keyboard(mut self, protocol: KeyboardProtocol) -> Self {
self.keyboard = protocol;
self
}
}
pub fn run<A: App>(app: A) -> io::Result<A::Output>
where
A::Msg: Clone,
{
run_with(app, RunOptions::default())
}
pub fn run_with<A: App>(app: A, options: RunOptions) -> io::Result<A::Output>
where
A::Msg: Clone,
{
let (width, height) = crossterm::terminal::size()?;
let mut runtime = Runtime::new(app, width, height);
let mut stdout = io::stdout().lock();
let _guard = RawModeGuard::enable(options.keyboard)?;
let (bytes, init_exit) = runtime.startup();
stdout.write_all(&bytes)?;
stdout.flush()?;
if let Some(output) = init_exit {
return Ok(output);
}
reject_effects(&mut runtime)?;
loop {
let timeout = runtime
.animation_interval()
.unwrap_or(Duration::from_secs(3600));
let bytes = if crossterm::event::poll(timeout)? {
use crossterm::event::{Event, KeyEventKind};
match crossterm::event::read()? {
Event::Key(k) if matches!(k.kind, KeyEventKind::Press | KeyEventKind::Repeat) => {
let (bytes, exit) = runtime.handle(InputEvent::Key(k));
reject_effects(&mut runtime)?;
if let Some(output) = exit {
stdout.write_all(&bytes)?;
stdout.flush()?;
return Ok(output);
}
bytes
}
Event::Paste(s) => {
let (bytes, exit) = runtime.handle(InputEvent::Paste(s));
reject_effects(&mut runtime)?;
if let Some(output) = exit {
stdout.write_all(&bytes)?;
stdout.flush()?;
return Ok(output);
}
bytes
}
Event::Resize(w, h) => runtime.resize(w, h),
_ => Vec::new(),
}
} else {
runtime.present()
};
if !bytes.is_empty() {
stdout.write_all(&bytes)?;
stdout.flush()?;
}
}
}
fn reject_effects<A: App>(runtime: &mut Runtime<A>) -> io::Result<()>
where
A::Msg: Clone,
{
if !runtime.take_effects().is_empty() {
return Err(io::Error::other(
"app spawned async work (ctx.spawn/perform); drive it with the tokio runtime \
(eye_declare::driver_tokio::run) instead of the sync run()",
));
}
if !runtime.app().subscriptions().is_empty() {
return Err(io::Error::other(
"app declares subscriptions; drive it with the tokio runtime \
(eye_declare::driver_tokio::run) instead of the sync run()",
));
}
Ok(())
}
pub(crate) struct RawModeGuard {
keyboard_enhanced: bool,
}
impl RawModeGuard {
pub(crate) fn enable(keyboard: KeyboardProtocol) -> io::Result<Self> {
crossterm::terminal::enable_raw_mode()?;
let mut stdout = io::stdout();
let _ = crossterm::execute!(stdout, crossterm::event::EnableBracketedPaste);
let keyboard_enhanced = keyboard == KeyboardProtocol::Enhanced
&& crossterm::terminal::supports_keyboard_enhancement().unwrap_or(false);
if keyboard_enhanced {
let _ = crossterm::execute!(
stdout,
crossterm::event::PushKeyboardEnhancementFlags(
crossterm::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
)
);
}
Ok(Self { keyboard_enhanced })
}
}
impl Drop for RawModeGuard {
fn drop(&mut self) {
let mut stdout = io::stdout();
if self.keyboard_enhanced {
let _ = crossterm::execute!(stdout, crossterm::event::PopKeyboardEnhancementFlags);
}
let _ = crossterm::execute!(stdout, crossterm::event::DisableBracketedPaste);
let _ = crossterm::terminal::disable_raw_mode();
let _ = stdout.write_all(b"\x1b[?25h");
let _ = stdout.flush();
}
}