app
Chip Fetcher (cf)
Public Made by Adomby adom
Crawls the web on your behalf to gather everything about the chips you care about: datasheets, symbols, footprints, 3D models, stock and pricing, straight from the manufacturer first, into your EDA tool. Say "cf" to your AI.
master
John Lauer
1.3.0: Dock Bar contract (serve --print-url, /api/status LEDs, CORS, say strip), settings modal, taskbar progress, pup vault for logins, adom-bridge + pup_* transport, dashboard overhaul per adom-ui-design, doctor for first run
83b2ae8
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
//! Windows taskbar progress on the dashboard's pup window.
//!
//! John (issue adom/adom-chip-fetcher#3): a part lookup is a long, open-ended
//! process, so it gets an INDETERMINATE taskbar bar for as long as it runs,
//! never a false estimate. Short verbs (cse, ds2sf, thumbnails) get a
//! determinate bar that fills over a persisted running average, the way pup
//! does it (`~/.adom/pup-verb-timing.json` shape: `{"<op>":{"n":N,"avgMs":M}}`).
//!
//! Everything here is best effort and gated by the `taskbar_progress`
//! setting: a paint must never fail a fetch. Each paint is one
//! `adom-bridge pup_progress` process, so we paint on change only.
use serde_json::{json, Value};
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
pub const DASHBOARD_SESSION: &str = "adom-chip-fetcher-dashboard";
fn enabled() -> bool {
crate::settings::get_str("taskbar_progress") != "off"
}
fn push(mode: &str, value: Option<u8>, note: &str) {
if !enabled() || !crate::bridge::available() {
return;
}
let mut body = json!({"sessionId": DASHBOARD_SESSION, "mode": mode});
if let Some(v) = value {
body["value"] = json!(v.min(100));
}
if !note.is_empty() {
body["note"] = json!(note);
}
// Fire and forget, bounded: a stuck bridge must not stall the caller.
let _ = std::process::Command::new("timeout")
.arg("8")
.arg(bridge_bin())
.args(["--ai-thread", &crate::bridge::ai_thread(), "pup_progress", &body.to_string()])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
}
fn bridge_bin() -> String {
let mut c = crate::bridge::command();
c.get_program().to_string_lossy().to_string()
}
/// Indeterminate bar for the whole lookup.
pub fn start_indeterminate(note: &str) {
push("indeterminate", None, note);
}
pub fn progress(pct: u8, note: &str) {
push("value", Some(pct), note);
}
pub fn clear() {
push("off", None, "");
}
// ------------------------------------------------------------- durations
fn durations_path() -> PathBuf {
crate::settings::config_dir().join("durations.json")
}
fn load_durations() -> Value {
std::fs::read_to_string(durations_path())
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_else(|| json!({}))
}
const DEFAULT_MS: &[(&str, u64)] = &[("cse", 45_000), ("ds2sf", 60_000), ("thumbnails", 20_000), ("validate", 30_000), ("refetch", 60_000)];
pub fn estimate_ms(op: &str) -> u64 {
load_durations()
.get(op)
.and_then(|t| t.get("avgMs"))
.and_then(|v| v.as_u64())
.or_else(|| DEFAULT_MS.iter().find(|(k, _)| *k == op).map(|(_, v)| *v))
.unwrap_or(30_000)
}
/// pup's rule: first sample sets the average, then a 0.75/0.25 EMA; absurd
/// samples (a clock jump, a forgotten terminal) are not samples.
pub fn record(op: &str, ms: u64) {
if ms > 30 * 60 * 1000 {
return;
}
let mut d = load_durations();
let (n, avg) = d
.get(op)
.map(|t| (t["n"].as_u64().unwrap_or(0), t["avgMs"].as_u64().unwrap_or(ms)))
.unwrap_or((0, ms));
let avg = if n == 0 { ms } else { (avg * 3 + ms) / 4 };
d[op] = json!({"n": (n + 1).min(100_000), "avgMs": avg});
let _ = std::fs::create_dir_all(crate::settings::config_dir());
let _ = std::fs::write(durations_path(), d.to_string());
}
pub fn durations_json() -> String {
json!({"ok": true, "path": durations_path().display().to_string(), "durations": load_durations()}).to_string()
}
// ------------------------------------------------------- determinate run
/// A determinate bar that fills to 92% over the running-average estimate and
/// completes on `finish()`. Dropped early (an `?` error path) it clears.
pub struct ProgressRun {
stop: Arc<AtomicBool>,
handle: Option<std::thread::JoinHandle<()>>,
op: String,
started: Instant,
finished: bool,
}
impl ProgressRun {
pub fn start(op: &str, note: &str) -> Self {
let est = estimate_ms(op).max(200) as f64;
let stop = Arc::new(AtomicBool::new(false));
let (s2, n2) = (stop.clone(), note.to_string());
let started = Instant::now();
let active = enabled();
let handle = std::thread::spawn(move || {
if !active {
return;
}
let mut last = 0u8;
while !s2.load(Ordering::Relaxed) {
let pct = ((started.elapsed().as_millis() as f64 / est).min(0.92) * 100.0) as u8;
let pct = pct.max(2);
if pct != last {
progress(pct, &n2);
last = pct;
}
std::thread::sleep(Duration::from_millis(500));
}
});
ProgressRun { stop, handle: Some(handle), op: op.into(), started, finished: false }
}
pub fn finish(mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(h) = self.handle.take() {
let _ = h.join();
}
record(&self.op, self.started.elapsed().as_millis() as u64);
if enabled() {
progress(100, "done");
std::thread::sleep(Duration::from_millis(220));
clear();
}
self.finished = true;
}
}
impl Drop for ProgressRun {
fn drop(&mut self) {
if self.finished {
return;
}
self.stop.store(true, Ordering::Relaxed);
if let Some(h) = self.handle.take() {
let _ = h.join();
}
clear();
}
}