pub mod view_helper;
pub extern crate crossterm;
pub mod command;
use std::{
any::Any,
io::{stdout, Result, Stdout},
sync::mpsc::{self, Sender},
thread,
};
use crossterm::{
cursor,
event::{read, Event},
execute,
style::Print,
terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType},
};
pub type Message = Box<dyn Any + Send>;
pub type Command = Box<dyn FnOnce() -> Option<Message> + Send + 'static>;
pub struct ResizeEvent(pub u16, pub u16);
pub trait App {
fn init(&self) -> Option<Command> {
None
}
fn update(&mut self, msg: Message) -> Option<Command>;
fn view(&self) -> String;
}
pub fn enable_mouse_capture() -> Result<()> {
execute!(stdout(), crossterm::event::EnableMouseCapture)
}
pub fn run(app: impl App) -> Result<()> {
let mut app = app;
let mut stdout = stdout();
let (msg_tx, msg_rx) = mpsc::channel::<Message>();
let msg_tx2 = msg_tx.clone();
let (cmd_tx, cmd_rx) = mpsc::channel::<Command>();
let cmd_tx2 = cmd_tx.clone();
thread::spawn(move || loop {
match read().unwrap() {
Event::Key(event) => msg_tx.send(Box::new(event)).unwrap(),
Event::Mouse(event) => msg_tx.send(Box::new(event)).unwrap(),
Event::Resize(x, y) => msg_tx.send(Box::new(ResizeEvent(x, y))).unwrap(),
}
});
thread::spawn(move || loop {
let cmd = match cmd_rx.recv() {
Ok(cmd) => cmd,
Err(_) => return,
};
let msg_tx2 = msg_tx2.clone();
thread::spawn(move || {
if let Some(msg) = cmd() {
msg_tx2.send(msg).unwrap();
}
});
});
initialize(&mut stdout, &app, cmd_tx2)?;
let mut prev = normalized_view(&app);
execute!(stdout, Print(&prev))?;
loop {
let msg = msg_rx.recv().unwrap();
if msg.is::<command::QuitMessage>() {
break;
} else if msg.is::<command::BatchMessage>() {
let batch = msg.downcast::<command::BatchMessage>().unwrap();
for cmd in batch.0 {
cmd_tx.send(cmd).unwrap();
}
} else if let Some(cmd) = app.update(msg) {
cmd_tx.send(cmd).unwrap();
}
let curr = normalized_view(&app);
clear_lines(&mut stdout, prev.matches("\r\n").count())?;
execute!(stdout, Print(&curr))?;
prev = curr;
}
deinitialize(&mut stdout)
}
fn initialize(stdout: &mut Stdout, app: &impl App, cmd_tx: Sender<Command>) -> Result<()> {
if let Some(cmd) = app.init() {
cmd_tx.send(cmd).unwrap();
}
enable_raw_mode()?;
execute!(stdout, cursor::Hide)
}
fn normalized_view(app: &impl App) -> String {
let view = app.view();
let view = if !view.ends_with('\n') {
view + "\n"
} else {
view
};
view.replace('\n', "\r\n")
}
fn clear_lines(stdout: &mut Stdout, count: usize) -> Result<()> {
for _ in 0..count {
execute!(
stdout,
cursor::MoveToPreviousLine(1),
Clear(ClearType::CurrentLine)
)?;
}
Ok(())
}
fn deinitialize(stdout: &mut Stdout) -> Result<()> {
execute!(stdout, cursor::Show)?;
execute!(stdout, crossterm::event::DisableMouseCapture)?;
disable_raw_mode()
}