chasm_cli/tui/
events.rs

1// Copyright (c) 2024-2026 Nervosys LLC
2// SPDX-License-Identifier: Apache-2.0
3//! Event handling and main TUI loop
4
5use std::io;
6
7use anyhow::Result;
8use crossterm::{
9    event::{
10        self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers,
11    },
12    execute,
13    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
14};
15use ratatui::{backend::CrosstermBackend, Terminal};
16
17use super::app::{App, AppMode};
18use super::ui;
19
20/// Run the TUI application
21pub fn run_tui() -> Result<()> {
22    // Setup terminal
23    enable_raw_mode()?;
24    let mut stdout = io::stdout();
25    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
26    let backend = CrosstermBackend::new(stdout);
27    let mut terminal = Terminal::new(backend)?;
28
29    // Create app state
30    let mut app = App::new()?;
31
32    // Main loop
33    let res = run_app(&mut terminal, &mut app);
34
35    // Restore terminal
36    disable_raw_mode()?;
37    execute!(
38        terminal.backend_mut(),
39        LeaveAlternateScreen,
40        DisableMouseCapture
41    )?;
42    terminal.show_cursor()?;
43
44    if let Err(err) = res {
45        eprintln!("Error: {}", err);
46    }
47
48    Ok(())
49}
50
51/// Main application loop
52fn run_app<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Result<()> {
53    // Initial draw
54    terminal.draw(|f| ui::render(f, app))?;
55
56    loop {
57        // Block waiting for input - no polling delay, instant response
58        if let Event::Key(key) = event::read()? {
59            // Only handle key press events, ignore release/repeat to prevent double-triggering
60            if key.kind != KeyEventKind::Press {
61                continue;
62            }
63
64            // Clear status message on any keypress
65            app.status_message = None;
66
67            // Handle filter input mode specially
68            if app.filter_active {
69                match key.code {
70                    KeyCode::Enter => app.confirm_filter(),
71                    KeyCode::Esc => app.cancel_filter(),
72                    KeyCode::Backspace => app.filter_backspace(),
73                    KeyCode::Char(c) => app.filter_input(c),
74                    _ => {}
75                }
76                terminal.draw(|f| ui::render(f, app))?;
77                continue;
78            }
79
80            // Help mode - any key closes it
81            if app.mode == AppMode::Help {
82                app.back();
83                terminal.draw(|f| ui::render(f, app))?;
84                continue;
85            }
86
87            // Normal key handling
88            match key.code {
89                KeyCode::Char('q') => {
90                    return Ok(());
91                }
92                KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
93                    return Ok(());
94                }
95                KeyCode::Char('?') => {
96                    app.toggle_help();
97                }
98                KeyCode::Char('j') | KeyCode::Down => {
99                    app.navigate_down();
100                }
101                KeyCode::Char('k') | KeyCode::Up => {
102                    app.navigate_up();
103                }
104                KeyCode::Char('g') => {
105                    app.go_to_top();
106                }
107                KeyCode::Char('G') => {
108                    app.go_to_bottom();
109                }
110                KeyCode::PageUp => {
111                    app.page_up();
112                }
113                KeyCode::PageDown => {
114                    app.page_down();
115                }
116                KeyCode::Enter => {
117                    app.enter();
118                }
119                KeyCode::Esc | KeyCode::Backspace => {
120                    app.back();
121                }
122                KeyCode::Char('/') if app.mode == AppMode::Workspaces => {
123                    app.start_filter();
124                }
125                KeyCode::Char('r') => {
126                    app.refresh();
127                }
128                _ => continue, // No redraw needed for unhandled keys
129            }
130
131            // Redraw after handling input
132            terminal.draw(|f| ui::render(f, app))?;
133        }
134    }
135}