241 lines
8.5 KiB
Rust
241 lines
8.5 KiB
Rust
use helios_common::protocol::{sanitize_label, WindowInfo};
|
|
use std::collections::HashMap;
|
|
|
|
// ── Windows implementation ──────────────────────────────────────────────────
|
|
|
|
#[cfg(windows)]
|
|
mod win_impl {
|
|
use super::*;
|
|
use windows::Win32::Foundation::{BOOL, HWND, LPARAM};
|
|
use windows::Win32::Graphics::Dwm::{DwmGetWindowAttribute, DWMWA_CLOAKED};
|
|
use windows::Win32::UI::WindowsAndMessaging::{
|
|
BringWindowToTop, EnumWindows, GetWindowTextW,
|
|
IsWindowVisible, SetForegroundWindow, ShowWindow,
|
|
SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE,
|
|
};
|
|
use windows::Win32::UI::Input::KeyboardAndMouse::{
|
|
keybd_event, KEYEVENTF_KEYUP, VK_MENU,
|
|
};
|
|
use windows::Win32::System::Threading::{
|
|
OpenProcess, QueryFullProcessImageNameW, PROCESS_NAME_FORMAT,
|
|
PROCESS_QUERY_LIMITED_INFORMATION,
|
|
};
|
|
use windows::Win32::System::ProcessStatus::GetModuleBaseNameW;
|
|
|
|
unsafe extern "system" fn enum_callback(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
|
let list = &mut *(lparam.0 as *mut Vec<HWND>);
|
|
list.push(hwnd);
|
|
BOOL(1)
|
|
}
|
|
|
|
fn get_all_hwnds() -> Vec<HWND> {
|
|
let mut list: Vec<HWND> = Vec::new();
|
|
unsafe {
|
|
let _ = EnumWindows(
|
|
Some(enum_callback),
|
|
LPARAM(&mut list as *mut Vec<HWND> as isize),
|
|
);
|
|
}
|
|
list
|
|
}
|
|
|
|
fn is_cloaked(hwnd: HWND) -> bool {
|
|
let mut cloaked: u32 = 0;
|
|
unsafe {
|
|
DwmGetWindowAttribute(
|
|
hwnd,
|
|
DWMWA_CLOAKED,
|
|
&mut cloaked as *mut u32 as *mut _,
|
|
std::mem::size_of::<u32>() as u32,
|
|
).is_err() == false && cloaked != 0
|
|
}
|
|
}
|
|
|
|
fn hwnd_title(hwnd: HWND) -> String {
|
|
let mut buf = [0u16; 512];
|
|
let len = unsafe { GetWindowTextW(hwnd, &mut buf) };
|
|
String::from_utf16_lossy(&buf[..len as usize])
|
|
}
|
|
|
|
/// 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<String> {
|
|
unsafe {
|
|
let mut pid: u32 = 0;
|
|
windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId(hwnd, Some(&mut pid));
|
|
if pid == 0 {
|
|
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);
|
|
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));
|
|
}
|
|
|
|
// 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), windows::core::PWSTR(buf2.as_mut_ptr()), &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
|
|
}
|
|
}
|
|
|
|
fn strip_exe_ext(name: &str) -> String {
|
|
name.strip_suffix(".exe")
|
|
.or(name.strip_suffix(".EXE"))
|
|
.unwrap_or(name)
|
|
.to_string()
|
|
}
|
|
|
|
pub fn list_windows() -> Result<Vec<WindowInfo>, String> {
|
|
let hwnds = get_all_hwnds();
|
|
|
|
// Collect visible windows with non-empty titles
|
|
let mut raw_windows: Vec<(HWND, String, String)> = Vec::new();
|
|
for hwnd in &hwnds {
|
|
let visible = unsafe { IsWindowVisible(*hwnd).as_bool() };
|
|
if !visible {
|
|
continue;
|
|
}
|
|
if is_cloaked(*hwnd) {
|
|
continue;
|
|
}
|
|
let title = hwnd_title(*hwnd);
|
|
if title.is_empty() {
|
|
continue;
|
|
}
|
|
// "Program Manager" is always the Windows desktop shell, never a real window
|
|
if title.trim().eq_ignore_ascii_case("program manager") {
|
|
continue;
|
|
}
|
|
let process_name = hwnd_process_name(*hwnd).unwrap_or_default();
|
|
let proc_lower = process_name.to_lowercase();
|
|
// ApplicationFrameHost is a UWP container — always a duplicate of the real app window
|
|
// MsEdgeWebView2 is an embedded browser component, never a standalone user window
|
|
if proc_lower == "applicationframehost" || proc_lower == "msedgewebview2" {
|
|
continue;
|
|
}
|
|
raw_windows.push((*hwnd, title, process_name));
|
|
}
|
|
|
|
// Generate labels with dedup numbering
|
|
let mut label_index: HashMap<String, usize> = HashMap::new();
|
|
let mut windows = Vec::new();
|
|
for (hwnd, 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() {
|
|
continue;
|
|
}
|
|
let idx = label_index.entry(base.clone()).or_insert(0);
|
|
*idx += 1;
|
|
let label = if *idx == 1 {
|
|
base.clone()
|
|
} else {
|
|
format!("{}{}", base, idx)
|
|
};
|
|
windows.push(WindowInfo {
|
|
id: hwnd.0 as u64,
|
|
title,
|
|
label,
|
|
visible: true,
|
|
});
|
|
}
|
|
Ok(windows)
|
|
}
|
|
|
|
pub fn minimize_all() -> Result<(), String> {
|
|
let hwnds = get_all_hwnds();
|
|
for hwnd in hwnds {
|
|
let visible = unsafe { IsWindowVisible(hwnd).as_bool() };
|
|
let title = hwnd_title(hwnd);
|
|
if visible && !title.is_empty() {
|
|
unsafe {
|
|
let _ = ShowWindow(hwnd, SW_MINIMIZE);
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
unsafe fn force_foreground(hwnd: HWND) {
|
|
keybd_event(VK_MENU.0 as u8, 0, Default::default(), 0);
|
|
keybd_event(VK_MENU.0 as u8, 0, KEYEVENTF_KEYUP, 0);
|
|
ShowWindow(hwnd, SW_RESTORE);
|
|
BringWindowToTop(hwnd).ok();
|
|
SetForegroundWindow(hwnd);
|
|
}
|
|
|
|
pub fn focus_window(window_id: u64) -> Result<(), String> {
|
|
let hwnd = HWND(window_id as isize);
|
|
unsafe { force_foreground(hwnd); }
|
|
Ok(())
|
|
}
|
|
|
|
pub fn maximize_and_focus(window_id: u64) -> Result<(), String> {
|
|
let hwnd = HWND(window_id as isize);
|
|
unsafe {
|
|
ShowWindow(hwnd, SW_MAXIMIZE);
|
|
force_foreground(hwnd);
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// ── Non-Windows stubs ───────────────────────────────────────────────────────
|
|
|
|
#[cfg(not(windows))]
|
|
mod win_impl {
|
|
use super::*;
|
|
|
|
pub fn list_windows() -> Result<Vec<WindowInfo>, String> {
|
|
Err("Window management is only supported on Windows".to_string())
|
|
}
|
|
|
|
pub fn minimize_all() -> Result<(), String> {
|
|
Err("Window management is only supported on Windows".to_string())
|
|
}
|
|
|
|
pub fn focus_window(_window_id: u64) -> Result<(), String> {
|
|
Err("Window management is only supported on Windows".to_string())
|
|
}
|
|
|
|
pub fn maximize_and_focus(_window_id: u64) -> Result<(), String> {
|
|
Err("Window management is only supported on Windows".to_string())
|
|
}
|
|
}
|
|
|
|
// ── Public API ──────────────────────────────────────────────────────────────
|
|
|
|
pub fn list_windows() -> Result<Vec<WindowInfo>, String> {
|
|
win_impl::list_windows()
|
|
}
|
|
|
|
pub fn minimize_all() -> Result<(), String> {
|
|
win_impl::minimize_all()
|
|
}
|
|
|
|
pub fn focus_window(window_id: u64) -> Result<(), String> {
|
|
win_impl::focus_window(window_id)
|
|
}
|
|
|
|
pub fn maximize_and_focus(window_id: u64) -> Result<(), String> {
|
|
win_impl::maximize_and_focus(window_id)
|
|
}
|