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 &minus;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 &minus;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}