Skip to main content

stynx_code_tui/event/
event_handler.rs

1use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEventKind};
2
3use crate::dialogs::{open_command_palette, open_file_mention, open_model_picker, open_session_list};
4use crate::state::{
5    AppState, InputMode, ModalKind, PermissionChoice, SelectKind, filter_options,
6};
7
8#[derive(Debug, Clone)]
9pub enum UiAction {
10    Submit(String),
11    CyclePermissionMode,
12    Quit,
13    SelectModel(String),
14    SelectSession(String),
15    PermissionDecision(PermissionChoice),
16    RunCommand(String),
17    ToggleSidebar,
18    InputConfirmed { kind: crate::state::InputKind, value: String },
19    Interrupt,
20    None,
21}
22
23const COMMANDS: &[(&str, &str)] = &[
24    ("/add",         "Pin a file path to every message"),
25    ("/commit",      "Generate a commit message from the diff"),
26    ("/compact",     "Compact context to save tokens"),
27    ("/config",      "Show merged config"),
28    ("/copy",        "Copy last response to clipboard"),
29    ("/diff",        "Show git diff"),
30    ("/effort",      "Set effort: low/medium/high/max"),
31    ("/exit",        "Exit session"),
32    ("/export",      "Export conversation to markdown"),
33    ("/fast",        "Toggle fast mode (haiku)"),
34    ("/files",       "List pinned files"),
35    ("/help",        "Show help"),
36    ("/intern",      "Delegate task to intern (DeepSeek)"),
37    ("/memory",      "Show CLAUDE.md"),
38    ("/mode",        "Cycle permission mode"),
39    ("/model",       "Switch model"),
40    ("/permissions", "Show allow / deny rules"),
41    ("/plan",        "Toggle plan mode"),
42    ("/quit",        "Exit session"),
43    ("/review",      "Review current git diff"),
44    ("/rewind",      "Remove last n exchanges"),
45    ("/skills",      "List available skills"),
46    ("/status",      "Show git status"),
47    ("/think",       "Toggle extended thinking"),
48    ("/usage",       "Show plan usage limits"),
49    ("/version",     "Show version"),
50];
51
52fn handle_ctrl_v(state: &mut AppState) {
53    use crate::clipboard::{PasteOutcome, read_clipboard};
54    match read_clipboard() {
55        PasteOutcome::Image { path } => {
56            let idx = state.input.insert_image_paste(path);
57            state.input.insert_char(' ');
58            state.toasts.success(format!("attached image #{idx}"));
59        }
60        PasteOutcome::Text(text) => {
61            if text.len() > 200 || text.matches('\n').count() > 4 {
62                let lines = text.lines().count().max(1);
63                let chars = text.chars().count();
64                let token = format!("[Pasted {lines} lines, {chars} chars]");
65                for c in token.chars() { state.input.insert_char(c); }
66                state.input.pasted_buffer = Some(text);
67            } else {
68                for c in text.chars() { state.input.insert_char(c); }
69            }
70        }
71        PasteOutcome::Empty => {
72            state.toasts.warn("clipboard empty");
73        }
74    }
75}
76
77fn update_suggestion(state: &mut AppState) {
78    let buf = state.input.buffer.clone();
79    if buf.starts_with('/') && !buf.contains(' ') {
80        let matches: Vec<(String, String)> = COMMANDS
81            .iter()
82            .filter(|(c, _)| c.starts_with(buf.as_str()) && *c != buf.as_str())
83            .map(|(c, d)| ((*c).to_string(), (*d).to_string()))
84            .collect();
85        state.input.suggestion = if matches.len() == 1 {
86            matches[0].0[buf.len()..].to_string()
87        } else {
88            String::new()
89        };
90        state.input.slash_matches = matches;
91        if state.input.slash_selected >= state.input.slash_matches.len() {
92            state.input.slash_selected = 0;
93        }
94    } else {
95        state.input.suggestion = String::new();
96        state.input.slash_matches.clear();
97        state.input.slash_selected = 0;
98    }
99}
100
101fn scroll_up(state: &mut AppState, n: usize) {
102    state.conversation.auto_scroll = false;
103    state.conversation.scroll_offset = state.conversation.scroll_offset.saturating_sub(n);
104}
105
106fn scroll_down(state: &mut AppState, n: usize) {
107    let next = state.conversation.scroll_offset.saturating_add(n);
108    if next >= state.conversation.total_lines {
109        state.conversation.auto_scroll = true;
110    } else {
111        state.conversation.scroll_offset = next;
112    }
113}
114
115fn dispatch_palette(state: &mut AppState, command: &str) -> UiAction {
116    match command {
117        "session.list" => { open_session_list(state); UiAction::None }
118        "session.new" => UiAction::RunCommand("session.new".into()),
119        "session.compact" => UiAction::RunCommand("session.compact".into()),
120        "session.export" => UiAction::RunCommand("session.export".into()),
121        "session.rename" => UiAction::RunCommand("session.rename".into()),
122        "status.show" => UiAction::RunCommand("status.show".into()),
123        "skills.show" => UiAction::RunCommand("skills.show".into()),
124        "model.list" => { open_model_picker(state); UiAction::None }
125        "model.cycle_recent" => UiAction::RunCommand("model.cycle_recent".into()),
126        "mode.cycle" => UiAction::CyclePermissionMode,
127        "tools.focus" => {
128            state.tool_history.focused = true;
129            if state.tool_history.selected.is_none() {
130                let total = crate::widgets::tool_history::flat_tools(state).len();
131                if total > 0 { state.tool_history.selected = Some(total - 1); }
132            }
133            UiAction::None
134        }
135        "theme.switch" => { crate::dialogs::open_theme_picker(state); UiAction::None }
136        "help.show" => UiAction::RunCommand("help.show".into()),
137        "app.quit" => UiAction::Quit,
138        _ => UiAction::None,
139    }
140}
141
142pub struct EventHandler;
143
144impl EventHandler {
145    pub fn new() -> Self { Self }
146
147    pub fn handle(event: Event, state: &mut AppState) -> UiAction {
148        match event {
149            Event::Mouse(m) => {
150                match m.kind {
151                    MouseEventKind::ScrollUp => scroll_up(state, 3),
152                    MouseEventKind::ScrollDown => scroll_down(state, 3),
153                    _ => {}
154                }
155                return UiAction::None;
156            }
157            Event::Paste(text) => {
158                if text.len() > 200 || text.matches('\n').count() > 4 {
159                    let lines = text.lines().count().max(1);
160                    let chars = text.chars().count();
161                    let token = format!("[Pasted {lines} lines, {chars} chars]");
162                    for c in token.chars() {
163                        state.input.insert_char(c);
164                    }
165                    state.input.pasted_buffer = Some(text);
166                } else {
167                    for c in text.chars() {
168                        state.input.insert_char(c);
169                    }
170                }
171                return UiAction::None;
172            }
173            Event::Key(key) => {
174                if key.kind != KeyEventKind::Press { return UiAction::None; }
175                if state.modal.active.is_some() { return Self::modal_key(key, state); }
176                if state.tool_history.focused && !key.modifiers.contains(KeyModifiers::CONTROL) {
177                    return Self::tool_history_key(key, state);
178                }
179                if state.input.mode == InputMode::Normal {
180                    return Self::normal_mode_key(key, state);
181                }
182                Self::insert_mode_key(key, state)
183            }
184            _ => UiAction::None,
185        }
186    }
187
188    fn insert_mode_key(key: KeyEvent, state: &mut AppState) -> UiAction {
189        match (key.code, key.modifiers) {
190            (KeyCode::Char('c'), KeyModifiers::CONTROL) => {
191                state.modal.open_quit_confirm();
192                UiAction::None
193            }
194            (KeyCode::Char('p'), KeyModifiers::CONTROL) => {
195                open_command_palette(state);
196                UiAction::None
197            }
198            (KeyCode::Char('s'), KeyModifiers::CONTROL) => {
199                open_session_list(state);
200                UiAction::None
201            }
202            (KeyCode::Char('m'), KeyModifiers::CONTROL) => {
203                open_model_picker(state);
204                UiAction::None
205            }
206            (KeyCode::Char('t'), KeyModifiers::CONTROL) => {
207                state.tool_history.focused = !state.tool_history.focused;
208                if state.tool_history.focused && state.tool_history.selected.is_none() {
209                    let total = crate::widgets::tool_history::flat_tools(state).len();
210                    if total > 0 { state.tool_history.selected = Some(total - 1); }
211                }
212                UiAction::None
213            }
214            (KeyCode::Char('v'), KeyModifiers::CONTROL) => {
215                handle_ctrl_v(state);
216                update_suggestion(state);
217                UiAction::None
218            }
219            (KeyCode::Esc, _) => {
220                if state.is_streaming {
221                    return UiAction::Interrupt;
222                }
223                if !state.input.slash_matches.is_empty() {
224                    state.input.slash_matches.clear();
225                    state.input.slash_selected = 0;
226                    return UiAction::None;
227                }
228                state.input.mode = InputMode::Normal;
229                UiAction::None
230            }
231            (KeyCode::BackTab, _) => UiAction::CyclePermissionMode,
232            (KeyCode::Tab, _) => { state.input.complete_suggestion(); UiAction::None }
233            (KeyCode::Enter, m) if m.contains(KeyModifiers::SHIFT)
234                || m.contains(KeyModifiers::ALT)
235                || m.contains(KeyModifiers::CONTROL) =>
236            {
237                state.input.insert_char('\n'); UiAction::None
238            }
239            (KeyCode::Char('j'), KeyModifiers::CONTROL) => {
240                state.input.insert_char('\n'); UiAction::None
241            }
242            (KeyCode::Enter, _) => {
243                if !state.input.slash_matches.is_empty() {
244                    let i = state.input.slash_selected.min(state.input.slash_matches.len() - 1);
245                    let cmd = state.input.slash_matches[i].0.clone();
246                    state.input.buffer = format!("{cmd} ");
247                    state.input.cursor_pos = state.input.buffer.len();
248                    state.input.slash_matches.clear();
249                    state.input.slash_selected = 0;
250                    state.input.suggestion.clear();
251                    return UiAction::None;
252                }
253                let display = state.input.buffer.trim().to_string();
254                if display.is_empty() { return UiAction::None; }
255                let real = state.input.expand_for_submit().trim().to_string();
256                state.input.push_history(display);
257                state.input.clear();
258                UiAction::Submit(real)
259            }
260            (KeyCode::Backspace, _) => {
261                state.input.delete_char(); update_suggestion(state); UiAction::None
262            }
263            (KeyCode::Delete, _) => {
264                state.input.delete_char_forward(); update_suggestion(state); UiAction::None
265            }
266            (KeyCode::Left, KeyModifiers::ALT) => { state.input.move_word_left(); UiAction::None }
267            (KeyCode::Right, KeyModifiers::ALT) => { state.input.move_word_right(); UiAction::None }
268            (KeyCode::Left, _) => { state.input.move_cursor_left(); UiAction::None }
269            (KeyCode::Right, _) => {
270                if state.input.cursor_pos == state.input.buffer.len() && !state.input.suggestion.is_empty() {
271                    state.input.complete_suggestion();
272                } else {
273                    state.input.move_cursor_right();
274                }
275                UiAction::None
276            }
277            (KeyCode::Home, _) | (KeyCode::Char('a'), KeyModifiers::CONTROL) => {
278                state.input.cursor_pos = 0; UiAction::None
279            }
280            (KeyCode::End, _) | (KeyCode::Char('e'), KeyModifiers::CONTROL) => {
281                state.input.cursor_pos = state.input.buffer.len(); UiAction::None
282            }
283            (KeyCode::Char('u'), KeyModifiers::CONTROL) => {
284                state.input.clear(); update_suggestion(state); UiAction::None
285            }
286            (KeyCode::Char('w'), KeyModifiers::CONTROL) => {
287                state.input.move_word_left(); update_suggestion(state); UiAction::None
288            }
289            (KeyCode::Up, KeyModifiers::SHIFT) => { state.input.history_prev(); UiAction::None }
290            (KeyCode::Down, KeyModifiers::SHIFT) => { state.input.history_next(); UiAction::None }
291            (KeyCode::Char('u'), m) if m.contains(KeyModifiers::CONTROL) && m.contains(KeyModifiers::ALT) => {
292                scroll_up(state, 10); UiAction::None
293            }
294            (KeyCode::Char('d'), m) if m.contains(KeyModifiers::CONTROL) && m.contains(KeyModifiers::ALT) => {
295                scroll_down(state, 10); UiAction::None
296            }
297            (KeyCode::Up, _) => {
298                if !state.input.slash_matches.is_empty() {
299                    let len = state.input.slash_matches.len();
300                    state.input.slash_selected = if state.input.slash_selected == 0 {
301                        len - 1
302                    } else {
303                        state.input.slash_selected - 1
304                    };
305                } else if state.input.buffer.is_empty() {
306                    scroll_up(state, 3);
307                } else if state.input.line_count() > 1 && state.input.cursor_up_line() {
308
309                } else {
310                    state.input.history_prev();
311                }
312                UiAction::None
313            }
314            (KeyCode::Down, _) => {
315                if !state.input.slash_matches.is_empty() {
316                    let len = state.input.slash_matches.len();
317                    state.input.slash_selected = (state.input.slash_selected + 1) % len;
318                } else if state.input.buffer.is_empty() {
319                    scroll_down(state, 3);
320                } else if state.input.line_count() > 1 && state.input.cursor_down_line() {
321
322                } else {
323                    state.input.history_next();
324                }
325                UiAction::None
326            }
327            (KeyCode::PageUp, _) => { scroll_up(state, 20); UiAction::None }
328            (KeyCode::PageDown, _) => { scroll_down(state, 20); UiAction::None }
329            (KeyCode::Char('@'), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
330                state.input.insert_char('@');
331                update_suggestion(state);
332                open_file_mention(state);
333                UiAction::None
334            }
335            (KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
336                state.input.insert_char(c); update_suggestion(state); UiAction::None
337            }
338            _ => UiAction::None,
339        }
340    }
341
342    fn tool_history_key(key: KeyEvent, state: &mut AppState) -> UiAction {
343        use crate::widgets::tool_history::flat_tools;
344        let total = flat_tools(state).len();
345        match key.code {
346            KeyCode::Esc | KeyCode::Char('h') | KeyCode::Left => {
347                if state.tool_history.detail_open {
348                    state.tool_history.detail_open = false;
349                } else {
350                    state.tool_history.focused = false;
351                }
352                UiAction::None
353            }
354            KeyCode::Up | KeyCode::Char('k') => {
355                if total == 0 { return UiAction::None; }
356                let cur = state.tool_history.selected.unwrap_or(0);
357                state.tool_history.selected = Some(cur.saturating_sub(1));
358                UiAction::None
359            }
360            KeyCode::Down | KeyCode::Char('j') => {
361                if total == 0 { return UiAction::None; }
362                let cur = state.tool_history.selected.unwrap_or(total - 1);
363                state.tool_history.selected = Some((cur + 1).min(total - 1));
364                UiAction::None
365            }
366            KeyCode::Char('g') => {
367                if total > 0 { state.tool_history.selected = Some(0); }
368                UiAction::None
369            }
370            KeyCode::Char('G') => {
371                if total > 0 { state.tool_history.selected = Some(total - 1); }
372                UiAction::None
373            }
374            KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right => {
375                if state.tool_history.selected.is_some() {
376                    state.tool_history.detail_open = true;
377                }
378                UiAction::None
379            }
380            _ => UiAction::None,
381        }
382    }
383
384    fn normal_mode_key(key: KeyEvent, state: &mut AppState) -> UiAction {
385        match (key.code, key.modifiers) {
386            (KeyCode::Char('c'), KeyModifiers::CONTROL) => {
387                state.modal.open_quit_confirm();
388                UiAction::None
389            }
390            (KeyCode::Char('p'), KeyModifiers::CONTROL) => {
391                open_command_palette(state);
392                UiAction::None
393            }
394            (KeyCode::Char('s'), KeyModifiers::CONTROL) => {
395                open_session_list(state);
396                UiAction::None
397            }
398            (KeyCode::Char('m'), KeyModifiers::CONTROL) => {
399                open_model_picker(state);
400                UiAction::None
401            }
402            (KeyCode::Char('b'), KeyModifiers::CONTROL) => UiAction::None,
403            (KeyCode::BackTab, _) => UiAction::CyclePermissionMode,
404            (KeyCode::Tab, _) => { state.input.mode = InputMode::Insert; UiAction::None }
405            (KeyCode::Char('i'), _) | (KeyCode::Char('a'), _) => {
406                if key.code == KeyCode::Char('a') { state.input.move_cursor_right(); }
407                state.input.mode = InputMode::Insert; UiAction::None
408            }
409            (KeyCode::Char('I'), _) => {
410                state.input.cursor_pos = 0; state.input.mode = InputMode::Insert; UiAction::None
411            }
412            (KeyCode::Char('A'), _) => {
413                state.input.cursor_pos = state.input.buffer.len(); state.input.mode = InputMode::Insert; UiAction::None
414            }
415            (KeyCode::Char('h'), _) | (KeyCode::Left, _) => { state.input.move_cursor_left(); UiAction::None }
416            (KeyCode::Char('l'), _) | (KeyCode::Right, _) => { state.input.move_cursor_right(); UiAction::None }
417            (KeyCode::Char('0'), _) => { state.input.cursor_pos = 0; UiAction::None }
418            (KeyCode::Char('$'), _) => { state.input.cursor_pos = state.input.buffer.len(); UiAction::None }
419            (KeyCode::Char('w'), _) => { state.input.move_word_right(); UiAction::None }
420            (KeyCode::Char('b'), _) => { state.input.move_word_left(); UiAction::None }
421            (KeyCode::Char('x'), _) => { state.input.delete_char_forward(); UiAction::None }
422            (KeyCode::Char('k'), _) | (KeyCode::Up, _) => { scroll_up(state, 3); UiAction::None }
423            (KeyCode::Char('j'), _) | (KeyCode::Down, _) => { scroll_down(state, 3); UiAction::None }
424            (KeyCode::Char('K'), _) => { state.input.history_prev(); UiAction::None }
425            (KeyCode::Char('J'), _) => { state.input.history_next(); UiAction::None }
426            (KeyCode::Char('u'), KeyModifiers::CONTROL) => { state.input.clear(); UiAction::None }
427            (KeyCode::Char('d'), KeyModifiers::CONTROL) => { scroll_down(state, 20); UiAction::None }
428            (KeyCode::PageUp, _) => { scroll_up(state, 20); UiAction::None }
429            (KeyCode::PageDown, _) => { scroll_down(state, 20); UiAction::None }
430            (KeyCode::Enter, _) => {
431                let text = state.input.buffer.trim().to_string();
432                if text.is_empty() { return UiAction::None; }
433                state.input.push_history(text.clone());
434                state.input.clear();
435                state.input.mode = InputMode::Insert;
436                UiAction::Submit(text)
437            }
438            _ => UiAction::None,
439        }
440    }
441
442    fn modal_key(key: KeyEvent, state: &mut AppState) -> UiAction {
443        if let Some(ModalKind::QuitConfirm) = &state.modal.active {
444            match (key.code, key.modifiers) {
445                (KeyCode::Char('y'), _) | (KeyCode::Char('Y'), _) | (KeyCode::Enter, _) => {
446                    return UiAction::Quit;
447                }
448                _ => {
449                    state.modal.close();
450                    return UiAction::None;
451                }
452            }
453        }
454
455        if let Some(ModalKind::Info { .. }) = &state.modal.active {
456            match (key.code, key.modifiers) {
457                (KeyCode::Esc, _) | (KeyCode::Enter, _) | (KeyCode::Char('q'), _) => {
458                    state.modal.close();
459                }
460                _ => {}
461            }
462            return UiAction::None;
463        }
464        if let Some(ModalKind::Input { buffer, kind, .. }) = state.modal.active.as_mut() {
465            match (key.code, key.modifiers) {
466                (KeyCode::Esc, _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => {
467                    state.modal.close();
468                    return UiAction::None;
469                }
470                (KeyCode::Enter, _) => {
471                    let value = buffer.trim().to_string();
472                    let k = *kind;
473                    state.modal.close();
474                    if value.is_empty() { return UiAction::None; }
475                    return UiAction::InputConfirmed { kind: k, value };
476                }
477                (KeyCode::Backspace, _) => { buffer.pop(); return UiAction::None; }
478                (KeyCode::Char('u'), KeyModifiers::CONTROL) => { buffer.clear(); return UiAction::None; }
479                (KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
480                    buffer.push(c);
481                    return UiAction::None;
482                }
483                _ => return UiAction::None,
484            }
485        }
486        let active = state.modal.active.as_mut();
487        let Some(active) = active else { return UiAction::None; };
488        match active {
489            ModalKind::Info { .. } | ModalKind::Input { .. } | ModalKind::QuitConfirm => UiAction::None,
490            ModalKind::Permission { choice, .. } => match (key.code, key.modifiers) {
491                (KeyCode::Esc, _) => {
492                    state.modal.close();
493                    UiAction::PermissionDecision(PermissionChoice::Reject)
494                }
495                (KeyCode::Left, _) | (KeyCode::Char('h'), _) => {
496                    *choice = match *choice {
497                        PermissionChoice::Once => PermissionChoice::Reject,
498                        PermissionChoice::Always => PermissionChoice::Once,
499                        PermissionChoice::Reject => PermissionChoice::Always,
500                    };
501                    UiAction::None
502                }
503                (KeyCode::Right, _) | (KeyCode::Char('l'), _) => {
504                    *choice = match *choice {
505                        PermissionChoice::Once => PermissionChoice::Always,
506                        PermissionChoice::Always => PermissionChoice::Reject,
507                        PermissionChoice::Reject => PermissionChoice::Once,
508                    };
509                    UiAction::None
510                }
511                (KeyCode::Enter, _) => {
512                    let decision = *choice;
513                    state.modal.close();
514                    UiAction::PermissionDecision(decision)
515                }
516                (KeyCode::Char('y'), _) | (KeyCode::Char('Y'), _) => {
517                    state.modal.close();
518                    UiAction::PermissionDecision(PermissionChoice::Once)
519                }
520                (KeyCode::Char('a'), _) | (KeyCode::Char('A'), _) => {
521                    state.modal.close();
522                    UiAction::PermissionDecision(PermissionChoice::Always)
523                }
524                (KeyCode::Char('n'), _) | (KeyCode::Char('N'), _) => {
525                    state.modal.close();
526                    UiAction::PermissionDecision(PermissionChoice::Reject)
527                }
528                _ => UiAction::None,
529            },
530            ModalKind::Select {
531                query,
532                options,
533                selected,
534                kind,
535                ..
536            } => match (key.code, key.modifiers) {
537                (KeyCode::Esc, _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => {
538                    state.modal.close();
539                    UiAction::None
540                }
541                (KeyCode::Up, _) => {
542                    let len = filter_options(options, query).len();
543                    if len > 0 {
544                        *selected = if *selected == 0 { len - 1 } else { *selected - 1 };
545                    }
546                    UiAction::None
547                }
548                (KeyCode::Down, _) => {
549                    let len = filter_options(options, query).len();
550                    if len > 0 {
551                        *selected = (*selected + 1) % len;
552                    }
553                    UiAction::None
554                }
555                (KeyCode::PageUp, _) => {
556                    *selected = selected.saturating_sub(10);
557                    UiAction::None
558                }
559                (KeyCode::PageDown, _) => {
560                    let len = filter_options(options, query).len();
561                    if len > 0 {
562                        *selected = (*selected + 10).min(len - 1);
563                    }
564                    UiAction::None
565                }
566                (KeyCode::Backspace, _) => {
567                    query.pop();
568                    *selected = 0;
569                    UiAction::None
570                }
571                (KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
572                    query.push(c);
573                    *selected = 0;
574                    UiAction::None
575                }
576                (KeyCode::Enter, _) => {
577                    let filtered = filter_options(options, query);
578                    let Some(&orig) = filtered.get(*selected) else {
579                        state.modal.close();
580                        return UiAction::None;
581                    };
582                    let value = options[orig].value.clone();
583                    let kind = *kind;
584                    state.modal.close();
585                    match kind {
586                        SelectKind::CommandPalette => dispatch_palette(state, &value),
587                        SelectKind::ModelPicker => UiAction::SelectModel(value),
588                        SelectKind::SessionList => {
589                            if value == "__empty__" {
590                                UiAction::None
591                            } else {
592                                UiAction::SelectSession(value)
593                            }
594                        }
595                        SelectKind::Help => UiAction::None,
596                        SelectKind::SkillPicker => {
597                            if let Some(name) = value.strip_prefix("skill:") {
598                                state.input.buffer = format!("/{name} ");
599                                state.input.cursor_pos = state.input.buffer.len();
600                                state.input.mode = InputMode::Insert;
601                            }
602                            UiAction::None
603                        }
604                        SelectKind::FileMention => {
605                            if value != "__empty__" {
606
607                                for c in value.chars() {
608                                    state.input.insert_char(c);
609                                }
610                                state.input.insert_char(' ');
611                            }
612                            state.input.mode = InputMode::Insert;
613                            UiAction::None
614                        }
615                        SelectKind::ThemePicker => {
616                            if crate::theme::set_theme(&value) {
617                                state.toasts.success(format!("theme → {value}"));
618                            }
619                            UiAction::None
620                        }
621                    }
622                }
623                _ => UiAction::None,
624            },
625        }
626    }
627}
628
629impl Default for EventHandler {
630    fn default() -> Self { Self }
631}