use crate::error::{KicadError, Result};
use regex::Regex;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
static SYMBOL_WRITE_LOCK: Mutex<()> = Mutex::new(());
pub struct LibraryManager {
output_path: PathBuf,
}
impl LibraryManager {
pub fn new(output_path: &Path) -> Self {
Self {
output_path: output_path.to_path_buf(),
}
}
pub fn create_directories(&self) -> Result<()> {
fs::create_dir_all(&self.output_path)
.map_err(KicadError::Io)?;
let pretty_dir = self.output_path.join("nlbn.pretty");
fs::create_dir_all(&pretty_dir)
.map_err(KicadError::Io)?;
let shapes_dir = self.output_path.join("nlbn.3dshapes");
fs::create_dir_all(&shapes_dir)
.map_err(KicadError::Io)?;
Ok(())
}
pub fn component_exists(&self, lib_path: &Path, component_name: &str) -> Result<bool> {
if !lib_path.exists() {
return Ok(false);
}
let content = fs::read_to_string(lib_path)
.map_err(KicadError::Io)?;
let v6_pattern = format!(r#"\(symbol\s+"{}""#, regex::escape(component_name));
if let Ok(re) = Regex::new(&v6_pattern) {
if re.is_match(&content) {
return Ok(true);
}
}
let v5_pattern = format!(r"DEF\s+{}\s+", regex::escape(component_name));
if let Ok(re) = Regex::new(&v5_pattern) {
if re.is_match(&content) {
return Ok(true);
}
}
Ok(false)
}
pub fn add_or_update_component(&self, lib_path: &Path, component_name: &str, component_data: &str, overwrite: bool) -> Result<()> {
let _lock = SYMBOL_WRITE_LOCK.lock().unwrap();
let exists = if lib_path.exists() {
let content = fs::read_to_string(lib_path)
.map_err(KicadError::Io)?;
let v6_pattern = format!(r#"\(symbol\s+"{}""#, regex::escape(component_name));
if let Ok(re) = Regex::new(&v6_pattern) {
re.is_match(&content)
} else {
false
}
} else {
false
};
if exists && overwrite {
self.update_component_internal(lib_path, component_name, component_data)?;
} else if !exists {
self.add_component_internal(lib_path, component_data)?;
}
Ok(())
}
fn add_component_internal(&self, lib_path: &Path, component_data: &str) -> Result<()> {
let mut content = if lib_path.exists() {
let existing = fs::read_to_string(lib_path)
.map_err(KicadError::Io)?;
existing.trim_end().trim_end_matches(')').to_string()
} else {
if component_data.contains("(symbol") {
String::from("(kicad_symbol_lib\n (version 20211014)\n (generator nlbn)")
} else {
String::from("EESchema-LIBRARY Version 2.4\n#encoding utf-8")
}
};
content.push('\n');
content.push_str(component_data);
if component_data.contains("(symbol") {
content.push('\n');
content.push(')');
}
content.push('\n');
fs::write(lib_path, content)
.map_err(KicadError::Io)?;
Ok(())
}
fn update_component_internal(&self, lib_path: &Path, component_name: &str, new_data: &str) -> Result<()> {
let content = fs::read_to_string(lib_path)
.map_err(KicadError::Io)?;
let search = format!(r#"(symbol "{}""#, component_name);
if let Some(start) = content.find(&search) {
let block_start = content[..start].rfind('(').unwrap_or(start);
let mut depth = 0;
let mut block_end = block_start;
for (i, ch) in content[block_start..].char_indices() {
match ch {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
block_end = block_start + i + 1;
break;
}
}
_ => {}
}
}
if block_end > block_start {
let mut new_content = String::with_capacity(content.len());
new_content.push_str(&content[..block_start]);
new_content.push_str(new_data);
new_content.push_str(&content[block_end..]);
fs::write(lib_path, &new_content)
.map_err(KicadError::Io)?;
return Ok(());
}
}
let v5_start = format!("DEF {} ", component_name);
if let Some(start) = content.find(&v5_start) {
if let Some(end_offset) = content[start..].find("ENDDEF") {
let block_end = start + end_offset + "ENDDEF".len();
let block_end = if content[block_end..].starts_with('\n') { block_end + 1 } else { block_end };
let mut new_content = String::with_capacity(content.len());
new_content.push_str(&content[..start]);
new_content.push_str(new_data);
new_content.push_str(&content[block_end..]);
fs::write(lib_path, &new_content)
.map_err(KicadError::Io)?;
return Ok(());
}
}
Err(KicadError::SymbolExport(format!("Component {} not found in library", component_name)).into())
}
pub fn add_component(&self, lib_path: &Path, component_data: &str) -> Result<()> {
let _lock = SYMBOL_WRITE_LOCK.lock().unwrap();
let mut content = if lib_path.exists() {
let existing = fs::read_to_string(lib_path)
.map_err(KicadError::Io)?;
existing.trim_end().trim_end_matches(')').to_string()
} else {
if component_data.contains("(symbol") {
String::from("(kicad_symbol_lib\n (version 20211014)\n (generator nlbn)")
} else {
String::from("EESchema-LIBRARY Version 2.4\n#encoding utf-8")
}
};
content.push('\n');
content.push_str(component_data);
if component_data.contains("(symbol") {
content.push('\n');
content.push(')');
}
content.push('\n');
fs::write(lib_path, content)
.map_err(KicadError::Io)?;
Ok(())
}
pub fn update_component(&self, lib_path: &Path, component_name: &str, new_data: &str) -> Result<()> {
let _lock = SYMBOL_WRITE_LOCK.lock().unwrap();
let content = fs::read_to_string(lib_path)
.map_err(KicadError::Io)?;
let search = format!(r#"(symbol "{}""#, component_name);
if let Some(start) = content.find(&search) {
let block_start = content[..start].rfind('(').unwrap_or(start);
let mut depth = 0;
let mut block_end = block_start;
for (i, ch) in content[block_start..].char_indices() {
match ch {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
block_end = block_start + i + 1;
break;
}
}
_ => {}
}
}
if block_end > block_start {
let mut new_content = String::with_capacity(content.len());
new_content.push_str(&content[..block_start]);
new_content.push_str(new_data);
new_content.push_str(&content[block_end..]);
fs::write(lib_path, &new_content)
.map_err(KicadError::Io)?;
return Ok(());
}
}
let v5_start = format!("DEF {} ", component_name);
if let Some(start) = content.find(&v5_start) {
if let Some(end_offset) = content[start..].find("ENDDEF") {
let block_end = start + end_offset + "ENDDEF".len();
let block_end = if content[block_end..].starts_with('\n') { block_end + 1 } else { block_end };
let mut new_content = String::with_capacity(content.len());
new_content.push_str(&content[..start]);
new_content.push_str(new_data);
new_content.push_str(&content[block_end..]);
fs::write(lib_path, &new_content)
.map_err(KicadError::Io)?;
return Ok(());
}
}
Err(KicadError::SymbolExport(format!("Component {} not found in library", component_name)).into())
}
pub fn write_footprint(&self, footprint_name: &str, data: &str) -> Result<PathBuf> {
let pretty_dir = self.output_path.join("nlbn.pretty");
let footprint_path = pretty_dir.join(format!("{}.kicad_mod", footprint_name));
fs::write(&footprint_path, data)
.map_err(KicadError::Io)?;
log::info!("Wrote footprint: {}", footprint_path.display());
Ok(footprint_path)
}
pub fn write_3d_model(&self, model_name: &str, wrl_data: &str, step_data: &[u8]) -> Result<(PathBuf, PathBuf)> {
let shapes_dir = self.output_path.join("nlbn.3dshapes");
let wrl_path = shapes_dir.join(format!("{}.wrl", model_name));
fs::write(&wrl_path, wrl_data)
.map_err(KicadError::Io)?;
log::info!("Wrote VRML model: {}", wrl_path.display());
let step_path = shapes_dir.join(format!("{}.step", model_name));
if !step_data.is_empty() {
fs::write(&step_path, step_data)
.map_err(KicadError::Io)?;
log::info!("Wrote STEP model: {}", step_path.display());
}
Ok((wrl_path, step_path))
}
pub fn write_wrl_model(&self, model_name: &str, wrl_data: &str) -> Result<PathBuf> {
let shapes_dir = self.output_path.join("nlbn.3dshapes");
let wrl_path = shapes_dir.join(format!("{}.wrl", model_name));
fs::write(&wrl_path, wrl_data)
.map_err(KicadError::Io)?;
log::info!("Wrote VRML model: {}", wrl_path.display());
Ok(wrl_path)
}
pub fn write_step_model(&self, model_name: &str, step_data: &[u8]) -> Result<PathBuf> {
let shapes_dir = self.output_path.join("nlbn.3dshapes");
let step_path = shapes_dir.join(format!("{}.step", model_name));
fs::write(&step_path, step_data)
.map_err(KicadError::Io)?;
log::info!("Wrote STEP model: {}", step_path.display());
Ok(step_path)
}
pub fn get_symbol_lib_path(&self, v5: bool) -> PathBuf {
if v5 {
self.output_path.join("nlbn.lib")
} else {
self.output_path.join("nlbn.kicad_sym")
}
}
}