//! A series of pre-defined layouts. These were mostly used to generate an //! editable config. use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::slice::Iter; use log::warn; use serde::{Deserialize, Serialize}; use crate::advanced::LedCode; use crate::aura_detection::LaptopLedData; use crate::error::Error; use crate::{AdvancedAuraType, AuraModeNum, AuraZone}; /// The `key_type` plays a role in effects (eventually). You could for example /// add a `ShapeType::Spacing` to pad out an effect, such as a laserbeam across /// a row so that it doesn't appear to *jump* across a gap /// /// w=1.0, h=1.0 should be considered the size of a typical key like 'A' #[derive(Debug, Deserialize, Serialize, Clone)] pub enum KeyShape { Led { width: f32, height: f32, pad_left: f32, pad_right: f32, pad_top: f32, pad_bottom: f32, }, Blank { width: f32, height: f32, }, } impl KeyShape { pub fn new_led( width: f32, height: f32, pad_left: f32, pad_right: f32, pad_top: f32, pad_bottom: f32, ) -> Self { Self::Led { width, height, pad_left, pad_right, pad_top, pad_bottom, } } pub fn new_blank(width: f32, height: f32) -> Self { Self::Blank { width, height } } /// Scale the shape up/down. Intended for use in UI on a clone pub fn scale(&mut self, scale: f32) { match self { KeyShape::Led { width, height, pad_left, pad_right, pad_top, pad_bottom, } => { *width *= scale; *height *= scale; *pad_left *= scale; *pad_right *= scale; *pad_top *= scale; *pad_bottom *= scale; } KeyShape::Blank { width, height } => { *width *= scale; *height *= scale; } } } } /// The first `Key` will determine the row height. /// /// Every row is considered to start a x=0, with the first row being y=0, /// and following rows starting after the previous `row_y + pad_top` and /// `row_x + pad_left` #[derive(Debug, Deserialize, Serialize, Clone)] pub struct KeyRow { pad_left: f32, pad_top: f32, /// The `Key` is what provides an RGB index location in the final USB /// packets row: Vec<(LedCode, String)>, /// The final data structure merged key_shapes and rows #[serde(skip)] built_row: Vec<(LedCode, KeyShape)>, } impl KeyRow { pub fn new(pad_left: f32, pad_top: f32, row: Vec<(LedCode, String)>) -> Self { Self { pad_left, pad_top, row, built_row: Default::default(), } } pub fn row(&self) -> Iter<'_, (LedCode, KeyShape)> { self.built_row.iter() } pub fn row_ref(&self) -> &[(LedCode, KeyShape)] { &self.built_row } /// Find and return the heightest height of this row pub fn height(&self) -> f32 { if self.built_row.is_empty() { return 0.0; } let mut h = 0.0; for k in &self.built_row { let height = match &k.1 { KeyShape::Led { height, pad_top, pad_bottom, .. } => height + pad_top + pad_bottom, KeyShape::Blank { height, .. } => *height, }; if h < height { h = height; } } h } /// Return the total row width pub fn width(&self) -> f32 { if self.built_row.is_empty() { return 0.0; } let mut w = 0.0; for k in &self.built_row { match &k.1 { KeyShape::Led { width, pad_left, pad_right, .. } => w += width + pad_left + pad_right, KeyShape::Blank { width, .. } => w += width, } } w } } #[derive(Debug, Deserialize, Serialize, Clone)] pub struct KeyLayout { /// Localization of this keyboard layout locale: String, /// The shapes of keys used key_shapes: HashMap, /// The rows of keys of this layout key_rows: Vec, /// Should be copied from the `LaptopLedData` as laptops may have the same /// layout, but different EC features #[serde(skip)] basic_modes: Vec, /// Should be copied from the `LaptopLedData` as laptops may have the same /// layout, but different EC features #[serde(skip)] basic_zones: Vec, /// Paired with the key selection in UI. Determines if individual keys are /// selectable, zones, or single zone. /// /// Should be copied from the `LaptopLedData` as laptops may have the same /// layout, but different EC features. #[serde(skip)] advanced_type: AdvancedAuraType, } impl KeyLayout { pub fn from_file(path: &Path) -> Result { let buf: String = std::fs::read_to_string(path) .map_err(|e| Error::IoPath(path.to_string_lossy().to_string(), e))?; if buf.is_empty() { Err(Error::IoPath( path.to_string_lossy().to_string(), std::io::ErrorKind::InvalidData.into(), )) } else { let mut data = ron::from_str::(&buf)?; let mut unused = HashSet::new(); for k in data.key_shapes.keys() { unused.insert(k); } let rows = &mut data.key_rows; for row in rows { for k in &mut row.row { if let Some(shape) = data.key_shapes.get(&k.1) { row.built_row.push((k.0, shape.clone())); unused.remove(&k.1); } else { warn!("Key {:?} was missing matching shape {}", k.0, k.1); } } } if !unused.is_empty() { warn!("The layout {path:?} had unused shapes {unused:?}",); } Ok(data) } } pub fn rows(&self) -> Iter<'_, KeyRow> { self.key_rows.iter() } pub fn rows_ref(&self) -> &[KeyRow] { &self.key_rows } pub fn basic_modes(&self) -> &[AuraModeNum] { &self.basic_modes } pub fn basic_zones(&self) -> &[AuraZone] { &self.basic_zones } pub fn advanced_type(&self) -> &AdvancedAuraType { &self.advanced_type } /// Find the total heighht of the keyboard, not including lightbar rows pub fn keyboard_height(&self) -> f32 { let mut height = 0.0; for r in &self.key_rows { if let Some(key) = r.row.first() { if !key.0.is_lightbar_zone() { height += r.height(); } } } height } pub fn max_height(&self) -> f32 { let mut height = 0.0; for r in &self.key_rows { height += r.height(); } height } pub fn max_width(&self) -> f32 { let mut width = 0.0; for r in &self.key_rows { let tmp = r.width(); if width < tmp { width = tmp; } } width } /// Find a layout matching the name in `LaptopLedData` in the provided dir pub fn find_layout(led_data: LaptopLedData, mut data_path: PathBuf) -> Result { // TODO: locales let layout_name = if led_data.layout_name.is_empty() { "ga401q".to_owned() // Need some sort of default here due to ROGCC // expecting it } else { led_data.layout_name }; let layout_file = format!("{layout_name}_US.ron"); data_path.push("layouts"); data_path.push(layout_file); let path = data_path.as_path(); let mut tmp = KeyLayout::from_file(path)?; tmp.basic_modes = led_data.basic_modes; tmp.basic_zones = led_data.basic_zones; tmp.advanced_type = led_data.advanced_type; Ok(tmp) } pub fn layout_files(mut data_path: PathBuf) -> Result, Error> { data_path.push("layouts"); let path = data_path.as_path(); let mut files = Vec::new(); std::fs::read_dir(path) .map_err(|e| { println!("{:?}, {e}", path); e }) .unwrap() .for_each(|p| { if let Ok(p) = p { files.push(p.path()); } }); Ok(files) } } impl KeyLayout { pub fn default_layout() -> Self { Self { locale: "US".to_owned(), basic_modes: vec![ AuraModeNum::Static, AuraModeNum::Breathe, AuraModeNum::Pulse, ], basic_zones: vec![AuraZone::None], advanced_type: AdvancedAuraType::None, key_shapes: HashMap::from([( "regular".to_owned(), KeyShape::new_led(1.0, 1.0, 0.1, 0.1, 0.1, 0.1), )]), key_rows: vec![ KeyRow::new( 0.1, 0.1, vec![ (LedCode::Esc, "regular".to_owned()), (LedCode::F1, "regular".to_owned()), (LedCode::F2, "regular".to_owned()), (LedCode::F3, "regular".to_owned()), (LedCode::F4, "regular".to_owned()), // not sure which key to put here (LedCode::F5, "regular".to_owned()), (LedCode::F6, "regular".to_owned()), (LedCode::F7, "regular".to_owned()), (LedCode::F8, "regular".to_owned()), (LedCode::F9, "regular".to_owned()), (LedCode::F10, "regular".to_owned()), (LedCode::F11, "regular".to_owned()), (LedCode::F12, "regular".to_owned()), ], ), KeyRow::new( 0.1, 0.1, vec![ (LedCode::Tilde, "regular".to_owned()), (LedCode::N1, "regular".to_owned()), (LedCode::N2, "regular".to_owned()), (LedCode::N3, "regular".to_owned()), (LedCode::N4, "regular".to_owned()), (LedCode::N5, "regular".to_owned()), (LedCode::N6, "regular".to_owned()), (LedCode::N7, "regular".to_owned()), (LedCode::N8, "regular".to_owned()), (LedCode::N9, "regular".to_owned()), (LedCode::N0, "regular".to_owned()), (LedCode::Hyphen, "regular".to_owned()), (LedCode::Equals, "regular".to_owned()), (LedCode::Backspace, "regular".to_owned()), ], ), KeyRow::new( 0.1, 0.1, vec![ (LedCode::Tab, "regular".to_owned()), (LedCode::Q, "regular".to_owned()), (LedCode::W, "regular".to_owned()), (LedCode::E, "regular".to_owned()), (LedCode::R, "regular".to_owned()), (LedCode::T, "regular".to_owned()), (LedCode::Y, "regular".to_owned()), (LedCode::U, "regular".to_owned()), (LedCode::I, "regular".to_owned()), (LedCode::O, "regular".to_owned()), (LedCode::P, "regular".to_owned()), (LedCode::LBracket, "regular".to_owned()), (LedCode::RBracket, "regular".to_owned()), (LedCode::BackSlash, "regular".to_owned()), ], ), KeyRow::new( 0.1, 0.1, vec![ (LedCode::Caps, "regular".to_owned()), (LedCode::A, "regular".to_owned()), (LedCode::S, "regular".to_owned()), (LedCode::D, "regular".to_owned()), (LedCode::F, "regular".to_owned()), (LedCode::G, "regular".to_owned()), (LedCode::H, "regular".to_owned()), (LedCode::J, "regular".to_owned()), (LedCode::K, "regular".to_owned()), (LedCode::L, "regular".to_owned()), (LedCode::SemiColon, "regular".to_owned()), (LedCode::Quote, "regular".to_owned()), (LedCode::Return, "regular".to_owned()), ], ), KeyRow::new( 0.1, 0.1, vec![ (LedCode::LShift, "regular".to_owned()), (LedCode::Z, "regular".to_owned()), (LedCode::X, "regular".to_owned()), (LedCode::C, "regular".to_owned()), (LedCode::V, "regular".to_owned()), (LedCode::B, "regular".to_owned()), (LedCode::N, "regular".to_owned()), (LedCode::M, "regular".to_owned()), (LedCode::Comma, "regular".to_owned()), (LedCode::Period, "regular".to_owned()), (LedCode::FwdSlash, "regular".to_owned()), (LedCode::Rshift, "regular".to_owned()), ], ), KeyRow::new( 0.1, 0.1, vec![ (LedCode::LCtrl, "regular".to_owned()), (LedCode::LFn, "regular".to_owned()), (LedCode::Meta, "regular".to_owned()), (LedCode::LAlt, "regular".to_owned()), (LedCode::Spacebar, "regular".to_owned()), (LedCode::RAlt, "regular".to_owned()), (LedCode::PrtSc, "regular".to_owned()), (LedCode::RCtrl, "regular".to_owned()), ], ), ], } } } #[cfg(test)] mod tests { use std::collections::HashSet; use std::fs::{self, OpenOptions}; use std::io::Read; use std::path::PathBuf; use crate::aura_detection::LedSupportFile; use crate::layouts::KeyLayout; #[test] fn check_parse_all() { const DATA_DIR: &str = env!("CARGO_MANIFEST_DIR"); let mut data_path = PathBuf::from(DATA_DIR); data_path.push("data"); data_path.push("layouts"); let path = data_path.as_path(); for p in fs::read_dir(path) .map_err(|e| { println!("{:?}, {e}", path); e }) .unwrap() { let mut buf = std::fs::read_to_string(p.unwrap().path()).unwrap(); let data: KeyLayout = ron::from_str(&buf).unwrap(); let mut unused = HashSet::new(); for k in data.key_shapes.keys() { unused.insert(k); } let rows = &data.key_rows; for row in rows { for k in &row.row { if data.key_shapes.get(&k.1).is_some() { unused.remove(&k.1); } else { panic!("Key {:?} was missing matching shape {}", k.0, k.1); } } } assert!( unused.is_empty(), "The layout {path:?} had unused shapes {unused:?}", ); buf.clear(); } // println!( // "RON: {}", // ron::ser::to_string_pretty(&tmp, // PrettyConfig::new().depth_limit(4)).unwrap() ); // let mut data = PathBuf::from(env!("CARGO_MANIFEST_DIR")); // data.push("data/aura-support2.json"); // let mut file = // OpenOptions::new().write(true).create(true).truncate(true).open(& // data).unwrap(); file.write_all(json.as_bytes()).unwrap(); } #[test] fn check_layout_file_links() { const DATA_DIR: &str = env!("CARGO_MANIFEST_DIR"); let mut data_path = PathBuf::from(DATA_DIR); data_path.push("data"); data_path.push("aura_support.ron"); let mut buf = std::fs::read_to_string(&data_path).unwrap(); let data: LedSupportFile = ron::from_str(&buf).unwrap(); data_path.pop(); data_path.push("layouts"); data_path.push("loop_prep"); for config in data.get().iter().rev() { buf.clear(); let layout_file = format!("{}_US.ron", config.layout_name); data_path.pop(); data_path.push(&layout_file); let mut file = OpenOptions::new() .read(true) .open(&data_path) .map_err(|e| { panic!( "Error checking {data_path:?} for {} : {e:?}", config.board_name ) }) .unwrap(); #[allow(clippy::verbose_file_reads)] if let Err(e) = file.read_to_string(&mut buf) { panic!( "Error checking {data_path:?} for {} : {e:?}", config.board_name ) } if let Err(e) = ron::from_str::(&buf) { panic!("Error checking {data_path:?} : {e:?}") } } } }