Files
Lumotia/crates/core/src/process_watch.rs
Jake 6837700ac9 style(clippy): clean up the two lints in phase3 new code
QC smoke sweep flagged two clippy -D warnings lints in code this branch
introduced:

- crates/core/src/process_watch.rs — collapsible_if on the meeting-pattern
  match loop, merged the two conditions with &&.
- crates/mcp/src/lib.rs — let-else on the id unwrap that short-circuits a
  notification, switched to ? since handle_message already returns Option.

All other clippy lints under -D warnings (audio/capture, hotkey/linux,
storage/file_storage, diagnostics, the duplicate-detection helpers in
live.rs) predate this branch and are out of scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:06:47 +01:00

86 lines
3.0 KiB
Rust

//! 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<String> {
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<String> {
let mut matches: Vec<String> = 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");
}
}