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#[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 Input {
26 title: String,
27 context: String,
28 placeholder: String,
29 reply: tokio::sync::oneshot::Sender<Option<String>>,
30 },
31 SetStatus {
32 key: String,
33 text: Option<String>,
34 },
35 SetWidget {
36 key: String,
37 content: Option<WidgetContent>,
38 },
39 Custom {
40 component: ComponentSpec,
41 reply: tokio::sync::oneshot::Sender<Option<serde_json::Value>>,
42 },
43}
44
45pub struct TuiInterface {
51 tx: mpsc::Sender<UiRequest>,
52}
53
54impl TuiInterface {
55 pub fn new(tx: mpsc::Sender<UiRequest>) -> Arc<Self> {
56 Arc::new(Self { tx })
57 }
58}
59
60#[async_trait]
61impl UserInterface for TuiInterface {
62 fn has_ui(&self) -> bool {
63 true
64 }
65
66 async fn notify(&self, message: &str, level: NotifyLevel) {
67 let _ = self
68 .tx
69 .send(UiRequest::Notify {
70 message: message.to_string(),
71 level,
72 })
73 .await;
74 }
75
76 async fn confirm(&self, title: &str, message: &str) -> Option<bool> {
77 let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
78 let _ = self
79 .tx
80 .send(UiRequest::Confirm {
81 title: title.to_string(),
82 message: message.to_string(),
83 reply: reply_tx,
84 })
85 .await;
86 reply_rx.await.ok().flatten()
87 }
88
89 async fn select_with_context(
90 &self,
91 title: &str,
92 context: &str,
93 options: &[SelectOption],
94 ) -> Option<usize> {
95 let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
96 let _ = self
97 .tx
98 .send(UiRequest::Select {
99 title: title.to_string(),
100 context: context.to_string(),
101 options: options.to_vec(),
102 reply: reply_tx,
103 })
104 .await;
105 reply_rx.await.ok().flatten()
106 }
107
108 async fn input_with_context(
109 &self,
110 title: &str,
111 context: &str,
112 placeholder: &str,
113 ) -> Option<String> {
114 let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
115 let _ = self
116 .tx
117 .send(UiRequest::Input {
118 title: title.to_string(),
119 context: context.to_string(),
120 placeholder: placeholder.to_string(),
121 reply: reply_tx,
122 })
123 .await;
124 reply_rx.await.ok().flatten()
125 }
126
127 async fn set_status(&self, key: &str, text: Option<&str>) {
128 let _ = self
129 .tx
130 .send(UiRequest::SetStatus {
131 key: key.to_string(),
132 text: text.map(String::from),
133 })
134 .await;
135 }
136
137 async fn set_widget(&self, key: &str, content: Option<WidgetContent>) {
138 let _ = self
139 .tx
140 .send(UiRequest::SetWidget {
141 key: key.to_string(),
142 content,
143 })
144 .await;
145 }
146
147 async fn custom(&self, component: ComponentSpec) -> Option<serde_json::Value> {
148 let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
149 let _ = self
150 .tx
151 .send(UiRequest::Custom {
152 component,
153 reply: reply_tx,
154 })
155 .await;
156 reply_rx.await.ok().flatten()
157 }
158}