1use std::io::{self, BufRead, BufReader, IsTerminal, Read, Write};
4use std::path::Path;
5
6use crate::config::cli::FilterArgs;
7use crate::engine::{self, EngineFlags, EngineKind};
8
9pub mod app;
10pub mod run;
11pub mod ui;
12pub use app::{FilterApp, Outcome};
13
14#[derive(Debug, Clone, Copy, Default)]
15pub struct FilterOptions {
16 pub invert: bool,
17 pub case_insensitive: bool,
18}
19
20impl FilterOptions {
21 fn flags(&self) -> EngineFlags {
22 EngineFlags {
23 case_insensitive: self.case_insensitive,
24 ..EngineFlags::default()
25 }
26 }
27}
28
29pub fn filter_lines(
36 lines: &[String],
37 pattern: &str,
38 options: FilterOptions,
39) -> Result<Vec<usize>, String> {
40 if pattern.is_empty() {
41 return Ok(if options.invert {
43 Vec::new()
44 } else {
45 (0..lines.len()).collect()
46 });
47 }
48
49 let engine = engine::create_engine(EngineKind::RustRegex);
50 let compiled = engine
51 .compile(pattern, &options.flags())
52 .map_err(|e| e.to_string())?;
53
54 let mut indices = Vec::with_capacity(lines.len());
55 for (idx, line) in lines.iter().enumerate() {
56 let matched = compiled
57 .find_matches(line)
58 .map(|v| !v.is_empty())
59 .unwrap_or(false);
60 if matched != options.invert {
61 indices.push(idx);
62 }
63 }
64 Ok(indices)
65}
66
67pub const EXIT_MATCH: i32 = 0;
69pub const EXIT_NO_MATCH: i32 = 1;
70pub const EXIT_ERROR: i32 = 2;
71
72pub fn emit_matches(
75 writer: &mut dyn Write,
76 lines: &[String],
77 matched: &[usize],
78 line_number: bool,
79) -> io::Result<()> {
80 for &idx in matched {
81 if line_number {
82 writeln!(writer, "{}:{}", idx + 1, lines[idx])?;
83 } else {
84 writeln!(writer, "{}", lines[idx])?;
85 }
86 }
87 Ok(())
88}
89
90pub fn emit_count(writer: &mut dyn Write, matched_count: usize) -> io::Result<()> {
92 writeln!(writer, "{matched_count}")
93}
94
95pub fn read_input(file: Option<&Path>, fallback: impl Read) -> io::Result<Vec<String>> {
99 let reader: Box<dyn BufRead> = match file {
100 Some(path) => Box::new(BufReader::new(std::fs::File::open(path)?)),
101 None => Box::new(BufReader::new(fallback)),
102 };
103 let mut out = Vec::new();
104 for line in reader.lines() {
105 out.push(line?);
106 }
107 Ok(out)
108}
109
110pub fn entry(args: FilterArgs) -> i32 {
113 match run_entry(args) {
114 Ok(code) => code,
115 Err(msg) => {
116 eprintln!("rgx filter: {msg}");
117 EXIT_ERROR
118 }
119 }
120}
121
122fn run_entry(args: FilterArgs) -> Result<i32, String> {
123 let lines =
124 read_input(args.file.as_deref(), io::stdin()).map_err(|e| format!("reading input: {e}"))?;
125
126 let options = FilterOptions {
127 invert: args.invert,
128 case_insensitive: args.case_insensitive,
129 };
130
131 let has_pattern = args.pattern.as_deref().is_some_and(|p| !p.is_empty());
134 let stdout_is_tty = io::stdout().is_terminal();
135 let non_interactive = args.count || args.line_number || (has_pattern && !stdout_is_tty);
136
137 if non_interactive {
138 let pattern = args.pattern.unwrap_or_default();
139 let matched =
140 filter_lines(&lines, &pattern, options).map_err(|e| format!("pattern: {e}"))?;
141
142 let mut stdout = io::stdout().lock();
143 if args.count {
144 emit_count(&mut stdout, matched.len()).map_err(|e| format!("writing output: {e}"))?;
145 } else {
146 emit_matches(&mut stdout, &lines, &matched, args.line_number)
147 .map_err(|e| format!("writing output: {e}"))?;
148 }
149 return Ok(if matched.is_empty() {
150 EXIT_NO_MATCH
151 } else {
152 EXIT_MATCH
153 });
154 }
155
156 let initial_pattern = args.pattern.unwrap_or_default();
158 let app = FilterApp::new(lines, &initial_pattern, options);
159 let (final_app, outcome) = run::run_tui(app).map_err(|e| format!("tui: {e}"))?;
160
161 match outcome {
162 Outcome::Emit => {
163 let mut stdout = io::stdout().lock();
164 emit_matches(&mut stdout, &final_app.lines, &final_app.matched, false)
165 .map_err(|e| format!("writing output: {e}"))?;
166 Ok(if final_app.matched.is_empty() {
167 EXIT_NO_MATCH
168 } else {
169 EXIT_MATCH
170 })
171 }
172 Outcome::Discard => Ok(EXIT_NO_MATCH),
173 Outcome::Pending => Ok(EXIT_ERROR),
174 }
175}