use crate::windows_api::WindowInfo;
use anyhow::Result;
use regex::{Regex, RegexBuilder};
#[derive(Debug, Clone, PartialEq)]
pub enum MatchMode {
Substring,
Wildcard,
Regex,
}
pub fn detect_mode(query: &str, force_regex: bool) -> MatchMode {
if force_regex {
return MatchMode::Regex;
}
if query.contains('*') || query.contains('?') {
MatchMode::Wildcard
} else {
MatchMode::Substring
}
}
fn wildcard_to_regex(pattern: &str) -> String {
let mut re = String::from("(?i)^");
for ch in pattern.chars() {
match ch {
'*' => re.push_str(".*"),
'?' => re.push('.'),
c => re.push_str(®ex::escape(&c.to_string())),
}
}
re.push('$');
re
}
pub fn filter_windows<'a>(
windows: &'a [WindowInfo],
query: &str,
mode: &MatchMode,
) -> Result<Vec<&'a WindowInfo>> {
let re: Option<Regex> = match mode {
MatchMode::Substring => None, MatchMode::Wildcard => {
let pat = wildcard_to_regex(query);
Some(Regex::new(&pat)?)
}
MatchMode::Regex => Some(RegexBuilder::new(query).case_insensitive(true).build()?),
};
let matches = windows
.iter()
.filter(|w| {
if let Some(ref r) = re {
r.is_match(&w.title)
} else {
w.title.to_lowercase().contains(&query.to_lowercase())
}
})
.collect();
Ok(matches)
}
#[allow(dead_code)]
pub fn is_valid_regex(query: &str) -> bool {
Regex::new(query).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::windows_api::WindowInfo;
fn make_windows() -> Vec<WindowInfo> {
vec![
WindowInfo {
hwnd: 1,
title: "Firefox — GitHub".into(),
},
WindowInfo {
hwnd: 2,
title: "Visual Studio Code".into(),
},
WindowInfo {
hwnd: 3,
title: "Windows Terminal".into(),
},
WindowInfo {
hwnd: 4,
title: "Notepad — readme.txt".into(),
},
]
}
#[test]
fn substring_match() {
let ws = make_windows();
let results = filter_windows(&ws, "terminal", &MatchMode::Substring).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].title, "Windows Terminal");
}
#[test]
fn wildcard_star() {
let ws = make_windows();
let results = filter_windows(&ws, "*studio*", &MatchMode::Wildcard).unwrap();
assert_eq!(results.len(), 1);
}
#[test]
fn regex_match() {
let ws = make_windows();
let results = filter_windows(&ws, r"fire|note", &MatchMode::Regex).unwrap();
assert_eq!(results.len(), 2);
}
#[test]
fn detect_wildcard() {
assert_eq!(detect_mode("*foo*", false), MatchMode::Wildcard);
assert_eq!(detect_mode("foo?", false), MatchMode::Wildcard);
}
#[test]
fn detect_substring() {
assert_eq!(detect_mode("firefox", false), MatchMode::Substring);
}
#[test]
fn detect_forced_regex() {
assert_eq!(detect_mode("firefox", true), MatchMode::Regex);
}
}