use std::io::Write;
use std::path::Path;
use std::process::ExitCode;
use skadoosh::audio::input::list_devices;
use skadoosh::{Agent, AgentEvent, Config, OutputMode, Pipeline, SkadooshError};
use tracing_subscriber::EnvFilter;
fn main() -> ExitCode {
let config = Config::parse();
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
)
.init();
match dispatch(config) {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
print_error_chain(&err);
ExitCode::FAILURE
}
}
}
fn dispatch(config: Config) -> skadoosh::Result<()> {
config.validate()?;
if config.list_devices {
for name in list_devices()? {
println!("{name}");
}
return Ok(());
}
if let Some(wav) = config.selftest.clone() {
let pipeline = Pipeline::new(config)?;
let report = pipeline.run_selftest(&wav, Path::new("selftest_out.wav"))?;
println!("{report}");
return Ok(());
}
let mut agent = Agent::builder().config(config.clone()).build()?;
if config.repl {
return agent.repl(std::io::BufReader::new(std::io::stdin()), std::io::stdout());
}
if let Some(text) = &config.say {
return match &config.out_wav {
Some(path) => agent.say_to_wav(text, path),
None => agent.say(text),
};
}
if config.output == OutputMode::Text {
let events = agent.events();
let out: Box<dyn Write + Send> = Box::new(std::io::stdout());
std::thread::spawn(move || print_text_mode(events, out));
}
let token = agent.shutdown_token();
let bridge = sigint::install(token.clone());
let result = agent.run();
bridge.done();
token.cancel();
if let Some(handle) = bridge.join {
let _ = handle.join();
}
result
}
fn print_text_mode(
mut events: tokio::sync::broadcast::Receiver<AgentEvent>,
mut out: Box<dyn Write + Send>,
) {
let mut mid_reply = false;
loop {
match events.blocking_recv() {
Ok(AgentEvent::Transcript(text)) => {
if mid_reply {
let _ = writeln!(out);
mid_reply = false;
}
if writeln!(out, "you: {}", text.trim()).is_err() {
return;
}
}
Ok(AgentEvent::Clause(clause)) => {
if mid_reply {
let _ = write!(out, " ");
} else {
let _ = write!(out, "bot: ");
mid_reply = true;
}
if write!(out, "{}", clause.trim()).is_err() || out.flush().is_err() {
return;
}
}
Ok(AgentEvent::ReplyDone) => {
if mid_reply {
mid_reply = false;
if writeln!(out).is_err() {
return;
}
}
}
Ok(AgentEvent::TurnCancelled) => {
if mid_reply {
mid_reply = false;
if writeln!(out).is_err() {
return;
}
}
if writeln!(out, " [interrupted]").is_err() {
return;
}
}
Ok(AgentEvent::ToolCall { name, arguments }) => {
if writeln!(out, " [tool: {name}({arguments})]").is_err() {
return;
}
}
Ok(AgentEvent::Error(err)) => {
if writeln!(out, "error: {err}").is_err() {
return;
}
}
Ok(_) => {}
Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
let _ = writeln!(out, " [... {n} events dropped ...]");
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => return,
}
}
}
fn print_error_chain(err: &SkadooshError) {
eprintln!("error: {err}");
let mut source = std::error::Error::source(err);
while let Some(err) = source {
eprintln!("caused by: {err}");
source = err.source();
}
}
mod sigint {
use std::thread::JoinHandle;
use tokio_util::sync::CancellationToken;
pub struct SigintBridge {
done: CancellationToken,
pub join: Option<JoinHandle<()>>,
}
impl SigintBridge {
pub fn done(&self) {
self.done.cancel();
}
}
pub fn install(token: CancellationToken) -> SigintBridge {
let done = CancellationToken::new();
let thread_done = done.clone();
let join = std::thread::Builder::new()
.name("skadoosh-sigint".to_string())
.spawn(move || {
let runtime = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(runtime) => runtime,
Err(err) => {
tracing::warn!(%err, "failed to build SIGINT runtime; ctrlc will kill the process");
return;
}
};
runtime.block_on(async move {
tokio::select! {
first = tokio::signal::ctrl_c() => {
if first.is_err() {
tracing::warn!("failed to listen for SIGINT; ctrlc will kill the process");
return;
}
tracing::info!("SIGINT received; shutting down (press ctrl-c again to force)");
token.cancel();
tokio::select! {
_ = tokio::signal::ctrl_c() => std::process::exit(128 + 2),
_ = thread_done.cancelled() => {}
}
}
_ = token.cancelled() => {}
_ = thread_done.cancelled() => {}
}
});
})
.map_err(|err| {
tracing::warn!(%err, "failed to spawn SIGINT bridge thread");
err
})
.ok();
SigintBridge { done, join }
}
}