#![cfg(not(target_arch = "wasm32"))]
use super::kicad_bulk::{
AppStoreWriter, BulkRun, Ledger, PartWriter, Progress, StoreWriter, Sweep,
};
use super::kicad_import::{
part_from_symbol, read_symbol_library, DiskFiles, Folder, FootprintChoice, Imported, KicadFiles,
KicadLibrary, KicadPart,
};
use crate::store::{default_model_store, ModelStore};
use brep_ecad_core::kicad_git::{self, Repo};
use brep_ecad_core::Symbol;
use eframe::egui;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
pub const DEFAULT_TAG: &str = kicad_git::TAG;
pub fn default_cache_root() -> PathBuf {
crate::plm::config::app_config_dir().join("kicad-library")
}
pub fn symbol_libraries(names: &[String]) -> Vec<String> {
let mut found: Vec<String> = names
.iter()
.filter_map(|name| name.strip_suffix(".kicad_sym"))
.map(str::to_owned)
.collect();
found.sort();
found
}
const SYMBOL_VARS: &[&str] = &[
"KICAD9_SYMBOL_DIR",
"KICAD8_SYMBOL_DIR",
"KICAD7_SYMBOL_DIR",
"KICAD6_SYMBOL_DIR",
];
pub fn symbols_folder(env: &dyn Fn(&str) -> Option<String>, footprints: &str, files: &dyn KicadFiles) -> String {
for var in SYMBOL_VARS {
if let Some(path) = env(var).filter(|path| files.is_dir(path)) {
return path;
}
}
std::path::Path::new(footprints)
.parent()
.map(|dir| dir.join("symbols").to_string_lossy().into_owned())
.unwrap_or_else(|| "symbols".into())
}
pub fn local_symbol_libraries(dir: &str, files: &dyn KicadFiles) -> Vec<String> {
symbol_libraries(&files.list(dir))
}
pub fn document_name(library_id: &str) -> String {
let bare = library_id.rsplit(':').next().unwrap_or(library_id);
let cleaned: String = bare
.chars()
.map(|c| if c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' { c } else { '_' })
.collect();
let trimmed = cleaned.trim_matches(['_', '.']).to_owned();
if trimmed.is_empty() {
"kicad-part".into()
} else {
trimmed
}
}
struct LibraryReader {
answer: std::sync::mpsc::Receiver<Result<(Vec<Symbol>, Vec<String>), String>>,
library: String,
}
pub struct LocalSource {
library: KicadLibrary,
}
impl LocalSource {
pub fn new(library: KicadLibrary) -> Self {
Self { library }
}
}
impl KicadFiles for LocalSource {
fn read(&self, path: &str) -> Option<Vec<u8>> {
DiskFiles.read(path)
}
fn is_file(&self, path: &str) -> bool {
DiskFiles.is_file(path)
}
fn is_dir(&self, path: &str) -> bool {
DiskFiles.is_dir(path)
}
fn list(&self, dir: &str) -> Vec<String> {
DiskFiles.list(dir)
}
}
impl PartSource for LocalSource {
fn library(&self) -> KicadLibrary {
self.library.clone()
}
}
pub trait PartSource: KicadFiles + Send + 'static {
fn library(&self) -> KicadLibrary;
}
pub struct PartReader {
answer: std::sync::mpsc::Receiver<KicadPart>,
}
impl PartReader {
pub fn take(&self) -> Option<KicadPart> {
match self.answer.try_recv() {
Ok(answer) => Some(answer),
Err(std::sync::mpsc::TryRecvError::Empty) => None,
Err(std::sync::mpsc::TryRecvError::Disconnected) => Some(KicadPart {
notes: vec!["the part reader stopped without an answer".into()],
..KicadPart::default()
}),
}
}
}
pub fn read_part(symbol: Symbol, choice: FootprintChoice, source: impl PartSource) -> PartReader {
let (send, answer) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let library = source.library();
let part = part_from_symbol(symbol, &choice, &library, &source).parsed();
let _ = send.send(part);
});
PartReader { answer }
}
pub struct CloneRun {
updates: std::sync::mpsc::Receiver<CloneUpdate>,
cancel: Arc<AtomicBool>,
pub line: String,
}
enum CloneUpdate {
Line(String),
Done(Result<(), String>),
}
impl CloneRun {
fn start(base: String, tag: String, root: PathBuf) -> Self {
let (send, updates) = std::sync::mpsc::channel();
let cancel = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&cancel);
std::thread::spawn(move || {
let mut result = kicad_git::git_version().map(|_| ());
for repo in Repo::ALL {
if result.is_err() {
break;
}
let mut report = |line: &str| {
let _ = send.send(CloneUpdate::Line(format!("{}: {line}", repo.project())));
};
let dest = root.join(repo.install_folder());
result = kicad_git::clone(&repo.url(&base), &tag, &dest, &mut report, &flag).map(|_| ());
}
let _ = send.send(CloneUpdate::Done(result));
});
Self { updates, cancel, line: "starting git".into() }
}
pub fn stop(&self) {
self.cancel.store(true, Ordering::Relaxed);
}
pub fn stopping(&self) -> bool {
self.cancel.load(Ordering::Relaxed)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Source {
Download,
Install,
}
impl Source {
fn label(self) -> &'static str {
match self {
Source::Download => "Clone KiCad's library",
Source::Install => "Use my KiCad install",
}
}
}
pub enum Stage {
Choose,
Libraries,
Opening { library: String },
Symbols { library: String, symbols: Vec<Symbol>, warnings: Vec<String> },
Reading { symbol: String },
Review { part: Box<KicadPart>, warnings: Vec<String> },
Bulk,
}
pub struct KicadLibraryWindow {
pub stage: Stage,
source: Source,
symbols_dir: String,
footprints_dir: String,
models_dir: String,
cache_dir: String,
tag: String,
clone_base: String,
libraries: Vec<String>,
pub cloning: Option<CloneRun>,
reader: Option<PartReader>,
library_reader: Option<LibraryReader>,
search: String,
pending_warnings: Vec<String>,
document: String,
pub status: String,
store: Box<dyn ModelStore>,
pub saved: Vec<String>,
pub bulk: Option<BulkRun>,
force_reimport: bool,
ledger_line: String,
new_writer: Arc<dyn Fn() -> Box<dyn PartWriter + Send> + Send + Sync>,
}
impl KicadLibraryWindow {
pub fn new() -> Self {
Self::with_store(default_model_store())
}
pub fn with_store(store: Box<dyn ModelStore>) -> Self {
let env = |var: &str| std::env::var(var).ok().filter(|value| !value.is_empty());
let library = KicadLibrary::load(store.as_ref(), &env, &DiskFiles);
let symbols = symbols_folder(&env, &library.footprints.path, &DiskFiles);
Self {
stage: Stage::Choose,
source: Source::Download,
symbols_dir: symbols,
footprints_dir: library.footprints.path.clone(),
models_dir: library.models.path.clone(),
cache_dir: default_cache_root().to_string_lossy().into_owned(),
tag: DEFAULT_TAG.into(),
clone_base: kicad_git::BASE_URL.into(),
libraries: Vec::new(),
cloning: None,
reader: None,
library_reader: None,
search: String::new(),
pending_warnings: Vec::new(),
document: String::new(),
status: String::new(),
store,
saved: Vec::new(),
bulk: None,
force_reimport: false,
ledger_line: String::new(),
new_writer: Arc::new(|| Box::new(AppStoreWriter)),
}
}
pub fn sweep_through(&mut self, write: Arc<dyn Fn() -> Box<dyn PartWriter + Send> + Send + Sync>) {
self.new_writer = write;
}
pub fn clone_from(&mut self, base: &str, cache_dir: &Path) {
self.clone_base = base.to_owned();
self.cache_dir = cache_dir.to_string_lossy().into_owned();
}
fn clone_root(&self) -> PathBuf {
Path::new(&self.cache_dir).join(&self.tag)
}
fn folders(&self) -> (String, String, String) {
match self.source {
Source::Install => (self.symbols_dir.clone(), self.footprints_dir.clone(), self.models_dir.clone()),
Source::Download => {
let root = self.clone_root();
let [symbols, footprints, models] =
Repo::ALL.map(|repo| root.join(repo.install_folder()).to_string_lossy().into_owned());
(symbols, footprints, models)
}
}
}
fn library(&self) -> KicadLibrary {
let (_, footprints, models) = self.folders();
let from = match self.source {
Source::Install => "your KiCad install",
Source::Download => "the clone of KiCad's library",
};
let folder = |path: String| Folder { from: format!("{from} ({path})"), path };
KicadLibrary { footprints: folder(footprints), models: folder(models) }
}
pub fn choose(&mut self, source: Source, ctx: Option<&egui::Context>) {
self.source = source;
self.stage = Stage::Libraries;
self.libraries.clear();
self.search.clear();
self.reader = None;
self.library_reader = None;
self.refresh_ledger_line();
let cloned = Repo::ALL.iter().all(|repo| self.clone_root().join(repo.install_folder()).is_dir());
if source == Source::Download && !cloned {
if self.cloning.is_none() {
self.cloning = Some(CloneRun::start(self.clone_base.clone(), self.tag.clone(), self.clone_root()));
}
self.status = format!(
"cloning KiCad {}'s libraries into {} with git (about 5.6 GB, once)\u{2026}",
self.tag,
self.clone_root().display()
);
if let Some(ctx) = ctx {
ctx.request_repaint_after(std::time::Duration::from_millis(100));
}
return;
}
self.list_libraries();
}
fn list_libraries(&mut self) {
let (symbols, _, _) = self.folders();
self.libraries = local_symbol_libraries(&symbols, &DiskFiles);
self.status = if self.libraries.is_empty() {
format!("no .kicad_sym library in {symbols}")
} else {
format!("{} symbol libraries in {symbols} (type to search)", self.libraries.len())
};
}
fn poll_clone(&mut self) {
let Some(run) = &mut self.cloning else {
return;
};
let mut done = None;
let mut gone = false;
loop {
match run.updates.try_recv() {
Ok(CloneUpdate::Line(line)) => run.line = line,
Ok(CloneUpdate::Done(result)) => done = Some(result),
Err(std::sync::mpsc::TryRecvError::Empty) => break,
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
gone = true;
break;
}
}
}
let done = match done {
Some(done) => done,
None if gone => Err("the clone stopped without saying why".into()),
None => return,
};
self.cloning = None;
match done {
Ok(()) if self.source == Source::Download => self.list_libraries(),
Ok(()) => self.status = format!("KiCad {}'s libraries are cloned", self.tag),
Err(error) => {
self.source = Source::Install;
self.stage = Stage::Choose;
self.status = format!(
"KiCad's libraries were not cloned: {error}. Install git, or name the folders of \
a KiCad library on this machine above and choose \"{}\".",
Source::Install.label()
);
}
}
}
pub fn open_library(&mut self, library: &str) {
let file = format!("{library}.kicad_sym");
let path = Path::new(&self.folders().0).join(&file);
let (send, answer) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let read = std::fs::read(&path).map_err(|error| format!("{}: {error}", path.display()));
let _ = send.send(read.and_then(|bytes| read_symbol_library(&file, &bytes)));
});
self.library_reader = Some(LibraryReader { answer, library: library.to_owned() });
self.status = format!("reading {library}\u{2026}");
self.stage = Stage::Opening { library: library.to_owned() };
}
pub fn read_symbol(&mut self, symbol: Symbol) {
let name = symbol.library_id.clone();
self.pending_warnings = match &self.stage {
Stage::Symbols { warnings, .. } => warnings.clone(),
_ => Vec::new(),
};
self.document = document_name(&name);
self.status = format!("reading {name}\u{2026}");
self.reader = Some(read_part(symbol, FootprintChoice::Named, LocalSource::new(self.library())));
self.stage = Stage::Reading { symbol: name };
}
pub fn poll(&mut self, ctx: Option<&egui::Context>) {
if let Some(run) = &mut self.bulk {
let was_running = !run.progress.finished;
let moved = run.poll();
let stopped = moved && was_running && run.progress.finished;
if !run.progress.finished {
if let Some(ctx) = ctx {
ctx.request_repaint_after(std::time::Duration::from_millis(250));
}
}
if stopped {
let line = run.progress.line();
self.status = format!("import {line}");
self.refresh_ledger_line();
}
}
if self.cloning.is_some() {
self.poll_clone();
if let (Some(ctx), Some(_)) = (ctx, &self.cloning) {
ctx.request_repaint_after(std::time::Duration::from_millis(250));
}
}
if let Some(reader) = &self.library_reader {
match reader.answer.try_recv() {
Ok(Ok((symbols, warnings))) => {
let library = reader.library.clone();
self.library_reader = None;
self.status = format!("{library} holds {} symbols", symbols.len());
self.search.clear();
self.stage = Stage::Symbols { library, symbols, warnings };
}
Ok(Err(error)) => {
self.status = format!("{} was not read: {error}", reader.library);
self.library_reader = None;
self.stage = Stage::Libraries;
}
Err(std::sync::mpsc::TryRecvError::Empty) => {
if let Some(ctx) = ctx {
ctx.request_repaint_after(std::time::Duration::from_millis(100));
}
}
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
self.status = format!("{} was not read: the reader stopped", reader.library);
self.library_reader = None;
self.stage = Stage::Libraries;
}
}
}
let Some(reader) = &self.reader else {
return;
};
match reader.take() {
Some(part) => {
self.reader = None;
self.status = "read".into();
let warnings = std::mem::take(&mut self.pending_warnings);
self.stage = Stage::Review { part: Box::new(part), warnings };
}
None => {
if let Some(ctx) = ctx {
ctx.request_repaint_after(std::time::Duration::from_millis(100));
}
}
}
}
pub fn save(&mut self) -> Result<Imported, String> {
let Stage::Review { part, .. } = &self.stage else {
return Err("nothing has been read yet".into());
};
let name = self.document.trim();
if name.is_empty() {
return Err("the part needs a name".into());
}
let name = name.to_owned();
let imported = super::kicad_bulk::write_part(&StoreWriter(self.store.as_ref()), &name, part)?;
self.saved.push(name.clone());
self.status = format!("saved {name}: {}", imported.summary);
self.stage = Stage::Libraries;
Ok(imported)
}
pub fn stage_name(&self) -> &'static str {
match self.stage {
Stage::Choose => "choose",
Stage::Libraries => "libraries",
Stage::Opening { .. } => "opening",
Stage::Symbols { .. } => "symbols",
Stage::Reading { .. } => "reading",
Stage::Review { .. } => "review",
Stage::Bulk => "bulk",
}
}
pub fn refresh_ledger_line(&mut self) {
let ledger = Ledger::read(std::path::Path::new(&self.cache_dir), &self.tag);
let (done, retry) = ledger.counts();
if done == 0 && retry == 0 {
self.ledger_line = format!("nothing imported into {} yet", self.cache_dir);
return;
}
let grades: Vec<String> = ledger
.grades()
.into_iter()
.filter(|(_, count)| *count > 0)
.map(|(grade, count)| format!("{count} {}", grade.as_str()))
.collect();
let torn = if ledger.dropped() > 0 {
format!(", {} unreadable line(s) ignored", ledger.dropped())
} else {
String::new()
};
self.ledger_line = format!(
"KiCad {}: {done} parts already imported ({}), {retry} to retry{torn} \u{2014} {}",
self.tag,
grades.join(", "),
ledger.path().display()
);
}
pub fn start_bulk(&mut self, libraries: Vec<String>) {
if libraries.is_empty() {
self.status = "there is no library to import".into();
return;
}
if self.bulk.as_ref().is_some_and(|run| !run.finished()) {
self.status = "a sweep is already running".into();
return;
}
let plan = Sweep {
cache_root: PathBuf::from(&self.cache_dir),
tag: self.tag.clone(),
symbols_dir: PathBuf::from(self.folders().0),
library: self.library(),
libraries,
force: self.force_reimport,
};
let wanted = plan.libraries.len();
match super::kicad_bulk::start(plan, Box::new(DiskFiles), (self.new_writer)()) {
Ok(run) => {
self.status = format!(
"importing {wanted} librar{} into {}; {} parts already done{}",
if wanted == 1 { "y" } else { "ies" },
self.cache_dir,
run.resumed,
if self.force_reimport {
", and every one of them again"
} else {
", which are skipped"
}
);
self.bulk = Some(run);
self.stage = Stage::Bulk;
}
Err(error) => self.status = format!("the import did not start: {error}"),
}
}
pub fn bulk_progress(&self) -> Option<&Progress> {
self.bulk.as_ref().map(|run| &run.progress)
}
fn matching(&self) -> Vec<String> {
let needle = self.search.to_lowercase();
self.libraries
.iter()
.filter(|name| name.to_lowercase().contains(&needle))
.cloned()
.collect()
}
}
impl eframe::App for KicadLibraryWindow {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
let ctx = ui.ctx().clone();
self.poll(Some(&ctx));
egui::Panel::bottom("kicad-library-status").show(ui, |ui| {
ui.horizontal_wrapped(|ui| ui.weak(&self.status));
if !self.saved.is_empty() {
ui.weak(format!("saved this session: {}", self.saved.join(", ")));
}
});
ui.heading("Import from KiCad");
ui.add_space(4.0);
self.show_source(ui);
ui.separator();
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| {
self.show_stage(ui);
});
}
}
impl KicadLibraryWindow {
fn show_source(&mut self, ui: &mut egui::Ui) {
let mut chosen = None;
ui.horizontal(|ui| {
ui.label("Library");
let choices = [
(Some(Source::Download), "download", Source::Download.label()),
(Some(Source::Install), "install", Source::Install.label()),
];
let mut picked = (!matches!(self.stage, Stage::Choose)).then_some(self.source);
let (changed, _) = crate::form::enum_dropdown(ui, "library:source", &choices, &mut picked, None, None);
if changed {
chosen = picked;
}
});
match self.source {
Source::Download => {
let idle = self.cloning.is_none();
egui::Grid::new("kicad-library-remote").num_columns(2).show(ui, |ui| {
ui.label("KiCad release");
ui.add_enabled(idle, egui::TextEdit::singleline(&mut self.tag));
ui.end_row();
ui.label("Cloned into");
ui.add_enabled(idle, egui::TextEdit::singleline(&mut self.cache_dir));
ui.end_row();
});
ui.weak(
"Symbols, footprints and 3D models are cloned with git from KiCad's own \
repositories, once (about 5.6 GB), into folders laid out like a KiCad install. \
git must be installed; without it, use your own KiCad folders instead.",
);
if let Some(run) = &self.cloning {
let stopping = run.stopping();
ui.horizontal(|ui| {
ui.spinner();
ui.label(&run.line);
if ui.add_enabled(!stopping, egui::Button::new("Stop")).clicked() {
run.stop();
}
});
}
}
Source::Install => {
egui::Grid::new("kicad-library-local").num_columns(2).show(ui, |ui| {
for (label, field) in [
("Symbols", &mut self.symbols_dir),
("Footprints", &mut self.footprints_dir),
("3D models", &mut self.models_dir),
] {
ui.label(label);
ui.text_edit_singleline(field);
ui.end_row();
}
});
}
}
if let Some(source) = chosen {
let ctx = ui.ctx().clone();
self.choose(source, Some(&ctx));
}
}
fn show_stage(&mut self, ui: &mut egui::Ui) {
let mut open = None;
let mut read = None;
let mut sweep = None;
if matches!(self.stage, Stage::Libraries) && !self.libraries.is_empty() {
sweep = self.show_bulk_controls(ui);
ui.separator();
}
if matches!(self.stage, Stage::Libraries | Stage::Symbols { .. }) {
ui.text_edit_singleline(&mut self.search);
}
match &self.stage {
Stage::Choose => {
ui.weak("Choose a source above.");
}
Stage::Libraries => {
for name in self.matching() {
ui.horizontal(|ui| {
if ui.selectable_label(false, &name).clicked() {
open = Some(name.clone());
}
if ui.small_button("import all of it").clicked() {
sweep = Some(vec![name.clone()]);
}
});
}
if self.libraries.is_empty() {
ui.weak("No symbol library here yet.");
}
}
Stage::Symbols { library, symbols, .. } => {
ui.label(format!("{library}: {} symbols", symbols.len()));
let needle = self.search.to_lowercase();
for symbol in symbols {
if !symbol.library_id.to_lowercase().contains(&needle) {
continue;
}
let row = ui.selectable_label(
false,
format!("{} ({})", symbol.library_id, crate::panels::count(symbol.pins.len(), "pin", "pins")),
);
if row.clicked() {
read = Some(symbol.clone());
}
}
}
Stage::Opening { library } => {
ui.horizontal(|ui| {
ui.spinner();
ui.label(format!("reading {library}\u{2026}"));
});
}
Stage::Reading { symbol } => {
ui.horizontal(|ui| {
ui.spinner();
ui.label(format!("reading {symbol} and its 3D model\u{2026}"));
});
}
Stage::Review { part, warnings } => {
show_review(ui, part, warnings);
}
Stage::Bulk => {
if let Some(run) = &self.bulk {
show_sweep(ui, run);
} else {
ui.weak("No import has been started.");
}
}
}
if matches!(self.stage, Stage::Bulk) {
ui.separator();
let running = self.bulk.as_ref().is_some_and(|run| !run.finished());
let stopping = self.bulk.as_ref().is_some_and(BulkRun::stopping);
ui.horizontal(|ui| {
if running {
if ui.add_enabled(!stopping, egui::Button::new("Stop")).clicked() {
if let Some(run) = &self.bulk {
run.stop();
}
self.status = "stopping after the part being imported \u{2014} \
it is finished and recorded first"
.into();
}
if stopping {
ui.weak("stopping\u{2026}");
}
} else if ui.button("Back to the libraries").clicked() {
self.stage = Stage::Libraries;
}
});
}
if matches!(self.stage, Stage::Review { .. }) {
ui.separator();
ui.horizontal(|ui| {
ui.label("Save as");
ui.text_edit_singleline(&mut self.document);
if ui.button("Save part").clicked() {
if let Err(error) = self.save() {
self.status = format!("not saved: {error}");
}
}
if ui.button("Back").clicked() {
self.stage = Stage::Libraries;
}
});
}
if let Some(libraries) = sweep {
self.start_bulk(libraries);
}
if let Some(name) = open {
self.open_library(&name);
}
if let Some(symbol) = read {
self.read_symbol(symbol);
}
}
fn show_bulk_controls(&mut self, ui: &mut egui::Ui) -> Option<Vec<String>> {
let mut wanted = None;
let matching = self.matching();
let running = self.bulk.as_ref().is_some_and(|run| !run.finished());
ui.horizontal_wrapped(|ui| {
let all = egui::Button::new(format!("Import every part of all {} libraries", self.libraries.len()));
if ui.add_enabled(!running && !self.libraries.is_empty(), all).clicked() {
wanted = Some(self.libraries.clone());
}
if matching.len() != self.libraries.len() && !matching.is_empty() {
let some = egui::Button::new(format!("\u{2026}or just the {} matching", matching.len()));
if ui.add_enabled(!running, some).clicked() {
wanted = Some(matching);
}
}
ui.checkbox(&mut self.force_reimport, "Force reimport")
.on_hover_text(
"Redo parts this cache has already imported, instead of skipping them. \
Off, an import carries on where the last one stopped.",
);
if running && ui.button("Show the import in progress").clicked() {
self.stage = Stage::Bulk;
}
});
ui.weak(&self.ledger_line);
ui.weak(
"An import can be stopped and started again as often as you like: what it finished \
is written down, and a later run skips it without converting it again. A part with no footprint or no 3D model still becomes a part, and says so.",
);
wanted
}
}
fn show_sweep(ui: &mut egui::Ui, run: &BulkRun) {
let progress = &run.progress;
ui.add(egui::ProgressBar::new(progress.fraction()).show_percentage());
if progress.symbols_total > 0 {
let within = progress.symbols_done as f32 / progress.symbols_total as f32;
ui.add(
egui::ProgressBar::new(within.clamp(0.0, 1.0))
.desired_height(6.0)
.text(format!(
"{}: {}/{}",
progress.library, progress.symbols_done, progress.symbols_total
)),
);
}
ui.add_space(4.0);
egui::Grid::new("kicad-library-sweep").num_columns(2).show(ui, |ui| {
for (label, value) in [
("Library", format!("{} of {}", progress.libraries_done.min(progress.libraries_total), progress.libraries_total)),
("Now", if progress.symbol.is_empty() { progress.library.clone() } else { progress.symbol.clone() }),
("Imported", progress.imported.to_string()),
("Already done", progress.skipped.to_string()),
("Failed, to retry later", progress.failed.to_string()),
("Recorded in", run.ledger.display().to_string()),
] {
ui.label(label);
ui.label(value);
ui.end_row();
}
if progress.libraries_failed > 0 {
ui.label("Libraries not read");
ui.label(progress.libraries_failed.to_string());
ui.end_row();
}
});
if let Some(error) = &progress.error {
ui.colored_label(egui::Color32::from_rgb(220, 120, 60), error);
}
if !progress.notes.is_empty() {
ui.add_space(4.0);
ui.weak("What went wrong, most recent last \u{2014} a later import retries exactly these:");
for note in &progress.notes {
ui.weak(note);
}
}
if progress.finished && !progress.cancelled && progress.error.is_none() {
ui.add_space(4.0);
ui.label("Finished.");
}
}
fn show_review(ui: &mut egui::Ui, part: &KicadPart, warnings: &[String]) {
egui::Grid::new("kicad-library-review").num_columns(2).show(ui, |ui| {
ui.label("Symbol");
ui.label(
part.symbol
.as_ref()
.map_or("none".into(), |s| format!("{} ({})", s.library_id, crate::panels::count(s.pins.len(), "pin", "pins"))),
);
ui.end_row();
ui.label("Pads");
ui.label(match (&part.footprint, &part.footprint_file) {
(Some(f), Some(file)) => format!("{} ({}) from {file}", f.name, crate::panels::count(f.pads.len(), "pad", "pads")),
(Some(f), None) => format!("{} ({})", f.name, crate::panels::count(f.pads.len(), "pad", "pads")),
(None, _) => "none".into(),
});
ui.end_row();
ui.label("3D model");
ui.label(match &part.model {
Some(model) => format!("{} ({})", model.step_file, crate::panels::count(model.bodies.len(), "body", "bodies")),
None => "none".into(),
});
ui.end_row();
if let Some(symbol) = &part.symbol {
ui.label("Connection points");
ui.label(format!(
"{} in the part's `Pins` group, {}",
symbol.pins.len(),
if part.footprint.is_some() { "each at its pin's pad" } else { "at the part origin" }
));
ui.end_row();
}
});
for note in warnings.iter().chain(&part.notes) {
ui.colored_label(ui.visuals().warn_fg_color, format!("\u{2022} {note}"));
}
}