1#[derive(Clone)]
2pub struct DialogOption {
3 pub value: String,
4 pub title: String,
5 pub description: Option<String>,
6 pub category: Option<String>,
7 pub footer: Option<String>,
8 pub disabled: bool,
9}
10
11impl DialogOption {
12 pub fn new(value: impl Into<String>, title: impl Into<String>) -> Self {
13 Self {
14 value: value.into(),
15 title: title.into(),
16 description: None,
17 category: None,
18 footer: None,
19 disabled: false,
20 }
21 }
22
23 pub fn with_description(mut self, d: impl Into<String>) -> Self {
24 self.description = Some(d.into());
25 self
26 }
27
28 pub fn with_category(mut self, c: impl Into<String>) -> Self {
29 self.category = Some(c.into());
30 self
31 }
32
33 pub fn with_footer(mut self, f: impl Into<String>) -> Self {
34 self.footer = Some(f.into());
35 self
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum PermissionChoice {
41 Once,
42 Always,
43 Reject,
44}
45
46pub enum ModalKind {
47 QuitConfirm,
48 Permission {
49 tool_name: String,
50 description: String,
51 choice: PermissionChoice,
52 },
53 Select {
54 title: String,
55 query: String,
56 options: Vec<DialogOption>,
57 selected: usize,
58 current_value: Option<String>,
59 kind: SelectKind,
60 footer_hint: Option<String>,
61 },
62 Info {
63 title: String,
64 rows: Vec<(String, String)>,
65 },
66 Input {
67 title: String,
68 prompt: String,
69 buffer: String,
70 kind: InputKind,
71 },
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum InputKind {
76 SessionRename,
77 AskUserQuestion,
78}
79
80#[derive(Clone, Copy, PartialEq, Eq)]
81pub enum SelectKind {
82 CommandPalette,
83 ModelPicker,
84 SessionList,
85 Help,
86 SkillPicker,
87 FileMention,
88 ThemePicker,
89}
90
91pub struct ModalState {
92 pub active: Option<ModalKind>,
93}
94
95impl ModalState {
96 pub fn new() -> Self {
97 Self { active: None }
98 }
99
100 pub fn open_select(
101 &mut self,
102 kind: SelectKind,
103 title: impl Into<String>,
104 options: Vec<DialogOption>,
105 current_value: Option<String>,
106 footer_hint: Option<String>,
107 ) {
108 let selected = current_value
109 .as_ref()
110 .and_then(|cv| options.iter().position(|o| &o.value == cv))
111 .unwrap_or(0);
112 self.active = Some(ModalKind::Select {
113 title: title.into(),
114 query: String::new(),
115 options,
116 selected,
117 current_value,
118 kind,
119 footer_hint,
120 });
121 }
122
123 pub fn open_quit_confirm(&mut self) {
124 self.active = Some(ModalKind::QuitConfirm);
125 }
126
127 pub fn open_permission(&mut self, tool_name: impl Into<String>, description: impl Into<String>) { self.active = Some(ModalKind::Permission {
128 tool_name: tool_name.into(),
129 description: description.into(),
130 choice: PermissionChoice::Once,
131 });
132 }
133
134 pub fn open_info(&mut self, title: impl Into<String>, rows: Vec<(String, String)>) {
135 self.active = Some(ModalKind::Info { title: title.into(), rows });
136 }
137
138 pub fn open_input(
139 &mut self,
140 title: impl Into<String>,
141 prompt: impl Into<String>,
142 initial: impl Into<String>,
143 kind: InputKind,
144 ) {
145 self.active = Some(ModalKind::Input {
146 title: title.into(),
147 prompt: prompt.into(),
148 buffer: initial.into(),
149 kind,
150 });
151 }
152
153 pub fn close(&mut self) {
154 self.active = None;
155 }
156}
157
158impl Default for ModalState {
159 fn default() -> Self {
160 Self::new()
161 }
162}
163
164pub fn filter_options(opts: &[DialogOption], query: &str) -> Vec<usize> {
165 let q = query.trim().to_lowercase();
166 if q.is_empty() {
167 return (0..opts.len()).collect();
168 }
169 opts.iter()
170 .enumerate()
171 .filter(|(_, o)| {
172 o.title.to_lowercase().contains(&q)
173 || o.description
174 .as_deref()
175 .map(|d| d.to_lowercase().contains(&q))
176 .unwrap_or(false)
177 || o.category
178 .as_deref()
179 .map(|c| c.to_lowercase().contains(&q))
180 .unwrap_or(false)
181 })
182 .map(|(i, _)| i)
183 .collect()
184}