use crate::error::CaptureError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Target {
Display(usize),
WindowTitled(String),
WindowId(u32),
WindowContaining(String),
}
impl Target {
pub fn display(index: usize) -> Self {
Target::Display(index)
}
pub fn window_titled(title: impl Into<String>) -> Self {
Target::WindowTitled(title.into())
}
pub fn window_id(id: u32) -> Self {
Target::WindowId(id)
}
pub fn window_containing(substring: impl Into<String>) -> Self {
Target::WindowContaining(substring.into())
}
}
pub(crate) enum WindowSelector<'a> {
Exact(&'a str),
Containing(&'a str),
Id(u32),
}
impl WindowSelector<'_> {
fn describe(&self) -> String {
match self {
WindowSelector::Exact(t) => format!("window titled {t:?}"),
WindowSelector::Containing(s) => format!("window containing {s:?}"),
WindowSelector::Id(id) => format!("window id {id}"),
}
}
}
pub(crate) fn select_window(
windows: &[(u32, String)],
selector: &WindowSelector<'_>,
) -> Result<u32, CaptureError> {
let matches: Vec<&(u32, String)> = match selector {
WindowSelector::Id(id) => windows.iter().filter(|(wid, _)| wid == id).collect(),
WindowSelector::Exact(title) => windows.iter().filter(|(_, t)| t == title).collect(),
WindowSelector::Containing(sub) => {
let needle = sub.to_lowercase();
windows
.iter()
.filter(|(_, t)| t.to_lowercase().contains(&needle))
.collect()
}
};
match matches.as_slice() {
[] => Err(CaptureError::TargetNotFound(selector.describe())),
[(id, _)] => Ok(*id),
many => {
let candidates = many
.iter()
.map(|(id, title)| format!("{id}: {title:?}"))
.collect::<Vec<_>>()
.join(", ");
Err(CaptureError::AmbiguousTarget(format!(
"{} matches {} windows ({candidates}); disambiguate with Target::window_id",
selector.describe(),
many.len(),
)))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constructors_build_expected_variants() {
assert_eq!(Target::display(2), Target::Display(2));
assert_eq!(
Target::window_titled("Elden Ring"),
Target::WindowTitled("Elden Ring".to_string())
);
assert_eq!(Target::window_id(42), Target::WindowId(42));
assert_eq!(
Target::window_containing("YouTube"),
Target::WindowContaining("YouTube".to_string())
);
}
fn windows() -> Vec<(u32, String)> {
vec![
(10, "Item-0".to_string()),
(11, "Item-0".to_string()),
(20, "Diablo IV - Twitch — Google Chrome".to_string()),
(30, "Terminal".to_string()),
]
}
#[test]
fn exact_single_match_wins() {
let sel = WindowSelector::Exact("Terminal");
assert_eq!(select_window(&windows(), &sel).unwrap(), 30);
}
#[test]
fn exact_duplicate_titles_are_ambiguous() {
let sel = WindowSelector::Exact("Item-0");
let err = select_window(&windows(), &sel).unwrap_err();
let msg = err.to_string();
assert!(matches!(err, CaptureError::AmbiguousTarget(_)));
assert!(msg.contains("10") && msg.contains("11"), "got: {msg}");
assert!(msg.contains("window_id"), "must point at the fix: {msg}");
}
#[test]
fn containing_is_case_insensitive() {
let sel = WindowSelector::Containing("google chrome");
assert_eq!(select_window(&windows(), &sel).unwrap(), 20);
}
#[test]
fn containing_multiple_matches_are_ambiguous() {
let sel = WindowSelector::Containing("item");
assert!(matches!(
select_window(&windows(), &sel),
Err(CaptureError::AmbiguousTarget(_))
));
}
#[test]
fn zero_matches_are_not_found() {
let sel = WindowSelector::Containing("Balatro");
assert!(matches!(
select_window(&windows(), &sel),
Err(CaptureError::TargetNotFound(_))
));
}
#[test]
fn id_selector_hits_and_misses() {
assert_eq!(
select_window(&windows(), &WindowSelector::Id(20)).unwrap(),
20
);
assert!(matches!(
select_window(&windows(), &WindowSelector::Id(999)),
Err(CaptureError::TargetNotFound(_))
));
}
}