rmk_types/
action.rs

1//! Keyboard actions and behaviors.
2//!
3//! This module defines the core action system used in RMK firmware.
4//! Actions represent what happens when a key is pressed, from simple key
5//! presses to complex behaviors like tap-hold, layer switching, and macros.
6//!
7//! Key types:
8//! - [`Action`] - Single operations that keyboards send or execute
9//! - [`KeyAction`] - Complex behaviors that keyboards should behave
10//! - [`EncoderAction`] - Rotary encoder actions
11
12use crate::keycode::KeyCode;
13use crate::modifier::ModifierCombination;
14
15/// EncoderAction is the action at a encoder position, stored in encoder_map.
16#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
17#[cfg_attr(feature = "defmt", derive(defmt::Format))]
18#[derive(postcard::experimental::max_size::MaxSize)]
19pub struct EncoderAction {
20    clockwise: KeyAction,
21    counter_clockwise: KeyAction,
22}
23
24impl Default for EncoderAction {
25    fn default() -> Self {
26        Self {
27            clockwise: KeyAction::No,
28            counter_clockwise: KeyAction::No,
29        }
30    }
31}
32
33impl EncoderAction {
34    /// Create a new encoder action.
35    pub const fn new(clockwise: KeyAction, counter_clockwise: KeyAction) -> Self {
36        Self {
37            clockwise,
38            counter_clockwise,
39        }
40    }
41
42    /// Set the clockwise action.
43    pub fn set_clockwise(&mut self, clockwise: KeyAction) {
44        self.clockwise = clockwise;
45    }
46
47    /// Set the counter clockwise action.
48    pub fn set_counter_clockwise(&mut self, counter_clockwise: KeyAction) {
49        self.counter_clockwise = counter_clockwise;
50    }
51
52    /// Get the clockwise action.
53    pub fn clockwise(&self) -> KeyAction {
54        self.clockwise
55    }
56
57    /// Get the counter clockwise action.
58    pub fn counter_clockwise(&self) -> KeyAction {
59        self.counter_clockwise
60    }
61}
62
63/// Mode for morse key behavior
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65#[cfg_attr(feature = "defmt", derive(defmt::Format))]
66#[repr(u8)]
67pub enum MorseMode {
68    /// Same as QMK's permissive hold: https://docs.qmk.fm/tap_hold#tap-or-hold-decision-modes
69    /// When another key is pressed and released during the current morse key is held,
70    /// the hold action of current morse key will be triggered
71    PermissiveHold,
72    /// Trigger hold immediately if any other non-morse key is pressed when the current morse key is held
73    HoldOnOtherPress,
74    /// Normal mode, the decision is made when timeout
75    Normal,
76}
77
78/// Configuration for morse, tap dance and tap-hold
79/// to save some RAM space, manually packed into 32 bits
80#[derive(PartialEq, Eq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
81#[cfg_attr(feature = "defmt", derive(defmt::Format))]
82#[derive(postcard::experimental::max_size::MaxSize)]
83pub struct MorseProfile(u32);
84
85impl MorseProfile {
86    pub const fn const_default() -> Self {
87        Self(0)
88    }
89
90    /// If the previous key is on the same "hand", the current key will be determined as a tap
91    pub fn unilateral_tap(self) -> Option<bool> {
92        match self.0 & 0x0000_C000 {
93            0x0000_C000 => Some(true),
94            0x0000_8000 => Some(false),
95            _ => None,
96        }
97    }
98
99    pub const fn with_unilateral_tap(self, b: Option<bool>) -> Self {
100        Self(
101            (self.0 & 0xFFFF_3FFF)
102                | match b {
103                    Some(true) => 0x0000_C000,
104                    Some(false) => 0x0000_8000,
105                    None => 0,
106                },
107        )
108    }
109
110    /// The decision mode of the morse/tap-hold key
111    /// - If neither of them is set, the decision is made when timeout
112    /// - If permissive_hold is set, same as QMK's permissive hold:
113    ///   When another key is pressed and released while the current morse key is held,
114    ///   the hold action of current morse key will be triggered
115    ///   https://docs.qmk.fm/tap_hold#tap-or-hold-decision-modes
116    /// - if hold_on_other_press is set - triggers hold immediately if any other non-morse
117    ///   key is pressed while the current morse key is held    
118    pub fn mode(self) -> Option<MorseMode> {
119        match self.0 & 0xC000_0000 {
120            0xC000_0000 => Some(MorseMode::Normal),
121            0x8000_0000 => Some(MorseMode::HoldOnOtherPress),
122            0x4000_0000 => Some(MorseMode::PermissiveHold),
123            _ => None,
124        }
125    }
126
127    pub const fn with_mode(self, m: Option<MorseMode>) -> Self {
128        Self(
129            (self.0 & 0x3FFF_FFFF)
130                | match m {
131                    Some(MorseMode::Normal) => 0xC000_0000,
132                    Some(MorseMode::HoldOnOtherPress) => 0x8000_0000,
133                    Some(MorseMode::PermissiveHold) => 0x4000_0000,
134                    None => 0,
135                },
136        )
137    }
138
139    /// If the key is pressed longer than this, it is accepted as `hold` (in milliseconds)
140    /// if given, should not be zero
141    pub fn hold_timeout_ms(self) -> Option<u16> {
142        // NonZero
143        let t = (self.0 & 0x3FFF) as u16;
144        if t == 0 { None } else { Some(t) }
145    }
146
147    pub const fn with_hold_timeout_ms(self, t: Option<u16>) -> Self {
148        if let Some(t) = t {
149            Self((self.0 & 0xFFFF_C000) | (t as u32 & 0x3FFF))
150        } else {
151            Self(self.0 & 0xFFFF_C000)
152        }
153    }
154
155    pub const fn set_hold_timeout_ms(&mut self, t: u16) {
156        self.0 = (self.0 & 0xFFFF_C000) | (t as u32 & 0x3FFF)
157    }
158
159    pub const fn set_gap_timeout_ms(&mut self, t: u16) {
160        self.0 = (self.0 & 0xC000_FFFF) | ((t as u32 & 0x3FFF) << 16)
161    }
162
163    /// The time elapsed from the last release of a key is longer than this, it will break the morse pattern (in milliseconds)
164    /// if given, should not be zero
165    pub fn gap_timeout_ms(self) -> Option<u16> {
166        // NonZero
167        let t = ((self.0 >> 16) & 0x3FFF) as u16;
168        if t == 0 { None } else { Some(t) }
169    }
170
171    pub const fn with_gap_timeout_ms(self, t: Option<u16>) -> Self {
172        if let Some(t) = t {
173            Self((self.0 & 0xC000_FFFF) | ((t as u32 & 0x3FFF) << 16))
174        } else {
175            Self(self.0 & 0xC000_FFFF)
176        }
177    }
178
179    pub const fn new(
180        unilateral_tap: Option<bool>,
181        mode: Option<MorseMode>,
182        hold_timeout_ms: Option<u16>,
183        gap_timeout_ms: Option<u16>,
184    ) -> Self {
185        let mut v = 0u32;
186        if let Some(t) = hold_timeout_ms {
187            //zero value also considered as None!
188            v = (t & 0x3FFF) as u32;
189        }
190
191        if let Some(t) = gap_timeout_ms {
192            //zero value also considered as None!
193            v |= ((t & 0x3FFF) as u32) << 16;
194        }
195
196        if let Some(b) = unilateral_tap {
197            v |= if b { 0x0000_C000 } else { 0x0000_8000 };
198        }
199
200        if let Some(m) = mode {
201            v |= match m {
202                MorseMode::Normal => 0xC000_0000,
203                MorseMode::HoldOnOtherPress => 0x8000_0000,
204                MorseMode::PermissiveHold => 0x4000_0000,
205            };
206        }
207
208        MorseProfile(v)
209    }
210}
211
212impl Default for MorseProfile {
213    fn default() -> Self {
214        MorseProfile::const_default()
215    }
216}
217
218impl From<u32> for MorseProfile {
219    fn from(v: u32) -> Self {
220        MorseProfile(v)
221    }
222}
223
224impl From<MorseProfile> for u32 {
225    fn from(val: MorseProfile) -> Self {
226        val.0
227    }
228}
229
230/// A KeyAction is the action at a keyboard position, stored in keymap.
231/// It can be a single action like triggering a key, or a composite keyboard action like tap/hold
232#[derive(Debug, Copy, Clone, Eq, serde::Serialize, serde::Deserialize)]
233#[cfg_attr(feature = "defmt", derive(defmt::Format))]
234#[derive(postcard::experimental::max_size::MaxSize)]
235pub enum KeyAction {
236    /// No action. Serialized as 0x0000.
237    No,
238    /// Transparent action, next layer will be checked. Serialized as 0x0001.
239    Transparent,
240    /// A single action, such as triggering a key, or activating a layer. Action is triggered when pressed and cancelled when released.
241    Single(Action),
242    /// Don't wait the release of the key, auto-release after a time threshold.
243    Tap(Action),
244    /// Tap hold action    
245    TapHold(Action, Action, MorseProfile),
246
247    /// Morse action, references a morse configuration by index.
248    Morse(u8),
249}
250
251impl KeyAction {
252    /// Convert `KeyAction` to the internal `Action`.
253    /// Only valid for `Single` and `Tap` variant, returns `Action::No` for other variants.
254    pub fn to_action(self) -> Action {
255        match self {
256            KeyAction::Single(a) | KeyAction::Tap(a) => a,
257            _ => Action::No,
258        }
259    }
260
261    /// 'morse' is an alias for the superset of tap dance and tap hold keys,
262    /// since their handling have many similarities
263    pub fn is_morse(&self) -> bool {
264        matches!(self, KeyAction::TapHold(_, _, _) | KeyAction::Morse(_))
265    }
266
267    pub fn is_empty(&self) -> bool {
268        matches!(self, KeyAction::No)
269    }
270}
271
272/// combo, fork, etc. compares key actions
273/// WARNING: this is not a perfect comparison, we ignores the profile config of TapHold!
274impl PartialEq for KeyAction {
275    fn eq(&self, other: &Self) -> bool {
276        match (self, other) {
277            (KeyAction::No, KeyAction::No) => true,
278            (KeyAction::Transparent, KeyAction::Transparent) => true,
279            (KeyAction::Single(a), KeyAction::Single(b)) => a == b,
280            (KeyAction::Tap(a), KeyAction::Tap(b)) => a == b,
281            (KeyAction::TapHold(a, b, _), KeyAction::TapHold(c, d, _)) => a == c && b == d,
282            (KeyAction::Morse(a), KeyAction::Morse(b)) => a == b,
283            _ => false,
284        }
285    }
286}
287
288/// A single basic action that a keyboard can execute.
289#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
290#[cfg_attr(feature = "defmt", derive(defmt::Format))]
291#[derive(postcard::experimental::max_size::MaxSize)]
292pub enum Action {
293    /// Default action, no action.
294    No,
295    /// Transparent action, next layer will be checked.
296    Transparent,
297    /// A normal key stroke, uses for all keycodes defined in `KeyCode` enum, including mouse key, consumer/system control, etc.
298    Key(KeyCode),
299    /// Modifier Combination, used for oneshot keyaction.
300    Modifier(ModifierCombination),
301    /// Key stroke with modifier combination triggered.
302    KeyWithModifier(KeyCode, ModifierCombination),
303    /// Activate a layer
304    LayerOn(u8),
305    /// Activate a layer with modifier combination triggered.
306    LayerOnWithModifier(u8, ModifierCombination),
307    /// Deactivate a layer
308    LayerOff(u8),
309    /// Toggle a layer
310    LayerToggle(u8),
311    /// Set default layer
312    DefaultLayer(u8),
313    /// Activate a layer and deactivate all other layers(except default layer)
314    LayerToggleOnly(u8),
315    /// Triggers the Macro at the 'index'.
316    /// this is an alternative trigger to
317    /// Macro keycodes (0x500 ~ 0x5FF; KeyCode::Macro0 ~ KeyCode::Macro31
318    /// e.g. `Action::TriggerMacro(6)`` will trigger the same Macro as `Action::Key(KeyCode::Macro6)`
319    /// the main purpose for this enum variant is to easily extend to more than 32 macros (to 256)
320    /// without introducing new Keycodes.
321    TriggerMacro(u8),
322    /// Oneshot layer, keep the layer active until the next key is triggered.
323    OneShotLayer(u8),
324    /// Oneshot modifier, keep the modifier active until the next key is triggered.
325    OneShotModifier(ModifierCombination),
326    /// Oneshot key, keep the key active until the next key is triggered.
327    OneShotKey(KeyCode),
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    #[test]
335    fn test_morse_profile_timeout_setters() {
336        // Test with all fields set to verify bit field isolation
337        let mut profile = MorseProfile::new(Some(true), Some(MorseMode::PermissiveHold), Some(1000), Some(2000));
338
339        // Verify initial state
340        assert_eq!(profile.hold_timeout_ms(), Some(1000));
341        assert_eq!(profile.gap_timeout_ms(), Some(2000));
342        assert_eq!(profile.unilateral_tap(), Some(true));
343        assert_eq!(profile.mode(), Some(MorseMode::PermissiveHold));
344
345        // Test set_hold_timeout_ms - should not affect other fields
346        profile.set_hold_timeout_ms(1500);
347        assert_eq!(profile.hold_timeout_ms(), Some(1500));
348        assert_eq!(profile.gap_timeout_ms(), Some(2000));
349        assert_eq!(profile.unilateral_tap(), Some(true));
350        assert_eq!(profile.mode(), Some(MorseMode::PermissiveHold));
351
352        // Test set_gap_timeout_ms - should not affect other fields (critical for unilateral_tap)
353        profile.set_gap_timeout_ms(2500);
354        assert_eq!(profile.hold_timeout_ms(), Some(1500));
355        assert_eq!(profile.gap_timeout_ms(), Some(2500));
356        assert_eq!(profile.unilateral_tap(), Some(true));
357        assert_eq!(profile.mode(), Some(MorseMode::PermissiveHold));
358
359        // Test maximum values (14 bits = 0x3FFF)
360        profile.set_hold_timeout_ms(0x3FFF);
361        profile.set_gap_timeout_ms(0x3FFF);
362        assert_eq!(profile.hold_timeout_ms(), Some(0x3FFF));
363        assert_eq!(profile.gap_timeout_ms(), Some(0x3FFF));
364        assert_eq!(profile.unilateral_tap(), Some(true));
365        assert_eq!(profile.mode(), Some(MorseMode::PermissiveHold));
366
367        // Test zero values (should return None)
368        profile.set_hold_timeout_ms(0);
369        profile.set_gap_timeout_ms(0);
370        assert_eq!(profile.hold_timeout_ms(), None);
371        assert_eq!(profile.gap_timeout_ms(), None);
372        assert_eq!(profile.unilateral_tap(), Some(true));
373        assert_eq!(profile.mode(), Some(MorseMode::PermissiveHold));
374    }
375}