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 slash_popup;
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, OnceLock};
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;
static PANIC_TTY: OnceLock<Mutex<std::fs::File>> = OnceLock::new();
fn install_panic_hook() {
static INIT: std::sync::Once = std::sync::Once::new();
INIT.call_once(|| {
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
if let Some(tty_mutex) = PANIC_TTY.get() {
if let Ok(mut tty) = tty_mutex.lock() {
let _ = crossterm::execute!(
&mut *tty,
crossterm::event::PopKeyboardEnhancementFlags,
crossterm::event::DisableBracketedPaste,
crossterm::cursor::Show,
);
}
}
let _ = crossterm::terminal::disable_raw_mode();
prev(info);
}));
});
}
pub(super) const TICK_INTERVAL: Duration = Duration::from_millis(90);
pub(super) const MAX_OUTPUT_BATCH: usize = 256;
#[cfg(windows)]
const CP_UTF8: u32 = 65001;
#[cfg(windows)]
#[derive(Copy, Clone)]
struct ConsoleCodePages {
output: u32,
input: u32,
}
#[cfg(windows)]
fn switch_console_to_utf8() -> ConsoleCodePages {
use windows_sys::Win32::System::Console::{
GetConsoleCP, GetConsoleOutputCP, SetConsoleCP, SetConsoleOutputCP,
};
unsafe {
let saved = ConsoleCodePages {
output: GetConsoleOutputCP(),
input: GetConsoleCP(),
};
SetConsoleOutputCP(CP_UTF8);
SetConsoleCP(CP_UTF8);
saved
}
}
#[cfg(windows)]
fn restore_console_code_pages(saved: ConsoleCodePages) {
use windows_sys::Win32::System::Console::{SetConsoleCP, SetConsoleOutputCP};
unsafe {
SetConsoleOutputCP(saved.output);
SetConsoleCP(saved.input);
}
}
struct TerminalGuard {
#[cfg(windows)]
saved_code_pages: ConsoleCodePages,
#[cfg(not(windows))]
_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();
#[cfg(windows)]
let saved_code_pages = switch_console_to_utf8();
Ok(Self {
#[cfg(windows)]
saved_code_pages,
#[cfg(not(windows))]
_private: (),
})
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
#[cfg(windows)]
restore_console_code_pages(self.saved_code_pages);
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<()> {
#[cfg(unix)]
let tty = OpenOptions::new().read(true).write(true).open("/dev/tty")?;
#[cfg(windows)]
let tty = OpenOptions::new().write(true).open("CONOUT$")?;
let tty_for_backend = tty.try_clone()?;
if let Ok(tty_for_panic) = tty.try_clone() {
let _ = PANIC_TTY.set(Mutex::new(tty_for_panic));
}
install_panic_hook();
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.clone());
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() {
if summary.panicked {
println!();
UI::print_warning("Session ended unexpectedly. See backtrace above for details.");
}
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;
}
}