Skip to main content

rgx/filter/
app.rs

1//! TUI-mode state for `rgx filter`.
2
3use crate::engine::{self, CompiledRegex, EngineFlags, EngineKind, RegexEngine};
4use crate::filter::FilterOptions;
5use crate::input::editor::Editor;
6
7pub struct FilterApp {
8    pub pattern_editor: Editor,
9    pub options: FilterOptions,
10    pub lines: Vec<String>,
11    /// Indices of `lines` that currently match the pattern.
12    pub matched: Vec<usize>,
13    /// Selected index into `matched` for the cursor in the match list.
14    pub selected: usize,
15    /// Scroll offset (first visible index into `matched`).
16    pub scroll: usize,
17    /// Compilation error from the last `recompute`, if any.
18    pub error: Option<String>,
19    /// Whether to quit the event loop on next tick.
20    pub should_quit: bool,
21    /// Outcome decided by the user: emit the filtered output, or discard.
22    pub outcome: Outcome,
23    engine: Box<dyn RegexEngine>,
24    engine_flags: EngineFlags,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Outcome {
29    Pending,
30    Emit,
31    Discard,
32}
33
34impl FilterApp {
35    pub fn new(lines: Vec<String>, initial_pattern: &str, options: FilterOptions) -> Self {
36        let pattern_editor = Editor::with_content(initial_pattern.to_string());
37        let engine_flags = EngineFlags {
38            case_insensitive: options.case_insensitive,
39            ..EngineFlags::default()
40        };
41        let engine = engine::create_engine(EngineKind::RustRegex);
42        let mut app = Self {
43            pattern_editor,
44            options,
45            lines,
46            matched: Vec::new(),
47            selected: 0,
48            scroll: 0,
49            error: None,
50            should_quit: false,
51            outcome: Outcome::Pending,
52            engine,
53            engine_flags,
54        };
55        app.recompute();
56        app
57    }
58
59    pub fn pattern(&self) -> &str {
60        self.pattern_editor.content()
61    }
62
63    pub fn recompute(&mut self) {
64        self.error = None;
65        let pattern = self.pattern().to_string();
66        if pattern.is_empty() {
67            self.matched = if self.options.invert {
68                Vec::new()
69            } else {
70                (0..self.lines.len()).collect()
71            };
72            self.clamp_selection();
73            return;
74        }
75        match self.engine.compile(&pattern, &self.engine_flags) {
76            Ok(compiled) => {
77                self.matched = self.collect_matches(&*compiled);
78                self.clamp_selection();
79            }
80            Err(err) => {
81                self.error = Some(err.to_string());
82                self.matched.clear();
83                self.selected = 0;
84                self.scroll = 0;
85            }
86        }
87    }
88
89    fn collect_matches(&self, compiled: &dyn CompiledRegex) -> Vec<usize> {
90        let mut out = Vec::with_capacity(self.lines.len());
91        for (idx, line) in self.lines.iter().enumerate() {
92            let hit = compiled
93                .find_matches(line)
94                .map(|v| !v.is_empty())
95                .unwrap_or(false);
96            if hit != self.options.invert {
97                out.push(idx);
98            }
99        }
100        out
101    }
102
103    fn clamp_selection(&mut self) {
104        if self.matched.is_empty() {
105            self.selected = 0;
106            self.scroll = 0;
107        } else if self.selected >= self.matched.len() {
108            self.selected = self.matched.len() - 1;
109        }
110    }
111
112    pub fn select_next(&mut self) {
113        if self.selected + 1 < self.matched.len() {
114            self.selected += 1;
115        }
116    }
117
118    pub fn select_prev(&mut self) {
119        self.selected = self.selected.saturating_sub(1);
120    }
121
122    pub fn toggle_case_insensitive(&mut self) {
123        self.options.case_insensitive = !self.options.case_insensitive;
124        self.engine_flags.case_insensitive = self.options.case_insensitive;
125        self.recompute();
126    }
127
128    pub fn toggle_invert(&mut self) {
129        self.options.invert = !self.options.invert;
130        self.recompute();
131    }
132}