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.30: NATIVE (Rust) build. 1.0.30: kicad_taskbar_repair fixes the white taskbar icon on standalone KiCad editors by w…
46293d1
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
//! Windows window-control primitives (phase 3 of docs/rust-port-plan.md), ported from the
//! Python bridge's handlers/win_focus.py, close_windows.py, win_menu.py, kicad_ui.py,
//! uia.py and navigate_symbol.ps1. Each module is one family of primitives; windows.rs
//! wires them into the `Platform` trait.
//!
//! House rules, enforced here and not left to callers:
//! - Every blocking cross-process message goes through `SendMessageTimeoutW` with
//! `SMTO_ABORTIFHUNG` and a deadline (`send_timeout`). Nothing in this layer may hang the
//! bridge on a stuck KiCad (0.9.160 stranded a parked window for hours, 0.9.165 took the
//! whole bridge down on the boot path, #83 deadlocked on a modal inside a button handler).
//! - Calls that Windows itself turns into synchronous messages (SetWindowPos, ShowWindow,
//! SetWindowPlacement, BringWindowToTop, PrintWindow, UIA) are gated on `responsive()`
//! first and refused on a hung window, for the same reason.
//! - No SendInput, keybd_event, mouse_event, SetCursorPos, SetWindowsHookEx, no minimize,
//! no off-screen parking, no WS_EX_NOACTIVATE. `push_to_background` is the only z-order
//! demotion and `bring_to_front` the only sanctioned activation. The two exceptions are
//! explicit: `tour_input` is the one SendInput site (phase 5, refused unless the target
//! is the measured foreground), and `etiquette` is the one module that may write a
//! window style (to CLEAR a stale WS_EX_NOACTIVATE, probe-gated), and the one
//! WinEventHook (a notification hook, not an input hook).
//! - Handles cross the trait as u64 and become `HWND` here; wide strings are built with
//! `wide()` and read back with `from_wide()`.
pub mod capture;
pub mod enumerate;
pub mod etiquette;
pub mod focus;
pub mod install;
pub mod menu;
pub mod messages;
pub mod process;
pub mod shortcuts;
pub mod tour_input;
pub mod uia;
use std::ffi::c_void;
use windows::Win32::Foundation::{GetLastError, ERROR_TIMEOUT, HWND, LPARAM, WPARAM};
use windows::Win32::UI::WindowsAndMessaging::{IsWindow, SendMessageTimeoutW, SMTO_ABORTIFHUNG};
/// Executable base names (lower case) that count as KiCad. Mirrors `_KICAD_EXES` in
/// win_focus.py plus gerbview, which close_windows.py's loose scan included.
pub const KICAD_EXES: &[&str] = &[
"kicad.exe",
"eeschema.exe",
"pcbnew.exe",
"kicad-cli.exe",
"pl_editor.exe",
"bitmap2component.exe",
"pcb_calculator.exe",
"gerbview.exe",
];
pub fn is_kicad_exe(exe: &str) -> bool {
KICAD_EXES.iter().any(|k| k.eq_ignore_ascii_case(exe))
}
/// u64 (trait side) to HWND (this side). A window handle is a 32-bit value zero-extended
/// into a pointer-sized field, so the round trip is exact.
pub fn hwnd(h: u64) -> HWND {
HWND(h as usize as *mut c_void)
}
pub fn hwnd_u64(h: HWND) -> u64 {
h.0 as usize as u64
}
/// Is this a live window handle? Cheap (no cross-thread message).
pub fn is_window(h: HWND) -> bool {
// SAFETY: IsWindow accepts any value and only reads user32's handle table.
unsafe { IsWindow(Some(h)).as_bool() }
}
/// Validate a trait-side handle before touching it.
pub fn live(h: u64) -> Result<HWND, String> {
let w = hwnd(h);
if h == 0 || !is_window(w) {
return Err(format!("hwnd {h} is not a window"));
}
Ok(w)
}
/// NUL-terminated UTF-16.
pub fn wide(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
/// UTF-16 buffer to String, stopping at the first NUL (or the whole slice).
pub fn from_wide(buf: &[u16]) -> String {
let end = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
String::from_utf16_lossy(&buf[..end])
}
/// Outcome of a time-boxed cross-process send.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Sent {
/// The window procedure returned; the value is the LRESULT written by the target.
Answered(usize),
/// Delivered, but the target did not return within the deadline (a modal loop inside
/// the handler, #83). The message is still being processed on the target's thread.
TimedOut,
/// Not delivered: the target thread is hung (SMTO_ABORTIFHUNG) or the handle is bad.
Failed,
}
/// The one blocking primitive in this layer: `SendMessageTimeoutW` with
/// `SMTO_ABORTIFHUNG` and a deadline. Never a plain SendMessageW.
pub fn send_timeout(h: HWND, msg: u32, wparam: usize, lparam: isize, timeout_ms: u32) -> Sent {
let mut result: usize = 0;
// SAFETY: `result` outlives the call; lparam is either a scalar or a pointer the caller
// keeps alive for the duration of this synchronous call.
let ret = unsafe {
SendMessageTimeoutW(h, msg, WPARAM(wparam), LPARAM(lparam), SMTO_ABORTIFHUNG, timeout_ms, Some(&mut result))
};
if ret.0 != 0 {
return Sent::Answered(result);
}
// SAFETY: plain thread-local error read.
if unsafe { GetLastError() } == ERROR_TIMEOUT {
Sent::TimedOut
} else {
Sent::Failed
}
}
/// Does the window's thread pump messages? `SendMessageTimeoutW(WM_NULL)` with
/// `SMTO_ABORTIFHUNG` (win_focus.py `_window_responsive`). The gate in front of every call
/// Windows would otherwise turn into a synchronous cross-process message.
pub fn responsive(h: HWND, timeout_ms: u32) -> bool {
use windows::Win32::UI::WindowsAndMessaging::WM_NULL;
matches!(send_timeout(h, WM_NULL, 0, 0, timeout_ms), Sent::Answered(_))
}
/// Refuse an operation on a window that failed the probe, with a message that says why.
pub fn require_responsive(h: HWND, what: &str, timeout_ms: u32) -> Result<(), String> {
if responsive(h, timeout_ms) {
Ok(())
} else {
Err(format!("{what}: window {} did not answer WM_NULL within {timeout_ms} ms (hung or busy); refusing so the bridge cannot block on it", hwnd_u64(h)))
}
}
/// Client-coordinate pair packed the way mouse messages carry it: y in the high word,
/// x in the low word (kicad_ui.py `_post_click`).
pub fn pack_point(x: i32, y: i32) -> isize {
(((y & 0xFFFF) << 16) | (x & 0xFFFF)) as isize
}
/// Menu and button captions carry '&' mnemonics; comparisons strip them.
pub fn strip_amp(s: &str) -> String {
s.replace('&', "")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wide_round_trip() {
let w = wide("KiCad 10.0");
assert_eq!(w.last(), Some(&0));
assert_eq!(from_wide(&w), "KiCad 10.0");
assert_eq!(from_wide(&[0x41, 0x42, 0, 0x43]), "AB");
}
#[test]
fn point_packing_matches_python() {
// (cy << 16) | (cx & 0xFFFF)
assert_eq!(pack_point(10, 58), (58 << 16) | 10);
assert_eq!(pack_point(0x1234, 0x5678), 0x5678_1234);
}
#[test]
fn hwnd_round_trip() {
assert_eq!(hwnd_u64(hwnd(0x0001_0ABC)), 0x0001_0ABC);
assert!(hwnd(0).is_invalid());
}
#[test]
fn kicad_exe_set() {
assert!(is_kicad_exe("PCBNEW.EXE"));
assert!(!is_kicad_exe("chrome.exe"));
}
}