use std::{
ops::ControlFlow,
path::PathBuf,
thread,
time::{Duration, Instant},
};
use crossbeam_channel::{Receiver, Sender, after, bounded, never, select, unbounded};
use crossterm::event;
use ratatui::{
layout::{Rect, Size},
style::Style,
};
use typed_builder::TypedBuilder;
use super::{TerminalGuard, app::App, event::RuntimeEvent, watch::NotifyConfigWatcher};
use crate::{
application::Workspace,
domain::port::{
AgentSessionStore, Notifier, PathCompleter, ProcessRunner, ProjectRegistry, SettingsStore,
},
error::Result,
};
#[derive(TypedBuilder)]
pub struct Adapters {
runner: Box<dyn ProcessRunner>,
registry: Box<dyn ProjectRegistry>,
completer: Box<dyn PathCompleter + Send>,
notifier: Box<dyn Notifier>,
settings_store: Box<dyn SettingsStore>,
agent_session_store: Box<dyn AgentSessionStore>,
}
const INPUT_POLL_INTERVAL: Duration = Duration::from_millis(50);
const OUTPUT_CAPACITY: usize = 1024;
const MAX_BATCH: usize = 512;
pub fn run(
guard: &mut TerminalGuard,
workspace: Workspace,
adapters: Adapters,
current_config: PathBuf,
selection_style: Style,
) -> Result<()> {
let Adapters {
runner,
registry,
completer,
notifier,
settings_store,
agent_session_store,
} = adapters;
let (control_tx, control_rx) = unbounded();
let (output_tx, output_rx) = bounded(OUTPUT_CAPACITY);
let watch_tx = output_tx.clone();
spawn_input_thread(control_tx);
let area = size_to_rect(guard.terminal_mut().size()?);
let mut app = App::new(
workspace,
runner,
output_tx,
area,
completer,
registry,
current_config,
);
app.spawn_completion_worker();
app.set_config_watcher(Box::new(NotifyConfigWatcher::new(watch_tx)));
app.set_notifier(notifier);
app.set_settings_store(settings_store);
app.set_agent_session_store(agent_session_store);
app.set_selection_style(selection_style);
app.start();
let result = run_loop(guard, &mut app, &control_rx, &output_rx);
app.shutdown();
result
}
fn run_loop(
guard: &mut TerminalGuard,
app: &mut App,
control_rx: &Receiver<RuntimeEvent>,
output_rx: &Receiver<RuntimeEvent>,
) -> Result<()> {
guard.terminal_mut().draw(|frame| app.render(frame))?;
while app.is_running() {
let outcome = drain(app, control_rx, output_rx);
if let Some(text) = app.take_pending_clipboard() {
guard.copy_to_clipboard(&text)?;
}
if let Some(shape) = app.take_pending_pointer_shape() {
guard.set_pointer_shape(shape)?;
}
match outcome {
ControlFlow::Break(()) => break,
ControlFlow::Continue(redraw) if redraw && app.is_running() => {
app.refresh_selection_view();
guard.terminal_mut().draw(|frame| app.render(frame))?;
},
ControlFlow::Continue(_) => {},
}
}
Ok(())
}
fn drain(
app: &mut App,
control_rx: &Receiver<RuntimeEvent>,
output_rx: &Receiver<RuntimeEvent>,
) -> ControlFlow<(), bool> {
let activity_timeout = app
.next_activity_deadline()
.map(|deadline| after(deadline.saturating_duration_since(Instant::now())))
.unwrap_or_else(never);
let activity_frame_timeout = app
.next_activity_frame_deadline()
.map(|deadline| after(deadline.saturating_duration_since(Instant::now())))
.unwrap_or_else(never);
let selection_timeout = app
.next_selection_deadline()
.map(|deadline| after(deadline.saturating_duration_since(Instant::now())))
.unwrap_or_else(never);
let mut redraw = false;
select! {
recv(control_rx) -> msg => match msg {
Ok(event) => if !apply(app, event) {
return ControlFlow::Break(());
} else {
redraw = true;
},
Err(_) => return ControlFlow::Break(()),
},
recv(output_rx) -> msg => if let Ok(event) = msg {
if !apply(app, event) {
return ControlFlow::Break(());
}
redraw = true;
},
recv(activity_timeout) -> now => if let Ok(now) = now {
redraw = app.expire_quiet_activity(now);
},
recv(activity_frame_timeout) -> now => if let Ok(now) = now {
redraw = app.advance_activity_frame(now);
},
recv(selection_timeout) -> now => if let Ok(now) = now {
redraw = app.advance_selection(now);
},
}
while let Ok(event) = control_rx.try_recv() {
if !apply(app, event) {
return ControlFlow::Break(());
}
redraw = true;
}
for _ in 0..MAX_BATCH {
match output_rx.try_recv() {
Ok(event) => {
if !apply(app, event) {
return ControlFlow::Break(());
}
redraw = true;
},
Err(_) => break,
}
}
let now = Instant::now();
redraw |= app.advance_activity_frame(now);
redraw |= app.advance_selection(now);
ControlFlow::Continue(redraw)
}
fn apply(app: &mut App, event: RuntimeEvent) -> bool {
match event {
RuntimeEvent::Input(event) => app.handle_input(event),
RuntimeEvent::Output {
pane,
generation,
output,
} => app.handle_output(pane, generation, output),
RuntimeEvent::Respawn { pane, generation } => app.handle_respawn(pane, generation),
RuntimeEvent::ForceStop {
pane,
spawn_generation,
shutdown_generation,
} => app.handle_force_stop(pane, spawn_generation, shutdown_generation),
RuntimeEvent::Completions {
generation,
candidates,
} => app.handle_completions(generation, candidates),
RuntimeEvent::ConfigChanged { path } => app.handle_config_changed(path),
RuntimeEvent::InputClosed => return false,
}
true
}
fn spawn_input_thread(sender: Sender<RuntimeEvent>) {
thread::spawn(move || {
loop {
match event::poll(INPUT_POLL_INTERVAL) {
Ok(true) => match event::read() {
Ok(event) => {
if sender.send(RuntimeEvent::Input(event)).is_err() {
break;
}
},
Err(_) => {
let _ = sender.send(RuntimeEvent::InputClosed);
break;
},
},
Ok(false) => {},
Err(_) => {
let _ = sender.send(RuntimeEvent::InputClosed);
break;
},
}
}
});
}
fn size_to_rect(size: Size) -> Rect {
Rect::new(0, 0, size.width, size.height)
}