use std::io;
use std::thread;
use std::time::Duration;
use crossbeam_channel::{Receiver, Sender, unbounded};
use crossterm::event::{self, Event, KeyEventKind};
use crossterm::execute;
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use rite_runtime::{ExecEvent, UiCommand};
use crate::model::Model;
use crate::msg::{Cmd, Msg};
use crate::update::update;
use crate::view::view;
const TICK_INTERVAL: Duration = Duration::from_millis(100);
const INPUT_POLL_INTERVAL: Duration = Duration::from_millis(50);
pub fn run(cmd_tx: &Sender<UiCommand>, event_rx: Receiver<ExecEvent>) -> io::Result<()> {
let _guard = RawTerminalGuard::enter()?;
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
let (msg_tx, msg_rx) = unbounded::<Msg>();
spawn_input_thread(msg_tx.clone());
spawn_tick_thread(msg_tx.clone());
spawn_exec_forwarder(event_rx, msg_tx);
let mut model = Model::new();
let mut applied_scroll = 0usize;
terminal.draw(|frame| {
applied_scroll = view(&model, frame);
})?;
model.log_scroll = applied_scroll;
let mut last_clock_key = Some(model.clock_display_key());
'main: loop {
let Ok(msg) = msg_rx.recv() else { break 'main };
let is_tick = matches!(msg, Msg::Tick);
let is_exec = matches!(msg, Msg::Exec(_));
let pending_before = model.pending_events.len();
for cmd in update(&mut model, msg) {
match cmd {
Cmd::SendCommand(c) => {
if cmd_tx.send(c).is_err() {
break 'main;
}
}
Cmd::Quit => break 'main,
}
}
let drip_applied = model.pending_events.len() < pending_before;
let needs_redraw = if is_tick {
let key = model.clock_display_key();
Some(key) != last_clock_key || model.has_active_spinner() || drip_applied
} else if is_exec {
false
} else {
true
};
if needs_redraw {
last_clock_key = Some(model.clock_display_key());
terminal.draw(|frame| {
applied_scroll = view(&model, frame);
})?;
model.log_scroll = applied_scroll;
}
}
Ok(())
}
fn spawn_input_thread(msg_tx: Sender<Msg>) {
thread::spawn(move || {
loop {
match event::poll(INPUT_POLL_INTERVAL) {
Ok(true) => {
let send_result = match event::read() {
Ok(Event::Key(key)) if key.kind == KeyEventKind::Press => {
msg_tx.send(Msg::Key(key))
}
Ok(Event::Resize(cols, rows)) => msg_tx.send(Msg::Resize { cols, rows }),
Ok(Event::Mouse(m)) => msg_tx.send(Msg::Mouse(m)),
_ => Ok(()),
};
if send_result.is_err() {
return;
}
}
Ok(false) => {}
Err(_) => return,
}
}
});
}
fn spawn_tick_thread(msg_tx: Sender<Msg>) {
thread::spawn(move || {
loop {
thread::sleep(TICK_INTERVAL);
if msg_tx.send(Msg::Tick).is_err() {
return;
}
}
});
}
fn spawn_exec_forwarder(event_rx: Receiver<ExecEvent>, msg_tx: Sender<Msg>) {
thread::spawn(move || {
while let Ok(event) = event_rx.recv() {
if msg_tx.send(Msg::Exec(event)).is_err() {
return;
}
}
let _ = msg_tx.send(Msg::Quit);
});
}
struct RawTerminalGuard;
impl RawTerminalGuard {
fn enter() -> io::Result<Self> {
enable_raw_mode()?;
execute!(io::stdout(), EnterAlternateScreen)?;
Ok(Self)
}
}
impl Drop for RawTerminalGuard {
fn drop(&mut self) {
let _ = execute!(io::stdout(), LeaveAlternateScreen);
let _ = disable_raw_mode();
}
}