Skip to main content

rmk_types/
morse.rs

1//! Morse key types shared between firmware and protocol layers.
2//!
3//! This module contains all morse-related types:
4//! - [`MorseMode`] / [`MorseProfile`] — timing and behavior configuration
5//! - [`MorsePattern`] — tap/hold pattern encoding (up to 15 steps in a u16)
6//! - [`Morse`] — full morse key definition (profile + pattern→action map)
7
8use heapless::LinearMap;
9use postcard::experimental::max_size::MaxSize;
10use serde::{Deserialize, Serialize};
11
12use crate::action::Action;
13use crate::constants::MORSE_SIZE;
14
15// ---------------------------------------------------------------------------
16// MorseMode & MorseProfile — timing/behavior configuration
17// ---------------------------------------------------------------------------
18
19/// Mode for morse key behavior
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, MaxSize)]
21#[cfg_attr(feature = "defmt", derive(defmt::Format))]
22#[repr(u8)]
23#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
24#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
25pub enum MorseMode {
26    /// Same as QMK's permissive hold: <https://docs.qmk.fm/tap_hold#tap-or-hold-decision-modes>
27    /// When another key is pressed and released during the current morse key is held,
28    /// the hold action of current morse key will be triggered
29    PermissiveHold,
30    /// Trigger hold immediately if any other non-morse key is pressed when the current morse key is held
31    HoldOnOtherPress,
32    /// Normal mode, the decision is made when timeout
33    Normal,
34}
35
36/// Configuration for morse, tap dance and tap-hold.
37/// Manually packed into 64 bits to save RAM.
38///
39/// Bit layout of the inner `u64`:
40/// ```text
41/// 63        46 | 45      | 44           32 | 31  30 | 29       17 | 16  15 | 14  13   | 12       0
42/// reserved     | qt_set  | quick_tap_tm    | mode   | gap_timeout | uni_tap| flow_tap | hold_timeout
43///   (18b)      |  (1b)   |   (13b ms)      |  (2b)  |   (13b ms)  |  (2b)  |   (2b)   |  (13b ms)
44/// ```
45///
46/// - `qt_set` (bit 45): when set, `quick_tap_timeout` is explicitly configured
47///   (even if 0, which means "disabled"). When clear, the field is unset and
48///   callers should fall back to the global default.
49/// - `quick_tap_timeout` (bits 44-32): quick-tap timeout in ms (max 8191).
50/// - `mode` (bits 31-30): `00` = None, `01` = PermissiveHold, `10` = HoldOnOtherPress, `11` = Normal
51/// - `flow_tap` (bits 14, 13): `00`/`01` = None, `10` = Some(false), `11` = Some(true)
52/// - `gap_timeout` (bits 29-17): gap timeout in ms (0 = None, max 8191)
53/// - `uni_tap` (bits 16-15): `00`/`01` = None, `10` = Some(false), `11` = Some(true)
54/// - `hold_timeout` (bits 12-0): hold timeout in ms (0 = None, max 8191)
55#[derive(PartialEq, Eq, Clone, Copy, Debug, MaxSize)]
56#[cfg_attr(feature = "defmt", derive(defmt::Format))]
57pub struct MorseProfile(u64);
58
59const TIMEOUT_MASK: u64 = 0x1FFF;
60const TIMEOUT_MAX_MS: u16 = TIMEOUT_MASK as u16;
61const GAP_TIMEOUT_SHIFT: u32 = 17;
62const HOLD_TIMEOUT_MASK: u64 = TIMEOUT_MASK;
63const GAP_TIMEOUT_MASK: u64 = TIMEOUT_MASK << GAP_TIMEOUT_SHIFT;
64const UNI_TAP_LOW_BIT: u64 = 0x0000_8000;
65const UNI_TAP_HIGH_BIT: u64 = 0x0001_0000;
66const UNI_TAP_MASK: u64 = UNI_TAP_LOW_BIT | UNI_TAP_HIGH_BIT;
67const FLOW_TAP_LOW_BIT: u64 = 0x0000_2000;
68const FLOW_TAP_HIGH_BIT: u64 = 0x0000_4000;
69const FLOW_TAP_MASK: u64 = FLOW_TAP_LOW_BIT | FLOW_TAP_HIGH_BIT;
70const MODE_MASK: u64 = 0xC000_0000;
71const QT_VALUE_MASK: u64 = TIMEOUT_MASK << 32;
72const QT_SET_BIT: u64 = 1 << 45;
73
74const fn encode_timeout_ms(t: u16) -> u64 {
75    if t > TIMEOUT_MAX_MS {
76        TIMEOUT_MAX_MS as u64
77    } else {
78        t as u64
79    }
80}
81
82impl MorseProfile {
83    pub const fn const_default() -> Self {
84        Self(0)
85    }
86
87    /// If the previous key is on the same "hand", the current key will be determined as a tap
88    pub fn unilateral_tap(self) -> Option<bool> {
89        match (self.0 & UNI_TAP_MASK) >> 15 {
90            3 => Some(true),
91            2 => Some(false),
92            _ => None,
93        }
94    }
95
96    pub const fn with_unilateral_tap(self, b: Option<bool>) -> Self {
97        Self(
98            (self.0 & !UNI_TAP_MASK)
99                | match b {
100                    Some(true) => UNI_TAP_MASK,
101                    Some(false) => UNI_TAP_HIGH_BIT,
102                    None => 0,
103                },
104        )
105    }
106
107    /// Per-profile override for flow tap. `None` falls back to the default
108    /// profile's bit, then the config-level `enable_flow_tap` switch.
109    pub fn enable_flow_tap(self) -> Option<bool> {
110        match (self.0 & FLOW_TAP_MASK) >> 13 {
111            3 => Some(true),
112            2 => Some(false),
113            _ => None,
114        }
115    }
116
117    pub const fn with_enable_flow_tap(self, b: Option<bool>) -> Self {
118        Self(
119            (self.0 & !FLOW_TAP_MASK)
120                | match b {
121                    Some(true) => FLOW_TAP_MASK,
122                    Some(false) => FLOW_TAP_HIGH_BIT,
123                    None => 0,
124                },
125        )
126    }
127
128    /// The decision mode of the morse/tap-hold key
129    pub fn mode(self) -> Option<MorseMode> {
130        match self.0 & MODE_MASK {
131            MODE_MASK => Some(MorseMode::Normal),
132            0x8000_0000 => Some(MorseMode::HoldOnOtherPress),
133            0x4000_0000 => Some(MorseMode::PermissiveHold),
134            _ => None,
135        }
136    }
137
138    pub const fn with_mode(self, m: Option<MorseMode>) -> Self {
139        Self(
140            (self.0 & !MODE_MASK)
141                | match m {
142                    Some(MorseMode::Normal) => MODE_MASK,
143                    Some(MorseMode::HoldOnOtherPress) => 0x8000_0000,
144                    Some(MorseMode::PermissiveHold) => 0x4000_0000,
145                    None => 0,
146                },
147        )
148    }
149
150    /// If the key is pressed longer than this, it is accepted as `hold` (in milliseconds)
151    pub fn hold_timeout_ms(self) -> Option<u16> {
152        let t = (self.0 & HOLD_TIMEOUT_MASK) as u16;
153        if t == 0 { None } else { Some(t) }
154    }
155
156    pub const fn with_hold_timeout_ms(self, t: Option<u16>) -> Self {
157        if let Some(t) = t {
158            Self((self.0 & !HOLD_TIMEOUT_MASK) | encode_timeout_ms(t))
159        } else {
160            Self(self.0 & !HOLD_TIMEOUT_MASK)
161        }
162    }
163
164    pub const fn set_hold_timeout_ms(&mut self, t: u16) {
165        self.0 = (self.0 & !HOLD_TIMEOUT_MASK) | encode_timeout_ms(t)
166    }
167
168    pub const fn set_gap_timeout_ms(&mut self, t: u16) {
169        self.0 = (self.0 & !GAP_TIMEOUT_MASK) | (encode_timeout_ms(t) << GAP_TIMEOUT_SHIFT)
170    }
171
172    /// The time elapsed from the last release of a key is longer than this, it will break the morse pattern (in milliseconds)
173    pub fn gap_timeout_ms(self) -> Option<u16> {
174        let t = ((self.0 & GAP_TIMEOUT_MASK) >> GAP_TIMEOUT_SHIFT) as u16;
175        if t == 0 { None } else { Some(t) }
176    }
177
178    pub const fn with_gap_timeout_ms(self, t: Option<u16>) -> Self {
179        if let Some(t) = t {
180            Self((self.0 & !GAP_TIMEOUT_MASK) | (encode_timeout_ms(t) << GAP_TIMEOUT_SHIFT))
181        } else {
182            Self(self.0 & !GAP_TIMEOUT_MASK)
183        }
184    }
185
186    pub const fn quick_tap_timeout_ms(self) -> Option<u16> {
187        if self.0 & QT_SET_BIT != 0 {
188            Some(((self.0 >> 32) & TIMEOUT_MASK) as u16)
189        } else {
190            None
191        }
192    }
193
194    pub const fn with_quick_tap_timeout_ms(self, t: Option<u16>) -> Self {
195        if let Some(t) = t {
196            Self((self.0 & !(QT_VALUE_MASK | QT_SET_BIT)) | (encode_timeout_ms(t) << 32) | QT_SET_BIT)
197        } else {
198            Self(self.0 & !(QT_VALUE_MASK | QT_SET_BIT))
199        }
200    }
201
202    pub const fn set_quick_tap_timeout_ms(&mut self, t: u16) {
203        self.0 = (self.0 & !(QT_VALUE_MASK | QT_SET_BIT)) | (encode_timeout_ms(t) << 32) | QT_SET_BIT;
204    }
205
206    pub const fn new(
207        unilateral_tap: Option<bool>,
208        mode: Option<MorseMode>,
209        hold_timeout_ms: Option<u16>,
210        gap_timeout_ms: Option<u16>,
211    ) -> Self {
212        let mut v = 0u64;
213        if let Some(t) = hold_timeout_ms {
214            v = encode_timeout_ms(t);
215        }
216        if let Some(t) = gap_timeout_ms {
217            v |= encode_timeout_ms(t) << GAP_TIMEOUT_SHIFT;
218        }
219        if let Some(b) = unilateral_tap {
220            v |= if b { UNI_TAP_MASK } else { UNI_TAP_HIGH_BIT };
221        }
222        if let Some(m) = mode {
223            v |= match m {
224                MorseMode::Normal => MODE_MASK,
225                MorseMode::HoldOnOtherPress => 0x8000_0000,
226                MorseMode::PermissiveHold => 0x4000_0000,
227            };
228        }
229        MorseProfile(v)
230    }
231}
232
233impl Default for MorseProfile {
234    fn default() -> Self {
235        MorseProfile::const_default()
236    }
237}
238
239impl From<u64> for MorseProfile {
240    fn from(v: u64) -> Self {
241        MorseProfile(v)
242    }
243}
244
245impl From<MorseProfile> for u64 {
246    fn from(val: MorseProfile) -> Self {
247        val.0
248    }
249}
250
251// Wire stays packed; human-readable serializers expose named fields.
252impl Serialize for MorseProfile {
253    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
254        if serializer.is_human_readable() {
255            #[derive(Serialize)]
256            struct Repr {
257                unilateral_tap: Option<bool>,
258                enable_flow_tap: Option<bool>,
259                mode: Option<MorseMode>,
260                hold_timeout_ms: Option<u16>,
261                gap_timeout_ms: Option<u16>,
262                quick_tap_timeout_ms: Option<u16>,
263            }
264            Repr {
265                unilateral_tap: self.unilateral_tap(),
266                enable_flow_tap: self.enable_flow_tap(),
267                mode: self.mode(),
268                hold_timeout_ms: self.hold_timeout_ms(),
269                gap_timeout_ms: self.gap_timeout_ms(),
270                quick_tap_timeout_ms: self.quick_tap_timeout_ms(),
271            }
272            .serialize(serializer)
273        } else {
274            serializer.serialize_u64(self.0)
275        }
276    }
277}
278
279impl<'de> Deserialize<'de> for MorseProfile {
280    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
281        if deserializer.is_human_readable() {
282            #[derive(Deserialize)]
283            struct Repr {
284                unilateral_tap: Option<bool>,
285                enable_flow_tap: Option<bool>,
286                mode: Option<MorseMode>,
287                hold_timeout_ms: Option<u16>,
288                gap_timeout_ms: Option<u16>,
289                quick_tap_timeout_ms: Option<u16>,
290            }
291            let r = Repr::deserialize(deserializer)?;
292            Ok(
293                MorseProfile::new(r.unilateral_tap, r.mode, r.hold_timeout_ms, r.gap_timeout_ms)
294                    .with_enable_flow_tap(r.enable_flow_tap)
295                    .with_quick_tap_timeout_ms(r.quick_tap_timeout_ms),
296            )
297        } else {
298            Ok(MorseProfile(u64::deserialize(deserializer)?))
299        }
300    }
301}
302
303// TS shape mirrors the `Repr` above; `Option<T>` renders as `T | undefined`.
304#[cfg(feature = "wasm")]
305const _: () = {
306    #[::wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
307    const TS_APPEND_CONTENT: &'static str = "export type MorseProfile = { unilateral_tap: boolean | undefined; enable_flow_tap: boolean | undefined; mode: MorseMode | undefined; hold_timeout_ms: number | undefined; gap_timeout_ms: number | undefined; quick_tap_timeout_ms: number | undefined; };";
308};
309crate::wasm_object_abi!(MorseProfile, "MorseProfile");
310
311// ---------------------------------------------------------------------------
312// MorsePattern & Morse — pattern encoding and key definition
313// ---------------------------------------------------------------------------
314
315/// MorsePattern is a sequence of maximum 15 taps or holds that can be encoded into an u16:
316/// 0x1 when empty, then 0 for tap or 1 for hold shifted from the right
317#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, MaxSize)]
318#[cfg_attr(feature = "defmt", derive(defmt::Format))]
319#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
320#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
321pub struct MorsePattern(u16);
322
323pub const TAP: MorsePattern = MorsePattern(0b10);
324pub const HOLD: MorsePattern = MorsePattern(0b11);
325pub const DOUBLE_TAP: MorsePattern = MorsePattern(0b100);
326pub const HOLD_AFTER_TAP: MorsePattern = MorsePattern(0b101);
327
328impl Default for MorsePattern {
329    fn default() -> Self {
330        MorsePattern(0b1) // 0b1 means empty
331    }
332}
333
334impl MorsePattern {
335    pub fn max_taps() -> usize {
336        15 // 15 taps can be encoded on u16 bits (1 bit used to mark the start position)
337    }
338
339    /// Creates a `MorsePattern` from a raw `u16`.
340    ///
341    /// # Panics (debug only)
342    /// Panics if `value` is 0, which is not a valid encoding
343    /// (the empty pattern is `0b1`).
344    pub fn from_u16(value: u16) -> Self {
345        debug_assert!(value != 0, "MorsePattern 0 is invalid; the empty pattern is 0b1");
346        MorsePattern(value)
347    }
348
349    pub fn to_u16(&self) -> u16 {
350        self.0
351    }
352
353    pub fn is_empty(&self) -> bool {
354        self.0 == 0b1
355    }
356
357    pub fn is_full(&self) -> bool {
358        (self.0 & 0b1000_0000_0000_0000) != 0
359    }
360
361    pub fn pattern_length(&self) -> usize {
362        // leading_zeros() is 16 for 0, which would underflow.
363        // Saturate to 0 for the (invalid) zero case.
364        15usize.saturating_sub(self.0.leading_zeros() as usize)
365    }
366
367    /// Checks if this pattern starts with the given one
368    pub fn starts_with(&self, pattern_start: MorsePattern) -> bool {
369        let n = pattern_start.0.leading_zeros();
370        let m = self.0.leading_zeros();
371        m <= n && (self.0 >> (n - m) == pattern_start.0)
372    }
373
374    /// Returns `true` if the last step in the pattern is a hold.
375    /// Returns `false` for empty patterns.
376    pub fn last_is_hold(&self) -> bool {
377        !self.is_empty() && self.0 & 0b1 == 0b1
378    }
379
380    pub fn followed_by_tap(&self) -> Self {
381        // Shift the bits to the left and set the last bit to 0 (tap)
382        MorsePattern(self.0 << 1)
383    }
384
385    pub fn followed_by_hold(&self) -> Self {
386        // Shift the bits to the left and set the last bit to 1 (hold)
387        MorsePattern((self.0 << 1) | 0b1)
388    }
389
390    /// `true` when the pattern consists only of tap steps (no holds).
391    /// Returns `false` for the empty pattern (encoding `0b1`).
392    pub fn is_all_taps(&self) -> bool {
393        self.0 > 0b1 && self.0 & (self.0 - 1) == 0
394    }
395}
396
397/// Definition of a morse key.
398///
399/// A morse key is a key that behaves differently according to the pattern of a tap/hold sequence.
400/// The maximum number of taps is limited to 15 by the internal u16 representation of MorsePattern.
401/// There is a list of (pattern, corresponding action) pairs for each morse key:
402/// The number of pairs is limited by `MORSE_SIZE` (from `constants.rs`, generated at build time).
403///
404/// Note: `MORSE_SIZE` is a **wire-format** capacity — on firmware it equals
405/// `MAX_PATTERNS_PER_KEY` (from `keyboard.toml`), on host it's a fixed upper bound.
406#[derive(Debug, Clone, Default, Serialize, Deserialize)]
407#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
408#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
409pub struct Morse {
410    /// The profile of this morse key, which defines the timing parameters, etc.
411    /// If some of its fields are filled with None, the global default value will be used.
412    pub profile: MorseProfile,
413    /// The list of pattern -> action pairs, which can be triggered
414    #[serde(with = "morse_actions_serde")]
415    #[cfg_attr(feature = "wasm", tsify(type = "[number, Action][]"))]
416    pub actions: LinearMap<MorsePattern, Action, MORSE_SIZE>,
417}
418
419impl MaxSize for Morse {
420    // The custom serializer in `morse_actions_serde` (below) emits the
421    // `LinearMap` as `Vec<(u16, Action), MORSE_SIZE>` on the wire — keep that
422    // shape in sync with the helper type parameter here.
423    const POSTCARD_MAX_SIZE: usize =
424        MorseProfile::POSTCARD_MAX_SIZE + crate::heapless_vec_max_size::<(u16, Action), MORSE_SIZE>();
425}
426
427#[cfg(feature = "defmt")]
428impl defmt::Format for Morse {
429    fn format(&self, f: defmt::Formatter<'_>) {
430        defmt::write!(f, "profile: MorseProfile({:?}), ", self.profile);
431        defmt::write!(f, "actions: [");
432        for item in self.actions.iter() {
433            defmt::write!(f, "{:?},", item);
434        }
435        defmt::write!(f, "]");
436    }
437}
438
439impl PartialEq for Morse {
440    fn eq(&self, other: &Self) -> bool {
441        if self.profile != other.profile || self.actions.len() != other.actions.len() {
442            return false;
443        }
444        self.actions.iter().all(|(k, v)| other.actions.get(k) == Some(v))
445    }
446}
447
448impl Eq for Morse {}
449
450/// Wire format note: `Morse` uses a custom serde impl for the `LinearMap`
451/// of actions. The on-wire shape is `(MorseProfile, Vec<(u16, Action)>)`.
452/// The `morse_wire_format` test below pins this contract.
453impl Morse {
454    pub fn new_from_vial(
455        tap: Action,
456        hold: Action,
457        hold_after_tap: Action,
458        double_tap: Action,
459        profile: MorseProfile,
460    ) -> Self {
461        let mut result = Self {
462            profile,
463            ..Default::default()
464        };
465
466        if tap != Action::No {
467            _ = result.actions.insert(TAP, tap);
468        }
469        if hold != Action::No {
470            _ = result.actions.insert(HOLD, hold);
471        }
472        if double_tap != Action::No {
473            _ = result.actions.insert(DOUBLE_TAP, double_tap);
474        }
475        if hold_after_tap != Action::No {
476            _ = result.actions.insert(HOLD_AFTER_TAP, hold_after_tap);
477        }
478        result
479    }
480
481    pub fn new_with_actions(
482        tap_actions: heapless::Vec<Action, MORSE_SIZE>,
483        hold_actions: heapless::Vec<Action, MORSE_SIZE>,
484        profile: MorseProfile,
485    ) -> Self {
486        let mut result = Self {
487            profile,
488            ..Default::default()
489        };
490
491        let mut pattern = 0b1u16;
492        for item in tap_actions.iter() {
493            pattern <<= 1;
494            let _ = result.put(MorsePattern::from_u16(pattern), *item);
495        }
496
497        let mut pattern = 0b1u16;
498        for item in hold_actions.iter() {
499            pattern <<= 1;
500            let _ = result.put(MorsePattern::from_u16(pattern | 0b1), *item);
501        }
502
503        result
504    }
505
506    pub fn max_pattern_length(&self) -> usize {
507        let mut max_length = 0;
508        for pair in self.actions.iter() {
509            max_length = max_length.max(pair.0.pattern_length());
510        }
511        max_length
512    }
513
514    pub fn try_predict_final_action(&self, pattern_start: MorsePattern) -> Option<Action> {
515        if !self.actions.contains_key(&pattern_start) {
516            return None;
517        }
518        for (pattern, _) in self.actions.iter() {
519            if *pattern != pattern_start && pattern.starts_with(pattern_start) {
520                return None;
521            }
522        }
523        self.actions.get(&pattern_start).copied()
524    }
525
526    pub fn can_fire_early(&self, pattern: MorsePattern) -> bool {
527        let Some(current_action) = self.actions.get(&pattern) else {
528            return false;
529        };
530        if self.actions.contains_key(&pattern.followed_by_tap()) {
531            return false;
532        }
533        self.actions
534            .get(&pattern.followed_by_hold())
535            .is_some_and(|a| *a == *current_action)
536    }
537
538    pub fn has_pattern_or_continuation(&self, pattern: MorsePattern) -> bool {
539        self.actions.iter().any(|(p, _)| p.starts_with(pattern))
540    }
541
542    pub fn get(&self, pattern: MorsePattern) -> Option<Action> {
543        self.actions.get(&pattern).copied()
544    }
545
546    /// Insert or update an action for the given pattern.
547    ///
548    /// An `Action::No` removes the pattern. Returns `Err((pattern, action))` if the map is full.
549    pub fn put(&mut self, pattern: MorsePattern, action: Action) -> Result<(), (MorsePattern, Action)> {
550        if action != Action::No {
551            self.actions.insert(pattern, action).map(|_| ())
552        } else {
553            let _ = self.actions.remove(&pattern);
554            Ok(())
555        }
556    }
557}
558
559// Custom serde module for LinearMap
560mod morse_actions_serde {
561    use serde::de::Error;
562    use serde::{Deserializer, Serializer};
563
564    use super::*;
565
566    pub fn serialize<S>(map: &LinearMap<MorsePattern, Action, MORSE_SIZE>, serializer: S) -> Result<S::Ok, S::Error>
567    where
568        S: Serializer,
569    {
570        // Convert to Vec for serialization
571        let vec: heapless::Vec<(u16, Action), MORSE_SIZE> = map.iter().map(|(k, v)| (k.to_u16(), *v)).collect();
572        vec.serialize(serializer)
573    }
574
575    pub fn deserialize<'de, D>(deserializer: D) -> Result<LinearMap<MorsePattern, Action, MORSE_SIZE>, D::Error>
576    where
577        D: Deserializer<'de>,
578    {
579        use core::fmt;
580
581        use serde::de::{SeqAccess, Visitor};
582
583        struct VecVisitor;
584
585        impl<'de> Visitor<'de> for VecVisitor {
586            type Value = heapless::Vec<(u16, Action), MORSE_SIZE>;
587
588            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
589                write!(formatter, "a sequence of (u16, Action) tuples")
590            }
591
592            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
593            where
594                A: SeqAccess<'de>,
595            {
596                let mut vec = heapless::Vec::new();
597                while let Some(elem) = seq.next_element::<(u16, Action)>()? {
598                    vec.push(elem)
599                        .map_err(|_| serde::de::Error::custom("Vec capacity exceeded"))?;
600                }
601                Ok(vec)
602            }
603        }
604
605        let vec = deserializer.deserialize_seq(VecVisitor)?;
606        let mut map = LinearMap::new();
607        for (pattern, action) in vec {
608            if pattern == 0 {
609                return Err(D::Error::custom("MorsePattern 0 is invalid; the empty pattern is 0b1"));
610            }
611            map.insert(MorsePattern::from_u16(pattern), action)
612                .map_err(|_| D::Error::custom("Failed to insert into LinearMap"))?;
613        }
614        Ok(map)
615    }
616}
617
618#[cfg(test)]
619mod tests {
620    extern crate alloc;
621
622    use super::*;
623    use crate::action::Action;
624    use crate::keycode::{HidKeyCode, KeyCode};
625
626    #[test]
627    fn test_linear_map_serde_empty() {
628        let morse = Morse::default();
629
630        let mut buffer = [0u8; 128];
631        let serialized = postcard::to_slice(&morse, &mut buffer).unwrap();
632        let deserialized: Morse = postcard::from_bytes(serialized).unwrap();
633
634        assert_eq!(morse.actions.len(), deserialized.actions.len());
635        assert_eq!(morse.actions.len(), 0);
636    }
637
638    #[test]
639    fn test_linear_map_serde_single_entry() {
640        let mut morse = Morse::default();
641        morse.actions.insert(TAP, Action::Key(KeyCode::Hid(HidKeyCode::A))).ok();
642
643        let mut buffer = [0u8; 128];
644        let serialized = postcard::to_slice(&morse, &mut buffer).unwrap();
645        let deserialized: Morse = postcard::from_bytes(serialized).unwrap();
646
647        assert_eq!(morse.actions.len(), deserialized.actions.len());
648        assert_eq!(
649            deserialized.actions.get(&TAP),
650            Some(&Action::Key(KeyCode::Hid(HidKeyCode::A)))
651        );
652    }
653
654    #[test]
655    fn test_linear_map_serde_multiple_entries() {
656        let mut morse = Morse::default();
657        morse.actions.insert(TAP, Action::Key(KeyCode::Hid(HidKeyCode::A))).ok();
658        morse
659            .actions
660            .insert(HOLD, Action::Key(KeyCode::Hid(HidKeyCode::B)))
661            .ok();
662        morse
663            .actions
664            .insert(DOUBLE_TAP, Action::Key(KeyCode::Hid(HidKeyCode::C)))
665            .ok();
666        morse
667            .actions
668            .insert(HOLD_AFTER_TAP, Action::Key(KeyCode::Hid(HidKeyCode::D)))
669            .ok();
670
671        let mut buffer = [0u8; 128];
672        let serialized = postcard::to_slice(&morse, &mut buffer).unwrap();
673        let deserialized: Morse = postcard::from_bytes(serialized).unwrap();
674
675        assert_eq!(morse.actions.len(), deserialized.actions.len());
676        assert_eq!(morse.actions.len(), 4);
677
678        assert_eq!(
679            deserialized.actions.get(&TAP),
680            Some(&Action::Key(KeyCode::Hid(HidKeyCode::A)))
681        );
682        assert_eq!(
683            deserialized.actions.get(&HOLD),
684            Some(&Action::Key(KeyCode::Hid(HidKeyCode::B)))
685        );
686        assert_eq!(
687            deserialized.actions.get(&DOUBLE_TAP),
688            Some(&Action::Key(KeyCode::Hid(HidKeyCode::C)))
689        );
690        assert_eq!(
691            deserialized.actions.get(&HOLD_AFTER_TAP),
692            Some(&Action::Key(KeyCode::Hid(HidKeyCode::D)))
693        );
694    }
695
696    #[test]
697    fn test_linear_map_serde_with_profile() {
698        let mut morse = Morse {
699            profile: MorseProfile::new(Some(true), Some(MorseMode::PermissiveHold), Some(200), Some(150)),
700            ..Default::default()
701        };
702        morse.actions.insert(TAP, Action::Key(KeyCode::Hid(HidKeyCode::H))).ok();
703        morse
704            .actions
705            .insert(HOLD, Action::Key(KeyCode::Hid(HidKeyCode::I)))
706            .ok();
707
708        let mut buffer = [0u8; 128];
709        let serialized = postcard::to_slice(&morse, &mut buffer).unwrap();
710        let deserialized: Morse = postcard::from_bytes(serialized).unwrap();
711
712        assert_eq!(morse.profile, deserialized.profile);
713        assert_eq!(morse.actions.len(), deserialized.actions.len());
714    }
715
716    #[test]
717    fn morse_pattern_max_size_matches_u16() {
718        // Morse actions serialize MorsePattern as u16 on the wire.
719        // If MorsePattern's MaxSize ever diverges from u16, the manual
720        // MaxSize impl on Morse would be wrong.
721        assert_eq!(MorsePattern::POSTCARD_MAX_SIZE, u16::POSTCARD_MAX_SIZE,);
722    }
723
724    #[test]
725    fn test_morse_profile_timeout_setters() {
726        let mut profile = MorseProfile::new(Some(true), Some(MorseMode::PermissiveHold), Some(1000), Some(2000));
727
728        assert_eq!(profile.hold_timeout_ms(), Some(1000));
729        assert_eq!(profile.gap_timeout_ms(), Some(2000));
730        assert_eq!(profile.unilateral_tap(), Some(true));
731        assert_eq!(profile.mode(), Some(MorseMode::PermissiveHold));
732
733        profile.set_hold_timeout_ms(1500);
734        assert_eq!(profile.hold_timeout_ms(), Some(1500));
735        assert_eq!(profile.gap_timeout_ms(), Some(2000));
736        assert_eq!(profile.unilateral_tap(), Some(true));
737        assert_eq!(profile.mode(), Some(MorseMode::PermissiveHold));
738
739        profile.set_gap_timeout_ms(2500);
740        assert_eq!(profile.hold_timeout_ms(), Some(1500));
741        assert_eq!(profile.gap_timeout_ms(), Some(2500));
742        assert_eq!(profile.unilateral_tap(), Some(true));
743        assert_eq!(profile.mode(), Some(MorseMode::PermissiveHold));
744
745        profile.set_hold_timeout_ms(0xFFFF);
746        profile.set_gap_timeout_ms(0xFFFF);
747        assert_eq!(profile.hold_timeout_ms(), Some(TIMEOUT_MAX_MS));
748        assert_eq!(profile.gap_timeout_ms(), Some(TIMEOUT_MAX_MS));
749
750        profile.set_hold_timeout_ms(0);
751        profile.set_gap_timeout_ms(0);
752        assert_eq!(profile.hold_timeout_ms(), None);
753        assert_eq!(profile.gap_timeout_ms(), None);
754
755        let p = MorseProfile::const_default().with_quick_tap_timeout_ms(Some(300));
756        assert_eq!(p.quick_tap_timeout_ms(), Some(300));
757        assert_eq!(p.hold_timeout_ms(), MorseProfile::const_default().hold_timeout_ms());
758        assert_eq!(p.gap_timeout_ms(), MorseProfile::const_default().gap_timeout_ms());
759
760        let mut p2 = p;
761        p2.set_quick_tap_timeout_ms(0);
762        assert_eq!(p2.quick_tap_timeout_ms(), Some(0), "set_*_ms(0) is explicit");
763
764        p2.set_quick_tap_timeout_ms(0xFFFF);
765        assert_eq!(p2.quick_tap_timeout_ms(), Some(TIMEOUT_MAX_MS));
766
767        let p3 = p.with_quick_tap_timeout_ms(None);
768        assert_eq!(p3.quick_tap_timeout_ms(), None, "with_*(None) clears the field");
769
770        let p4 = MorseProfile::const_default().with_quick_tap_timeout_ms(Some(0));
771        assert_eq!(p4.quick_tap_timeout_ms(), Some(0), "Some(0) is explicitly disabled");
772    }
773
774    #[test]
775    fn is_all_taps_encoding_invariant() {
776        let tap = MorsePattern::from_u16(0b10);
777        let tap_tap = MorsePattern::from_u16(0b100);
778        let tap_tap_tap = MorsePattern::from_u16(0b1000);
779        let hold = MorsePattern::from_u16(0b11);
780        let tap_hold = MorsePattern::from_u16(0b101);
781        let hold_tap = MorsePattern::from_u16(0b110);
782        let empty = MorsePattern::default();
783
784        assert!(tap.is_all_taps());
785        assert!(tap_tap.is_all_taps());
786        assert!(tap_tap_tap.is_all_taps());
787        assert!(!hold.is_all_taps());
788        assert!(!tap_hold.is_all_taps());
789        assert!(!hold_tap.is_all_taps());
790        assert!(!empty.is_all_taps());
791
792        assert_eq!(tap, MorsePattern::default().followed_by_tap());
793        assert_eq!(tap_tap, tap.followed_by_tap());
794        assert_eq!(hold, MorsePattern::default().followed_by_hold());
795    }
796
797    #[test]
798    fn test_morse_profile_packed_layout_matches_docs() {
799        let profile = MorseProfile::new(
800            Some(false),
801            Some(MorseMode::HoldOnOtherPress),
802            Some(0x0123),
803            Some(0x0456),
804        )
805        .with_enable_flow_tap(Some(false));
806        assert_eq!(
807            u64::from(profile),
808            0x8000_0000 | (0x0456u64 << 17) | 0x0001_0000 | 0x0000_4000 | 0x0123
809        );
810        assert_eq!(profile.unilateral_tap(), Some(false));
811        assert_eq!(profile.enable_flow_tap(), Some(false));
812
813        let profile = MorseProfile::new(Some(true), Some(MorseMode::Normal), Some(0x0123), Some(0x0456))
814            .with_enable_flow_tap(Some(true));
815        assert_eq!(
816            u64::from(profile),
817            0xC000_0000 | (0x0456u64 << 17) | 0x0001_8000 | 0x0000_6000 | 0x0123
818        );
819        assert_eq!(profile.unilateral_tap(), Some(true));
820        assert_eq!(profile.enable_flow_tap(), Some(true));
821    }
822
823    #[test]
824    fn test_morse_profile_enable_flow_tap_accessors_preserve_packed_fields() {
825        assert_eq!(core::mem::size_of::<MorseProfile>(), 8);
826        assert_eq!(MorseProfile::POSTCARD_MAX_SIZE, u64::POSTCARD_MAX_SIZE);
827        assert_eq!(MorseProfile::const_default().enable_flow_tap(), None);
828
829        let profile = MorseProfile::new(Some(true), Some(MorseMode::PermissiveHold), Some(1000), Some(2000));
830        let profile = profile.with_enable_flow_tap(Some(true));
831        assert_eq!(profile.enable_flow_tap(), Some(true));
832        assert_eq!(profile.hold_timeout_ms(), Some(1000));
833        assert_eq!(profile.gap_timeout_ms(), Some(2000));
834        assert_eq!(profile.unilateral_tap(), Some(true));
835        assert_eq!(profile.mode(), Some(MorseMode::PermissiveHold));
836
837        let profile = profile.with_enable_flow_tap(Some(false));
838        assert_eq!(profile.enable_flow_tap(), Some(false));
839        assert_eq!(profile.hold_timeout_ms(), Some(1000));
840        assert_eq!(profile.gap_timeout_ms(), Some(2000));
841        assert_eq!(profile.unilateral_tap(), Some(true));
842        assert_eq!(profile.mode(), Some(MorseMode::PermissiveHold));
843
844        let profile = profile.with_enable_flow_tap(None);
845        assert_eq!(profile.enable_flow_tap(), None);
846        assert_eq!(profile.hold_timeout_ms(), Some(1000));
847        assert_eq!(profile.gap_timeout_ms(), Some(2000));
848        assert_eq!(profile.unilateral_tap(), Some(true));
849        assert_eq!(profile.mode(), Some(MorseMode::PermissiveHold));
850    }
851
852    /// The human-readable serde goes `MorseProfile` -> decoded parts -> `new()`.
853    /// All 32 bits are covered by the five fields, so that path must be lossless.
854    #[test]
855    fn morse_profile_parts_roundtrip() {
856        for p in [
857            MorseProfile::new(
858                Some(false),
859                Some(MorseMode::HoldOnOtherPress),
860                Some(TIMEOUT_MAX_MS),
861                Some(1),
862            )
863            .with_enable_flow_tap(Some(false)),
864            MorseProfile::new(Some(true), Some(MorseMode::Normal), Some(200), Some(150))
865                .with_enable_flow_tap(Some(true)),
866            MorseProfile::const_default(),
867        ] {
868            let parts = MorseProfile::new(p.unilateral_tap(), p.mode(), p.hold_timeout_ms(), p.gap_timeout_ms())
869                .with_enable_flow_tap(p.enable_flow_tap());
870            assert_eq!(p, parts);
871        }
872    }
873
874    /// Pins the on-wire shape of `Morse`:
875    ///   `(MorseProfile, Vec<(u16, Action)>)`
876    ///
877    /// `Morse` uses a custom serde impl for the `LinearMap` of actions; this
878    /// test verifies a Morse value can be reconstructed by manually
879    /// deserializing those two fields from the same byte stream.
880    #[test]
881    fn morse_wire_format() {
882        use postcard::to_slice;
883
884        // Build a Morse with known data
885        let mut morse = Morse::default();
886        morse.actions.insert(MorsePattern::from_u16(0b11), Action::No).unwrap();
887
888        // Serialize the whole Morse
889        let mut buf = [0u8; 256];
890        let bytes = to_slice(&morse, &mut buf).unwrap();
891
892        // Now manually deserialize field-by-field in the order the Schema declares:
893        // 1. profile: MorseProfile (a newtype around u64)
894        let (profile, rest): (MorseProfile, &[u8]) =
895            postcard::take_from_bytes(bytes).expect("should deserialize MorseProfile first");
896        assert_eq!(profile, MorseProfile::const_default());
897
898        // 2. actions: Vec<(u16, Action)> — which is what the custom serde produces
899        let (actions, rest): (heapless::Vec<(u16, Action), MORSE_SIZE>, &[u8]) =
900            postcard::take_from_bytes(rest).expect("should deserialize actions vec second");
901        assert!(rest.is_empty(), "no trailing bytes should remain");
902
903        assert_eq!(actions.len(), 1);
904        assert_eq!(actions[0], (0b11u16, Action::No));
905    }
906}