Skip to main content

imp_tui/
tui_interface.rs

1use std::sync::Arc;
2
3use async_trait::async_trait;
4use imp_core::ui::{ComponentSpec, NotifyLevel, SelectOption, UserInterface, WidgetContent};
5use tokio::sync::mpsc;
6
7/// Events sent from the TuiInterface to the main App event loop.
8#[derive(Debug)]
9pub enum UiRequest {
10    Notify {
11        message: String,
12        level: NotifyLevel,
13    },
14    Confirm {
15        title: String,
16        message: String,
17        reply: tokio::sync::oneshot::Sender<Option<bool>>,
18    },
19    Select {
20        title: String,
21        context: String,
22        options: Vec<SelectOption>,
23        reply: tokio::sync::oneshot::Sender<Option<usize>>,
24    },
25    MultiSelect {
26        title: String,
27        context: String,
28        options: Vec<SelectOption>,
29        reply: tokio::sync::oneshot::Sender<Option<Vec<usize>>>,
30    },
31    Input {
32        title: String,
33        context: String,
34        placeholder: String,
35        reply: tokio::sync::oneshot::Sender<Option<String>>,
36    },
37    SetStatus {
38        key: String,
39        text: Option<String>,
40    },
41    SetWidget {
42        key: String,
43        content: Option<WidgetContent>,
44    },
45    Custom {
46        component: ComponentSpec,
47        reply: tokio::sync::oneshot::Sender<Option<serde_json::Value>>,
48    },
49}
50
51/// UserInterface implementation that sends requests to the TUI event loop.
52///
53/// Tools and extensions call methods on this trait. The implementation
54/// sends a request to the main event loop, which renders the appropriate
55/// UI element and sends back the response.
56pub struct TuiInterface {
57    tx: mpsc::Sender<UiRequest>,
58}
59
60impl TuiInterface {
61    pub fn new(tx: mpsc::Sender<UiRequest>) -> Arc<Self> {
62        Arc::new(Self { tx })
63    }
64}
65
66#[async_trait]
67impl UserInterface for TuiInterface {
68    fn has_ui(&self) -> bool {
69        true
70    }
71
72    async fn notify(&self, message: &str, level: NotifyLevel) {
73        let _ = self
74            .tx
75            .send(UiRequest::Notify {
76                message: message.to_string(),
77                level,
78            })
79            .await;
80    }
81
82    async fn confirm(&self, title: &str, message: &str) -> Option<bool> {
83        let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
84        let _ = self
85            .tx
86            .send(UiRequest::Confirm {
87                title: title.to_string(),
88                message: message.to_string(),
89                reply: reply_tx,
90            })
91            .await;
92        reply_rx.await.ok().flatten()
93    }
94
95    async fn select_with_context(
96        &self,
97        title: &str,
98        context: &str,
99        options: &[SelectOption],
100    ) -> Option<usize> {
101        let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
102        let _ = self
103            .tx
104            .send(UiRequest::Select {
105                title: title.to_string(),
106                context: context.to_string(),
107                options: options.to_vec(),
108                reply: reply_tx,
109            })
110            .await;
111        reply_rx.await.ok().flatten()
112    }
113
114    async fn multi_select_with_context(
115        &self,
116        title: &str,
117        context: &str,
118        options: &[SelectOption],
119    ) -> Option<Vec<usize>> {
120        let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
121        let _ = self
122            .tx
123            .send(UiRequest::MultiSelect {
124                title: title.to_string(),
125                context: context.to_string(),
126                options: options.to_vec(),
127                reply: reply_tx,
128            })
129            .await;
130        reply_rx.await.ok().flatten()
131    }
132
133    async fn input_with_context(
134        &self,
135        title: &str,
136        context: &str,
137        placeholder: &str,
138    ) -> Option<String> {
139        let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
140        let _ = self
141            .tx
142            .send(UiRequest::Input {
143                title: title.to_string(),
144                context: context.to_string(),
145                placeholder: placeholder.to_string(),
146                reply: reply_tx,
147            })
148            .await;
149        reply_rx.await.ok().flatten()
150    }
151
152    async fn set_status(&self, key: &str, text: Option<&str>) {
153        let _ = self
154            .tx
155            .send(UiRequest::SetStatus {
156                key: key.to_string(),
157                text: text.map(String::from),
158            })
159            .await;
160    }
161
162    async fn set_widget(&self, key: &str, content: Option<WidgetContent>) {
163        let _ = self
164            .tx
165            .send(UiRequest::SetWidget {
166                key: key.to_string(),
167                content,
168            })
169            .await;
170    }
171
172    async fn custom(&self, component: ComponentSpec) -> Option<serde_json::Value> {
173        let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
174        let _ = self
175            .tx
176            .send(UiRequest::Custom {
177                component,
178                reply: reply_tx,
179            })
180            .await;
181        reply_rx.await.ok().flatten()
182    }
183}