Skip to main content

imp_tui/
interactive.rs

1use std::panic::AssertUnwindSafe;
2use std::path::PathBuf;
3
4use futures::FutureExt;
5use imp_core::config::Config;
6use imp_core::session::SessionManager;
7use imp_llm::model::ModelRegistry;
8
9use crate::app::App;
10use crate::terminal::TerminalSession;
11
12pub struct InteractiveRunner {
13    app: App,
14    terminal: TerminalSession,
15}
16
17#[derive(Debug)]
18pub enum InteractiveRunError {
19    Runtime(Box<dyn std::error::Error>),
20    Panic(String),
21}
22
23impl std::fmt::Display for InteractiveRunError {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        match self {
26            Self::Runtime(error) => write!(f, "{error}"),
27            Self::Panic(message) => write!(f, "interactive panic: {message}"),
28        }
29    }
30}
31
32impl std::error::Error for InteractiveRunError {}
33
34impl InteractiveRunner {
35    pub fn new(
36        config: Config,
37        session: SessionManager,
38        model_registry: ModelRegistry,
39        cwd: PathBuf,
40    ) -> Result<Self, Box<dyn std::error::Error>> {
41        let app = App::new(config, session, model_registry, cwd);
42        let terminal = TerminalSession::enter()?;
43        Ok(Self { app, terminal })
44    }
45
46    pub fn app_mut(&mut self) -> &mut App {
47        &mut self.app
48    }
49
50    pub async fn run(&mut self) -> Result<(), Box<dyn std::error::Error>> {
51        let _ = self.terminal.set_window_title(&self.app.terminal_title());
52        self.app.run(self.terminal.terminal_mut()).await
53    }
54
55    pub async fn run_guarded(&mut self) -> Result<(), InteractiveRunError> {
56        let result = AssertUnwindSafe(self.run()).catch_unwind().await;
57        let _ = self.terminal.restore();
58
59        match result {
60            Ok(Ok(())) => Ok(()),
61            Ok(Err(error)) => Err(InteractiveRunError::Runtime(error)),
62            Err(payload) => Err(InteractiveRunError::Panic(panic_payload_message(payload))),
63        }
64    }
65}
66
67fn panic_payload_message(payload: Box<dyn std::any::Any + Send>) -> String {
68    let payload = match payload.downcast::<String>() {
69        Ok(message) => return *message,
70        Err(payload) => payload,
71    };
72
73    match payload.downcast::<&'static str>() {
74        Ok(message) => (*message).to_string(),
75        Err(_) => "panic with non-string payload".to_string(),
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn panic_payload_message_supports_string_and_str() {
85        assert_eq!(panic_payload_message(Box::new("boom")), "boom".to_string());
86        assert_eq!(
87            panic_payload_message(Box::new("kaboom".to_string())),
88            "kaboom".to_string()
89        );
90    }
91}