- Device labels: lowercase, no whitespace, only a-z 0-9 - _ (enforced at config time) - Session IDs removed: device label is the sole identifier - Routes changed: /sessions/:id → /devices/:label - Removed commands: click, type, find-window, wait-for-window, label, old version, server-version - Renamed: status → version (compares relay/remote.py/client commits) - Unified screenshot: takes 'screen' or a window label as argument - Windows listed with human-readable labels (same format as device labels) - Single instance enforcement via PID lock file - Removed input.rs (click/type functionality) - All docs and code in English - Protocol: Hello.label is now required (String, not Option<String>) - Client auto-migrates invalid labels on startup
146 lines
4.7 KiB
Rust
146 lines
4.7 KiB
Rust
use helios_common::protocol::{sanitize_label, WindowInfo};
|
|
|
|
// ── Windows implementation ──────────────────────────────────────────────────
|
|
|
|
#[cfg(windows)]
|
|
mod win_impl {
|
|
use super::*;
|
|
use windows::Win32::Foundation::{BOOL, HWND, LPARAM};
|
|
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,
|
|
};
|
|
|
|
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 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])
|
|
}
|
|
|
|
/// Generate a human-readable label from a window title.
|
|
/// E.g. "Google Chrome" -> "google_chrome", "Discord" -> "discord"
|
|
fn window_label(title: &str) -> String {
|
|
sanitize_label(title)
|
|
}
|
|
|
|
pub fn list_windows() -> Result<Vec<WindowInfo>, String> {
|
|
let hwnds = get_all_hwnds();
|
|
let mut windows = Vec::new();
|
|
for hwnd in hwnds {
|
|
let visible = unsafe { IsWindowVisible(hwnd).as_bool() };
|
|
let title = hwnd_title(hwnd);
|
|
if !visible || title.is_empty() {
|
|
continue;
|
|
}
|
|
let label = window_label(&title);
|
|
if label.is_empty() {
|
|
continue;
|
|
}
|
|
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)
|
|
}
|