diff --git a/crates/client/src/windows_mgmt.rs b/crates/client/src/windows_mgmt.rs index 8210a21..2f2034d 100644 --- a/crates/client/src/windows_mgmt.rs +++ b/crates/client/src/windows_mgmt.rs @@ -16,7 +16,8 @@ mod win_impl { keybd_event, KEYEVENTF_KEYUP, VK_MENU, }; use windows::Win32::System::Threading::{ - OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, + OpenProcess, QueryFullProcessImageNameW, PROCESS_NAME_FORMAT, + PROCESS_QUERY_LIMITED_INFORMATION, }; use windows::Win32::System::ProcessStatus::GetModuleBaseNameW; @@ -46,6 +47,8 @@ mod win_impl { } /// Get the process name (exe without extension) for a window handle. + /// Tries `GetModuleBaseNameW` first, then `QueryFullProcessImageNameW` + /// (which works for elevated processes with `PROCESS_QUERY_LIMITED_INFORMATION`). fn hwnd_process_name(hwnd: HWND) -> Option { unsafe { let mut pid: u32 = 0; @@ -54,24 +57,50 @@ mod win_impl { return None; } let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid).ok()?; + + // Try GetModuleBaseNameW first let mut buf = [0u16; 260]; let len = GetModuleBaseNameW(handle, None, &mut buf); - let _ = windows::Win32::Foundation::CloseHandle(handle); - if len == 0 { - return None; + if len > 0 { + let _ = windows::Win32::Foundation::CloseHandle(handle); + let name = String::from_utf16_lossy(&buf[..len as usize]); + return Some(strip_exe_ext(&name)); } - let name = String::from_utf16_lossy(&buf[..len as usize]); - // Strip .exe extension - Some(name.strip_suffix(".exe").or(name.strip_suffix(".EXE")).unwrap_or(&name).to_string()) + + // Fallback: QueryFullProcessImageNameW (works for elevated processes) + let mut buf2 = [0u16; 1024]; + let mut size = buf2.len() as u32; + let ok = QueryFullProcessImageNameW(handle, PROCESS_NAME_FORMAT(0), &mut buf2, &mut size); + let _ = windows::Win32::Foundation::CloseHandle(handle); + if ok.is_ok() && size > 0 { + let full_path = String::from_utf16_lossy(&buf2[..size as usize]); + // Extract filename from full path + let filename = full_path.rsplit('\\').next().unwrap_or(&full_path); + return Some(strip_exe_ext(filename)); + } + + None } } - /// Minimal filter: only block explorer's "Program Manager" desktop window. - fn is_ghost_window(_hwnd: HWND, title: &str, process_name: &str) -> bool { - let proc_lower = process_name.to_lowercase(); - // Only block explorer when title is "Program Manager" or empty - if proc_lower == "explorer" { - let t = title.trim(); + fn strip_exe_ext(name: &str) -> String { + name.strip_suffix(".exe") + .or(name.strip_suffix(".EXE")) + .unwrap_or(name) + .to_string() + } + + /// Filter out ghost/invisible windows. Works even without a process name. + fn is_ghost_window(title: &str, process_name: &str) -> bool { + let t = title.trim(); + if t.is_empty() { + return true; + } + if t.eq_ignore_ascii_case("program manager") { + return true; + } + // Block explorer when title is empty or "Program Manager" + if !process_name.is_empty() && process_name.to_lowercase() == "explorer" { if t.is_empty() || t.eq_ignore_ascii_case("program manager") { return true; } @@ -109,10 +138,7 @@ mod win_impl { continue; } let process_name = hwnd_process_name(*hwnd).unwrap_or_default(); - if process_name.is_empty() { - continue; - } - if is_ghost_window(*hwnd, &title, &process_name) { + if is_ghost_window(&title, &process_name) { continue; } raw_windows.push((*hwnd, title, process_name)); @@ -120,16 +146,26 @@ mod win_impl { // Second pass: generate labels from process names with dedup numbering let mut label_counts: HashMap = HashMap::new(); - // First count how many of each process name we have - for (_, _, proc_name) in &raw_windows { - let base = sanitize_label(proc_name); - *label_counts.entry(base).or_insert(0) += 1; + // First count how many of each label base we have + for (_, title, proc_name) in &raw_windows { + let base = if proc_name.is_empty() { + sanitize_label(title) + } else { + sanitize_label(proc_name) + }; + if !base.is_empty() { + *label_counts.entry(base).or_insert(0) += 1; + } } let mut label_index: HashMap = HashMap::new(); let mut windows = Vec::new(); for (hwnd, title, proc_name) in raw_windows { - let base = sanitize_label(&proc_name); + let base = if proc_name.is_empty() { + sanitize_label(&title) + } else { + sanitize_label(&proc_name) + }; if base.is_empty() { continue; }