use std::collections::{HashMap, HashSet}; use std::env; use std::fmt::{Display, Formatter}; use std::fs::File; use std::io::Write; use std::path::{Path, PathBuf}; fn main() { // Put `memory.x` in our output directory and ensure it's // on the linker search path. let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); File::create(out.join("memory.x")) .unwrap() .write_all(include_bytes!("memory.x")) .unwrap(); println!("cargo:rustc-link-search={}", out.display()); // By default, Cargo will re-run a build script whenever // any file in the project changes. By specifying `memory.x` // here, we ensure the build script is only re-run when // `memory.x` is changed. println!("cargo:rerun-if-changed=memory.x"); println!("cargo:rustc-link-arg-bins=--nmagic"); println!("cargo:rustc-link-arg-bins=-Tlink.x"); println!("cargo:rustc-link-arg-bins=-Tlink-rp.x"); println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); let client = reqwest::blocking::Client::new(); let res = if Path::new("./target/keycodes.h").exists() { std::fs::read_to_string("./target/keycodes.h").unwrap() } else { let res = client .get("https://raw.githubusercontent.com/qmk/qmk_firmware/master/quantum/keycodes.h") .send() .unwrap() .text() .unwrap(); std::fs::write("./target/keycodes.h", res.as_bytes()).unwrap(); res }; let mut log_file = std::fs::OpenOptions::new() .create(true) .append(false) .write(true) .open("./target/tokens.log") .unwrap(); let mut global_register = HashMap::with_capacity(9086); let mut enums = Vec::with_capacity(64); let iter = res.lines(); let mut is_block_comment = false; for line in iter { parse_line( line, &mut enums, &mut is_block_comment, &mut global_register, &mut log_file, ); } let mut mod_file = std::fs::OpenOptions::new() .create(true) .append(false) .write(true) .open("./src/keycodes/mod.rs") .unwrap(); for info in enums { mod_file.write_fmt(format_args!("{info}\n")).unwrap(); mod_file.flush().unwrap(); } } fn parse_line( line: &str, enums: &mut Vec, is_block_comment: &mut bool, global_register: &mut HashMap, log_file: &mut File, ) { log_file.write_fmt(format_args!("Line {line:?}\n")).unwrap(); log_file.flush().unwrap(); let line = line.trim(); let mut it = line .split_ascii_whitespace() .filter(|s| !s.is_empty()) .peekable(); while let Some(token) = it.next().map(ToOwned::to_owned) { log_file .write_fmt(format_args!("token {token:?}\n")) .unwrap(); log_file.flush().unwrap(); println!("token {token:?}"); match token.as_str() { _ if token.starts_with("#") => { return; } "//" => { log_file .write_fmt(format_args!("skip {token:?}\n")) .unwrap(); log_file.flush().unwrap(); return; } "enum" => { log_file .write_fmt(format_args!("new enum {token:?}\n")) .unwrap(); log_file.flush().unwrap(); enums.push(EnumInfo::new( it.next().expect("enum must have name").to_owned(), )); } "{" => {} "," => {} "}" | "};" => {} _ if token.starts_with("/*") && token.ends_with("*/") => { println!("closed comment"); } _ if token.ends_with("*/") => { *is_block_comment = false; } _ if *is_block_comment || token.starts_with("/*") => { *is_block_comment = true; println!("comment {token:?}"); log_file .write_fmt(format_args!("comment {token:?}\n")) .unwrap(); log_file.flush().unwrap(); while let Some(t) = it.next() { println!("looking for \"*/\" : {t:?}"); log_file .write_fmt(format_args!("looking for \"*/\" : {t:?}\n")) .unwrap(); log_file.flush().unwrap(); if t.ends_with("*/") { println!("ends looking for closing comment with {t:?}"); *is_block_comment = false; break; } } } _ => { if enums.is_empty() { return; } let name = token.as_str(); println!("new variant name is {name:?}"); it.next().expect("variant must be followed by ="); let val = it.next().expect("variant must have value").trim_end_matches(","); enums .last_mut() .expect("must have enum token before here") .add_variant(name, val, global_register); } } } } pub type VariantName = String; pub type VariantValue = String; pub type EnumName = String; #[derive(Debug)] pub struct EnumInfo { pub name: String, pub variants: HashMap, pub values_cache: HashSet, } impl Display for EnumInfo { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str("#[derive(Debug)]\n")?; f.write_str("#[repr(C)]\n")?; f.write_str("pub enum ")?; f.write_str(Self::rust_enum_name(&self.name).as_str())?; f.write_str(" {\n")?; for (var_name, var_val) in &self.variants { f.write_fmt(format_args!( " {} = {},\n", Self::rust_enum_name(var_name), var_val ))?; } f.write_str("}")?; Ok(()) } } impl EnumInfo { const CACHE_SIZE: usize = 1024 * 5; pub fn new(name: String) -> EnumInfo { Self { name, variants: HashMap::with_capacity(Self::CACHE_SIZE), values_cache: HashSet::with_capacity(Self::CACHE_SIZE), } } pub fn add_variant<'global>( &mut self, name: &str, val: &str, global_register: &'global mut HashMap, ) { println!("adding variant {name:?} {val:?}"); if self.values_cache.contains(val) { return; } self.variants.insert( name.to_owned(), global_register .get(val) .map(|enum_name| format!("{enum_name}::{val} as usize")) .unwrap_or_else(|| val.to_owned()), ); global_register.insert(name.to_owned(), self.name.clone()); } fn rust_enum_name(name: &str) -> String { let mut prev_underscore = false; name.chars() .enumerate() .filter_map(|(idx, c)| { if idx == 0 { Some(c.to_ascii_uppercase()) } else if c == '_' { prev_underscore = true; None } else if prev_underscore { prev_underscore = false; Some(c.to_ascii_uppercase()) } else { Some(c) } }) .collect() } }