//! Lightweight meeting-process detection. //! //! Scope (per Jake's ideology note): single signal only — poll the process //! list and match user-editable patterns. No mic-activity heuristic, no //! calendar integration. If the user opts in, we surface a non-modal toast //! so they can decide to start recording. We never start recording //! ourselves from this signal. use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System}; /// Snapshot the current process list's executable/command names. Lowercased /// for case-insensitive pattern matching. pub fn list_running_process_names() -> Vec { let mut system = System::new_with_specifics( RefreshKind::nothing().with_processes(ProcessRefreshKind::nothing()), ); system.refresh_processes(ProcessesToUpdate::All, true); system .processes() .values() .map(|process| process.name().to_string_lossy().to_lowercase()) .collect() } /// Match a snapshot of process names against case-insensitive substring /// `patterns`. Returns the set of patterns that matched at least once, in /// input order, deduped. Empty / whitespace-only patterns are skipped so /// a stray blank entry in the user's list never matches everything. pub fn match_meeting_patterns(process_names: &[String], patterns: &[String]) -> Vec { let mut matches: Vec = Vec::new(); for raw_pattern in patterns { let needle = raw_pattern.trim().to_lowercase(); if needle.is_empty() { continue; } if process_names.iter().any(|name| name.contains(&needle)) && !matches.iter().any(|existing| existing == &needle) { matches.push(needle); } } matches } #[cfg(test)] mod tests { use super::*; #[test] fn matches_are_case_insensitive_substrings() { let processes = vec![ "Zoom Meeting".to_lowercase(), "firefox".to_lowercase(), "Microsoft Teams".to_lowercase(), ]; let patterns = vec!["ZOOM".into(), "teams".into(), "discord".into()]; let got = match_meeting_patterns(&processes, &patterns); assert_eq!(got, vec!["zoom", "teams"]); } #[test] fn empty_and_whitespace_patterns_are_ignored() { let processes = vec!["anything".to_lowercase()]; let patterns = vec!["".into(), " ".into()]; assert!(match_meeting_patterns(&processes, &patterns).is_empty()); } #[test] fn matches_are_deduped() { let processes = vec!["zoomclient".into(), "zoomhelper".into()]; let patterns = vec!["zoom".into(), "zoom".into()]; assert_eq!(match_meeting_patterns(&processes, &patterns), vec!["zoom"]); } #[test] fn list_running_returns_something_on_this_host() { // Smoke check — this is the test host and always has running procs. let names = list_running_process_names(); assert!(!names.is_empty(), "expected at least one running process"); } }