#![deny(
rustdoc::broken_intra_doc_links,
unreachable_pub,
unreachable_patterns,
unused,
unused_qualifications,
missing_docs,
while_true,
trivial_casts,
trivial_bounds,
trivial_numeric_casts,
unconditional_panic,
clippy::all
)]
use std::{cell::RefCell, rc::Rc};
use backend::{DehydratedSpan, YewBackend};
use base16_palettes::Palette;
use prelude::utils::{
process_resize_event, process_touch_init_event, process_touch_move_event, process_wheel_event,
TouchScroll,
};
use ratatui::{prelude::Rect, Frame, Terminal};
use yew::{Component, Context, Properties};
pub mod backend;
pub mod prelude;
mod utils;
pub struct WebTerminal<A> {
app: A,
term: RefCell<Terminal<YewBackend>>,
}
pub enum WebTermMessage<M> {
Inner(M),
Resized,
Scrolled(ScrollMotion),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollMotion {
Up,
Down,
}
impl<M> WebTermMessage<M> {
pub fn new<I: Into<M>>(inner: I) -> Self {
Self::Inner(inner.into())
}
}
impl<M> From<M> for WebTermMessage<M> {
fn from(value: M) -> Self {
Self::Inner(value)
}
}
#[derive(Properties, PartialEq)]
pub struct WebTermProps<M: PartialEq> {
inner: M,
palette: Palette,
}
impl<M: PartialEq> WebTermProps<M> {
pub fn new(inner: M) -> Self {
Self {
inner,
palette: Palette::default(),
}
}
pub fn new_with_palette(inner: M, palette: Palette) -> Self {
Self { inner, palette }
}
}
pub struct TermContext<'a, A: TerminalApp> {
ctx: &'a Context<WebTerminal<A>>,
term: &'a mut Terminal<YewBackend>,
}
impl<'a, A: TerminalApp> TermContext<'a, A> {
pub fn terminal(&mut self) -> &mut Terminal<YewBackend> {
self.term
}
pub fn ctx(&self) -> &Context<WebTerminal<A>> {
self.ctx
}
}
pub trait TerminalApp: 'static + Clone + PartialEq {
type Message;
#[allow(unused_variables)]
fn setup(&mut self, ctx: &Context<WebTerminal<Self>>) {}
#[allow(unused_variables)]
fn scroll(&mut self, scroll: ScrollMotion) -> bool {
false
}
fn update(&mut self, ctx: TermContext<'_, Self>, msg: Self::Message) -> bool;
fn render(&self, area: Rect, frame: &mut Frame<'_>);
#[allow(unused_variables)]
fn hydrate(&self, ctx: &Context<WebTerminal<Self>>, span: &mut DehydratedSpan) {}
}
impl<A: Default + TerminalApp> WebTerminal<A> {
pub fn render() {
yew::Renderer::<Self>::new().render();
}
}
impl<A: Default> Default for WebTerminal<A> {
fn default() -> Self {
Self {
app: A::default(),
term: RefCell::new(Terminal::new(YewBackend::new()).unwrap()),
}
}
}
impl<M: PartialEq + Default> Default for WebTermProps<M> {
fn default() -> Self {
Self {
inner: M::default(),
palette: Palette::default(),
}
}
}
pub fn run_tui<A: TerminalApp>(app: A) {
yew::Renderer::<WebTerminal<A>>::with_props(WebTermProps::new(app)).render();
}
impl<A: TerminalApp> Component for WebTerminal<A> {
type Message = WebTermMessage<A::Message>;
type Properties = WebTermProps<A>;
fn create(ctx: &Context<Self>) -> Self {
let mut app = ctx.props().inner.clone();
app.setup(ctx);
let palette = ctx.props().palette;
let term = RefCell::new(Terminal::new(YewBackend::new_with_palette(palette)).unwrap());
let window = web_sys::window().unwrap();
window.set_onresize(Some(&process_resize_event(ctx)));
window.set_onwheel(Some(&process_wheel_event(ctx)));
let acc = Rc::new(RefCell::new(TouchScroll::new()));
window.set_ontouchstart(Some(&process_touch_init_event(acc.clone())));
window.set_ontouchmove(Some(&process_touch_move_event(ctx, acc)));
Self { app, term }
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
let ctx = TermContext {
ctx,
term: self.term.get_mut(),
};
match msg {
WebTermMessage::Inner(msg) => self.app.update(ctx, msg),
WebTermMessage::Scrolled(dir) => self.app.scroll(dir),
WebTermMessage::Resized => {
self.term.get_mut().backend_mut().resize_buffer();
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> yew::Html {
let mut term = self.term.borrow_mut();
let area = term.size().unwrap();
term.draw(|frame| self.app.render(area, frame)).unwrap();
term.backend_mut()
.hydrate(|span| self.app.hydrate(ctx, span))
}
}