From 670d570598fdb7c415da69c3ece6379d40758b86 Mon Sep 17 00:00:00 2001 From: Nikita Shirokov Date: Mon, 9 Feb 2026 12:56:06 +0700 Subject: [PATCH] feat: add Tabber action --- .../keymap_configuration/tabber.md | 85 +++++++ docs/docs/main/docs/configuration/layout.md | 4 +- rmk-config/src/keymap.pest | 11 +- rmk-config/src/layout.rs | 5 + rmk-macro/src/layout.rs | 19 ++ rmk-types/src/action.rs | 4 + rmk/src/keyboard.rs | 37 ++- rmk/src/keyboard/tabber.rs | 84 ++++++ rmk/src/layout_macro.rs | 27 ++ rmk/tests/keyboard_tabber_test.rs | 239 ++++++++++++++++++ 10 files changed, 504 insertions(+), 11 deletions(-) create mode 100644 docs/docs/main/docs/configuration/keymap_configuration/tabber.md create mode 100644 rmk/src/keyboard/tabber.rs create mode 100644 rmk/tests/keyboard_tabber_test.rs diff --git a/docs/docs/main/docs/configuration/keymap_configuration/tabber.md b/docs/docs/main/docs/configuration/keymap_configuration/tabber.md new file mode 100644 index 000000000..a1597c39e --- /dev/null +++ b/docs/docs/main/docs/configuration/keymap_configuration/tabber.md @@ -0,0 +1,85 @@ +# Tabber + +Tabber is a special action that provides Alt+Tab-like tab/window switching behavior. When activated, it sends a `Modifier` + `Tab` key combination and holds the modifier key. Subsequent presses of Tabber send only the `Tab` key. The modifier is automatically released when you switch to a different layer. + +::: warning +Tabber cannot be used in the base layer. This is a safety measure to prevent held modifiers from being stuck. +::: + +## Syntax + +### TOML Configuration + +```toml +# Single modifier +TABBER(LCtrl) + +# Multiple modifiers +TABBER(LCtrl|LGui) +TABBER(LAlt|LShift) +``` + +### Rust Configuration + +```rust +use rmk::action::Action; +use rmk::config::tabber_action::TabberAction; + +Action::Tabber(TabberAction::new(ModifierCombination::LCtrl)) +``` + +## How It Works + +When you press a Tabber key: + +1. **First press**: Sends the specified modifier(s) + Tab, then holds the modifier(s) +2. **Subsequent presses**: Sends only Tab (while modifier is held) +3. **Layer switch**: Releases the held modifier(s) + +This behavior mimics the operating system's Alt+Tab functionality, where holding Alt and pressing Tab cycles through windows. + +## Configuration + +### Available Modifiers + +Any single modifier or combination of modifiers can be used with Tabber: + +| Modifier | Description | +| -------- | --------------------------- | +| `LCtrl` | Left Control | +| `RCtrl` | Right Control | +| `LAlt` | Left Alt (Option on macOS) | +| `RAlt` | Right Alt (Option on macOS) | +| `LGui` | Left GUI (Windows/Command) | +| `RGui` | Right GUI (Windows/Command) | +| `LShift` | Left Shift | +| `RShift` | Right Shift | + +Combine modifiers using `|` (pipe): + +```toml +TABBER(LCtrl|LGui) # Ctrl + Windows +TABBER(LAlt|LShift) # Alt + Shift +``` + +::: tip +You can use any Shift key along with Tabber to reverse the direction of tab/window cycling. +::: + +## Platform-Specific Usage + +### Windows + +- `TABBER(LCtrl)`: Cycle through browser tabs +- `TABBER(LGui)`: Bring up Task View and cycle through windows +- `TABBER(LAlt)`: Cycle through windows in the regular window switcher + +### macOS + +- `TABBER(LCtrl)`: Cycle through browser tabs +- `TABBER(LGui)`: Cycle through windows in Mission Control + +### Linux + +- `TABBER(LCtrl)`: Cycle through browser tabs +- `TABBER(LAlt)`: Cycle through windows in the regular window switcher diff --git a/docs/docs/main/docs/configuration/layout.md b/docs/docs/main/docs/configuration/layout.md index d0c8e5d09..a4740882f 100644 --- a/docs/docs/main/docs/configuration/layout.md +++ b/docs/docs/main/docs/configuration/layout.md @@ -139,7 +139,9 @@ The definitions of these operations are the same as QMK's; you can find them [he 8. For Morse/Tap Dance, use `TD(n)` or `Morse(n)`, they are same -9. For keyboard macros, use `Macro(n)` +9. For [keyboard macros](./keymap_configuration/keyboard_macros.md), use `Macro(n)` + +10. For [Tabber](./keymap_configuration/tabber.md), use `TABBER(modifier)` ## Aliases diff --git a/rmk-config/src/keymap.pest b/rmk-config/src/keymap.pest index d6deac513..208a3639d 100644 --- a/rmk-config/src/keymap.pest +++ b/rmk-config/src/keymap.pest @@ -4,7 +4,7 @@ WHITESPACE = _{ " " | "\t" | "\n" | "\r" } // Optional comments -COMMENT = _{ "//" ~ (!"\n" ~ ANY)* } +COMMENT = _{ "//" ~ (!"\n" ~ ANY)* } // --- Helper Rules --- @@ -23,7 +23,7 @@ number = @{ ASCII_DIGIT+ } layer_number = @{ number } layer_name = @{ loose_identifier } -// The order is important here, as we want to match the number first +// The order is important here, as we want to match the number first layer_reference = _{ layer_number | layer_name } // Modifier Names @@ -46,7 +46,7 @@ no_action = @{ ^"No" ~ !(ASCII_ALPHANUMERIC) } // Case-insensitive "No" not foll // Rule 3: Transparent Key -transparent_action = @{ ("_")+ | (^"Trns" ~ !ASCII_ALPHANUMERIC) } // One or more underscores or "Trns" followed by non-alphanumeric +transparent_action = @{ ("_")+ | (^"Trns" ~ !ASCII_ALPHANUMERIC) } // One or more underscores or "Trns" followed by non-alphanumeric // Rule 1: WM(key, modifier) - Key with Modifier wm_action = { ^"WM" ~ "(" ~ keycode_name ~ "," ~ modifier_combination ~ ")" } @@ -99,12 +99,15 @@ morse_action = { (^"TD" | ^"MORSE") ~ "(" ~ number ~ ")" } // Rule 9: Macro(n) - Trigger Macro trigger_macro_action = { ^"MACRO" ~ "(" ~ number ~ ")" } +// Rule 10: TABBER(modifier) - Tabber Action (holds modifier, taps tab) +tabber_action = { ^"TABBER" ~ "(" ~ modifier_combination ~ ")" } + // --- Top Level Rules --- // A single key action entry in the map // Order is important: more specific function-like rules first, then aliases/specials, then simple keycodes. key_action = _{ // Consume surrounding whitespace/comments implicitly - wm_action | osm_action | layer_action | mt_action | th_action | shifted_action | morse_action | trigger_macro_action | no_action | transparent_action | simple_keycode + wm_action | osm_action | layer_action | mt_action | th_action | shifted_action | morse_action | trigger_macro_action | tabber_action | no_action | transparent_action | simple_keycode } // The entire key map string: Start, zero or more key actions, End. diff --git a/rmk-config/src/layout.rs b/rmk-config/src/layout.rs index f91cc7822..e87be3959 100644 --- a/rmk-config/src/layout.rs +++ b/rmk-config/src/layout.rs @@ -451,6 +451,11 @@ impl KeyboardTomlConfig { key_action_sequence.push(action); } + Rule::tabber_action => { + let action = inner_pair.as_str().to_string(); + key_action_sequence.push(action); + } + Rule::EOI | Rule::WHITESPACE => { // Ignore End of input marker } diff --git a/rmk-macro/src/layout.rs b/rmk-macro/src/layout.rs index dfab451fb..1a73f93a0 100644 --- a/rmk-macro/src/layout.rs +++ b/rmk-macro/src/layout.rs @@ -247,6 +247,25 @@ pub(crate) fn parse_key( ); } } + s if s.to_lowercase().starts_with("tabber(") => { + let prefix = s.get(0..7).unwrap(); + if let Some(internal) = s.trim_start_matches(prefix).strip_suffix(")") { + let modifiers = parse_modifiers(internal); + + if modifiers.is_empty() { + panic!( + "\n❌ keyboard.toml: modifier in Tabber(modifier) is not valid! Please check the documentation: https://rmk.rs/docs/features/configuration/layout.html" + ); + } + quote! { + ::rmk::tabber!(#modifiers) + } + } else { + panic!( + "\n❌ keyboard.toml: Tabber(modifier) invalid, please check the documentation: https://rmk.rs/docs/features/configuration/layout.html" + ); + } + } s if s.to_lowercase().starts_with("lm(") => { let prefix = s.get(0..3).unwrap(); if let Some(internal) = s.trim_start_matches(prefix).strip_suffix(")") { diff --git a/rmk-types/src/action.rs b/rmk-types/src/action.rs index d91151697..8d9d70ec6 100644 --- a/rmk-types/src/action.rs +++ b/rmk-types/src/action.rs @@ -328,6 +328,10 @@ pub enum Action { Special(SpecialKey), /// User Keys User(u8), + /// Tabber action for Alt+Tab-like behavior + /// Holds the specified modifier while repeatedly tapping Tab + /// Cannot be used in base layer (layer 0) - requires higher layer for cleanup mechanism + Tabber(ModifierCombination), } /// Actions for controlling the keyboard or changing the keyboard's state, for example, enable/disable a particular function diff --git a/rmk/src/keyboard.rs b/rmk/src/keyboard.rs index 9afac7cc3..368a147e6 100644 --- a/rmk/src/keyboard.rs +++ b/rmk/src/keyboard.rs @@ -28,6 +28,7 @@ use crate::hid::Report; use crate::input_device::Runnable; use crate::input_device::rotary_encoder::Direction; use crate::keyboard::held_buffer::{HeldBuffer, HeldKey, KeyState}; +use crate::keyboard::tabber::TabberState; use crate::keyboard_macros::MacroOperation; use crate::keymap::KeyMap; use crate::morse::{MorsePattern, TAP}; @@ -40,6 +41,7 @@ pub(crate) mod held_buffer; pub(crate) mod morse; pub(crate) mod mouse; pub(crate) mod oneshot; +pub(crate) mod tabber; const HOLD_BUFFER_SIZE: usize = 16; @@ -211,6 +213,9 @@ pub struct Keyboard<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usi /// Caps Word state machine caps_word: CapsWordState, + /// Tabber state tracking + tabber_state: TabberState, + /// The modifiers coming from (last) Action::KeyWithModifier with_modifiers: ModifierCombination, @@ -263,6 +268,7 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize, const NUM_E osl_state: OneShotState::default(), osm_state: OneShotState::default(), caps_word: CapsWordState::default(), + tabber_state: TabberState::default(), with_modifiers: ModifierCombination::default(), macro_texting: false, macro_caps: false, @@ -1148,18 +1154,22 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize, const NUM_E match action { Action::No => {} Action::Key(key) => self.process_action_key(key, event).await, - Action::LayerOn(layer_num) => self.process_action_layer_switch(layer_num, event), + Action::LayerOn(layer_num) => self.process_action_layer_switch(layer_num, event).await, Action::LayerOff(layer_num) => { // Turn off a layer temporarily when the key is pressed // Reactivate the layer after the key is released if event.pressed { self.keymap.borrow_mut().deactivate_layer(layer_num); + // Clean up Tabber state when layer changes + self.cleanup_tabber_on_layer_change().await; } } Action::LayerToggle(layer_num) => { // Toggle a layer when the key is release if !event.pressed { self.keymap.borrow_mut().toggle_layer(layer_num); + // Clean up Tabber state when layer changes + self.cleanup_tabber_on_layer_change().await; } } Action::LayerToggleOnly(layer_num) => { @@ -1174,11 +1184,15 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize, const NUM_E } // Activate the target layer self.keymap.borrow_mut().activate_layer(layer_num); + // Clean up Tabber state when layers change + self.cleanup_tabber_on_layer_change().await; } } Action::DefaultLayer(layer_num) => { // Set the default layer self.keymap.borrow_mut().set_default_layer(layer_num); + // Clean up Tabber state when default layer changes + self.cleanup_tabber_on_layer_change().await; } Action::Modifier(modifiers) => { if event.pressed { @@ -1213,7 +1227,7 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize, const NUM_E // they will be "released" the same time as the key (in same hid report) self.held_modifiers &= !(modifiers); } - self.process_action_layer_switch(layer_num, event); + self.process_action_layer_switch(layer_num, event).await; self.send_keyboard_report_with_resolved_modifiers(event.pressed).await } Action::OneShotLayer(l) => { @@ -1231,14 +1245,17 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize, const NUM_E Action::KeyboardControl(c) => self.process_action_keyboard_control(c, event).await, Action::Special(special_key) => self.process_action_special(special_key, event).await, Action::User(id) => self.process_user(id, event).await, + Action::Tabber(modifiers) => { + self.process_action_tabber(modifiers, event).await; + } Action::TriLayerLower => { // Tri-layer lower, turn layer 1 on and update layer state - self.process_action_layer_switch(1, event); + self.process_action_layer_switch(1, event).await; self.keymap.borrow_mut().update_fn_layer_state(); } Action::TriLayerUpper => { // Tri-layer upper, turn layer 2 on and update layer state - self.process_action_layer_switch(2, event); + self.process_action_layer_switch(2, event).await; self.keymap.borrow_mut().update_fn_layer_state(); } } @@ -1363,6 +1380,7 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize, const NUM_E /// Calculates the combined effect of "explicit modifiers": /// - registered modifiers /// - one-shot modifiers + /// - tabber modifiers (when active) pub fn resolve_explicit_modifiers(&self, pressed: bool) -> ModifierCombination { // if a one-shot modifier is active, decorate the hid report of keypress with those modifiers let mut result = self.held_modifiers; @@ -1380,6 +1398,11 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize, const NUM_E result |= osm; }; + // Add tabber modifiers if tabber is active + if let Some(tabber_mods) = self.tabber_state.value() { + result |= *tabber_mods; + } + result } @@ -1542,12 +1565,14 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize, const NUM_E } /// Process layer switch action. - fn process_action_layer_switch(&mut self, layer_num: u8, event: KeyboardEvent) { + async fn process_action_layer_switch(&mut self, layer_num: u8, event: KeyboardEvent) { // Change layer state only when the key's state is changed if event.pressed { self.keymap.borrow_mut().activate_layer(layer_num); } else { self.keymap.borrow_mut().deactivate_layer(layer_num); + // Clean up Tabber state when layer changes + self.cleanup_tabber_on_layer_change().await; } } @@ -2582,7 +2607,7 @@ mod test { let mut keyboard = create_test_keyboard(); // Activate layer 1 - keyboard.process_action_layer_switch(1, KeyboardEvent::key(0, 0, true)); + keyboard.process_action_layer_switch(1, KeyboardEvent::key(0, 0, true)).await; // Press Transparent key (Q on lower layer) keyboard.process_inner(KeyboardEvent::key(1, 1, true)).await; diff --git a/rmk/src/keyboard/tabber.rs b/rmk/src/keyboard/tabber.rs new file mode 100644 index 000000000..995eb03da --- /dev/null +++ b/rmk/src/keyboard/tabber.rs @@ -0,0 +1,84 @@ +//! Tabber action implementation +//! +//! Tabber provides Alt+Tab-like window switching behavior by holding a modifier +//! while repeatedly tapping Tab. The modifier is held across multiple key presses +//! until the layer changes. + +use rmk_types::keycode::HidKeyCode; +use rmk_types::modifier::ModifierCombination; + +use crate::event::KeyboardEvent; +use crate::keyboard::Keyboard; + +/// State for Tabber action +#[derive(Default)] +pub(crate) enum TabberState { + /// Tabber is active and holding modifiers + Active(T), + /// Tabber is inactive + #[default] + None, +} + +impl TabberState { + /// Get the held modifiers if Tabber is active + pub fn value(&self) -> Option<&T> { + match self { + TabberState::Active(mods) => Some(mods), + TabberState::None => None, + } + } +} + +impl + Keyboard<'_, ROW, COL, NUM_LAYER, NUM_ENCODER> +{ + /// Process Tabber action + /// + /// Mechanism: + /// - First press: Send Modifier + Tab + /// - First release: Release Tab, keep modifier held (via TabberState) + /// - Subsequent presses: Send Tab only (modifier stays held via resolve_explicit_modifiers) + /// - Subsequent releases: Release Tab only (modifier stays held via resolve_explicit_modifiers) + /// - Layer change: Cleanup releases all Tabber-held modifiers + /// - Shift integration: If Shift is held when Tabber is pressed, add Shift to Tab + pub(crate) async fn process_action_tabber(&mut self, modifiers: ModifierCombination, event: KeyboardEvent) { + // Safety check: Tabber cannot be used in base layer (layer 0) + // This is essential because layer 0 has no mechanism to trigger cleanup + let current_layer = self.keymap.borrow().get_activated_layer(); + if current_layer == 0 { + warn!("Tabber action cannot be used in base layer (layer 0)"); + return; + } + + if event.pressed { + // Key press + if let TabberState::None = self.tabber_state { + self.tabber_state = TabberState::Active(modifiers); + } + + self.register_key(HidKeyCode::Tab, event); + self.send_keyboard_report_with_resolved_modifiers(true).await; + } else { + // Key release + if let TabberState::Active(_) = self.tabber_state { + self.unregister_key(HidKeyCode::Tab, event); + self.send_keyboard_report_with_resolved_modifiers(false).await; + } + } + } + + /// Clean up Tabber state when layer changes + /// + /// This should be called after any layer deactivation to ensure + /// Tabber-held modifiers are properly released. + pub(crate) async fn cleanup_tabber_on_layer_change(&mut self) { + if let TabberState::Active(_) = self.tabber_state { + debug!("Cleaning up Tabber due to layer change"); + // Clear Tabber state + self.tabber_state = TabberState::None; + // Send a report to reflect the modifier release + self.send_keyboard_report_with_resolved_modifiers(false).await; + } + } +} diff --git a/rmk/src/layout_macro.rs b/rmk/src/layout_macro.rs index 5168f9b88..105c1f577 100644 --- a/rmk/src/layout_macro.rs +++ b/rmk/src/layout_macro.rs @@ -351,6 +351,33 @@ macro_rules! osm { }; } +/// Create a Tabber action for Alt+Tab-like tab/window switching. +/// +/// This macro creates a key that holds a modifier and presses Tab. +/// Subsequent presses of Tabber action send only Tab key while the modifier keeps being held. +/// +/// **Important**: Tabber cannot be used in base layer (layer 0). It requires a +/// higher layer to provide a cleanup mechanism when returning to layer 0. +/// +/// # Parameters +/// - `$m`: `ModifierCombination` to hold while tapping Tab +/// +/// # Example +/// ```ignore +/// // Alt+Tab window switcher +/// tabber!(ModifierCombination::LALT) +/// // Gui+Tab (macOS application switcher) +/// tabber!(ModifierCombination::LGUI) +/// // Ctrl+Tab (browser tab switcher) +/// tabber!(ModifierCombination::LCTRL) +/// ``` +#[macro_export] +macro_rules! tabber { + ($m: expr) => { + $crate::types::action::KeyAction::Single($crate::types::action::Action::Tabber($m)) + }; +} + /// Create a layer toggle action. /// /// This macro creates a key that toggles a layer on/off with each press. diff --git a/rmk/tests/keyboard_tabber_test.rs b/rmk/tests/keyboard_tabber_test.rs new file mode 100644 index 000000000..31e133b37 --- /dev/null +++ b/rmk/tests/keyboard_tabber_test.rs @@ -0,0 +1,239 @@ +//! Tests for Tabber action +//! +//! Tabber provides Alt+Tab-like window switching behavior + +pub mod common; + +use rmk::config::BehaviorConfig; +use rmk::types::modifier::ModifierCombination; + +mod tabber_test { + use std::cell::RefCell; + + use rmk::config::PositionalConfig; + use rmk::keyboard::Keyboard; + use rmk::keymap::KeyMap; + use rmk::types::action::{Action, KeyAction}; + use rmk::{a, k, mo}; + use rusty_fork::rusty_fork_test; + + use super::*; + use crate::common::{KC_LALT, KC_LCTRL, KC_LGUI, KC_LSHIFT, wrap_keymap}; + + // KEYMAP + // Layer 0: A B C MO(1) LShift RShift + // Layer 1: Tabber(LGui) Tabber(LCtrl) Tabber(LAlt) Transparent Transparent Transparent + + const KEYMAP: [[[KeyAction; 6]; 1]; 2] = [ + [[ + // Layer 0 + k!(A), + k!(B), + k!(C), + mo!(1), // MO(1) to activate layer 1 + k!(LShift), + k!(RShift), + ]], + [[ + // Layer 1 + KeyAction::Single(Action::Tabber(ModifierCombination::LGUI)), + KeyAction::Single(Action::Tabber(ModifierCombination::LCTRL)), + KeyAction::Single(Action::Tabber(ModifierCombination::LALT)), + a!(Transparent), // MO(1) is transparent + a!(Transparent), // LShift is transparent + a!(Transparent), // RShift is transparent + ]], + ]; + + fn create_test_keyboard() -> Keyboard<'static, 1, 6, 2> { + static BEHAVIOR_CONFIG: static_cell::StaticCell = static_cell::StaticCell::new(); + let behavior_config = BEHAVIOR_CONFIG.init(BehaviorConfig::default()); + static KEY_CONFIG: static_cell::StaticCell> = static_cell::StaticCell::new(); + let per_key_config = KEY_CONFIG.init(PositionalConfig::default()); + let keymap: &RefCell> = wrap_keymap(KEYMAP, per_key_config, behavior_config); + Keyboard::new(keymap) + } + + rusty_fork_test! { + /// Tabber Test Case 1: Basic Flow + /// + /// Sequence: + /// - Press MO(1) to activate layer 1 + /// - Press Tabber(LGui) → Should send LGui+Tab + /// - Release Tabber → Should release Tab, keep LGui held + /// - Press Tabber again → Should send Tab only + /// - Release Tabber → Should release Tab only + /// - Release MO(1) → Should release LGui + /// + /// Expected: + /// - LGui+Tab on first press + /// - Only LGui held after first release + /// - LGui+Tab on second press + /// - Only LGui held after second release + /// - All released after MO(1) release + #[test] + fn test_tabber_basic_flow() { + key_sequence_test! { + keyboard: create_test_keyboard(), + sequence: [ + [0, 3, true, 10], // Press MO(1) + [0, 0, true, 10], // Press Tabber(LGui) + [0, 0, false, 10], // Release Tabber + [0, 0, true, 10], // Press Tabber again + [0, 0, false, 10], // Release Tabber again + [0, 3, false, 10], // Release MO(1) + ], + expected_reports: [ + [KC_LGUI, [kc_to_u8!(Tab), 0, 0, 0, 0, 0]], // LGui+Tab on first press + [KC_LGUI, [0, 0, 0, 0, 0, 0]], // Only LGui held after first release + [KC_LGUI, [kc_to_u8!(Tab), 0, 0, 0, 0, 0]], // LGui+Tab on second press + [KC_LGUI, [0, 0, 0, 0, 0, 0]], // Only LGui held after second release + [0, [0, 0, 0, 0, 0, 0]], // All released after MO(1) release + ] + }; + } + + /// Tabber Test Case 2: Shift Integration + /// + /// Sequence: + /// - Press MO(1) to activate layer 1 + /// - Press Tabber(LCtrl) → Should send LCtrl+Tab + /// - Release Tabber → Should release Tab, keep LCtrl held + /// - Press LShift + /// - Press Tabber → Should send LCtrl+LShift+Tab + /// - Release Tabber → Should release Tab, keep LCtrl held + /// - Release LShift + /// - Release MO(1) → Should release LCtrl + /// + /// Expected: + /// - LCtrl+Tab on first press + /// - Only LCtrl held after first release + /// - LShift is added + /// - LCtrl+LShift+Tab on second press + /// - LCtrl+LShift held after second release + /// - Only LCtrl held after LShift release + /// - All released after MO(1) release + #[test] + fn test_tabber_with_shift() { + key_sequence_test! { + keyboard: create_test_keyboard(), + sequence: [ + [0, 3, true, 10], // Press MO(1) + [0, 1, true, 10], // Press Tabber(LCtrl) + [0, 1, false, 10], // Release Tabber(LCtrl) + [0, 4, true, 10], // Press LShift + [0, 1, true, 10], // Press Tabber(LCtrl) + [0, 1, false, 10], // Release Tabber(LCtrl) + [0, 4, false, 10], // Release LShift + [0, 3, false, 10], // Release MO(1) + ], + expected_reports: [ + [KC_LCTRL, [kc_to_u8!(Tab), 0, 0, 0, 0, 0]], // LCtrl+Tab on first press + [KC_LCTRL, [0, 0, 0, 0, 0, 0]], // Only LCtrl held + [KC_LCTRL | KC_LSHIFT, [0, 0, 0, 0, 0, 0]], // LShift pressed + [KC_LCTRL | KC_LSHIFT, [kc_to_u8!(Tab), 0, 0, 0, 0, 0]], // LCtrl+LShift+Tab + [KC_LCTRL | KC_LSHIFT, [0, 0, 0, 0, 0, 0]], // LCtrl+LShift held + [KC_LCTRL, [0, 0, 0, 0, 0, 0]], // Only LCtrl held + [0, [0, 0, 0, 0, 0, 0]], // All released + ] + }; + } + + /// Tabber Test Case 3: Rapid Presses + /// + /// Sequence: + /// - Press MO(1) to activate layer 1 + /// - Rapidly press and release Tabber 3 times + /// - Release MO(1) + /// + /// Expected: + /// - LGui+Tab on each press + /// - Only LGui held after each release + /// - All released after MO(1) release + #[test] + fn test_tabber_rapid_presses() { + key_sequence_test! { + keyboard: create_test_keyboard(), + sequence: [ + [0, 3, true, 10], // Press MO(1) + [0, 0, true, 10], // Press Tabber + [0, 0, false, 10], // Release Tabber + [0, 0, true, 10], // Press Tabber + [0, 0, false, 10], // Release Tabber + [0, 0, true, 10], // Press Tabber + [0, 0, false, 10], // Release Tabber + [0, 3, false, 10], // Release MO(1) + ], + expected_reports: [ + [KC_LGUI, [kc_to_u8!(Tab), 0, 0, 0, 0, 0]], // LGui+Tab + [KC_LGUI, [0, 0, 0, 0, 0, 0]], // Only LGui held + [KC_LGUI, [kc_to_u8!(Tab), 0, 0, 0, 0, 0]], // LGui+Tab + [KC_LGUI, [0, 0, 0, 0, 0, 0]], // Only LGui held + [KC_LGUI, [kc_to_u8!(Tab), 0, 0, 0, 0, 0]], // LGui+Tab + [KC_LGUI, [0, 0, 0, 0, 0, 0]], // Only LGui held + [0, [0, 0, 0, 0, 0, 0]], // All released + ] + }; + } + + /// Tabber Test Case 4: Different Modifiers + /// + /// Sequence: + /// - Press MO(1) to activate layer 1 + /// - Press Tabber(LAlt) + /// - Release Tabber + /// - Release MO(1) + /// + /// Expected: + /// - LAlt+Tab on press + /// - Only LAlt held after release + /// - All released after MO(1) release + #[test] + fn test_tabber_with_alt() { + key_sequence_test! { + keyboard: create_test_keyboard(), + sequence: [ + [0, 3, true, 10], // Press MO(1) + [0, 2, true, 10], // Press Tabber(LAlt) + [0, 2, false, 10], // Release Tabber + [0, 3, false, 10], // Release MO(1) + ], + expected_reports: [ + [KC_LALT, [kc_to_u8!(Tab), 0, 0, 0, 0, 0]], // LAlt+Tab + [KC_LALT, [0, 0, 0, 0, 0, 0]], // Only LAlt held + [0, [0, 0, 0, 0, 0, 0]], // All released + ] + }; + } + + /// Tabber Test Case 5: Layer Change Cleanup + /// + /// Sequence: + /// - Press MO(1) to activate layer 1 + /// - Press Tabber(LGui) + /// - Release Tabber + /// - Release MO(1) immediately (should clean up) + /// + /// Expected: + /// - LGui+Tab on press + /// - Only LGui held after release + /// - All released after MO(1) release (cleanup) + #[test] + fn test_tabber_layer_change_cleanup() { + key_sequence_test! { + keyboard: create_test_keyboard(), + sequence: [ + [0, 3, true, 10], // Press MO(1) + [0, 0, true, 10], // Press Tabber(LGui) + [0, 0, false, 10], // Release Tabber + [0, 3, false, 10], // Release MO(1) - should clean up Tabber + ], + expected_reports: [ + [KC_LGUI, [kc_to_u8!(Tab), 0, 0, 0, 0, 0]], // LGui+Tab + [KC_LGUI, [0, 0, 0, 0, 0, 0]], // Only LGui held + [0, [0, 0, 0, 0, 0, 0]], // All released (cleanup) + ] + }; + } + } +}