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    Permission {
48        tool_name: String,
49        description: String,
50        choice: PermissionChoice,
51    },
52    Select {
53        title: String,
54        query: String,
55        options: Vec<DialogOption>,
56        selected: usize,
57        current_value: Option<String>,
58        kind: SelectKind,
59        footer_hint: Option<String>,
60    },
61    Info {
62        title: String,
63        rows: Vec<(String, String)>,
64    },
65    Input {
66        title: String,
67        prompt: String,
68        buffer: String,
69        kind: InputKind,
70    },
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum InputKind {
75    SessionRename,
76    AskUserQuestion,
77}
78
79#[derive(Clone, Copy, PartialEq, Eq)]
80pub enum SelectKind {
81    CommandPalette,
82    ModelPicker,
83    SessionList,
84    Help,
85    SkillPicker,
86    FileMention,
87    ThemePicker,
88}
89
90pub struct ModalState {
91    pub active: Option<ModalKind>,
92}
93
94impl ModalState {
95    pub fn new() -> Self {
96        Self { active: None }
97    }
98
99    pub fn open_select(
100        &mut self,
101        kind: SelectKind,
102        title: impl Into<String>,
103        options: Vec<DialogOption>,
104        current_value: Option<String>,
105        footer_hint: Option<String>,
106    ) {
107        let selected = current_value
108            .as_ref()
109            .and_then(|cv| options.iter().position(|o| &o.value == cv))
110            .unwrap_or(0);
111        self.active = Some(ModalKind::Select {
112            title: title.into(),
113            query: String::new(),
114            options,
115            selected,
116            current_value,
117            kind,
118            footer_hint,
119        });
120    }
121
122    pub fn open_permission(&mut self, tool_name: impl Into<String>, description: impl Into<String>) {
123        self.active = Some(ModalKind::Permission {
124            tool_name: tool_name.into(),
125            description: description.into(),
126            choice: PermissionChoice::Once,
127        });
128    }
129
130    pub fn open_info(&mut self, title: impl Into<String>, rows: Vec<(String, String)>) {
131        self.active = Some(ModalKind::Info { title: title.into(), rows });
132    }
133
134    pub fn open_input(
135        &mut self,
136        title: impl Into<String>,
137        prompt: impl Into<String>,
138        initial: impl Into<String>,
139        kind: InputKind,
140    ) {
141        self.active = Some(ModalKind::Input {
142            title: title.into(),
143            prompt: prompt.into(),
144            buffer: initial.into(),
145            kind,
146        });
147    }
148
149    pub fn close(&mut self) {
150        self.active = None;
151    }
152}
153
154impl Default for ModalState {
155    fn default() -> Self {
156        Self::new()
157    }
158}
159
160pub fn filter_options(opts: &[DialogOption], query: &str) -> Vec<usize> {
161    let q = query.trim().to_lowercase();
162    if q.is_empty() {
163        return (0..opts.len()).collect();
164    }
165    opts.iter()
166        .enumerate()
167        .filter(|(_, o)| {
168            o.title.to_lowercase().contains(&q)
169                || o.description
170                    .as_deref()
171                    .map(|d| d.to_lowercase().contains(&q))
172                    .unwrap_or(false)
173                || o.category
174                    .as_deref()
175                    .map(|c| c.to_lowercase().contains(&q))
176                    .unwrap_or(false)
177        })
178        .map(|(i, _)| i)
179        .collect()
180}