tui_test/terminal/
locator.rs1use regex::Regex;
5
6use super::cell::EmuCell;
7
8pub enum Pattern {
9 Text(String),
10 Regex(Regex),
11}
12
13impl Pattern {
14 pub fn new(text: &str, is_regex: bool) -> anyhow::Result<Self> {
15 if is_regex {
16 Ok(Pattern::Regex(Regex::new(text)?))
17 } else {
18 Ok(Pattern::Text(text.to_string()))
19 }
20 }
21
22 pub fn describe(&self) -> String {
23 match self {
24 Pattern::Text(t) => t.clone(),
25 Pattern::Regex(r) => r.as_str().to_string(),
26 }
27 }
28
29 pub fn matches(&self, haystack: &str) -> bool {
35 match self {
36 Pattern::Text(t) => haystack.contains(t.as_str()),
37 Pattern::Regex(r) => r.is_match(haystack),
38 }
39 }
40}
41
42#[derive(Debug, Clone)]
43pub struct MatchedCell {
44 pub x: usize,
45 pub y: usize,
46 pub cell: EmuCell,
47}
48
49pub fn find(
52 rows: &[Vec<EmuCell>],
53 pattern: &Pattern,
54 strict: bool,
55) -> anyhow::Result<Option<Vec<MatchedCell>>> {
56 if rows.is_empty() {
57 return Ok(None);
58 }
59 let width = rows.iter().map(|r| r.len()).max().unwrap_or(0);
60 let chars: Vec<char> = rows
64 .iter()
65 .flat_map(|row| {
66 (0..width).map(move |x| row.get(x).and_then(|c| c.ch.chars().next()).unwrap_or(' '))
67 })
68 .collect();
69
70 let (index, length) = match pattern {
71 Pattern::Text(text) => {
72 let needle: Vec<char> = text.chars().collect();
73 if needle.is_empty() {
74 return Ok(None);
75 }
76 let occurrences = count_occurrences(&chars, &needle);
77 if occurrences == 0 {
78 return Ok(None);
79 }
80 if occurrences > 1 && strict {
81 anyhow::bail!(
82 "strict mode expected one match for '{}', but found {}",
83 text,
84 occurrences
85 );
86 }
87 let first = first_occurrence(&chars, &needle).unwrap();
88 (first, needle.len())
89 }
90 Pattern::Regex(re) => {
91 let block: String = chars.iter().collect();
92 let matches: Vec<_> = re.find_iter(&block).collect();
93 if matches.is_empty() {
94 return Ok(None);
95 }
96 if matches.len() > 1 && strict {
97 anyhow::bail!(
98 "strict mode expected one match for '{}', but found {}",
99 re.as_str(),
100 matches.len()
101 );
102 }
103 let m = &matches[0];
104 let start = block[..m.start()].chars().count();
105 let len = m.as_str().chars().count();
106 (start, len)
107 }
108 };
109
110 let mut cells = Vec::with_capacity(length);
111 for (y, row) in rows.iter().enumerate() {
112 for x in 0..width {
113 let pos = x + y * width;
114 if pos >= index && pos < index + length {
115 if let Some(cell) = row.get(x) {
116 cells.push(MatchedCell {
117 x,
118 y,
119 cell: cell.clone(),
120 });
121 }
122 }
123 }
124 }
125 Ok(Some(cells))
126}
127
128fn count_occurrences(haystack: &[char], needle: &[char]) -> usize {
129 if needle.is_empty() || haystack.len() < needle.len() {
130 return 0;
131 }
132 let mut count = 0;
133 let mut i = 0;
134 while i + needle.len() <= haystack.len() {
135 if haystack[i..i + needle.len()] == *needle {
136 count += 1;
137 i += needle.len();
138 } else {
139 i += 1;
140 }
141 }
142 count
143}
144
145fn first_occurrence(haystack: &[char], needle: &[char]) -> Option<usize> {
146 if needle.is_empty() || haystack.len() < needle.len() {
147 return None;
148 }
149 (0..=haystack.len() - needle.len()).find(|&i| haystack[i..i + needle.len()] == *needle)
150}