pub mod app;
pub mod event;
pub mod event_loop;
pub mod inline_terminal;
pub mod inline_tui;
pub mod input;
pub mod keymap;
pub mod output;
pub mod scrollback;
pub mod sgr;
pub mod ui;
pub mod worker;
use std::fs::OpenOptions;
use std::sync::atomic::AtomicBool;
use std::sync::mpsc as std_mpsc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use ratatui::backend::CrosstermBackend;
use tokio::sync::mpsc;
use crate::error::Result;
use crate::repl::{Repl, SteerBuffer};
use crate::ui::UI;
use app::App;
use event::{Job, UiEvent};
use event_loop::event_loop;
use input::spawn_input_reader;
use keymap::install_confirm_handler;
use output::OutputCapture;
pub(super) const TICK_INTERVAL: Duration = Duration::from_millis(90);
pub(super) const MAX_OUTPUT_BATCH: usize = 256;
struct TerminalGuard {
_private: (),
}
impl TerminalGuard {
fn install() -> std::io::Result<Self> {
crossterm::terminal::enable_raw_mode()?;
use std::io::Write;
let _ = crossterm::execute!(
std::io::stdout(),
crossterm::event::EnableBracketedPaste,
crossterm::event::PushKeyboardEnhancementFlags(
crossterm::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES,
),
);
let _ = std::io::stdout().flush();
Ok(Self { _private: () })
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
use std::io::Write;
let _ = crossterm::execute!(
std::io::stdout(),
crossterm::event::PopKeyboardEnhancementFlags,
crossterm::event::DisableBracketedPaste,
);
let _ = std::io::stdout().flush();
let _ = crossterm::terminal::disable_raw_mode();
}
}
pub fn run(mut repl: Repl) -> Result<()> {
let tty = OpenOptions::new().read(true).write(true).open("/dev/tty")?;
let tty_for_backend = tty.try_clone()?;
let (ui_tx, ui_rx) = mpsc::unbounded_channel::<UiEvent>();
let (job_tx, job_rx) = std_mpsc::channel::<Job>();
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.map_err(|e| crate::error::SofosError::Config(format!("runtime: {}", e)))?;
let _terminal_guard = TerminalGuard::install()?;
let backend = CrosstermBackend::new(tty_for_backend);
let terminal = inline_terminal::Terminal::new(backend)?;
drop(tty);
let mut inline_tui = inline_tui::InlineTui::new(terminal);
let capture = OutputCapture::install(ui_tx.clone())?;
colored::control::set_override(true);
install_confirm_handler(ui_tx.clone());
let interrupt = Arc::new(AtomicBool::new(false));
repl.install_interrupt_flag(Arc::clone(&interrupt));
let steer_buffer: SteerBuffer = Arc::new(Mutex::new(Vec::new()));
repl.install_steer_buffer(Arc::clone(&steer_buffer));
let model_label = repl.model_label();
let startup_banner = repl.take_startup_banner();
let worker_handle = worker::spawn(repl, job_rx, ui_tx.clone(), Arc::clone(&interrupt))?;
spawn_input_reader(ui_tx.clone())?;
let mut app = App::new(model_label);
if !startup_banner.is_empty() {
print!("{}", startup_banner);
}
UI::print_welcome();
drop(ui_tx);
let result = runtime.block_on(async {
event_loop(
&mut inline_tui,
&mut app,
ui_rx,
job_tx.clone(),
Arc::clone(&interrupt),
Arc::clone(&steer_buffer),
)
.await
});
let _ = job_tx.send(Job::Shutdown);
let _ = worker_handle.thread.join();
drop(capture);
drop(_terminal_guard);
colored::control::unset_override();
if let Some(summary) = app.exit_summary.take() {
let summary_printed = UI::display_session_summary(
&summary.model,
summary.input_tokens,
summary.output_tokens,
summary.cache_read_tokens,
summary.cache_creation_tokens,
summary.peak_single_turn_input_tokens,
);
if !summary_printed {
println!();
}
UI::print_goodbye();
}
result
}
pub(super) fn request_shutdown(app: &mut App, job_tx: &std_mpsc::Sender<Job>) {
if job_tx.send(Job::Shutdown).is_err() {
app.should_quit = true;
}
}