app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
master
John Lauer
1.0.42: 1.0.42: two switches the user can flip from the dashboard, both applied at once with no restart. backgroundGuar…
c27149e
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
//! Foreground, z-order and placement. The rules here are John's, verbatim from
//! win_focus.py 0.9.181: never minimize (a minimized window cannot be screenshotted),
//! never move a window off screen, never WS_EX_NOACTIVATE (it blocks the user's own
//! click), never install a hook. One z-order demotion (`push_to_background`) and one
//! sanctioned activation (`bring_to_front`), which reports what Windows actually did.
//! Python: `_set_foreground_robust`, `bring_to_user`, `show_without_activating`,
//! `_push_kicad_to_background`, `capture_placement`, `restore_placement`.
use windows::Win32::Foundation::{HWND, RECT};
use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::Win32::UI::WindowsAndMessaging::{
BringWindowToTop, GetForegroundWindow, GetWindowPlacement, SetForegroundWindow, SetWindowPlacement, SetWindowPos, ShowWindow,
HWND_BOTTOM, SC_MAXIMIZE, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SW_RESTORE, SW_SHOW, SW_SHOWMAXIMIZED, SW_SHOWNOACTIVATE,
SW_SHOWMINIMIZED, WINDOWPLACEMENT, WM_SYSCOMMAND, WPF_RESTORETOMAXIMIZED,
};
use super::enumerate::{minimized, thread_of};
use super::messages::post;
use super::{hwnd_u64, require_responsive};
use crate::Placement;
/// Deadline for the WM_NULL gate in front of the calls Windows makes synchronous.
const GATE_MS: u32 = 300;
pub fn foreground() -> u64 {
// SAFETY: no arguments; reads the foreground handle.
hwnd_u64(unsafe { GetForegroundWindow() })
}
/// THE backgrounding primitive (0.9.181): exactly one SetWindowPos to HWND_BOTTOM with
/// SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE. Not minimized, not moved, not restyled.
/// Once-per-window bookkeeping belongs to the etiquette layer, not here.
pub fn push_to_background(h: HWND) -> Result<(), String> {
// SetWindowPos sends WM_WINDOWPOSCHANGING to the target synchronously; gate it.
require_responsive(h, "push_to_background", GATE_MS)?;
// SAFETY: valid, responsive window; flags forbid move, size and activation.
unsafe { SetWindowPos(h, Some(HWND_BOTTOM), 0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE) }
.map_err(|e| format!("SetWindowPos(HWND_BOTTOM) on {}: {e}", hwnd_u64(h)))
}
/// Make a window visible (restoring it if minimized) without activating it. The
/// strongest thing the bridge does to a window on its own initiative.
pub fn show_no_activate(h: HWND) -> Result<(), String> {
require_responsive(h, "show_no_activate", GATE_MS)?;
// SAFETY: valid, responsive window. Return value is the previous visibility, not an error.
let _ = unsafe { ShowWindow(h, SW_SHOWNOACTIVATE) };
Ok(())
}
/// The one sanctioned foreground. A plain SetForegroundWindow from a background process
/// is refused by the foreground lock, so we attach our input queue to the current
/// foreground thread and the target thread first (the documented AttachThreadInput
/// technique), raise, activate, detach, then MEASURE: the result is whether
/// GetForegroundWindow reports the target afterwards, not whether the calls returned.
/// Minimized windows are restored; anything else keeps its size (SW_RESTORE would
/// un-maximize a maximized editor, which the Python version got wrong).
/// Cancel Windows' orange attention flash on a taskbar button (FLASHW_STOP). Windows starts that
/// flash by itself whenever an app asks for the foreground and is refused, or is sent behind right
/// after taking it, which is what happens to every KiCad window the bridge opens in the background.
/// The Python build cancelled it (handlers/win_taskbar.py); the native build never did until 1.0.42,
/// so the user had to click each button to stop it (John: "whackamole").
pub fn stop_flash(h: HWND) {
use windows::Win32::UI::WindowsAndMessaging::{FlashWindowEx, FLASHWINFO, FLASHW_STOP};
if h.is_invalid() {
return;
}
let fi = FLASHWINFO { cbSize: std::mem::size_of::<FLASHWINFO>() as u32, hwnd: h, dwFlags: FLASHW_STOP, uCount: 0, dwTimeout: 0 };
// SAFETY: a fully initialised FLASHWINFO for a window handle; the call only clears a flag.
let _ = unsafe { FlashWindowEx(&fi) };
}
/// Ask Windows to make `h` the foreground window: one SetForegroundWindow and nothing else. No
/// AttachThreadInput, no ShowWindow, no BringWindowToTop. Returns whether Windows agreed.
pub fn request_foreground(h: HWND) -> bool {
if h.is_invalid() || !super::responsive(h, GATE_MS) {
return false;
}
// SAFETY: a plain request on a valid, responsive window handle.
unsafe { SetForegroundWindow(h) }.as_bool()
}
pub fn bring_to_front(h: HWND) -> Result<bool, String> {
require_responsive(h, "bring_to_front", GATE_MS)?;
// SAFETY: reading the foreground handle.
let fg = unsafe { GetForegroundWindow() };
if fg == h {
return Ok(true);
}
// SAFETY: valid, responsive window.
let _ = unsafe { ShowWindow(h, if minimized(h) { SW_RESTORE } else { SW_SHOW }) };
// SAFETY: thread ids are plain integers; every successful attach is detached below
// on the same thread before this function returns.
let attached = unsafe {
use windows::Win32::System::Threading::AttachThreadInput;
let cur = GetCurrentThreadId();
let mut attached = Vec::with_capacity(2);
let fg_tid = if fg.is_invalid() { 0 } else { thread_of(fg) };
for tid in [fg_tid, thread_of(h)] {
if tid != 0 && tid != cur && !attached.contains(&tid) && AttachThreadInput(cur, tid, true).as_bool() {
attached.push(tid);
}
}
let _ = BringWindowToTop(h);
let _ = SetForegroundWindow(h);
attached
};
// SAFETY: detaching exactly what was attached above.
unsafe {
use windows::Win32::System::Threading::AttachThreadInput;
let cur = GetCurrentThreadId();
for tid in attached {
let _ = AttachThreadInput(cur, tid, false);
}
}
// Let the shell settle before measuring; the activation is processed asynchronously
// by the target thread.
std::thread::sleep(std::time::Duration::from_millis(60));
// SAFETY: reading the foreground handle.
Ok(unsafe { GetForegroundWindow() } == h)
}
fn read_placement(h: HWND) -> Result<WINDOWPLACEMENT, String> {
let mut wp = WINDOWPLACEMENT { length: std::mem::size_of::<WINDOWPLACEMENT>() as u32, ..Default::default() };
// SAFETY: length is set; `wp` outlives the call. GetWindowPlacement reads window state
// without sending a message.
unsafe { GetWindowPlacement(h, &mut wp) }.map_err(|e| format!("GetWindowPlacement({}): {e}", hwnd_u64(h)))?;
Ok(wp)
}
/// The restored (normal) rectangle and the maximized flag, in the workspace coordinates
/// GetWindowPlacement uses, so `set_placement` round-trips exactly across a restart.
pub fn placement(h: HWND) -> Result<Placement, String> {
let wp = read_placement(h)?;
let r = wp.rcNormalPosition;
Ok(Placement {
x: r.left,
y: r.top,
width: r.right - r.left,
height: r.bottom - r.top,
// A minimized window remembers whether it was maximized in the flags.
maximized: wp.showCmd == SW_SHOWMAXIMIZED.0 as u32
|| (wp.showCmd == SW_SHOWMINIMIZED.0 as u32 && wp.flags.contains(WPF_RESTORETOMAXIMIZED)),
})
}
/// Put a window back where the user had it, without activating it and NEVER minimized
/// (a minimized window blanks PrintWindow, which silently breaks the evidence pipeline,
/// #48 ask 4). The rect lands through SetWindowPlacement with SW_SHOWNOACTIVATE; a
/// maximized placement is then requested from the window's own thread with a posted
/// WM_SYSCOMMAND SC_MAXIMIZE, because every maximize command Windows offers a foreign
/// process activates the window.
pub fn set_placement(h: HWND, p: &Placement) -> Result<(), String> {
require_responsive(h, "set_placement", GATE_MS)?;
if p.width < 50 || p.height < 50 {
return Err(format!("refusing a {}x{} placement (a minimized or parked stub is not a placement)", p.width, p.height));
}
let mut wp = read_placement(h)?;
wp.flags = Default::default();
wp.showCmd = SW_SHOWNOACTIVATE.0 as u32;
wp.rcNormalPosition = RECT { left: p.x, top: p.y, right: p.x + p.width, bottom: p.y + p.height };
// SAFETY: length set by read_placement; `wp` outlives the call; the window is responsive.
unsafe { SetWindowPlacement(h, &wp) }.map_err(|e| format!("SetWindowPlacement({}): {e}", hwnd_u64(h)))?;
if p.maximized {
post(h, WM_SYSCOMMAND, SC_MAXIMIZE as usize, 0)?;
}
Ok(())
}