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}