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
}
fn window_matches(w: &WindowInfo, query: &str, re: &Option<Regex>) -> bool {
let type_label = w.type_label();
let candidates = [w.title.as_str(), type_label.as_str()];
if let Some(ref r) = re {
candidates.iter().any(|s| r.is_match(s))
} else {
let q = query.to_lowercase();
candidates.iter().any(|s| s.to_lowercase().contains(&q))
}
}
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| window_matches(w, query, &re))
.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(),
process_name: "firefox.exe".into(),
class_name: "MozillaWindowClass".into(),
},
WindowInfo {
hwnd: 2,
title: "Visual Studio Code".into(),
process_name: "Code.exe".into(),
class_name: "Chrome_WidgetWin_1".into(),
},
WindowInfo {
hwnd: 3,
title: "my-project".into(),
process_name: "wt.exe".into(),
class_name: "CASCADIA_HOSTING_WINDOW_CLASS".into(),
},
WindowInfo {
hwnd: 4,
title: "ssh prod".into(),
process_name: "wt.exe".into(),
class_name: "CASCADIA_HOSTING_WINDOW_CLASS".into(),
},
WindowInfo {
hwnd: 5,
title: "Notepad — readme.txt".into(),
process_name: "notepad.exe".into(),
class_name: "Notepad".into(),
},
]
}
#[test]
fn substring_match_title() {
let ws = make_windows();
let r = filter_windows(&ws, "readme", &MatchMode::Substring).unwrap();
assert_eq!(r.len(), 1);
assert_eq!(r[0].title, "Notepad — readme.txt");
}
#[test]
fn substring_match_type_label() {
let ws = make_windows();
let r = filter_windows(&ws, "terminal", &MatchMode::Substring).unwrap();
assert_eq!(r.len(), 2, "both wt windows should match 'terminal'");
assert!(r.iter().all(|w| w.process_name == "wt.exe"));
}
#[test]
fn substring_match_wt_label() {
let ws = make_windows();
let r = filter_windows(&ws, "windows terminal", &MatchMode::Substring).unwrap();
assert_eq!(r.len(), 2);
}
#[test]
fn wildcard_star() {
let ws = make_windows();
let r = filter_windows(&ws, "*studio*", &MatchMode::Wildcard).unwrap();
assert_eq!(r.len(), 1);
}
#[test]
fn regex_match() {
let ws = make_windows();
let r = filter_windows(&ws, r"fire|note", &MatchMode::Regex).unwrap();
assert_eq!(r.len(), 2);
}
#[test]
fn regex_match_type_label() {
let ws = make_windows();
let r = filter_windows(&ws, "terminal", &MatchMode::Regex).unwrap();
assert_eq!(r.len(), 2);
}
#[test]
fn detect_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);
}
}