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.11: guided demo tour with a play/pause/next/prev/exit floaty, ab desktop captions, pre-baked adom-tts voiceover clips behind a checkbox, spotlighting, and a front-and-center Play-the-1-minute-demo button in the single-X newbie panel
a79bc20
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
//! chip-fetcher settings: one JSON file, typed by the default's type, enum
//! values validated, read back literal for literal (settings-layout skill:
//! "never trust the 200"). Lives in `~/.config/adom-chip-fetcher/settings.json`.
use anyhow::{anyhow, Result};
use serde_json::{json, Map, Value};
use std::path::PathBuf;
pub fn config_dir() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| "/home/adom".into());
PathBuf::from(home).join(".config/adom-chip-fetcher")
}
pub fn path() -> PathBuf {
config_dir().join("settings.json")
}
/// (key, default, allowed enum values or empty, human label)
pub const SCHEMA: &[(&str, &str, &[&str], &str)] = &[
("taskbar_progress", "indeterminate", &["indeterminate", "off"], "Taskbar progress during a lookup"),
("auto_zoom", "false", &[], "Auto-zoom the active card"),
("auto_select_active", "true", &[], "Follow the active lookup"),
("show_stock_badges", "true", &[], "Stock and lifecycle badges on cards"),
("show_welcome", "true", &[], "Newbie panel on the dashboard"),
("seed_samples", "true", &[], "Seed sample parts into an empty library"),
("demo_voiceover", "true", &[], "Voiceover during the guided demo"),
("activity_stale_seconds", "90", &[], "Heartbeat considered stale after (seconds)"),
];
fn parse_default(s: &str) -> Value {
match s {
"true" => Value::Bool(true),
"false" => Value::Bool(false),
_ => s.parse::<i64>().map(Value::from).unwrap_or_else(|_| Value::String(s.to_string())),
}
}
pub fn defaults() -> Map<String, Value> {
SCHEMA.iter().map(|(k, d, _, _)| (k.to_string(), parse_default(d))).collect()
}
/// Coerce + validate one value against its default's type.
fn coerce(key: &str, v: &Value) -> Result<Value> {
let (_, d, allowed, _) = SCHEMA
.iter()
.find(|(k, _, _, _)| *k == key)
.ok_or_else(|| anyhow!("unknown setting '{key}'"))?;
let def = parse_default(d);
let out = match def {
Value::Bool(_) => match v {
Value::Bool(b) => Value::Bool(*b),
Value::String(s) => match s.as_str() {
"true" | "on" | "1" | "yes" => Value::Bool(true),
"false" | "off" | "0" | "no" => Value::Bool(false),
_ => return Err(anyhow!("'{key}' wants true or false, got '{s}'")),
},
Value::Number(n) => Value::Bool(n.as_i64().unwrap_or(0) != 0),
_ => return Err(anyhow!("'{key}' wants true or false")),
},
Value::Number(_) => match v {
Value::Number(n) => Value::from(n.as_i64().ok_or_else(|| anyhow!("'{key}' wants an integer"))?),
Value::String(s) => Value::from(s.trim().parse::<i64>().map_err(|_| anyhow!("'{key}' wants an integer, got '{s}'"))?),
_ => return Err(anyhow!("'{key}' wants an integer")),
},
_ => {
let s = match v {
Value::String(s) => s.clone(),
other => other.to_string().trim_matches('"').to_string(),
};
if !allowed.is_empty() && !allowed.contains(&s.as_str()) {
return Err(anyhow!("'{key}' must be one of {}", allowed.join(", ")));
}
Value::String(s)
}
};
Ok(out)
}
pub fn load() -> Map<String, Value> {
let mut m = defaults();
if let Ok(text) = std::fs::read_to_string(path()) {
if let Ok(Value::Object(stored)) = serde_json::from_str::<Value>(&text) {
for (k, v) in stored {
if let Ok(c) = coerce(&k, &v) {
m.insert(k, c);
}
}
}
}
m
}
pub fn get(key: &str) -> Value {
load().get(key).cloned().unwrap_or(Value::Null)
}
pub fn get_str(key: &str) -> String {
match get(key) {
Value::String(s) => s,
Value::Bool(b) => b.to_string(),
Value::Number(n) => n.to_string(),
_ => String::new(),
}
}
pub fn get_bool(key: &str) -> bool {
get(key).as_bool().unwrap_or(false)
}
fn save(m: &Map<String, Value>) -> Result<()> {
std::fs::create_dir_all(config_dir())?;
let text = serde_json::to_string_pretty(&Value::Object(m.clone()))?;
std::fs::write(path(), text)?;
Ok(())
}
/// Merge a JSON object of changes; every value is validated before anything
/// is written. Returns the full settings after the write.
pub fn merge(changes: &Value) -> Result<Map<String, Value>> {
let obj = changes.as_object().ok_or_else(|| anyhow!("settings body must be a JSON object"))?;
let mut m = load();
for (k, v) in obj {
let c = coerce(k, v)?;
m.insert(k.clone(), c);
}
save(&m)?;
Ok(load())
}
pub fn reset() -> Result<Map<String, Value>> {
let d = defaults();
save(&d)?;
Ok(load())
}
pub fn describe_json() -> String {
let labels: Map<String, Value> = SCHEMA.iter().map(|(k, _, _, l)| (k.to_string(), json!(l))).collect();
let enums: Map<String, Value> = SCHEMA
.iter()
.filter(|(_, _, a, _)| !a.is_empty())
.map(|(k, _, a, _)| (k.to_string(), json!(a)))
.collect();
json!({
"ok": true,
"settings": load(),
"defaults": defaults(),
"labels": labels,
"enums": enums,
"path": path().display().to_string(),
})
.to_string()
}