app
Adom Schematic
Public Made by Adomby adom
Interactive schematic viewer: your EDA's own render plus per-symbol highlighting and live MPN, stock and price on hover
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
//! adom-schematic — interactive schematic viewer for a whole `.kicad_sch`.
//!
//! Sibling of adom-2dboard. The visual base is the EDA's OWN render
//! (service-kicad `sch/export/svg` — authentic symbols/wires/fonts); we overlay
//! a per-component hotspot layer so hovering a symbol shows what it is plus live
//! stock & price. Meant to be embedded as a live schematic "view" in any app.
mod sexpr;
mod sch_parse;
mod kicad_sch;
mod kicad_self;
mod eagle_sch;
mod altium_sch;
mod enrich;
mod server;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use std::fs;
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "adom-schematic", version, about = "Interactive schematic viewer (.kicad_sch): KiCad-native render + per-component hover with stock & price")]
struct Cli { #[command(subcommand)] command: Cmd }
#[derive(Subcommand)]
enum Cmd {
/// Render a `.kicad_sch` to a composed interactive SVG (+ optional -meta.json).
Render { #[arg(long)] file: PathBuf, #[arg(long)] out: PathBuf, #[arg(long)] meta: bool },
/// Export a self-contained INTERACTIVE HTML for embedding as a schematic view.
Embed { #[arg(long)] file: PathBuf, #[arg(long)] out: Option<PathBuf>,
/// Directory holding this design's sibling `.kicad_sch` files. When
/// given, every hierarchical child sheet is pre-rendered and bundled
/// into the embed so navigating between sheets is instant (no reload).
#[arg(long)] sheet_dir: Option<PathBuf> },
/// Run the live app server (the embeddable live schematic view).
Serve { #[arg(long)] file: Option<PathBuf>, #[arg(long, default_value_t = 8785)] port: u16 },
/// Look up stock / price / MPN / wiki for one part, as JSON on stdout.
/// Lets a HOST app (e.g. adom-project-manager) serve `/enrich` for an
/// embedded view, which has no server of its own.
Enrich { #[arg(long, default_value = "")] mpn: String, #[arg(long, default_value = "")] lcsc: String },
/// Drive the running server's front-end (the AI's live UI channel) exactly as a
/// user's click would: `ui toast "done"` · `ui fit` · `ui sheet Data` ·
/// `ui highlight U1` · `ui clear`.
Ui { /// toast | fit | sheet | highlight | clear
action: String,
/// message (toast) or argument (sheet file / component ref)
arg: Option<String>,
#[arg(long = "type", default_value = "info")] ty: String,
#[arg(long, default_value_t = 8785)] port: u16 },
/// Check the live server is up on --port (prints OK:/ERROR: + a Hint:).
Health { #[arg(long, default_value_t = 8785)] port: u16 },
/// Install this binary to ~/.local/bin so host apps can shell it by name.
Install,
}
const APP_HTML: &str = include_str!("app.html");
const VIEWER_JS: &str = include_str!("viewer.js");
pub fn build_embed(svg: &str, meta_json: &str) -> String {
let payload = format!(
"<script>window.__SCH__={{svg:{},meta:{}}};</script>\n<script>{}</script>",
serde_json::to_string(svg).unwrap_or_else(|_| "\"\"".into()), meta_json, VIEWER_JS);
APP_HTML.replace(r#"<script src="viewer.js"></script>"#, &payload)
}
/// Like build_embed, but also inlines `window.__SHEETS__` = {filename: {svg,meta}}
/// (root under "__root__") so the frontend can switch sheets instantly.
pub fn build_embed_bundle(svg: &str, meta_json: &str, bundle_json: &str) -> String {
let payload = format!(
"<script>window.__SCH__={{svg:{},meta:{}}};window.__SHEETS__={};</script>\n<script>{}</script>",
serde_json::to_string(svg).unwrap_or_else(|_| "\"\"".into()), meta_json, bundle_json, VIEWER_JS);
APP_HTML.replace(r#"<script src="viewer.js"></script>"#, &payload)
}
// CLI contract: every response starts with `OK:` or `ERROR:`, and every error
// carries an actionable `Hint:` naming the next command.
fn main() {
if let Err(e) = run() {
println!("ERROR: {e}");
println!("Hint: check the file path / arguments; run `adom-schematic --help`");
std::process::exit(1);
}
}
fn run() -> Result<()> {
match Cli::parse().command {
Cmd::Render { file, out, meta } => {
let raw = fs::read(&file).with_context(|| format!("read {}", file.display()))?;
let (svg, m) = kicad_sch::render_sheet_bytes(&raw)?;
fs::write(&out, &svg)?;
println!("OK: rendered {} -> {} ({} components)", file.display(), out.display(), m.comp_count);
if meta { fs::write(out.with_extension("meta.json"), serde_json::to_string_pretty(&m)?)?; }
}
Cmd::Embed { file, out, sheet_dir } => {
let raw = fs::read(&file).with_context(|| format!("read {}", file.display()))?;
// an authentic pre-rendered base sits beside the file as `<name>.base.svg`
let base_of = |p: &std::path::Path| -> Option<String> {
fs::read_to_string(format!("{}.base.svg", p.display())).ok()
};
let (svg, m) = kicad_sch::render_sheet_bytes_base(&raw, base_of(&file).as_deref())?;
// Bundle: the root plus every child sheet, keyed by filename, so the
// frontend can swap sheets in-place with zero load time.
let mut bundle = serde_json::Map::new();
bundle.insert("__root__".into(), serde_json::json!({"svg": svg, "meta": m}));
let mut n_sheets = 0usize;
if let Some(dir) = &sheet_dir {
for sh in &m.sub_sheets {
// render each child; a child may itself be hierarchical (its own
// sub_sheets carry into that sheet's meta for deeper nav).
let p = dir.join(&sh.file);
if let Ok(cbytes) = fs::read(&p) {
if let Ok((csvg, cm)) = kicad_sch::render_sheet_bytes_base(&cbytes, base_of(&p).as_deref()) {
bundle.insert(sh.file.clone(), serde_json::json!({"svg": csvg, "meta": cm}));
n_sheets += 1;
}
}
}
}
let html = build_embed_bundle(&svg, &serde_json::to_string(&m)?, &serde_json::to_string(&bundle)?);
let out = out.unwrap_or_else(|| file.with_extension("schview.html"));
fs::write(&out, &html)?;
println!("OK: embed -> {} ({} components, {} child sheets bundled)", out.display(), m.comp_count, n_sheets);
}
Cmd::Serve { file, port } => {
if let Some(f) = file {
let content = fs::read_to_string(&f).with_context(|| format!("read {}", f.display()))?;
let name = f.file_stem().and_then(|s| s.to_str()).unwrap_or("sheet").to_string();
server::load_sch(&name, &content)?;
println!("OK: loaded {}", f.display());
}
server::run(port)?;
}
Cmd::Enrich { mpn, lcsc } => println!("{}", enrich::lookup(&mpn, &lcsc)),
Cmd::Ui { action, arg, ty, port } => {
let base = format!("http://127.0.0.1:{port}");
let (path, payload) = match action.as_str() {
"toast" => ("/ui/toast", serde_json::json!({ "message": arg.unwrap_or_default(), "type": ty }).to_string()),
"fit" | "clear" => ("/ui/cmd", serde_json::json!({ "action": action }).to_string()),
"sheet" | "highlight" => ("/ui/cmd", serde_json::json!({ "action": action, "arg": arg.unwrap_or_default() }).to_string()),
other => {
println!("ERROR: unknown ui action '{other}'");
println!("Hint: use one of: toast <msg> | fit | sheet <file> | highlight <ref> | clear");
return Ok(());
}
};
match ureq::post(&format!("{base}{path}")).set("Content-Type", "application/json").send_string(&payload) {
Ok(_) => println!("OK: ui {action} sent to {base}"),
Err(_) => {
println!("ERROR: could not reach the viewer server on port {port}");
println!("Hint: start it with `adom-schematic serve --port {port} &`");
}
}
}
Cmd::Health { port } => match ureq::get(&format!("http://127.0.0.1:{port}/version")).call() {
Ok(r) => println!("OK: adom-schematic healthy on port {port} ({})", r.into_string().unwrap_or_default().trim()),
Err(_) => {
println!("ERROR: no adom-schematic server responding on port {port}");
println!("Hint: start it with `adom-schematic serve --port {port} &`");
}
},
Cmd::Install => {
let exe = std::env::current_exe()?;
let home = std::env::var("HOME").unwrap_or_default();
let bin = format!("{home}/.local/bin");
fs::create_dir_all(&bin)?;
let dst = format!("{bin}/adom-schematic");
fs::copy(&exe, &dst)?;
println!("OK: installed adom-schematic to {dst}");
println!("Hint: ensure ~/.local/bin is on your PATH");
}
}
Ok(())
}