Repository: HaoboGu/rmk (language: rust)

Issue:
Tri Layer support
Reference:
- QMK Tri Layers: https://docs.qmk.fm/features/tri_layer
- ZMK Conditional Layers: https://zmk.dev/docs/keymaps/conditional-layers


Discussion / hints:
I was thinking about adding an optional parameter to the layout config to specify the corresponding layers.

Fix patch:
diff --git a/docs/src/keyboard_configuration.md b/docs/src/keyboard_configuration.md
index 14ddd9b9..2cc38df0 100644
--- a/docs/src/keyboard_configuration.md
+++ b/docs/src/keyboard_configuration.md
@@ -147,6 +147,30 @@ The key string should follow several rules:
     
   The definitions of those operations are same with QMK, you can found [here](https://docs.qmk.fm/#/feature_layers). If you want other actions, please [fire an issue](https://github.com/HaoboGu/rmk/issues/new).
 
+
+### `[behavior]`
+
+`[behavior]` section contains configuration for how different keyboard actions should behave:
+
+```toml
+[behavior]
+tri_layer = { uppper = 1, lower = 2, adjust = 3 }
+```
+
+#### Tri Layer
+
+`Tri Layer` works by enabling a layer (called `adjust`) when other two layers (`upper` and `lower`) are both enabled.
+
+You can enable Tri Layer by specifying the `upper`, `lower` and `adjust` layers in the `tri_layer` sub-table:
+
+```toml
+[behavior.tri_layer]
+uppper = 1
+lower = 2
+adjust = 3
+```
+In this example, when both layers 1 (`upper`) and 2 (`lower`) are active, layer 3 (`adjust`) will also be enabled.
+
 ### `[light]`
 
 `[light]` section defines lights of the keyboard, aka `capslock`, `scrolllock` and `numslock`. They are actually an input pin, so there are two fields available: `pin` and `low_active`.
@@ -290,6 +314,11 @@ keymap = [
     ],
 ]
 
+# Behavior configuration, if you don't want to customize anything, just ignore this section.
+[behavior]
+# Tri Layer configuration.
+tri_layer = { uppper = 1, lower = 2, adjust = 3 }
+
 # Lighting configuration, if you don't have any light, just ignore this section.
 [light]
 # LED pins, capslock, scrolllock, numslock. You can safely ignore any of them if you don't have
diff --git a/rmk-config/src/keyboard_config.rs b/rmk-config/src/keyboard_config.rs
index 051ddf30..cfeecf65 100644
--- a/rmk-config/src/keyboard_config.rs
+++ b/rmk-config/src/keyboard_config.rs
@@ -17,6 +17,7 @@ pub struct RmkConfig<'a, O: OutputPin> {
     pub vial_config: VialConfig<'a>,
     pub light_config: LightConfig<O>,
     pub storage_config: StorageConfig,
+    pub behavior_config: BehaviorConfig,
     #[cfg(feature = "_nrf_ble")]
     pub ble_battery_config: BleBatteryConfig<'a>,
     #[cfg(feature = "_esp_ble")]
@@ -31,12 +32,19 @@ impl<'a, O: OutputPin> Default for RmkConfig<'a, O> {
             vial_config: VialConfig::default(),
             light_config: LightConfig::default(),
             storage_config: StorageConfig::default(),
+            behavior_config: BehaviorConfig::default(),
             #[cfg(any(feature = "_nrf_ble", feature = "_esp_ble"))]
             ble_battery_config: BleBatteryConfig::default(),
         }
     }
 }
 
