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.3: 1.0.3: placement. kicad_placement_state, kicad_move_footprint (a batch of moves as one native undo step, refused…
2569119
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
//! Window enumeration and per-window facts: EnumWindows, EnumChildWindows, class names,
//! titles that never hang, rects, owner, monitor test, the GL canvas child.
//! Python: close_windows.py `_win_find_windows_by_pid`, `_win_title_nohang`,
//! `_win_class_name`, `_win_get_dialog_body_text`; kicad_ui.py `_gl_canvas_rect`;
//! win_focus.py `_enum_kicad_hwnds`, `_visible_on_any_monitor`;
//! open_symbol_editor.py `_find_child_by_class_and_parent`.
use std::collections::HashMap;
use windows::core::BOOL;
use windows::Win32::Foundation::{HWND, LPARAM, RECT};
use windows::Win32::Graphics::Gdi::{GetMonitorInfoW, MonitorFromRect, MONITORINFO, MONITOR_DEFAULTTONULL};
use windows::Win32::UI::WindowsAndMessaging::{
EnumChildWindows, EnumWindows, GetClassNameW, GetParent, GetWindow, GetWindowRect, GetWindowThreadProcessId,
InternalGetWindowText, IsIconic, IsWindowVisible, IsZoomed, GW_OWNER, WM_GETTEXT,
};
use super::{from_wide, hwnd_u64, is_kicad_exe, responsive, send_timeout, Sent};
use crate::WindowInfo;
/// Enumeration callback: push every handle into the Vec behind LPARAM.
unsafe extern "system" fn collect(h: HWND, l: LPARAM) -> BOOL {
// SAFETY: LPARAM carries a `*mut Vec<HWND>` that the enumerating function keeps alive
// for the whole (synchronous) enumeration; no other reference exists meanwhile.
let v = unsafe { &mut *(l.0 as *mut Vec<HWND>) };
v.push(h);
BOOL(1)
}
/// Every top-level window, visible or not, in z-order (topmost first).
pub fn top_level_windows() -> Vec<HWND> {
let mut out: Vec<HWND> = Vec::with_capacity(256);
// SAFETY: `out` outlives the call; `collect` only pushes into it.
let _ = unsafe { EnumWindows(Some(collect), LPARAM(&mut out as *mut _ as isize)) };
out
}
/// Every descendant of `parent` (EnumChildWindows walks the whole subtree, which is what
/// the Python callers relied on: the symbol editor's filter `Edit` sits two levels down).
pub fn descendants(parent: HWND) -> Vec<HWND> {
let mut out: Vec<HWND> = Vec::with_capacity(64);
// SAFETY: as above; EnumChildWindows is synchronous.
let _ = unsafe { EnumChildWindows(Some(parent), Some(collect), LPARAM(&mut out as *mut _ as isize)) };
out
}
pub fn class_name(h: HWND) -> String {
let mut buf = [0u16; 256];
// SAFETY: buffer length is passed with the buffer; GetClassNameW reads no cross-thread state.
let n = unsafe { GetClassNameW(h, &mut buf) };
from_wide(&buf[..n.max(0) as usize])
}
/// A window's caption without a cross-thread message: `InternalGetWindowText` reads the
/// window's own text store, so a frame frozen behind a modal still yields its title
/// (winvm 2026-07: GetWindowTextW hung the dialog scan). Empty when the store is empty,
/// which is the case for a standard Edit control (its text lives in the control).
pub fn title_nohang(h: HWND) -> String {
let mut buf = [0u16; 512];
// SAFETY: buffer length passed with the buffer.
let n = unsafe { InternalGetWindowText(h, &mut buf) };
from_wide(&buf[..n.max(0) as usize])
}
/// Text via a time-boxed WM_GETTEXT (`SMTO_ABORTIFHUNG`). Empty on timeout or hang.
pub fn text_timeout(h: HWND, timeout_ms: u32) -> Option<String> {
let mut buf = vec![0u16; 4096];
match send_timeout(h, WM_GETTEXT, buf.len() - 1, buf.as_mut_ptr() as isize, timeout_ms) {
Sent::Answered(n) => Some(from_wide(&buf[..n.min(buf.len() - 1)])),
_ => None,
}
}
pub fn pid_of(h: HWND) -> u32 {
let mut pid = 0u32;
// SAFETY: `pid` outlives the call.
unsafe { GetWindowThreadProcessId(h, Some(&mut pid)) };
pid
}
pub fn thread_of(h: HWND) -> u32 {
// SAFETY: no out pointer requested.
unsafe { GetWindowThreadProcessId(h, None) }
}
/// (x, y, width, height) in physical pixels (the process is per-monitor DPI aware).
pub fn rect(h: HWND) -> (i32, i32, i32, i32) {
let mut r = RECT::default();
// SAFETY: `r` outlives the call.
if unsafe { GetWindowRect(h, &mut r) }.is_err() {
return (0, 0, 0, 0);
}
(r.left, r.top, r.right - r.left, r.bottom - r.top)
}
pub fn owner(h: HWND) -> u64 {
// SAFETY: GetWindow reads the window manager's tables only.
unsafe { GetWindow(h, GW_OWNER) }.map(hwnd_u64).unwrap_or(0)
}
pub fn parent(h: HWND) -> u64 {
// SAFETY: as above.
unsafe { GetParent(h) }.map(hwnd_u64).unwrap_or(0)
}
pub fn visible(h: HWND) -> bool {
// SAFETY: table read.
unsafe { IsWindowVisible(h).as_bool() }
}
pub fn minimized(h: HWND) -> bool {
// SAFETY: table read.
unsafe { IsIconic(h).as_bool() }
}
pub fn maximized(h: HWND) -> bool {
// SAFETY: table read.
unsafe { IsZoomed(h).as_bool() }
}
/// Cache of pid -> executable base name so a full enumeration opens each process once.
#[derive(Default)]
pub struct ExeCache(HashMap<u32, String>);
impl ExeCache {
pub fn exe(&mut self, pid: u32) -> String {
self.0.entry(pid).or_insert_with(|| super::process::exe_base_name(pid)).clone()
}
}
/// Assemble a `WindowInfo` for a top-level window. `probe_ms` > 0 runs the WM_NULL
/// responsiveness probe (only worth paying for on KiCad windows during a bulk listing).
pub fn info(h: HWND, cache: &mut ExeCache, probe_ms: u32) -> WindowInfo {
let pid = pid_of(h);
let exe = cache.exe(pid);
let hung = probe_ms > 0 && !responsive(h, probe_ms);
WindowInfo {
hwnd: hwnd_u64(h),
title: title_nohang(h),
class_name: class_name(h),
pid,
exe,
visible: visible(h),
owner: owner(h),
rect: rect(h),
minimized: minimized(h),
maximized: maximized(h),
hung,
}
}
/// Every visible top-level window. KiCad windows get the 150 ms WM_NULL probe so `hung`
/// means something; other applications' threads are left alone.
/// The first visible, non-minimized, titled top-level window in Z order (EnumWindows walks
/// top to bottom), skipping the shell's own windows. 0 when nothing qualifies.
pub fn z_top() -> u64 {
for h in top_level_windows() {
if !visible(h) || minimized(h) {
continue;
}
let t = title_nohang(h);
if t.is_empty() || t == "Program Manager" || t == "Windows Input Experience" {
continue;
}
let (l, tp, r, b) = rect(h);
if r - l < 50 || b - tp < 50 {
continue;
}
let c = class_name(h);
if c == "Shell_TrayWnd" || c == "Shell_SecondaryTrayWnd" || c == "Progman" || c == "WorkerW" || c == "Windows.UI.Core.CoreWindow" {
continue;
}
return h.0 as u64;
}
0
}
pub fn list_visible() -> Vec<WindowInfo> {
let mut cache = ExeCache::default();
top_level_windows()
.into_iter()
.filter(|&h| visible(h))
.map(|h| {
let probe = if is_kicad_exe(&cache.exe(pid_of(h))) { 150 } else { 0 };
info(h, &mut cache, probe)
})
.collect()
}
/// Every descendant of a window with class and text, `owner` carrying the PARENT handle
/// so a caller can join "the `Edit` whose parent is titled `searchCtrl`". Text comes from
/// the window's own store first; when the dialog's thread answers a WM_NULL, controls
/// with an empty store (Edit boxes) get a short time-boxed WM_GETTEXT as well.
pub fn children(parent_hwnd: HWND) -> Vec<WindowInfo> {
let mut cache = ExeCache::default();
let pumping = responsive(parent_hwnd, 150);
descendants(parent_hwnd)
.into_iter()
.map(|h| {
let mut w = info(h, &mut cache, 0);
w.owner = parent(h);
if w.title.is_empty() && pumping {
if let Some(t) = text_timeout(h, 150) {
w.title = t;
}
}
w
})
.collect()
}
/// The wxGLCanvas child's rect relative to the top-left of `frame`'s window rect (the
/// same box `capture_window` renders), as (x, y, width, height). None when no GL canvas
/// exists (Cairo fallback canvas, or not an editor).
pub fn gl_canvas_rect(frame: HWND) -> Option<(i32, i32, i32, i32)> {
let canvas = descendants(frame).into_iter().find(|&c| class_name(c) == "wxGLCanvas")?;
let (cx, cy, cw, ch) = rect(canvas);
let (fx, fy, _, _) = rect(frame);
Some((cx - fx, cy - fy, cw, ch))
}
/// Is any part of the (x, y, w, h) rect on a monitor? `MONITOR_DEFAULTTONULL`, so a
/// second or third display counts exactly like the primary.
pub fn on_a_monitor(r: (i32, i32, i32, i32)) -> bool {
let rc = RECT { left: r.0, top: r.1, right: r.0 + r.2, bottom: r.1 + r.3 };
// SAFETY: `rc` outlives the call.
let hmon = unsafe { MonitorFromRect(&rc, MONITOR_DEFAULTTONULL) };
if hmon.is_invalid() {
return false;
}
let mut mi = MONITORINFO { cbSize: std::mem::size_of::<MONITORINFO>() as u32, ..Default::default() };
// SAFETY: cbSize is set; `mi` outlives the call.
if !unsafe { GetMonitorInfoW(hmon, &mut mi) }.as_bool() {
return false;
}
let m = mi.rcMonitor;
let vis_w = rc.right.min(m.right) - rc.left.max(m.left);
let vis_h = rc.bottom.min(m.bottom) - rc.top.max(m.top);
vis_w > 0 && vis_h > 0
}