use std::io;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::sync::Once;
use std::time::Instant;
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use tachyonfx::{Effect, Shader};
use super::draw::render_frame;
use super::lifecycle;
use super::render_model::RenderModel;
pub(crate) enum RenderCmd {
Teardown { ack: mpsc::SyncSender<()> },
SpawnBootFx { fx: Effect },
SpawnExitFx { fx: Effect },
Clear,
Pause { ack: mpsc::SyncSender<()> },
Resume,
}
#[derive(Clone)]
pub(crate) struct FrameSlot {
inner: Arc<parking_lot::Mutex<Option<Arc<RenderModel>>>>,
render_thread: std::thread::Thread,
}
impl FrameSlot {
fn new(
inner: Arc<parking_lot::Mutex<Option<Arc<RenderModel>>>>,
render_thread: std::thread::Thread,
) -> Self {
FrameSlot { inner, render_thread }
}
pub(crate) fn publish(&self, model: Arc<RenderModel>) {
*self.inner.lock() = Some(model);
self.render_thread.unpark();
}
}
pub(crate) struct RenderHandle {
slot: FrameSlot,
pub(crate) cmd_tx: mpsc::Sender<RenderCmd>,
join_handle: Option<std::thread::JoinHandle<()>>,
}
impl RenderHandle {
pub(crate) fn publish(&self, model: std::sync::Arc<super::render_model::RenderModel>) {
self.slot.publish(model);
}
fn wake(&self) {
self.slot.render_thread.unpark();
}
pub(crate) fn send_boot_fx(&self, fx: Effect) {
let _ = self.cmd_tx.send(RenderCmd::SpawnBootFx { fx });
self.wake();
}
pub(crate) fn send_exit_fx(&self, fx: Effect) {
let _ = self.cmd_tx.send(RenderCmd::SpawnExitFx { fx });
self.wake();
}
pub(crate) fn send_clear(&self) {
let _ = self.cmd_tx.send(RenderCmd::Clear);
self.wake();
}
pub(crate) fn pause(&self) -> bool {
let (ack_tx, ack_rx) = mpsc::sync_channel::<()>(1);
let _ = self.cmd_tx.send(RenderCmd::Pause { ack: ack_tx });
self.wake();
ack_rx.recv_timeout(std::time::Duration::from_secs(2)).is_ok()
}
pub(crate) fn resume(&self) {
let _ = self.cmd_tx.send(RenderCmd::Resume);
self.wake();
}
pub(crate) fn teardown(mut self, timeout: std::time::Duration) -> bool {
let (ack_tx, ack_rx) = mpsc::sync_channel::<()>(1);
let _ = self.cmd_tx.send(RenderCmd::Teardown { ack: ack_tx });
self.wake();
let acked = ack_rx.recv_timeout(timeout).is_ok();
if acked {
if let Some(handle) = self.join_handle.take() {
let _ = handle.join();
}
}
acked
}
}
impl Drop for RenderHandle {
fn drop(&mut self) {
let (dead_tx, _dead_rx) = mpsc::channel::<RenderCmd>();
let _old_tx = std::mem::replace(&mut self.cmd_tx, dead_tx);
drop(_old_tx); self.slot.render_thread.unpark();
drop(self.join_handle.take());
}
}
static PANIC_HOOK_INSTALLED: Once = Once::new();
fn install_panic_hook_once() {
PANIC_HOOK_INSTALLED.call_once(|| {
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
super::lifecycle::emergency_teardown_terminal();
prev(info);
}));
});
}
pub(crate) fn spawn_render_thread(
terminal: Terminal<CrosstermBackend<io::Stdout>>,
) -> (RenderHandle, Arc<AtomicBool>, Arc<AtomicBool>) {
install_panic_hook_once();
let inner: Arc<parking_lot::Mutex<Option<Arc<RenderModel>>>> =
Arc::new(parking_lot::Mutex::new(None));
let inner_thread = Arc::clone(&inner);
let (cmd_tx, cmd_rx) = mpsc::channel::<RenderCmd>();
let boot_done = Arc::new(AtomicBool::new(false));
let boot_done_thread = Arc::clone(&boot_done);
let exit_done = Arc::new(AtomicBool::new(false));
let exit_done_thread = Arc::clone(&exit_done);
let (thread_tx, thread_rx) = mpsc::sync_channel::<std::thread::Thread>(1);
let join_handle = std::thread::Builder::new()
.name("agent-tui-render".to_string())
.spawn(move || {
let _ = thread_tx.send(std::thread::current());
let mut terminal_opt = Some(terminal);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let term = terminal_opt.take().expect("terminal already taken");
render_thread_body(term, inner_thread, cmd_rx, boot_done_thread, Arc::clone(&exit_done_thread));
}));
if let Err(payload) = result {
let msg = payload
.downcast_ref::<&str>()
.copied()
.or_else(|| payload.downcast_ref::<String>().map(|s| s.as_str()))
.unwrap_or("<non-string panic payload>");
tracing::error!(panic = msg, "render thread panicked — restoring terminal");
if let Some(mut term) = terminal_opt {
do_teardown(&mut term);
}
exit_done_thread.store(true, Ordering::Release);
}
})
.expect("failed to spawn render thread");
let render_thread = thread_rx.recv().expect("render thread failed to send its Thread handle");
let slot = FrameSlot::new(inner, render_thread);
let handle = RenderHandle {
slot,
cmd_tx,
join_handle: Some(join_handle),
};
(handle, boot_done, exit_done)
}
fn render_thread_body(
mut terminal: Terminal<CrosstermBackend<io::Stdout>>,
inner: Arc<parking_lot::Mutex<Option<Arc<RenderModel>>>>,
cmd_rx: mpsc::Receiver<RenderCmd>,
boot_done: Arc<AtomicBool>,
exit_done: Arc<AtomicBool>,
) {
let mut last_frame = Instant::now();
let mut boot_fx: Option<Effect> = None;
let mut exit_fx: Option<Effect> = None;
let mut pending_clear = false;
let mut paused = false;
loop {
std::thread::park();
loop {
match cmd_rx.try_recv() {
Ok(RenderCmd::SpawnBootFx { fx }) => {
boot_fx = Some(fx);
}
Ok(RenderCmd::SpawnExitFx { fx }) => {
exit_fx = Some(fx);
exit_done.store(false, Ordering::Release);
}
Ok(RenderCmd::Clear) => {
pending_clear = true;
}
Ok(RenderCmd::Pause { ack }) => {
inner.lock().take();
let _ = ack.send(());
paused = true;
}
Ok(RenderCmd::Resume) => {
paused = false;
pending_clear = true;
}
Ok(RenderCmd::Teardown { ack }) => {
do_teardown(&mut terminal);
let _ = ack.send(());
return; }
Err(mpsc::TryRecvError::Empty) => break,
Err(mpsc::TryRecvError::Disconnected) => {
do_teardown(&mut terminal);
return;
}
}
}
if paused {
inner.lock().take();
continue;
}
if pending_clear {
terminal.clear().ok();
pending_clear = false;
}
while let Some(model) = inner.lock().take() {
if let Err(e) = render_frame(
&mut terminal,
&model,
&mut boot_fx,
&mut exit_fx,
&mut last_frame,
) {
tracing::warn!(err = %e, "render thread: terminal write failed — PTY likely closed");
}
}
if boot_fx.as_ref().is_some_and(|fx| fx.done()) {
boot_done.store(true, Ordering::Release);
boot_fx = None; }
if exit_fx.as_ref().is_some_and(|fx| fx.done()) {
exit_done.store(true, Ordering::Release);
}
}
}
fn do_teardown(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) {
lifecycle::emergency_teardown_terminal();
terminal.show_cursor().ok();
}