Skip to main content

rgx/filter/
run.rs

1//! TUI event loop for `rgx filter`.
2
3use std::io;
4
5use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
6use crossterm::execute;
7use crossterm::terminal::{
8    disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
9};
10use ratatui::backend::CrosstermBackend;
11use ratatui::Terminal;
12
13use crate::filter::{FilterApp, Outcome};
14
15/// Run the TUI event loop to completion. Returns when the user hits Enter
16/// (emit) or Esc/q (discard). Input events come from crossterm's blocking
17/// `event::read()` — the filter UI has no background work, so async is not
18/// needed.
19pub fn run_tui(mut app: FilterApp) -> io::Result<(FilterApp, Outcome)> {
20    let mut stdout = io::stdout();
21    enable_raw_mode()?;
22    execute!(stdout, EnterAlternateScreen)?;
23
24    let backend = CrosstermBackend::new(stdout);
25    let mut terminal = Terminal::new(backend)?;
26
27    let result = event_loop(&mut terminal, &mut app);
28
29    disable_raw_mode()?;
30    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
31
32    result?;
33    let outcome = app.outcome;
34    Ok((app, outcome))
35}
36
37fn event_loop(
38    terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
39    app: &mut FilterApp,
40) -> io::Result<()> {
41    while !app.should_quit {
42        terminal.draw(|frame| crate::filter::ui::render(frame, app))?;
43        match crossterm::event::read()? {
44            Event::Key(key) => handle_key(app, key),
45            Event::Resize(_, _) => {}
46            _ => {}
47        }
48    }
49    Ok(())
50}
51
52pub fn handle_key(app: &mut FilterApp, key: KeyEvent) {
53    match key.code {
54        KeyCode::Esc => {
55            app.outcome = Outcome::Discard;
56            app.should_quit = true;
57        }
58        KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
59            app.outcome = Outcome::Discard;
60            app.should_quit = true;
61        }
62        KeyCode::Enter => {
63            app.outcome = Outcome::Emit;
64            app.should_quit = true;
65        }
66        KeyCode::Up => app.select_prev(),
67        KeyCode::Down => app.select_next(),
68        KeyCode::Char('i') if key.modifiers.contains(KeyModifiers::ALT) => {
69            app.toggle_case_insensitive();
70        }
71        KeyCode::Char('v') if key.modifiers.contains(KeyModifiers::ALT) => {
72            app.toggle_invert();
73        }
74        KeyCode::Backspace => {
75            app.pattern_editor.delete_back();
76            app.recompute();
77        }
78        KeyCode::Left => app.pattern_editor.move_left(),
79        KeyCode::Right => app.pattern_editor.move_right(),
80        KeyCode::Home => app.pattern_editor.move_home(),
81        KeyCode::End => app.pattern_editor.move_end(),
82        KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
83            app.pattern_editor.insert_char(c);
84            app.recompute();
85        }
86        _ => {}
87    }
88}