fix: process-name window labels, ghost window filtering, self-restart after setup
This commit is contained in:
parent
0b4a6de8ae
commit
450604bbbd
2 changed files with 134 additions and 15 deletions
|
|
@ -258,7 +258,14 @@ async fn main() {
|
||||||
} else {
|
} else {
|
||||||
display::info_line("✅", "config:", "saved");
|
display::info_line("✅", "config:", "saved");
|
||||||
}
|
}
|
||||||
c
|
// Self-restart after first-time setup so all config takes effect cleanly
|
||||||
|
println!();
|
||||||
|
display::info_line("🔄", "restart:", "Config saved. Restarting...");
|
||||||
|
release_instance_lock();
|
||||||
|
let exe = std::env::current_exe().expect("Failed to get current exe path");
|
||||||
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
|
let _ = std::process::Command::new(exe).args(&args).spawn();
|
||||||
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use helios_common::protocol::{sanitize_label, WindowInfo};
|
use helios_common::protocol::{sanitize_label, WindowInfo};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
// ── Windows implementation ──────────────────────────────────────────────────
|
// ── Windows implementation ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -7,12 +8,38 @@ mod win_impl {
|
||||||
use super::*;
|
use super::*;
|
||||||
use windows::Win32::Foundation::{BOOL, HWND, LPARAM};
|
use windows::Win32::Foundation::{BOOL, HWND, LPARAM};
|
||||||
use windows::Win32::UI::WindowsAndMessaging::{
|
use windows::Win32::UI::WindowsAndMessaging::{
|
||||||
BringWindowToTop, EnumWindows, GetWindowTextW, IsWindowVisible, SetForegroundWindow,
|
BringWindowToTop, EnumWindows, GetWindowLong, GetWindowTextW, IsIconic,
|
||||||
ShowWindow, SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE,
|
IsWindowVisible, SetForegroundWindow, ShowWindow, GWL_EXSTYLE,
|
||||||
|
SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE, WS_EX_TOOLWINDOW,
|
||||||
};
|
};
|
||||||
use windows::Win32::UI::Input::KeyboardAndMouse::{
|
use windows::Win32::UI::Input::KeyboardAndMouse::{
|
||||||
keybd_event, KEYEVENTF_KEYUP, VK_MENU,
|
keybd_event, KEYEVENTF_KEYUP, VK_MENU,
|
||||||
};
|
};
|
||||||
|
use windows::Win32::System::Threading::{
|
||||||
|
OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
|
||||||
|
};
|
||||||
|
use windows::Win32::System::ProcessStatus::GetModuleBaseNameW;
|
||||||
|
|
||||||
|
/// Process names that are always excluded (ghost/system windows).
|
||||||
|
const GHOST_PROCESSES: &[&str] = &[
|
||||||
|
"textinputhost",
|
||||||
|
"shellexperiencehost",
|
||||||
|
"searchhost",
|
||||||
|
"startmenuexperiencehost",
|
||||||
|
"lockapp",
|
||||||
|
"systemsettings",
|
||||||
|
"widgets",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Process names excluded unless the window has a "real" (non-empty) title.
|
||||||
|
const GHOST_UNLESS_TITLED: &[&str] = &[
|
||||||
|
"applicationframehost",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Window titles that are always excluded.
|
||||||
|
const GHOST_TITLES: &[&str] = &[
|
||||||
|
"program manager",
|
||||||
|
];
|
||||||
|
|
||||||
unsafe extern "system" fn enum_callback(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
unsafe extern "system" fn enum_callback(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||||
let list = &mut *(lparam.0 as *mut Vec<HWND>);
|
let list = &mut *(lparam.0 as *mut Vec<HWND>);
|
||||||
|
|
@ -37,25 +64,110 @@ mod win_impl {
|
||||||
String::from_utf16_lossy(&buf[..len as usize])
|
String::from_utf16_lossy(&buf[..len as usize])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a human-readable label from a window title.
|
/// Get the process name (exe without extension) for a window handle.
|
||||||
/// E.g. "Google Chrome" -> "google_chrome", "Discord" -> "discord"
|
fn hwnd_process_name(hwnd: HWND) -> Option<String> {
|
||||||
fn window_label(title: &str) -> String {
|
unsafe {
|
||||||
sanitize_label(title)
|
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()?;
|
||||||
|
let mut buf = [0u16; 260];
|
||||||
|
let len = GetModuleBaseNameW(handle, None, &mut buf);
|
||||||
|
let _ = windows::Win32::Foundation::CloseHandle(handle);
|
||||||
|
if len == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a window is a ghost/invisible window that should be filtered out.
|
||||||
|
fn is_ghost_window(hwnd: HWND, title: &str, process_name: &str) -> bool {
|
||||||
|
let title_lower = title.to_lowercase();
|
||||||
|
let proc_lower = process_name.to_lowercase();
|
||||||
|
|
||||||
|
// Exclude by title
|
||||||
|
if GHOST_TITLES.iter().any(|&t| title_lower == t) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude always-ghost processes
|
||||||
|
if GHOST_PROCESSES.iter().any(|&p| proc_lower == p) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude ghost-unless-titled processes (title == process name means no real title)
|
||||||
|
if GHOST_UNLESS_TITLED.iter().any(|&p| proc_lower == p) {
|
||||||
|
// If the title is just the process name or very generic, filter it
|
||||||
|
if title_lower == proc_lower || title.trim().is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude tool windows (WS_EX_TOOLWINDOW)
|
||||||
|
let ex_style = unsafe { GetWindowLong(hwnd, GWL_EXSTYLE) } as u32;
|
||||||
|
if ex_style & WS_EX_TOOLWINDOW.0 != 0 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_windows() -> Result<Vec<WindowInfo>, String> {
|
pub fn list_windows() -> Result<Vec<WindowInfo>, String> {
|
||||||
let hwnds = get_all_hwnds();
|
let hwnds = get_all_hwnds();
|
||||||
|
|
||||||
|
// First pass: collect valid windows with their process names
|
||||||
|
let mut raw_windows: Vec<(HWND, String, String)> = Vec::new(); // (hwnd, title, process_name)
|
||||||
|
for hwnd in &hwnds {
|
||||||
|
let visible = unsafe { IsWindowVisible(*hwnd).as_bool() };
|
||||||
|
if !visible {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let title = hwnd_title(*hwnd);
|
||||||
|
if title.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Skip minimized-to-nothing windows (iconic)
|
||||||
|
let iconic = unsafe { IsIconic(*hwnd).as_bool() };
|
||||||
|
if iconic {
|
||||||
|
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) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
raw_windows.push((*hwnd, title, process_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: generate labels from process names with dedup numbering
|
||||||
|
let mut label_counts: HashMap<String, usize> = 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut label_index: HashMap<String, usize> = HashMap::new();
|
||||||
let mut windows = Vec::new();
|
let mut windows = Vec::new();
|
||||||
for hwnd in hwnds {
|
for (hwnd, title, proc_name) in raw_windows {
|
||||||
let visible = unsafe { IsWindowVisible(hwnd).as_bool() };
|
let base = sanitize_label(&proc_name);
|
||||||
let title = hwnd_title(hwnd);
|
if base.is_empty() {
|
||||||
if !visible || title.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let label = window_label(&title);
|
|
||||||
if label.is_empty() {
|
|
||||||
continue;
|
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 {
|
windows.push(WindowInfo {
|
||||||
id: hwnd.0 as u64,
|
id: hwnd.0 as u64,
|
||||||
title,
|
title,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue