Skip to main content

stynx_code_tui/state/
modal_state.rs

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}