use crate::{
InputDeviceConfig, KeyboardTomlConfig, LayoutTomlConfig, MatrixConfig, MatrixType, SplitConfig, SplitConnection,
};
#[derive(Clone, Debug)]
#[allow(clippy::large_enum_variant)]
pub enum BoardConfig {
Split(SplitConfig),
UniBody(UniBodyConfig),
}
#[derive(Clone, Debug, Default)]
pub struct UniBodyConfig {
pub matrix: MatrixConfig,
pub input_device: InputDeviceConfig,
}
impl Default for BoardConfig {
fn default() -> Self {
BoardConfig::UniBody(UniBodyConfig::default())
}
}
impl BoardConfig {
pub fn get_num_peripheral(&self) -> usize {
match self {
BoardConfig::Split(split_config) => split_config.peripheral.len(),
BoardConfig::UniBody(_) => 0,
}
}
pub fn get_num_encoder(&self) -> Vec<usize> {
let mut num_encoder = Vec::new();
match self {
BoardConfig::Split(split) => {
num_encoder.push(
split
.central
.input_device
.clone()
.unwrap_or_default()
.encoder
.unwrap_or(Vec::new())
.len(),
);
for peri in &split.peripheral {
num_encoder.push(
peri.input_device
.clone()
.unwrap_or_default()
.encoder
.unwrap_or(Vec::new())
.len(),
);
}
}
BoardConfig::UniBody(uni_body_config) => {
num_encoder.push(uni_body_config.input_device.encoder.clone().unwrap_or(Vec::new()).len());
}
};
num_encoder
}
}
impl KeyboardTomlConfig {
pub(crate) fn total_encoders(&self) -> usize {
let count =
|device: &Option<InputDeviceConfig>| device.as_ref().and_then(|d| d.encoder.as_ref()).map_or(0, Vec::len);
match &self.split {
Some(split) => {
count(&split.central.input_device)
+ split.peripheral.iter().map(|p| count(&p.input_device)).sum::<usize>()
}
None => count(&self.input_device),
}
}
}
fn validate_matrix_dims(matrix: &MatrixConfig, rows: usize, cols: usize, ctx: &str) -> Result<(), String> {
match matrix.matrix_type {
MatrixType::Normal => {
let row_pins = matrix.row_pins.as_ref().map_or(0, |v| v.len());
let col_pins = matrix.col_pins.as_ref().map_or(0, |v| v.len());
if row_pins != rows {
return Err(format!(
"keyboard.toml: {ctx} has {row_pins} row_pins but rows = {rows}"
));
}
if col_pins != cols {
return Err(format!(
"keyboard.toml: {ctx} has {col_pins} col_pins but cols = {cols}"
));
}
}
MatrixType::DirectPin => {
let direct = matrix.direct_pins.as_ref();
let n_rows = direct.map_or(0, |v| v.len());
if n_rows != rows {
return Err(format!(
"keyboard.toml: {ctx} direct_pins has {n_rows} rows but rows = {rows}"
));
}
if let Some(direct) = direct {
for (r, row) in direct.iter().enumerate() {
if row.len() != cols {
return Err(format!(
"keyboard.toml: {ctx} direct_pins row {r} has {} pins but cols = {cols}",
row.len()
));
}
}
}
}
}
Ok(())
}
fn validate_split_config(split: &SplitConfig, layout: Option<&LayoutTomlConfig>) -> Result<(), String> {
let boards = || {
std::iter::once((&split.central, "[split.central]".to_string())).chain(
split
.peripheral
.iter()
.enumerate()
.map(|(i, p)| (p, format!("[[split.peripheral]] #{i}"))),
)
};
match split.connection {
SplitConnection::Serial => {
for (board, ctx) in boards() {
if board.ble_addr.is_some() {
return Err(format!(
"keyboard.toml: {ctx} sets `ble_addr`, but split.connection = \"serial\""
));
}
}
let central_ports = split.central.serial.as_ref().map_or(0, |s| s.len());
if central_ports < split.peripheral.len() {
return Err(format!(
"keyboard.toml: [split.central] defines {central_ports} serial port(s) for {} peripheral(s) — one port per peripheral is required, in peripheral order",
split.peripheral.len()
));
}
for (i, peri) in split.peripheral.iter().enumerate() {
let n = peri.serial.as_ref().map_or(0, |s| s.len());
if n != 1 {
return Err(format!(
"keyboard.toml: [[split.peripheral]] #{i} must define exactly 1 serial port, got {n}"
));
}
}
}
SplitConnection::Ble => {
for (board, ctx) in boards() {
if board.serial.is_some() {
return Err(format!(
"keyboard.toml: {ctx} sets `serial`, but split.connection = \"ble\""
));
}
}
}
}
let regions: Vec<_> = boards()
.map(|(b, ctx)| {
(
b.row_offset,
b.row_offset + b.rows,
b.col_offset,
b.col_offset + b.cols,
ctx,
)
})
.collect();
if let Some(layout) = layout {
let (rows, cols) = (layout.rows as usize, layout.cols as usize);
for (r0, r1, c0, c1, ctx) in ®ions {
if *r1 > rows || *c1 > cols {
return Err(format!(
"keyboard.toml: {ctx} occupies rows {r0}..{r1}, cols {c0}..{c1}, which exceeds [layout] ({rows} rows x {cols} cols)"
));
}
}
}
for (i, a) in regions.iter().enumerate() {
for b in ®ions[i + 1..] {
if a.0 < b.1 && b.0 < a.1 && a.2 < b.3 && b.2 < a.3 {
return Err(format!(
"keyboard.toml: {} and {} overlap in the unified matrix — adjust row_offset/col_offset",
a.4, b.4
));
}
}
}
Ok(())
}
impl KeyboardTomlConfig {
pub(crate) fn get_board_config(&self) -> Result<BoardConfig, String> {
let matrix = self.matrix.clone();
let split = self.split.clone();
let input_device = self.input_device.clone();
match (matrix, split) {
(None, Some(s)) => {
validate_split_config(&s, self.layout.as_ref())?;
validate_matrix_dims(&s.central.matrix, s.central.rows, s.central.cols, "[split.central]")?;
for (i, peri) in s.peripheral.iter().enumerate() {
validate_matrix_dims(&peri.matrix, peri.rows, peri.cols, &format!("[[split.peripheral]] #{i}"))?;
}
Ok(BoardConfig::Split(s))
},
(Some(m), None) => {
match m.matrix_type {
MatrixType::Normal => {
if m.row_pins.is_none() || m.col_pins.is_none() {
return Err("`row_pins` and `col_pins` is required for normal matrix".to_string());
}
},
MatrixType::DirectPin => {
if m.direct_pins.is_none() {
return Err("`direct_pins` is required for direct pin matrix".to_string());
}
},
}
if let Some(layout) = &self.layout {
validate_matrix_dims(&m, layout.rows as usize, layout.cols as usize, "[matrix]")?;
}
Ok(BoardConfig::UniBody(UniBodyConfig{matrix: m, input_device: input_device.unwrap_or_default()}))
},
(None, None) => Err("[matrix] section in keyboard.toml is required for non-split keyboard".to_string()),
_ => Err("Use at most one of [matrix] or [split] in your keyboard.toml!\n-> [matrix] is used to define a normal matrix of non-split keyboard\n-> [split] is used to define a split keyboard\n".to_string()),
}
}
}
#[cfg(test)]
mod tests {
use super::{validate_matrix_dims, validate_split_config};
use crate::{
LayoutTomlConfig, MatrixConfig, MatrixType, SerialConfig, SplitBoardConfig, SplitConfig, SplitConnection,
};
fn normal(rows: &[&str], cols: &[&str]) -> MatrixConfig {
MatrixConfig {
matrix_type: MatrixType::Normal,
row_pins: Some(rows.iter().map(|s| s.to_string()).collect()),
col_pins: Some(cols.iter().map(|s| s.to_string()).collect()),
..Default::default()
}
}
#[test]
fn normal_matrix_dims_must_match_pins() {
let m = normal(&["r0", "r1"], &["c0", "c1", "c2"]);
assert!(validate_matrix_dims(&m, 2, 3, "[matrix]").is_ok());
assert!(validate_matrix_dims(&m, 3, 3, "[matrix]").is_err()); assert!(validate_matrix_dims(&m, 2, 2, "[matrix]").is_err()); }
#[test]
fn direct_pin_dims_must_match_grid() {
let m = MatrixConfig {
matrix_type: MatrixType::DirectPin,
direct_pins: Some(vec![vec!["a".into(), "b".into()], vec!["c".into(), "d".into()]]),
..Default::default()
};
assert!(validate_matrix_dims(&m, 2, 2, "[matrix]").is_ok());
assert!(validate_matrix_dims(&m, 3, 2, "[matrix]").is_err());
assert!(validate_matrix_dims(&m, 2, 3, "[matrix]").is_err());
}
#[test]
fn direct_pin_rejects_jagged_rows() {
let m = MatrixConfig {
matrix_type: MatrixType::DirectPin,
direct_pins: Some(vec![vec!["a".into(), "b".into()], vec!["c".into()]]),
..Default::default()
};
assert!(validate_matrix_dims(&m, 2, 2, "[matrix]").is_err());
}
fn board(rows: usize, cols: usize, row_offset: usize, col_offset: usize) -> SplitBoardConfig {
SplitBoardConfig {
rows,
cols,
row_offset,
col_offset,
..Default::default()
}
}
fn ble_split() -> SplitConfig {
SplitConfig {
connection: SplitConnection::Ble,
central: board(2, 2, 0, 0),
peripheral: vec![board(2, 1, 2, 2)],
}
}
fn layout_4x3() -> LayoutTomlConfig {
LayoutTomlConfig {
rows: 4,
cols: 3,
map: None,
default_variant: None,
shapes: None,
variant: None,
}
}
#[test]
fn ble_split_without_addr_is_valid() {
assert!(validate_split_config(&ble_split(), Some(&layout_4x3())).is_ok());
}
#[test]
fn transport_must_match_connection() {
let mut split = ble_split();
split.peripheral[0].serial = Some(vec![SerialConfig::default()]);
let err = validate_split_config(&split, None).unwrap_err();
assert!(err.contains("sets `serial`"), "{err}");
let mut split = ble_split();
split.connection = SplitConnection::Serial;
split.central.serial = Some(vec![SerialConfig::default()]);
split.peripheral[0].serial = Some(vec![SerialConfig::default()]);
split.peripheral[0].ble_addr = Some([0; 6]);
let err = validate_split_config(&split, None).unwrap_err();
assert!(err.contains("sets `ble_addr`"), "{err}");
}
#[test]
fn serial_ports_must_cover_peripherals() {
let mut split = ble_split();
split.connection = SplitConnection::Serial;
split.central.serial = Some(vec![SerialConfig::default()]);
split.peripheral = vec![board(2, 1, 2, 2), board(2, 1, 2, 0)];
split.peripheral[0].serial = Some(vec![SerialConfig::default()]);
split.peripheral[1].serial = Some(vec![SerialConfig::default()]);
let err = validate_split_config(&split, None).unwrap_err();
assert!(err.contains("1 serial port(s) for 2 peripheral(s)"), "{err}");
split.central.serial = Some(vec![SerialConfig::default(); 3]);
assert!(validate_split_config(&split, None).is_ok());
}
#[test]
fn peripheral_needs_exactly_one_serial_port() {
let mut split = ble_split();
split.connection = SplitConnection::Serial;
split.central.serial = Some(vec![SerialConfig::default()]);
let err = validate_split_config(&split, None).unwrap_err();
assert!(err.contains("exactly 1 serial port, got 0"), "{err}");
}
#[test]
fn regions_must_fit_layout() {
let mut split = ble_split();
split.peripheral[0].row_offset = 3; let err = validate_split_config(&split, Some(&layout_4x3())).unwrap_err();
assert!(err.contains("exceeds [layout]"), "{err}");
}
#[test]
fn overlapping_regions_are_rejected() {
let mut split = ble_split();
split.peripheral.push(board(2, 1, 2, 2)); let err = validate_split_config(&split, Some(&layout_4x3())).unwrap_err();
assert!(err.contains("overlap"), "{err}");
}
}