imp-tui 0.2.0

Terminal UI for the imp coding agent
Documentation
use std::panic::AssertUnwindSafe;
use std::path::PathBuf;

use futures::FutureExt;
use imp_core::config::Config;
use imp_core::session::SessionManager;
use imp_llm::model::ModelRegistry;

use crate::app::App;
use crate::terminal::TerminalSession;

pub struct InteractiveRunner {
    app: App,
    terminal: TerminalSession,
}

#[derive(Debug)]
pub enum InteractiveRunError {
    Runtime(Box<dyn std::error::Error>),
    Panic(String),
}

impl std::fmt::Display for InteractiveRunError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Runtime(error) => write!(f, "{error}"),
            Self::Panic(message) => write!(f, "interactive panic: {message}"),
        }
    }
}

impl std::error::Error for InteractiveRunError {}

impl InteractiveRunner {
    pub fn new(
        config: Config,
        session: SessionManager,
        model_registry: ModelRegistry,
        cwd: PathBuf,
    ) -> Result<Self, Box<dyn std::error::Error>> {
        let app = App::new(config, session, model_registry, cwd);
        let terminal = TerminalSession::enter()?;
        Ok(Self { app, terminal })
    }

    pub fn app_mut(&mut self) -> &mut App {
        &mut self.app
    }

    pub async fn run(&mut self) -> Result<(), Box<dyn std::error::Error>> {
        let _ = self.terminal.set_window_title(&self.app.terminal_title());
        self.app.run(self.terminal.terminal_mut()).await
    }

    pub async fn run_guarded(&mut self) -> Result<(), InteractiveRunError> {
        let result = AssertUnwindSafe(self.run()).catch_unwind().await;
        let _ = self.terminal.restore();

        match result {
            Ok(Ok(())) => Ok(()),
            Ok(Err(error)) => Err(InteractiveRunError::Runtime(error)),
            Err(payload) => Err(InteractiveRunError::Panic(panic_payload_message(payload))),
        }
    }
}

fn panic_payload_message(payload: Box<dyn std::any::Any + Send>) -> String {
    let payload = match payload.downcast::<String>() {
        Ok(message) => return *message,
        Err(payload) => payload,
    };

    match payload.downcast::<&'static str>() {
        Ok(message) => (*message).to_string(),
        Err(_) => "panic with non-string payload".to_string(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn panic_payload_message_supports_string_and_str() {
        assert_eq!(panic_payload_message(Box::new("boom")), "boom".to_string());
        assert_eq!(
            panic_payload_message(Box::new("kaboom".to_string())),
            "kaboom".to_string()
        );
    }
}