1#[derive(Debug, Clone)]
5pub struct CommandPaletteState {
6 pub commands: Vec<PaletteCommand>,
8 pub input: String,
10 pub cursor: usize,
12 pub open: bool,
14 pub last_selected: Option<usize>,
17 selected: usize,
18}
19
20impl CommandPaletteState {
21 pub fn new(commands: Vec<PaletteCommand>) -> Self {
23 Self {
24 commands,
25 input: String::new(),
26 cursor: 0,
27 open: false,
28 last_selected: None,
29 selected: 0,
30 }
31 }
32
33 pub fn toggle(&mut self) {
35 self.open = !self.open;
36 if self.open {
37 self.input.clear();
38 self.cursor = 0;
39 self.selected = 0;
40 }
41 }
42
43 fn fuzzy_score(pattern: &str, text: &str) -> Option<i32> {
44 let pattern = pattern.trim();
45 if pattern.is_empty() {
46 return Some(0);
47 }
48
49 let text_chars: Vec<char> = text.chars().collect();
50 let mut score = 0;
51 let mut search_start = 0usize;
52 let mut prev_match: Option<usize> = None;
53
54 for p in pattern.chars() {
55 let mut found = None;
56 for (idx, ch) in text_chars.iter().enumerate().skip(search_start) {
57 if ch.eq_ignore_ascii_case(&p) {
58 found = Some(idx);
59 break;
60 }
61 }
62
63 let idx = found?;
64 if prev_match.is_some_and(|prev| idx == prev + 1) {
65 score += 3;
66 } else {
67 score += 1;
68 }
69
70 if idx == 0 {
71 score += 2;
72 } else {
73 let prev = text_chars[idx - 1];
74 let curr = text_chars[idx];
75 if matches!(prev, ' ' | '_' | '-') || prev.is_uppercase() || curr.is_uppercase() {
76 score += 2;
77 }
78 }
79
80 prev_match = Some(idx);
81 search_start = idx + 1;
82 }
83
84 Some(score)
85 }
86
87 pub(crate) fn filtered_indices(&self) -> Vec<usize> {
88 let query = self.input.trim();
89 if query.is_empty() {
90 return (0..self.commands.len()).collect();
91 }
92
93 let mut scored: Vec<(usize, i32)> = self
94 .commands
95 .iter()
96 .enumerate()
97 .filter_map(|(i, cmd)| {
98 let mut haystack =
99 String::with_capacity(cmd.label.len() + cmd.description.len() + 1);
100 haystack.push_str(&cmd.label);
101 haystack.push(' ');
102 haystack.push_str(&cmd.description);
103 Self::fuzzy_score(query, &haystack).map(|score| (i, score))
104 })
105 .collect();
106
107 if scored.is_empty() {
108 let tokens: Vec<String> = query.split_whitespace().map(|t| t.to_lowercase()).collect();
109 return self
110 .commands
111 .iter()
112 .enumerate()
113 .filter(|(_, cmd)| {
114 let label = cmd.label.to_lowercase();
115 let desc = cmd.description.to_lowercase();
116 tokens.iter().all(|token| {
117 label.contains(token.as_str()) || desc.contains(token.as_str())
118 })
119 })
120 .map(|(i, _)| i)
121 .collect();
122 }
123
124 scored.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
125 scored.into_iter().map(|(idx, _)| idx).collect()
126 }
127
128 pub(crate) fn selected(&self) -> usize {
129 self.selected
130 }
131
132 pub(crate) fn set_selected(&mut self, s: usize) {
133 self.selected = s;
134 }
135}
136
137#[derive(Debug, Clone)]
142pub struct StreamingTextState {
143 pub content: String,
145 pub streaming: bool,
147 pub(crate) cursor_visible: bool,
149 pub(crate) cursor_tick: u64,
150}
151
152impl StreamingTextState {
153 pub fn new() -> Self {
155 Self {
156 content: String::new(),
157 streaming: false,
158 cursor_visible: true,
159 cursor_tick: 0,
160 }
161 }
162
163 pub fn push(&mut self, chunk: &str) {
165 self.content.push_str(chunk);
166 }
167
168 pub fn finish(&mut self) {
170 self.streaming = false;
171 }
172
173 pub fn start(&mut self) {
175 self.content.clear();
176 self.streaming = true;
177 self.cursor_visible = true;
178 self.cursor_tick = 0;
179 }
180
181 pub fn clear(&mut self) {
183 self.content.clear();
184 self.streaming = false;
185 self.cursor_visible = true;
186 self.cursor_tick = 0;
187 }
188}
189
190impl Default for StreamingTextState {
191 fn default() -> Self {
192 Self::new()
193 }
194}
195
196#[derive(Debug, Clone)]
201pub struct StreamingMarkdownState {
202 pub content: String,
204 pub streaming: bool,
206 pub cursor_visible: bool,
208 pub cursor_tick: u64,
210 pub in_code_block: bool,
212 pub code_block_lang: String,
214}
215
216impl StreamingMarkdownState {
217 pub fn new() -> Self {
219 Self {
220 content: String::new(),
221 streaming: false,
222 cursor_visible: true,
223 cursor_tick: 0,
224 in_code_block: false,
225 code_block_lang: String::new(),
226 }
227 }
228
229 pub fn push(&mut self, chunk: &str) {
231 self.content.push_str(chunk);
232 }
233
234 pub fn start(&mut self) {
236 self.content.clear();
237 self.streaming = true;
238 self.cursor_visible = true;
239 self.cursor_tick = 0;
240 self.in_code_block = false;
241 self.code_block_lang.clear();
242 }
243
244 pub fn finish(&mut self) {
246 self.streaming = false;
247 }
248
249 pub fn clear(&mut self) {
251 self.content.clear();
252 self.streaming = false;
253 self.cursor_visible = true;
254 self.cursor_tick = 0;
255 self.in_code_block = false;
256 self.code_block_lang.clear();
257 }
258}
259
260impl Default for StreamingMarkdownState {
261 fn default() -> Self {
262 Self::new()
263 }
264}
265
266#[derive(Debug, Clone)]
288pub struct ScreenState {
289 stack: Vec<String>,
290 focus_state: std::collections::HashMap<String, (usize, usize)>,
291}
292
293impl ScreenState {
294 pub fn new(initial: impl Into<String>) -> Self {
296 Self {
297 stack: vec![initial.into()],
298 focus_state: std::collections::HashMap::new(),
299 }
300 }
301
302 pub fn current(&self) -> &str {
304 self.stack
305 .last()
306 .expect("ScreenState always contains at least one screen")
307 .as_str()
308 }
309
310 pub fn push(&mut self, name: impl Into<String>) {
312 self.stack.push(name.into());
313 }
314
315 pub fn pop(&mut self) {
317 if self.can_pop() {
318 self.stack.pop();
319 }
320 }
321
322 pub fn depth(&self) -> usize {
324 self.stack.len()
325 }
326
327 pub fn can_pop(&self) -> bool {
329 self.stack.len() > 1
330 }
331
332 pub fn reset(&mut self) {
334 self.stack.truncate(1);
335 }
336
337 pub(crate) fn save_focus(&mut self, name: &str, focus_index: usize, focus_count: usize) {
338 self.focus_state
339 .insert(name.to_string(), (focus_index, focus_count));
340 }
341
342 pub(crate) fn restore_focus(&self, name: &str) -> (usize, usize) {
343 self.focus_state.get(name).copied().unwrap_or((0, 0))
344 }
345}
346
347#[derive(Debug, Clone)]
366pub struct ModeState {
367 modes: std::collections::HashMap<String, ScreenState>,
368 active: String,
369}
370
371impl ModeState {
372 pub fn new(mode: impl Into<String>, screen: impl Into<String>) -> Self {
374 let mode = mode.into();
375 let mut modes = std::collections::HashMap::new();
376 modes.insert(mode.clone(), ScreenState::new(screen));
377 Self {
378 modes,
379 active: mode,
380 }
381 }
382
383 pub fn add_mode(&mut self, mode: impl Into<String>, screen: impl Into<String>) {
385 let mode = mode.into();
386 self.modes
387 .entry(mode)
388 .or_insert_with(|| ScreenState::new(screen));
389 }
390
391 pub fn switch_mode(&mut self, mode: impl Into<String>) {
396 let mode = mode.into();
397 assert!(
398 self.modes.contains_key(&mode),
399 "mode '{}' not found",
400 mode
401 );
402 self.active = mode;
403 }
404
405 pub fn try_switch_mode(&mut self, mode: impl Into<String>) -> bool {
413 let mode = mode.into();
414 if !self.modes.contains_key(&mode) {
415 return false;
416 }
417 self.active = mode;
418 true
419 }
420
421 pub fn active_mode(&self) -> &str {
423 &self.active
424 }
425
426 pub fn screens(&self) -> &ScreenState {
428 self.modes
429 .get(&self.active)
430 .expect("active mode must exist")
431 }
432
433 pub fn screens_mut(&mut self) -> &mut ScreenState {
435 self.modes
436 .get_mut(&self.active)
437 .expect("active mode must exist")
438 }
439}
440
441#[cfg(test)]
442mod mode_state_tests {
443 use super::ModeState;
444
445 #[test]
446 fn try_switch_mode_returns_false_for_unknown_mode() {
447 let mut modes = ModeState::new("app", "home");
448 modes.add_mode("settings", "general");
449 assert!(modes.try_switch_mode("settings"));
450 assert_eq!(modes.active_mode(), "settings");
451 assert!(!modes.try_switch_mode("nonexistent"));
452 assert_eq!(modes.active_mode(), "settings");
454 }
455}
456
457#[non_exhaustive]
459#[derive(Debug, Clone, Copy, PartialEq, Eq)]
460pub enum ApprovalAction {
461 Pending,
463 Approved,
465 Rejected,
467}
468
469#[derive(Debug, Clone)]
475pub struct ToolApprovalState {
476 pub tool_name: String,
478 pub description: String,
480 pub action: ApprovalAction,
482}
483
484impl ToolApprovalState {
485 pub fn new(tool_name: impl Into<String>, description: impl Into<String>) -> Self {
487 Self {
488 tool_name: tool_name.into(),
489 description: description.into(),
490 action: ApprovalAction::Pending,
491 }
492 }
493
494 pub fn reset(&mut self) {
496 self.action = ApprovalAction::Pending;
497 }
498}
499
500#[derive(Debug, Clone)]
502pub struct ContextItem {
503 pub label: String,
505 pub tokens: usize,
507}
508
509impl ContextItem {
510 pub fn new(label: impl Into<String>, tokens: usize) -> Self {
512 Self {
513 label: label.into(),
514 tokens,
515 }
516 }
517}