Skip to main content

imp_core/
ui.rs

1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3
4/// Abstraction over user interaction. Tools and extensions use this
5/// without knowing whether they're in a TUI, headless, or print mode.
6#[async_trait]
7pub trait UserInterface: Send + Sync {
8    /// Whether this interface can show interactive UI.
9    fn has_ui(&self) -> bool;
10
11    /// Non-blocking notification.
12    async fn notify(&self, message: &str, level: NotifyLevel);
13
14    /// Yes/no confirmation. Returns None if no UI or cancelled.
15    async fn confirm(&self, title: &str, message: &str) -> Option<bool>;
16
17    /// Select from options. Returns None if no UI or cancelled.
18    async fn select(&self, title: &str, options: &[SelectOption]) -> Option<usize> {
19        self.select_with_context(title, "", options).await
20    }
21
22    /// Select from options with context. Returns None if no UI or cancelled.
23    async fn select_with_context(
24        &self,
25        title: &str,
26        context: &str,
27        options: &[SelectOption],
28    ) -> Option<usize>;
29
30    /// Select multiple options. Returns None if no UI or cancelled.
31    async fn multi_select_with_context(
32        &self,
33        title: &str,
34        context: &str,
35        options: &[SelectOption],
36    ) -> Option<Vec<usize>> {
37        self.select_with_context(title, context, options)
38            .await
39            .map(|index| vec![index])
40    }
41
42    /// Text input. Returns None if no UI or cancelled.
43    async fn input(&self, title: &str, placeholder: &str) -> Option<String> {
44        self.input_with_context(title, "", placeholder).await
45    }
46
47    /// Text input with context. Returns None if no UI or cancelled.
48    async fn input_with_context(
49        &self,
50        title: &str,
51        context: &str,
52        placeholder: &str,
53    ) -> Option<String>;
54
55    /// Persistent status in footer.
56    async fn set_status(&self, key: &str, text: Option<&str>);
57
58    /// Widget above/below editor.
59    async fn set_widget(&self, key: &str, content: Option<WidgetContent>);
60
61    /// Full declarative custom component. Returns the serialized result.
62    async fn custom(&self, component: ComponentSpec) -> Option<serde_json::Value>;
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
66pub enum NotifyLevel {
67    Info,
68    Warning,
69    Error,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct SelectOption {
74    pub label: String,
75    pub description: Option<String>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub enum WidgetContent {
80    Lines(Vec<String>),
81    Component(ComponentSpec),
82}
83
84/// Declarative component specification (from Lua tables or native code).
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct ComponentSpec {
87    pub component_type: String,
88    pub props: serde_json::Value,
89    pub children: Vec<ComponentSpec>,
90}
91
92/// Null interface for print mode — returns None for everything.
93pub struct NullInterface;
94
95#[async_trait]
96impl UserInterface for NullInterface {
97    fn has_ui(&self) -> bool {
98        false
99    }
100    async fn notify(&self, _message: &str, _level: NotifyLevel) {}
101    async fn confirm(&self, _title: &str, _message: &str) -> Option<bool> {
102        None
103    }
104    async fn select_with_context(
105        &self,
106        _title: &str,
107        _context: &str,
108        _options: &[SelectOption],
109    ) -> Option<usize> {
110        None
111    }
112    async fn multi_select_with_context(
113        &self,
114        _title: &str,
115        _context: &str,
116        _options: &[SelectOption],
117    ) -> Option<Vec<usize>> {
118        None
119    }
120    async fn input_with_context(
121        &self,
122        _title: &str,
123        _context: &str,
124        _placeholder: &str,
125    ) -> Option<String> {
126        None
127    }
128    async fn set_status(&self, _key: &str, _text: Option<&str>) {}
129    async fn set_widget(&self, _key: &str, _content: Option<WidgetContent>) {}
130    async fn custom(&self, _component: ComponentSpec) -> Option<serde_json::Value> {
131        None
132    }
133}