pub mod event_ring;
pub mod input;
pub mod log;
pub mod mode;
pub mod picker;
pub mod prompt;
pub mod render;
pub mod session;
pub mod state;
pub mod terminal;
use std::io;
use std::sync::Arc;
use std::time::{Duration, Instant};
use crossterm::event::{Event, EventStream};
use futures::StreamExt;
use parking_lot::RwLock;
use thiserror::Error;
use tokio::sync::broadcast;
use zero_commands::{DispatchContext, run_bypass_friction};
use zero_engine_client::{EngineEvent, EngineState};
pub use mode::Mode;
pub use session::SessionSink;
pub use state::{ActiveOverlay, AppState, FrictionOutcome, FrictionPause};
#[derive(Debug, Error)]
pub enum AppError {
#[error("io: {0}")]
Io(#[from] io::Error),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct AppExit {
pub wrap_off: bool,
}
#[derive(Debug)]
pub struct App {
state: AppState,
ctx: DispatchContext,
events: Option<broadcast::Receiver<EngineEvent>>,
}
impl App {
#[must_use]
pub fn new(engine: Arc<RwLock<EngineState>>, ctx: DispatchContext) -> Self {
let rate_budget = ctx.http.as_ref().and_then(|c| c.rate_budget().cloned());
let mut state = AppState::new(engine);
state.rate_budget = rate_budget;
Self {
state,
ctx,
events: None,
}
}
#[must_use]
pub fn new_with_sink(
engine: Arc<RwLock<EngineState>>,
ctx: DispatchContext,
sink: SessionSink,
) -> Self {
let rate_budget = ctx.http.as_ref().and_then(|c| c.rate_budget().cloned());
let mut state = AppState::new_with_sink(engine, Some(sink));
state.rate_budget = rate_budget;
Self {
state,
ctx,
events: None,
}
}
#[must_use]
pub fn with_events(mut self, rx: broadcast::Receiver<EngineEvent>) -> Self {
self.events = Some(rx);
self
}
pub fn state_mut(&mut self) -> &mut AppState {
&mut self.state
}
pub async fn run(mut self) -> Result<AppExit, AppError> {
let mut term = terminal::TerminalGuard::init()?;
let mut events = EventStream::new();
let mut ticker = tokio::time::interval(Duration::from_millis(100));
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
term.tty.draw(|f| render::render(f, &self.state))?;
let run_result = self.drive(&mut term, &mut events, &mut ticker).await;
if let Some(sink) = &self.state.sink {
sink.end();
}
run_result.map(|()| AppExit {
wrap_off: self.state.wrap_off,
})
}
async fn drive(
&mut self,
term: &mut terminal::TerminalGuard,
events: &mut EventStream,
ticker: &mut tokio::time::Interval,
) -> Result<(), AppError> {
while !self.state.should_quit {
tokio::select! {
_ = ticker.tick() => {
self.tick_friction().await;
term.tty.draw(|f| render::render(f, &self.state))?;
}
maybe_event = events.next() => {
match maybe_event {
Some(Ok(Event::Key(key))) => {
if matches!(
key.kind,
crossterm::event::KeyEventKind::Press
| crossterm::event::KeyEventKind::Repeat,
) {
input::handle_key(&mut self.state, key);
}
}
Some(Ok(_)) => {}
Some(Err(e)) => {
tracing::warn!(err = %e, "event stream error");
}
None => break,
}
if let Some(line) = self.state.pending_input.take() {
let ctx = self
.ctx
.clone()
.with_verbose(self.state.verbose)
.with_wrap_off(self.state.wrap_off);
match zero_commands::dispatch(&ctx, &line).await {
Ok(Some(out)) => self.state.apply_dispatch(out),
Ok(None) => {}
Err(e) => tracing::warn!(err = ?e, "dispatch error"),
}
}
self.tick_friction().await;
term.tty.draw(|f| render::render(f, &self.state))?;
}
ev = Self::next_engine_event(&mut self.events) => {
match ev {
Ok(event) => {
self.state.record_engine_event(event);
term.tty.draw(|f| render::render(f, &self.state))?;
}
Err(broadcast::error::RecvError::Lagged(skipped)) => {
tracing::warn!(skipped, "ws broadcast lagged");
self.state.record_events_lagged(skipped);
term.tty.draw(|f| render::render(f, &self.state))?;
}
Err(broadcast::error::RecvError::Closed) => {
tracing::info!("ws broadcast channel closed");
self.events = None;
}
}
}
}
}
Ok(())
}
async fn next_engine_event(
rx: &mut Option<broadcast::Receiver<EngineEvent>>,
) -> Result<EngineEvent, broadcast::error::RecvError> {
match rx.as_mut() {
Some(r) => r.recv().await,
None => std::future::pending().await,
}
}
async fn tick_friction(&mut self) {
let now = Instant::now();
if let Some(cmd) = self.state.take_confirmed_friction_command(now) {
let out = run_bypass_friction(&self.ctx, cmd).await;
self.state.apply_dispatch(out);
}
self.state.poll_risk_overlay(now);
}
}