+/// Config for configurable action behavior
+#[derive(Default)]
+pub struct BehaviorConfig {
+    pub tri_layer: Option<[u8; 3]>,
+}
+
 /// Config for storage
 #[derive(Clone, Copy, Debug)]
 pub struct StorageConfig {
diff --git a/rmk-config/src/toml_config.rs b/rmk-config/src/toml_config.rs
index 8a9b62cf..ffdf7038 100644
--- a/rmk-config/src/toml_config.rs
+++ b/rmk-config/src/toml_config.rs
@@ -10,6 +10,8 @@ pub struct KeyboardTomlConfig {
     /// Layout config.
     /// For split keyboard, the total row/col should be defined in this section
     pub layout: LayoutConfig,
+    /// Behavior config
+    pub behavior: Option<BehaviorConfig>,
     /// Light config
     pub light: Option<LightConfig>,
     /// Storage config
@@ -124,6 +126,20 @@ pub struct LayoutConfig {
     pub keymap: Vec<Vec<Vec<String>>>,
 }
 
+/// Configurations for actions behavior
+#[derive(Clone, Debug, Default, Deserialize)]
+pub struct BehaviorConfig {
+    pub tri_layer: Option<TriLayerConfig>,
+}
+
+/// Configurations for tri layer
+#[derive(Clone, Debug, Deserialize)]
+pub struct TriLayerConfig {
+    pub upper: u8,
+    pub lower: u8,
+    pub adjust: u8,
+}
+
 /// Configurations for split keyboards
 #[derive(Clone, Debug, Default, Deserialize)]
 pub struct SplitConfig {
diff --git a/rmk-macro/src/behavior.rs b/rmk-macro/src/behavior.rs
new file mode 100644
index 00000000..79ffe5ea
--- /dev/null
+++ b/rmk-macro/src/behavior.rs
@@ -0,0 +1,29 @@
+//! Initialize behavior config boilerplate of RMK
+//!
+
+use quote::quote;
+use rmk_config::toml_config::TriLayerConfig;
+
+use crate::keyboard_config::KeyboardConfig;
+
+fn expand_tri_layer(tri_layer: &Option<TriLayerConfig>) -> proc_macro2::TokenStream {
+    match tri_layer {
+        Some(tri_layer) => {
+            let upper = tri_layer.upper;
+            let lower = tri_layer.lower;
+            let adjust = tri_layer.adjust;
+            quote! {::core::option::Option::Some([#upper, #lower, #adjust])}
+        }
+        None => quote! {::core::option::Option::None::<[u8; 3]>},
+    }
+}
+
+pub(crate) fn expand_behavior_config(keyboard_config: &KeyboardConfig) -> proc_macro2::TokenStream {
+    let tri_layer = expand_tri_layer(&keyboard_config.behavior.tri_layer);
+
+    quote! {
+        let behavior_config = ::rmk::config::keyboard_config::BehaviorConfig {
+            tri_layer: #tri_layer,
+        };
+    }
+}
diff --git a/rmk-macro/src/keyboard.rs b/rmk-macro/src/keyboard.rs
index 27619df7..8e79918a 100644
--- a/rmk-macro/src/keyboard.rs
+++ b/rmk-macro/src/keyboard.rs
@@ -4,6 +4,7 @@ use quote::quote;
 use syn::ItemMod;
 
 use crate::{
+    behavior::expand_behavior_config,
     bind_interrupt::expand_bind_interrupt,
     ble::expand_ble_config,
     chip_init::expand_chip_init,
@@ -115,6 +116,7 @@ fn expand_main(
     let usb_init = expand_usb_init(keyboard_config, &item_mod);
     let flash_init = expand_flash_init(keyboard_config);
     let light_config = expand_light_config(keyboard_config);
+    let behavior_config = expand_behavior_config(keyboard_config);
     let matrix_config = expand_matrix_config(keyboard_config, async_matrix);
     let run_rmk = expand_rmk_entry(keyboard_config, &item_mod);
     let (ble_config, set_ble_config) = expand_ble_config(keyboard_config);
@@ -151,6 +153,9 @@ fn expand_main(
             // Initialize light config as `light_config`
             #light_config
 
+            // Initialize behavior config config as `behavior_config`
+            #behavior_config
+
             // Initialize matrix config as `(input_pins, output_pins)` or `direct_pins`
             #matrix_config
 
@@ -162,6 +167,7 @@ fn expand_main(
                 vial_config: VIAL_CONFIG,
                 light_config,
                 storage_config,
+                behavior_config,
                 #set_ble_config
                 ..Default::default()
             };
diff --git a/rmk-macro/src/keyboard_config.rs b/rmk-macro/src/keyboard_config.rs
index fae57983..60f348d8 100644
--- a/rmk-macro/src/keyboard_config.rs
+++ b/rmk-macro/src/keyboard_config.rs
@@ -1,8 +1,8 @@
 use proc_macro2::TokenStream as TokenStream2;
 use quote::quote;
 use rmk_config::toml_config::{
-    BleConfig, DependencyConfig, KeyboardInfo, KeyboardTomlConfig, LayoutConfig, LightConfig,
-    MatrixConfig, MatrixType, SplitConfig, StorageConfig,
+    BehaviorConfig, BleConfig, DependencyConfig, KeyboardInfo, KeyboardTomlConfig, LayoutConfig,
+    LightConfig, MatrixConfig, MatrixType, SplitConfig, StorageConfig,
 };
 use serde::Deserialize;
 use std::fs;
@@ -71,6 +71,8 @@ pub(crate) struct KeyboardConfig {
     pub(crate) board: BoardConfig,
     // Layout config
     pub(crate) layout: LayoutConfig,
+    // Begavior Config
+    pub(crate) behavior: BehaviorConfig,
     // Light config
     pub(crate) light: LightConfig,
     // Storage config
@@ -164,6 +166,10 @@ impl KeyboardConfig {
         // Layout config
         config.layout = Self::get_layout_from_toml(toml_config.layout)?;
 
+        // Behavior config
+        config.behavior =
+            Self::get_behavior_from_toml(config.behavior, toml_config.behavior, &config.layout)?;
+
         // Light config
         config.light = Self::get_light_from_toml(config.light, toml_config.light);
 
@@ -404,6 +410,40 @@ impl KeyboardConfig {
         Ok(layout)
     }
 
+    fn get_behavior_from_toml(
+        default: BehaviorConfig,
+        toml: Option<BehaviorConfig>,
+        layout: &LayoutConfig,
+    ) -> Result<BehaviorConfig, TokenStream2> {
+        match toml {
+            Some(mut behavior) => {
+                // Use default setting if the corresponding field is not set
+                behavior.tri_layer = match behavior.tri_layer {
+                    Some(tri_layer) => {
+                        if tri_layer.upper >= layout.layers {
+                            return rmk_compile_error!(
+                                "keyboard.toml: Tri layer upper is larger than [layout.layers]"
+                            );
+                        } else if tri_layer.lower >= layout.layers {
+                            return rmk_compile_error!(
+                                "keyboard.toml: Tri layer lower is larger than [layout.layers]"
+                            );
+                        } else if tri_layer.adjust >= layout.layers {
+                            return rmk_compile_error!(
+                                "keyboard.toml: Tri layer adjust is larger than [layout.layers]"
+                            );
+                        }
+                        Some(tri_layer)
+                    }
+                    None => default.tri_layer,
+                };
+
+                Ok(behavior)
+            }
+            None => Ok(default),
+        }
+    }
+
     fn get_light_from_toml(default: LightConfig, toml: Option<LightConfig>) -> LightConfig {
         match toml {
             Some(mut light_config) => {
diff --git a/rmk-macro/src/lib.rs b/rmk-macro/src/lib.rs
index 8161e27a..8566599c 100644
--- a/rmk-macro/src/lib.rs
+++ b/rmk-macro/src/lib.rs
@@ -1,3 +1,4 @@
+mod behavior;
 mod bind_interrupt;
 mod ble;
 mod chip_init;
diff --git a/rmk-macro/src/split/central.rs b/rmk-macro/src/split/central.rs
index 03d08aeb..e8038bd5 100644
--- a/rmk-macro/src/split/central.rs
+++ b/rmk-macro/src/split/central.rs
@@ -6,6 +6,7 @@ use rmk_config::toml_config::{SerialConfig, SplitConfig};
 use syn::ItemMod;
 
 use crate::{
+    behavior::expand_behavior_config,
     bind_interrupt::expand_bind_interrupt,
     ble::expand_ble_config,
     chip_init::expand_chip_init,
@@ -74,6 +75,7 @@ fn expand_split_central(
     let usb_init = expand_usb_init(keyboard_config, &item_mod);
     let flash_init = expand_flash_init(keyboard_config);
     let light_config = expand_light_config(keyboard_config);
+    let behavior_config = expand_behavior_config(keyboard_config);
 
     let matrix_config = expand_matrix_input_output_pins(
         &keyboard_config.chip,
@@ -118,6 +120,9 @@ fn expand_split_central(
             // Initialize light config as `light_config`
             #light_config
 
+            // Initialize behavior config config as `behavior_config`
+            #behavior_config
+
             // Initialize matrix config as `(input_pins, output_pins)`
             #matrix_config
 
@@ -130,6 +135,7 @@ fn expand_split_central(
                 vial_config: VIAL_CONFIG,
                 light_config,
                 storage_config,
+                behavior_config,
                 #set_ble_config
                 ..Default::default()
             };
diff --git a/rmk/src/ble/esp/mod.rs b/rmk/src/ble/esp/mod.rs
index 0c0ac008..cd8a1fd1 100644
--- a/rmk/src/ble/esp/mod.rs
+++ b/rmk/src/ble/esp/mod.rs
@@ -62,7 +62,11 @@ pub(crate) async fn initialize_esp_ble_keyboard_with_config_and_run<
     let keyboard_report_sender = keyboard_report_channel.sender();
     let keyboard_report_receiver = keyboard_report_channel.receiver();
 
-    let mut keyboard = Keyboard::new(&keymap, &keyboard_report_sender);
+    let mut keyboard = Keyboard::new(
+        &keymap,
+        &keyboard_report_sender,
+        keyboard_config.behavior_config,
+    );
     // esp32c3 doesn't have USB device, so there is no usb here
     // TODO: add usb service for other chips of esp32 which have USB device
 
diff --git a/rmk/src/ble/nrf/mod.rs b/rmk/src/ble/nrf/mod.rs
index f41dd9ff..56f6283a 100644
--- a/rmk/src/ble/nrf/mod.rs
+++ b/rmk/src/ble/nrf/mod.rs
@@ -292,7 +292,11 @@ pub(crate) async fn initialize_nrf_ble_keyboard_and_run<
     let keyboard_report_receiver = keyboard_report_channel.receiver();
 
     // Keyboard services
-    let mut keyboard = Keyboard::new(&keymap, &keyboard_report_sender);
+    let mut keyboard = Keyboard::new(
+        &keymap,
+        &keyboard_report_sender,
+        keyboard_config.behavior_config,
+    );
     #[cfg(not(feature = "_no_usb"))]
     let mut usb_device = KeyboardUsbDevice::new(usb_driver, keyboard_config.usb_config);
     let mut vial_service = VialService::new(&keymap, keyboard_config.vial_config);
diff --git a/rmk/src/keyboard.rs b/rmk/src/keyboard.rs
index 33627182..31fa0496 100644
--- a/rmk/src/keyboard.rs
+++ b/rmk/src/keyboard.rs
@@ -17,6 +17,7 @@ use embassy_sync::{
 use embassy_time::{Instant, Timer};
 use heapless::{FnvIndexMap, Vec};
 use postcard::experimental::max_size::MaxSize;
+use rmk_config::BehaviorConfig;
 use serde::{Deserialize, Serialize};
 use usbd_hid::descriptor::KeyboardReport;
 
@@ -131,6 +132,9 @@ pub(crate) struct Keyboard<'a, const ROW: usize, const COL: usize, const NUM_LAY
     /// Timer which records the timestamp of key changes
     pub(crate) timer: [[Option<Instant>; ROW]; COL],
 
+    /// Options for configurable action behavior
+    behavior: BehaviorConfig,
+
     /// One shot modifier state
     osm_state: OneShotState<ModifierCombination>,
 
@@ -163,11 +167,13 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize>
     pub(crate) fn new(
         keymap: &'a RefCell<KeyMap<'a, ROW, COL, NUM_LAYER>>,
         sender: &'a Sender<'a, CriticalSectionRawMutex, KeyboardReportMessage, REPORT_CHANNEL_SIZE>,
+        behavior: BehaviorConfig,
     ) -> Self {
         Keyboard {
             keymap,
             sender,
             timer: [[None; ROW]; COL],
+            behavior,
             osm_state: OneShotState::default(),
             osl_state: OneShotState::default(),
             unprocessed_events: Vec::new(),
@@ -294,6 +300,11 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize>
                     .await;
             }
         }
+
+        // Tri Layer
+        if let Some(ref tri_layer) = self.behavior.tri_layer {
+            self.keymap.borrow_mut().update_tri_layer(tri_layer);
+        }
     }
 
     async fn update_osm(&mut self, key_event: KeyEvent) {
diff --git a/rmk/src/keymap.rs b/rmk/src/keymap.rs
index feaf0bdb..b4674eb8 100644
--- a/rmk/src/keymap.rs
+++ b/rmk/src/keymap.rs
@@ -253,6 +253,12 @@ impl<'a, const ROW: usize, const COL: usize, const NUM_LAYER: usize>
         self.layer_cache[row][col] = layer_num;
     }
 
+    /// Update given Tri Layer state
+    pub(crate) fn update_tri_layer(&mut self, tri_layer: &[u8; 3]) {
+        self.layer_state[tri_layer[2] as usize] =
+            self.layer_state[tri_layer[0] as usize] && self.layer_state[tri_layer[1] as usize];
+    }
+
     /// Activate given layer
     pub(crate) fn activate_layer(&mut self, layer_num: u8) {
         if layer_num as usize >= NUM_LAYER {
diff --git a/rmk/src/lib.rs b/rmk/src/lib.rs
index 8488c4e8..f1291464 100644
--- a/rmk/src/lib.rs
+++ b/rmk/src/lib.rs
@@ -251,7 +251,11 @@ pub(crate) async fn initialize_usb_keyboard_and_run<
 
     // Create keyboard services and devices
     let (mut keyboard, mut usb_device, mut vial_service, mut light_service) = (
-        Keyboard::new(&keymap, &keyboard_report_sender),
+        Keyboard::new(
+            &keymap,
+            &keyboard_report_sender,
+            keyboard_config.behavior_config,
+        ),
         KeyboardUsbDevice::new(usb_driver, keyboard_config.usb_config),
         VialService::new(&keymap, keyboard_config.vial_config),
         LightService::from_config(keyboard_config.light_config),
diff --git a/rmk/src/split/central.rs b/rmk/src/split/central.rs
index 6f00f90a..f49651c2 100644
--- a/rmk/src/split/central.rs
+++ b/rmk/src/split/central.rs
@@ -194,7 +194,11 @@ pub(crate) async fn initialize_usb_split_central_and_run<
 
     // Create keyboard services and devices
     let (mut keyboard, mut usb_device, mut vial_service, mut light_service) = (
-        Keyboard::new(&keymap, &keyboard_report_sender),
+        Keyboard::new(
+            &keymap,
+            &keyboard_report_sender,
+            keyboard_config.behavior_config,
+        ),
         KeyboardUsbDevice::new(usb_driver, keyboard_config.usb_config),
         VialService::new(&keymap, keyboard_config.vial_config),
         LightService::from_config(keyboard_config.light_config),


Test patch: