app
Altium - the Altium Bridge
Public Made by Adomby adom
Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.
main
John Lauer
0.2.1: a finished verb can no longer leave a stale partial progress bar on Altium's taskbar icon — the terminal clear i…
ca58f6d
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
//! "Adom is driving Altium" on the Windows taskbar — badge overlay + progress bar, both painted
//! by ab's generic `desktop_taskbar` verb (never an in-process ITaskbarList3 call: that returns
//! S_OK cross-process and the shell silently ignores it; pup, fusion and kicad each paid for it).
//! No AUMID stamping — a per-window AppUserModelID poisons the shell's icon cache (kicad's lesson).
//!
//! John's rules: progress rides the taskbar icon, never toasts; the overlay auto-clears after
//! 5 minutes without a driving verb (a permanent overlay stops meaning "an AI is driving now").
use crate::{ab, instance};
use serde_json::{json, Value};
use std::collections::{BTreeMap, BTreeSet};
use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
use std::sync::Mutex;
use std::time::Duration;
static BADGED: Mutex<BTreeSet<i64>> = Mutex::new(BTreeSet::new());
static LAST_DRIVING: AtomicI64 = AtomicI64::new(0);
static IDLE_WATCHER: AtomicBool = AtomicBool::new(false);
struct ProgressHandle {
stop: std::sync::Arc<AtomicBool>,
failed: std::sync::Arc<AtomicBool>,
}
static PROGRESS_STOP: Mutex<BTreeMap<String, ProgressHandle>> = Mutex::new(BTreeMap::new());
pub const IDLE_CLEAR_SEC: i64 = 300;
fn now() -> i64 {
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0)
}
fn badge_b64() -> String {
static B64: Mutex<Option<String>> = Mutex::new(None);
if let Ok(g) = B64.lock() {
if let Some(s) = g.as_ref() {
return s.clone();
}
}
let p = std::env::current_exe()
.ok()
.and_then(|e| e.parent().map(|d| d.join("resources").join("adom-overlay-32.png")));
let s = p.and_then(|p| std::fs::read(p).ok()).map(|b| b64encode(&b)).unwrap_or_default();
if let Ok(mut g) = B64.lock() {
*g = Some(s.clone());
}
s
}
fn taskbar(args: Value) -> bool {
let r = ab::call("desktop_taskbar", args, &[], 15);
r.get("success").and_then(|b| b.as_bool()).unwrap_or(false)
|| r.to_string().contains("Taskbar updated")
}
fn main_hwnd() -> Option<i64> {
instance::main_window().and_then(|w| w.get("hwnd").and_then(|h| h.as_i64()))
}
// ---- badge -------------------------------------------------------------------
pub fn set_driving_badge() -> bool {
let hwnd = match main_hwnd() {
Some(h) => h,
None => return false,
};
if BADGED.lock().map(|g| g.contains(&hwnd)).unwrap_or(false) {
return true;
}
let icon = badge_b64();
if icon.is_empty() {
return false; // badge asset missing beside the exe — not worth failing a verb over
}
let ok = taskbar(json!({"hwnd": hwnd, "overlay": {"icon": icon, "tooltip": "Adom is driving Altium Designer"}}));
if ok {
if let Ok(mut g) = BADGED.lock() {
g.insert(hwnd);
}
}
ok
}
pub fn clear_driving_badge() -> bool {
let targets: Vec<i64> = BADGED.lock().map(|g| g.iter().cloned().collect()).unwrap_or_default();
let mut ok = true;
for h in targets {
if taskbar(json!({"hwnd": h, "overlay": {"badge": "none"}})) {
if let Ok(mut g) = BADGED.lock() {
g.remove(&h);
}
} else {
ok = false;
}
}
ok
}
pub fn badge_active() -> bool {
BADGED.lock().map(|g| !g.is_empty()).unwrap_or(false)
}
/// Called by dispatch on every driving verb; feeds the idle clock.
pub fn note_driving() {
LAST_DRIVING.store(now(), Ordering::Relaxed);
}
/// Clear the overlay after IDLE_CLEAR_SEC without a driving verb. Idempotent; the next driving
/// verb re-badges instantly.
pub fn start_idle_watcher() -> bool {
if IDLE_WATCHER.swap(true, Ordering::SeqCst) {
return false;
}
std::thread::Builder::new()
.name("altium-badge-idle-clear".into())
.spawn(|| loop {
std::thread::sleep(Duration::from_secs(15));
let last = LAST_DRIVING.load(Ordering::Relaxed);
if badge_active() && last > 0 && now() - last > IDLE_CLEAR_SEC {
clear_driving_badge();
}
})
.is_ok()
}
// ---- progress ----------------------------------------------------------------
fn taskbar_progress(state: &str, value: Option<i64>) -> bool {
let hwnd = match main_hwnd() {
Some(h) => h,
None => return false,
};
let mut prog = json!({"state": state});
if let Some(v) = value {
prog["value"] = json!(v.clamp(0, 100));
}
taskbar(json!({"hwnd": hwnd, "progress": prog}))
}
/// Begin a progress bar for `verb`, paced by its measured estimate (marquee when none).
///
/// The FINAL clear is painted by this ticker thread, not by `end_activity_progress`: every paint
/// is an ab round trip, so a clear painted from another thread can lose the race to a tick still
/// in flight — leaving a stale partial bar parked on Altium's taskbar icon forever (John saw
/// exactly that, 2026-09-02, minutes after the Rust swap). Paints are sequential within this
/// thread, so a clear IT paints is guaranteed to be last.
pub fn start_activity_progress(verb: &str, estimate_s: Option<f64>, confidence: &str) {
let stop = std::sync::Arc::new(AtomicBool::new(false));
let failed = std::sync::Arc::new(AtomicBool::new(false));
if let Ok(mut g) = PROGRESS_STOP.lock() {
g.insert(verb.to_string(), ProgressHandle { stop: stop.clone(), failed: failed.clone() });
}
let est = estimate_s.filter(|e| *e > 0.0 && confidence != "unknown");
let _ = std::thread::Builder::new().name(format!("altium-progress-{verb}")).spawn(move || {
let started = std::time::Instant::now();
match est {
None => {
taskbar_progress("indeterminate", None);
while !stop.load(Ordering::Relaxed) {
std::thread::sleep(Duration::from_millis(250));
}
}
Some(e) => {
let mut fails = 0u32;
'ticks: while !stop.load(Ordering::Relaxed) {
// sleep in small slices so the final clear lands promptly after the verb ends
for _ in 0..7 {
std::thread::sleep(Duration::from_millis(100));
if stop.load(Ordering::Relaxed) {
break 'ticks;
}
}
let pct = ((95.0 * started.elapsed().as_secs_f64() / e) as i64).min(95);
if taskbar_progress("normal", Some(pct)) {
fails = 0;
} else {
fails += 1; // window may not exist yet during a cold launch — keep trying
if fails > 120 {
break; // still fall through to the clear below
}
}
}
}
}
// terminal frames, from THIS thread only
if failed.load(Ordering::Relaxed) {
taskbar_progress("error", Some(100));
std::thread::sleep(Duration::from_millis(1200));
}
taskbar_progress("none", None);
if let Some(h) = main_hwnd() {
// cancel Windows' own orange attention flash (John: "can u not orange flash the taskbar")
taskbar(json!({"hwnd": h, "flash": "stop"}));
}
});
}
pub fn end_activity_progress(verb: &str, failed: bool) {
if let Some(h) = PROGRESS_STOP.lock().ok().and_then(|mut g| g.remove(verb)) {
h.failed.store(failed, Ordering::Relaxed);
h.stop.store(true, Ordering::Relaxed); // the ticker paints the clear (see above)
}
}
// ---- base64 (no external crate) ----------------------------------------------
fn b64encode(data: &[u8]) -> String {
const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
for chunk in data.chunks(3) {
let b = [chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0)];
let n = ((b[0] as u32) << 16) | ((b[1] as u32) << 8) | b[2] as u32;
out.push(T[(n >> 18) as usize & 63] as char);
out.push(T[(n >> 12) as usize & 63] as char);
out.push(if chunk.len() > 1 { T[(n >> 6) as usize & 63] as char } else { '=' });
out.push(if chunk.len() > 2 { T[n as usize & 63] as char } else { '=' });
}
out
}
#[cfg(test)]
mod tests {
use super::b64encode;
#[test]
fn base64_reference_vectors() {
assert_eq!(b64encode(b""), "");
assert_eq!(b64encode(b"f"), "Zg==");
assert_eq!(b64encode(b"fo"), "Zm8=");
assert_eq!(b64encode(b"foo"), "Zm9v");
assert_eq!(b64encode(b"foobar"), "Zm9vYmFy");
}
}