main
John Lauer 0.2.0: the bridge is Rust end to end — all 112 verbs dispatch from altium-bridge.exe (spawn.kind exe); Python runtime n… 4fb2c47 1mo ago
//! Background proof capture. John's etiquette (2026-09-01): never park an Altium window
//! off-screen — a user reaching for Altium must find it where they left it. If it is minimized,
//! restore it INTO THE BACKGROUND (bottom of the z-order), capture, and hand the user's
//! foreground straight back (~0.6 s end to end). A capture under MIN_PROOF_WIDTH px is a
//! minimized-window sliver, not evidence — refused as proofUnavailable rather than returned.
use crate::{ab, instance};
use serde_json::{json, Value};

/// Anything narrower is a minimized-window sliver, not a screenshot.
pub const MIN_PROOF_WIDTH: i64 = 400;

fn is_minimized(rect: &Value) -> bool {
    if !rect.is_object() {
        return false;
    }
    let g = |k: &str| rect.get(k).and_then(|v| v.as_i64()).unwrap_or(0);
    g("left") <= -30000 || g("top") <= -30000 || (g("width") < MIN_PROOF_WIDTH && g("height") < 200)
}

/// Restore a minimized window and push it BEHIND the user's windows, then hand the foreground
/// straight back. Returns what it had to do, so a caller can report honestly whether the user's
/// screen was touched.
pub fn make_capturable(hwnd: i64) -> Value {
    let mut out = json!({"restored": false, "sentToBack": false, "foregroundReturned": false});
    let before = ab::call("desktop_get_foreground", json!({}), &[], 8).get("hwnd").and_then(|h| h.as_i64());
    let rect = instance::altium_windows()
        .into_iter()
        .find(|w| w.get("hwnd").and_then(|h| h.as_i64()) == Some(hwnd))
        .map(|w| w.get("rect").cloned().unwrap_or(Value::Null))
        .unwrap_or(Value::Null);
    if is_minimized(&rect) {
        ab::call("desktop_set_window_state", json!({"hwnd": hwnd, "state": "restore",
            "reason": "restore a minimized Altium into the background for a proof capture"}), &[], 10);
        out["restored"] = json!(true);
        instance::invalidate_snapshot();
    }
    ab::call("desktop_set_window_state", json!({"hwnd": hwnd, "state": "bottom",
        "reason": "keep Altium behind the user's windows"}), &[], 10);
    out["sentToBack"] = json!(true);
    let after = ab::call("desktop_get_foreground", json!({}), &[], 8).get("hwnd").and_then(|h| h.as_i64());
    if let Some(b) = before {
        if after != Some(b) {
            ab::call("desktop_raise_window", json!({"hwnd": b,
                "reason": "hand the user's window back after restoring Altium into the background"}), &[], 10);
            out["foregroundReturned"] = json!(true);
        }
        out["userForeground"] = json!(b);
    }
    out
}

/// A capture of the selected Altium's main window, or Null — never a sliver-as-proof.
pub fn shot_of_main() -> Value {
    let mw = match instance::main_window() {
        Some(w) => w,
        None => return Value::Null,
    };
    let hwnd = match mw.get("hwnd").and_then(|h| h.as_i64()) {
        Some(h) => h,
        None => return Value::Null,
    };
    let title = mw.get("title").cloned().unwrap_or(Value::Null);
    let prepared = make_capturable(hwnd);
    let r = ab::call("desktop_screenshot_base64", json!({"hwnd": hwnd}), &[], 20);
    let image = r.get("image").or_else(|| r.get("base64")).and_then(|i| i.as_str()).map(str::to_string);
    match image {
        Some(img) => {
            let w = r.get("width").and_then(|v| v.as_i64());
            let h = r.get("height").and_then(|v| v.as_i64());
            if let Some(wpx) = w {
                if wpx < MIN_PROOF_WIDTH {
                    return json!({"title": title, "hwnd": hwnd, "proofUnavailable": true,
                        "reason": format!("the Altium window is minimized (captured {}x{}) — Windows does not render a minimized window, so there is no image to prove anything", wpx, h.unwrap_or(0)),
                        "prepared": prepared});
                }
            }
            json!({"title": title, "hwnd": hwnd, "image": img, "width": w, "height": h, "prepared": prepared})
        }
        None => Value::Null,
    }
}

/// Altium PAINTS its dialog text (no Static child, not in WM_GETTEXT) — the only way to READ a
/// dialog is to look at it. Attach a base64 PNG per dialog row.
pub fn attach_dialog_shots(dialogs: &mut [Value]) {
    for d in dialogs.iter_mut() {
        if let Some(hwnd) = d.get("hwnd").and_then(|h| h.as_i64()) {
            let r = ab::call("desktop_screenshot_base64", json!({"hwnd": hwnd}), &[], 10);
            if let Some(img) = r.get("image").or_else(|| r.get("base64")).and_then(|i| i.as_str()) {
                d["shot"] = json!(img);
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn minimized_detection() {
        assert!(is_minimized(&json!({"left": -32000, "top": -32000, "width": 160, "height": 28})));
        assert!(is_minimized(&json!({"left": 100, "top": 100, "width": 237, "height": 39})));
        assert!(!is_minimized(&json!({"left": 0, "top": 0, "width": 1920, "height": 1080})));
        assert!(!is_minimized(&Value::Null));
    }
}