kle_serial/lib.rs
1#![warn(missing_docs, dead_code)]
2#![warn(clippy::all, clippy::pedantic, clippy::cargo)]
3
4//! A Rust library for deserialising [Keyboard Layout Editor] files. Designed to be used in
5//! conjunction with [`serde_json`] to deserialize JSON files exported from KLE.
6//!
7//! # Example
8//!
9//! ![example]
10//!
11//! ```
12//! // kle_serial::Keyboard uses f64 coordinates by default. If you need f32 coordinates use
13//! // kle_serial::Keyboard<f32> or kle_serial::f32::Keyboard instead.
14//! use kle_serial::Keyboard;
15//!
16//! let keyboard: Keyboard = serde_json::from_str(
17//! r#"[
18//! {"name": "example"},
19//! [{"f": 4}, "!\n1\n¹\n¡"]
20//! ]"#
21//! ).unwrap();
22//!
23//! assert_eq!(keyboard.metadata.name, "example");
24//! assert_eq!(keyboard.keys.len(), 1);
25//!
26//! assert!(keyboard.keys[0].legends[0].is_some());
27//! let legend = keyboard.keys[0].legends[0].as_ref().unwrap();
28//!
29//! assert_eq!(legend.text, "!");
30//! assert_eq!(legend.size, 4);
31//!
32//! assert!(keyboard.keys[0].legends[1].is_none());
33//! ```
34//!
35//! [Keyboard Layout Editor]: http://www.keyboard-layout-editor.com/
36//! [`serde_json`]: https://crates.io/crates/serde_json
37//! [example]: https://raw.githubusercontent.com/staticintlucas/kle-serial-rs/main/doc/example.png
38
39mod de;
40pub mod f32;
41pub mod f64;
42mod utils;
43
44use num_traits::real::Real;
45use serde::Deserialize;
46
47use de::{KleKeyboard, KleLayoutIterator};
48use utils::FontSize;
49
50/// Colour type used for deserialising. Type alias of [`rgb::RGBA8`].
51pub type Color = rgb::RGBA8;
52
53const NUM_LEGENDS: usize = 12; // Number of legends on a key
54
55pub(crate) mod color {
56 use crate::Color;
57
58 pub(crate) const BACKGROUND: Color = Color::new(0xEE, 0xEE, 0xEE, 0xFF); // #EEEEEE
59 pub(crate) const KEY: Color = Color::new(0xCC, 0xCC, 0xCC, 0xFF); // #CCCCCC
60 pub(crate) const LEGEND: Color = Color::new(0x00, 0x00, 0x00, 0xFF); // #000000
61}
62
63/// A struct representing a single legend.
64///
65/// <div class="warning">
66///
67/// This is also referred to as a `label` in the official TypeScript [`kle-serial`] library as well
68/// as some others. It is named `Legend` here to follow the more prevalent terminology and to match
69/// KLE's own UI.
70///
71/// [`kle-serial`]: https://github.com/ijprest/kle-serial
72///
73/// </div>
74#[derive(Debug, Clone, PartialEq)]
75pub struct Legend {
76 /// The legend's text.
77 pub text: String,
78 /// The legend size (in KLE's font size unit). KLE clamps this to the range `1..=9`.
79 pub size: usize,
80 /// The legend colour.
81 pub color: Color,
82}
83
84impl Default for Legend {
85 fn default() -> Self {
86 Self {
87 text: String::default(),
88 size: usize::from(FontSize::default()),
89 color: color::LEGEND,
90 }
91 }
92}
93
94/// A struct representing a key switch.
95#[derive(Debug, Clone, Default, PartialEq)]
96pub struct Switch {
97 /// The switch mount. Typically either `"cherry"` or `"alps"`.
98 pub mount: String,
99 /// The switch brand. KLE uses lowercase brand names.
100 pub brand: String,
101 /// The switch type. KLE uses either part number or colour depending on the brand.
102 pub typ: String,
103}
104
105/// A struct representing a single key.
106#[derive(Debug, Clone, PartialEq)]
107#[allow(clippy::struct_excessive_bools)]
108pub struct Key<T = f64>
109where
110 T: Real,
111{
112 /// The key's legends. This array is indexed in left to right, top to bottom order as shown in
113 /// the image below.
114 ///
115 /// ![alignment]
116 ///
117 /// Legends that are empty in KLE will be deserialised as [`None`].
118 ///
119 /// [alignment]: https://raw.githubusercontent.com/staticintlucas/kle-serial-rs/main/doc/alignment.png
120 pub legends: [Option<Legend>; NUM_LEGENDS],
121 /// The colour of the key
122 pub color: Color,
123 /// The X position of the key measured in keyboard units (typically 19.05 mm or 0.75 in).
124 ///
125 /// <div class="warning">
126 ///
127 /// KLE has some strange behaviour when it comes to positioning stepped and L-shaped keys.
128 /// The 'true' X position of the top left corner will be less if the key's `x2` field is
129 /// negative.
130 ///
131 /// The actual position of the top left corner can be found using:
132 ///
133 /// ```
134 /// # let key = kle_serial::Key::<f64>::default();
135 /// let x = key.x.min(key.x + key.x2);
136 /// ```
137 ///
138 /// This behaviour can be observed by placing an ISO enter in the top left corner in KLE;
139 /// `x` is 0.25 and `x2` is −0.25.
140 ///
141 /// </div>
142 pub x: T,
143 /// The Y position of the key measured in keyboard units (typically 19.05 mm or 0.75 in).
144 ///
145 /// <div class="warning">
146 ///
147 /// KLE has some strange behaviour when it comes to positioning stepped and L-shaped keys.
148 /// The 'true' Y position of the top left corner will be less if the key's `y2` field is
149 /// negative.
150 ///
151 /// The actual position of the top left corner can be found using:
152 ///
153 /// ```
154 /// # let key = kle_serial::Key::<f64>::default();
155 /// let y = key.y.min(key.y + key.y2);
156 /// ```
157 ///
158 /// This behaviour can be observed by placing an ISO enter in the top left corner in KLE;
159 /// `x` is 0.25 and `x2` is −0.25.
160 ///
161 /// </div>
162 pub y: T,
163 /// The width of the key measured in keyboard units (typically 19.05 mm or 0.75 in).
164 pub width: T,
165 /// The height of the key measured in keyboard units (typically 19.05 mm or 0.75 in).
166 pub height: T,
167 /// The relative X position of a stepped or L-shaped part of the key. Measured in keyboard units
168 /// (typically 19.05 mm or 0.75 in).
169 ///
170 /// This is set to `0.0` for regular keys, but is used for stepped caps lock and ISO enter keys,
171 /// amongst others.
172 pub x2: T,
173 /// The relative Y position of a stepped or L-shaped part of the key. Measured in keyboard units
174 /// (typically 19.05 mm or 0.75 in).
175 ///
176 /// This is set to `0.0` for regular keys, but is used for stepped caps lock and ISO enter keys,
177 /// amongst others.
178 pub y2: T,
179 /// The width of a stepped or L-shaped part of the key. Measured in keyboard units (typically
180 /// 19.05 mm or 0.75 in).
181 ///
182 /// This is equal to `width` for regular keys, but is used for stepped caps lock and ISO
183 /// enter keys, amongst others.
184 pub width2: T,
185 /// The height of a stepped or L-shaped part of the key. Measured in keyboard units (typically
186 /// 19.05 mm or 0.75 in).
187 ///
188 /// This is equal to `height` for regular keys, but is used for stepped caps lock and ISO
189 /// enter keys, amongst others.
190 pub height2: T,
191 /// The rotation of the key in degrees. Positive rotation values are clockwise.
192 pub rotation: T,
193 /// The X coordinate for the centre of rotation of the key. Measured in keyboard units
194 /// (typically 19.05 mm or 0.75 in) from the top left corner of the layout.
195 pub rx: T,
196 /// The Y coordinate for the centre of rotation of the key. Measured in keyboard units
197 /// (typically 19.05 mm or 0.75 in) from the top left corner of the layout.
198 pub ry: T,
199 /// The keycap profile and row number of the key.
200 ///
201 /// KLE uses special rendering for `"SA"`, `"DSA"`, `"DCS"`, `"OEM"`, `"CHICKLET"`, and `"FLAT"`
202 /// profiles. It expects the row number to be one of `"R1"`, `"R2"`, `"R3"`, `"R4"`, `"R5"`, or
203 /// `"SPACE"`, although it only uses special rendering for `"SPACE"`.
204 ///
205 /// KLE suggests the format `"<profile> [<row>]"`, but it will recognise any string containing
206 /// one of its supported profiles and/or rows. Any value is considered valid, but empty or
207 /// unrecognised values are rendered using the unnamed default profile.
208 pub profile: String,
209 /// The key switch.
210 pub switch: Switch,
211 /// Whether the key is ghosted.
212 pub ghosted: bool,
213 /// Whether the key is stepped.
214 pub stepped: bool,
215 /// Whether this is a homing key.
216 pub homing: bool,
217 /// Whether this is a decal.
218 pub decal: bool,
219}
220
221impl<T> Default for Key<T>
222where
223 T: Real,
224{
225 fn default() -> Self {
226 Self {
227 legends: std::array::from_fn(|_| None),
228 color: color::KEY,
229 x: T::zero(),
230 y: T::zero(),
231 width: T::one(),
232 height: T::one(),
233 x2: T::zero(),
234 y2: T::zero(),
235 width2: T::one(),
236 height2: T::one(),
237 rotation: T::zero(),
238 rx: T::zero(),
239 ry: T::zero(),
240 profile: String::new(),
241 switch: Switch::default(),
242 ghosted: false,
243 stepped: false,
244 homing: false,
245 decal: false,
246 }
247 }
248}
249
250/// The background style of a KLE layout.
251#[derive(Debug, Clone, Default, PartialEq)]
252pub struct Background {
253 /// The name of the background.
254 ///
255 /// When generated by KLE, this is the same as the name shown in the dropdown menu, for example
256 /// `"Carbon fibre 1"`.
257 pub name: String,
258 /// The CSS style of the background.
259 ///
260 /// When generated by KLE, this sets the CSS [`background-image`] property to a relative url
261 /// where the associated image is located. For example the *Carbon fibre 1* background will set
262 /// `style` to `"background-image: url('/bg/carbonfibre/carbon_texture1879.png');"`.
263 ///
264 /// [`background-image`]: https://developer.mozilla.org/en-US/docs/Web/CSS/background-image
265 pub style: String,
266}
267
268/// The metadata for the keyboard layout.
269#[derive(Debug, Clone, PartialEq)]
270pub struct Metadata {
271 /// Background colour for the layout.
272 pub background_color: Color,
273 /// Background style information for the layout.
274 pub background: Background,
275 /// Corner radii for the background using CSS [`border-radius`] syntax.
276 ///
277 /// [`border-radius`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border-radius
278 pub radii: String,
279 /// The name of the layout.
280 pub name: String,
281 /// The author of the layout.
282 pub author: String,
283 /// The default switch type used in this layout. This can be set separately for individual keys.
284 pub switch: Switch,
285 /// Whether the switch is plate mounted.
286 pub plate_mount: bool,
287 /// Whether the switch is PCB mounted.
288 pub pcb_mount: bool,
289 /// Notes for the layout. KLE expects GitHub-flavoured Markdown and can render this using the
290 /// *preview* button, but any string data is considered valid.
291 pub notes: String,
292}
293
294impl Default for Metadata {
295 fn default() -> Self {
296 Self {
297 background_color: color::BACKGROUND,
298 background: Background::default(),
299 radii: String::new(),
300 name: String::new(),
301 author: String::new(),
302 switch: Switch::default(),
303 plate_mount: false,
304 pcb_mount: false,
305 notes: String::new(),
306 }
307 }
308}
309
310/// A keyboard deserialised from a KLE JSON file.
311#[derive(Debug, Clone, Default, PartialEq)]
312pub struct Keyboard<T = f64>
313where
314 T: Real,
315{
316 /// Keyboard layout's metadata.
317 pub metadata: Metadata,
318 /// The layout's keys.
319 pub keys: Vec<Key<T>>,
320}
321
322impl<'de, T> Deserialize<'de> for Keyboard<T>
323where
324 T: Real + Deserialize<'de>,
325{
326 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
327 where
328 D: serde::Deserializer<'de>,
329 {
330 let KleKeyboard { meta, layout } = KleKeyboard::deserialize(deserializer)?;
331
332 Ok(Self {
333 metadata: meta.into(),
334 keys: KleLayoutIterator::new(layout).collect(),
335 })
336 }
337}
338
339/// An iterator of [`Key`]s deserialised from a KLE JSON file.
340#[derive(Debug, Clone)]
341pub struct KeyIterator<T = f64>(KleLayoutIterator<T>)
342where
343 T: Real;
344
345impl<'de, T> Deserialize<'de> for KeyIterator<T>
346where
347 T: Real + Deserialize<'de>,
348{
349 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
350 where
351 D: serde::Deserializer<'de>,
352 {
353 let KleKeyboard { meta: _, layout } = KleKeyboard::deserialize(deserializer)?;
354
355 Ok(Self(KleLayoutIterator::new(layout)))
356 }
357}
358
359impl<T> Iterator for KeyIterator<T>
360where
361 T: Real,
362{
363 type Item = Key<T>;
364
365 fn next(&mut self) -> Option<Self::Item> {
366 self.0.next()
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use isclose::assert_is_close;
373
374 use super::*;
375
376 #[test]
377 fn test_legend_default() {
378 let legend = Legend::default();
379
380 assert_eq!(legend.text, "");
381 assert_eq!(legend.size, 3);
382 assert_eq!(legend.color, Color::new(0, 0, 0, 255));
383 }
384
385 #[test]
386 fn test_key_default() {
387 let key = <Key>::default();
388
389 for leg in key.legends {
390 assert!(leg.is_none());
391 }
392 assert_eq!(key.color, Color::new(204, 204, 204, 255));
393 assert_is_close!(key.x, 0.0);
394 assert_is_close!(key.y, 0.0);
395 assert_is_close!(key.width, 1.0);
396 assert_is_close!(key.height, 1.0);
397 assert_is_close!(key.x2, 0.0);
398 assert_is_close!(key.y2, 0.0);
399 assert_is_close!(key.width2, 1.0);
400 assert_is_close!(key.height2, 1.0);
401 assert_is_close!(key.rotation, 0.0);
402 assert_is_close!(key.rx, 0.0);
403 assert_is_close!(key.ry, 0.0);
404 assert_eq!(key.profile, "");
405 assert_eq!(key.switch.mount, "");
406 assert_eq!(key.switch.brand, "");
407 assert_eq!(key.switch.typ, "");
408 assert!(!key.ghosted);
409 assert!(!key.stepped);
410 assert!(!key.homing);
411 assert!(!key.decal);
412 }
413
414 #[test]
415 fn test_metadata_default() {
416 let meta = Metadata::default();
417
418 assert_eq!(meta.background_color, Color::new(238, 238, 238, 255));
419 assert_eq!(meta.background.name, "");
420 assert_eq!(meta.background.style, "");
421 assert_eq!(meta.radii, "");
422 assert_eq!(meta.name, "");
423 assert_eq!(meta.author, "");
424 assert_eq!(meta.switch.mount, "");
425 assert_eq!(meta.switch.brand, "");
426 assert_eq!(meta.switch.typ, "");
427 assert!(!meta.plate_mount);
428 assert!(!meta.pcb_mount);
429 assert_eq!(meta.notes, "");
430 }
431
432 #[test]
433 fn test_keyboard_deserialize() {
434 let kb: Keyboard = serde_json::from_str(
435 r#"[
436 {
437 "name": "test",
438 "unknown": "key"
439 },
440 [
441 {
442 "a": 4,
443 "unknown2": "key"
444 },
445 "A",
446 "B",
447 "C"
448 ],
449 [
450 "D"
451 ]
452 ]"#,
453 )
454 .unwrap();
455 assert_eq!(kb.metadata.name, "test");
456 assert_eq!(kb.keys.len(), 4);
457
458 let kb: Keyboard = serde_json::from_str(r#"[["A"]]"#).unwrap();
459 assert_eq!(kb.metadata.name, "");
460 assert_eq!(kb.keys.len(), 1);
461
462 let kb: Keyboard = serde_json::from_str(r#"[{"notes": "'tis a test"}]"#).unwrap();
463 assert_eq!(kb.metadata.notes, "'tis a test");
464 assert_eq!(kb.keys.len(), 0);
465
466 assert!(serde_json::from_str::<Keyboard>("null").is_err());
467 }
468
469 #[test]
470 fn test_key_iterator_deserialize() {
471 let keys: Vec<_> = serde_json::from_str::<KeyIterator>(
472 r#"[
473 {
474 "name": "test",
475 "unknown": "key"
476 },
477 [
478 {
479 "a": 4,
480 "unknown2": "key"
481 },
482 "A",
483 "B",
484 "C"
485 ],
486 [
487 "D"
488 ]
489 ]"#,
490 )
491 .unwrap()
492 .collect();
493
494 assert_eq!(keys.len(), 4);
495 assert_eq!(keys[2].legends[0].as_ref().unwrap().text, "C");
496
497 let keys: Vec<_> = serde_json::from_str::<KeyIterator>(r#"[["A"]]"#)
498 .unwrap()
499 .collect();
500 assert_eq!(keys.len(), 1);
501 assert_eq!(keys[0].legends[0].as_ref().unwrap().text, "A");
502
503 let keys: Vec<_> = serde_json::from_str::<KeyIterator>(r#"[{"notes": "'tis a test"}]"#)
504 .unwrap()
505 .collect();
506 assert_eq!(keys.len(), 0);
507
508 assert!(serde_json::from_str::<KeyIterator>("null").is_err());
509 }
510}