Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions features/keymap/key/tri_state.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
Feature: Tri-state Key

A tri-state key owns a session across later presses of the same key.
The first press starts (typically a modifier and tap a key),
later presses of the same key continue (tap again while the hold stays),
and any other resolved key interrupts (releases the hold).

Classic use is Alt-Tab (a "swapper"):
one key holds Alt and taps Tab on the first press,
taps Tab again on re-press,
and releases Alt when another key is pressed.

For examples of this key in other smart keyboard firmware, see e.g.:

- [Getreuer's Cyclotab](https://getreuer.info/posts/keyboards/cyclotab/)

- [DhruvinSh's ZMK tri-state](https://github.com/dhruvinsh/zmk-tri-state)

Background:

Given a keymap.ncl:
"""
let K = import "keys.ncl" in
{
keys = [
K.tri_state.alt_tab,
K.A,
]
}
"""

Example: first press is the Alt+Tab chord; Alt stays after release
When the keymap registers the following input
"""
[
press K.tri_state.alt_tab,
]
"""
Then the HID keyboard report should equal
"""
{ modifiers = { left_alt = true }, key_codes = [K.Tab] }
"""

Example: releasing the tri-state key leaves Alt held
When the keymap registers the following input
"""
[
tap K.tri_state.alt_tab,
]
"""
Then the HID keyboard report should equal
"""
{ modifiers = { left_alt = true } }
"""

Example: re-press leaves Alt held
When the keymap registers the following input
"""
[
tap K.tri_state.alt_tab,
tap K.tri_state.alt_tab,
]
"""
Then the HID keyboard report should equal
"""
{ modifiers = { left_alt = true } }
"""
1 change: 1 addition & 0 deletions ncl/key-extensions.ncl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
& (import "smart_keys/sequence/key-extensions.ncl")
& (import "smart_keys/sticky/key-extensions.ncl")
& (import "smart_keys/tap_hold/key-extensions.ncl")
& (import "smart_keys/tri_state/key-extensions.ncl")
& {
extend_keys = fun K key_extension =>
std.typeof key_extension
Expand Down
21 changes: 21 additions & 0 deletions ncl/key_system/families.ncl
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,27 @@
expr = "%{module}::Context::from_config(config.tap_hold)",
},
},
tri_state = {
module = "smart_keymap::key::tri_state",
context_events = 'ContextEvents,
system =
'SystemWithData {
data_lengths = [{ const_name = "TRI_STATE", data_field = "tri_state" }],
rust_expr = smart_keymap.tri_state.system.rust_expr,
ty.array = m%"%{module}::System<
Ref,
[%{module}::Key; super::TRI_STATE]
>"%,
ty.vec = m%"%{module}::System<
Ref,
Vec<%{module}::Key>
>"%,
},
context = {
ty = "%{module}::Context",
expr = "%{module}::Context::new()",
},
},
},
},
}
2 changes: 2 additions & 0 deletions ncl/key_system/keymap-codegen.ncl
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,7 @@ pub mod key_system {
"Sticky",
"TapDance",
"TapHold",
"TriState",
],
},
check_pending = {
Expand Down Expand Up @@ -1114,6 +1115,7 @@ pub mod key_system {
"Sticky",
"TapDance",
"TapHold",
"TriState",
]
|> std.array.all (fun needle => std.string.is_match needle rust_mod),
expected = true,
Expand Down
2 changes: 2 additions & 0 deletions ncl/keymap-codegen.ncl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
& (import "smart_keys/sticky/keymap-codegen.ncl")
& (import "smart_keys/tap_dance/keymap-codegen.ncl")
& (import "smart_keys/tap_hold/keymap-codegen.ncl")
& (import "smart_keys/tri_state/keymap-codegen.ncl")
& (import "key_system/families.ncl")
& (import "key_system/keymap-codegen.ncl")
& {
Expand Down Expand Up @@ -235,6 +236,7 @@
smart_keymap.sticky.key,
smart_keymap.tap_dance.key,
smart_keymap.tap_hold.key,
smart_keymap.tri_state.key,
],

json_validator =
Expand Down
2 changes: 2 additions & 0 deletions ncl/keymap-ncl-to-json.ncl
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
& (import "smart_keys/sticky/keymap-ncl-to-json.ncl")
& (import "smart_keys/tap_dance/keymap-ncl-to-json.ncl")
& (import "smart_keys/tap_hold/keymap-ncl-to-json.ncl")
& (import "smart_keys/tri_state/keymap-ncl-to-json.ncl")
& (import "passes/fold-layers.ncl")
& (import "passes/attach-chords.ncl")
& (import "passes/attach-sequences.ncl")
Expand Down Expand Up @@ -330,6 +331,7 @@
keymap_ncl.tap_dance,
keymap_ncl.layered,
keymap_ncl.tap_hold,
keymap_ncl.tri_state,
keymap_ncl.layer_modifier,
keymap_ncl.transparent_layer_exit,
keymap_ncl.keyboard,
Expand Down
1 change: 1 addition & 0 deletions ncl/keys.ncl
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ std.array.fold_left
key_extensions.sequence,
key_extensions.sticky,
key_extensions.tap_hold,
key_extensions.tri_state,
]
12 changes: 12 additions & 0 deletions ncl/smart_keys/tri_state/key-extensions.ncl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
key_extensions.tri_state = fun keys =>
{
tri_state = {
# General constructor: K.tri_state.custom { hold = K.LeftAlt, tap = K.Tab }
custom = fun spec => { tri_state = spec },
alt_tab = { tri_state = { hold = keys.LeftAlt, tap = keys.Tab } },
cmd_tab = { tri_state = { hold = keys.LeftGUI, tap = keys.Tab } },
ctrl_tab = { tri_state = { hold = keys.LeftCtrl, tap = keys.Tab } },
},
},
}
65 changes: 65 additions & 0 deletions ncl/smart_keys/tri_state/keymap-codegen.ncl
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
validators,

lib,

key_data_and_refs,

key_output,

smart_keymap.tri_state
| doc "for key::tri_state::Key."
= {
module = "smart_keymap::key::tri_state",

key = {
Json = std.contract.from_validator json_validator,

key_type = "%{module}::Key",

json_validator =
validators.record.validator {
fields_validator = validators.record.has_exact_fields ["hold", "tap"],
field_validators = {
hold = key_output.json_validator,
tap = key_output.json_validator,
},
},

is_json = fun json => 'Ok == json_validator json,

codegen_values = fun json @ { hold, tap } =>
{
include json,
include module,
include key_type,
rust_expr = "%{module}::Key::new(%{key_output.rust_expr hold}, %{key_output.rust_expr tap})",
},

traverse = fun f acc cv => f acc cv,

data_and_ref = fun key_data cv =>
let { tri_state = tri_state_, ..other_data } = key_data & { tri_state | default = [] } in
let new_index = std.array.length tri_state_ in
let new_key = {
json = cv.json,
rust_expr = cv.rust_expr,
}
in
{
key_data = other_data & { tri_state = std.array.append new_key tri_state_ },
ref = {
include module,
json = new_index,
rust_expr = "%{module}::Ref(%{std.to_string new_index})",
},
},
},

system = {
rust_expr =
let tri_state_data = (key_data_and_refs.key_data & { tri_state | default = [] }).tri_state in
"%{module}::System::new(%{tri_state_data |> lib.array_rust_expr})",
},
},
}
78 changes: 78 additions & 0 deletions ncl/smart_keys/tri_state/keymap-ncl-to-json.ncl
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
validators,

keyboard_modifiers,

keymap_ncl.tri_state
| doc "for key::tri_state::Key."
= {
Key = std.contract.from_validator key_validator,

spec_validator =
validators.record.validator {
fields_validator = validators.record.has_exact_fields ["hold", "tap"],
field_validators = {
hold = keymap_ncl.keyboard.key_validator,
tap = keymap_ncl.keyboard.key_validator,
},
},

key_validator = fun k =>
k
|> match {
{ tri_state = spec } => spec_validator spec,
_ => 'Error { message = "Expected { tri_state = { hold, tap } }" },
},

is_key = fun k => 'Ok == key_validator k,

# Keyboard key (author form) → KeyOutput JSON used by codegen / serde.
keyboard_key_to_key_output = fun k =>
k
|> match {
{ key_code = kc, modifiers = km } =>
{
key_code = { Keyboard = kc },
key_modifiers = keyboard_modifiers.to_json_value km,
},
{ key_code = kc } => { key_code = { Keyboard = kc } },
{ modifiers = km } =>
{
key_modifiers = keyboard_modifiers.to_json_value km,
},
{} => {},
},

to_json_value = fun { tri_state = spec } =>
{
hold = keyboard_key_to_key_output spec.hold,
tap = keyboard_key_to_key_output spec.tap,
},

# Leaves have no nested keys; map_accum maps children only.
map_accum = fun f acc k => { include acc, include k },
},

checks.check_tri_state =
let K = import "keys.ncl" in
{
check_custom_ok =
keymap_ncl.tri_state.key_validator (
K.tri_state.custom {
hold = K.LeftAlt,
tap = K.Tab,
}
) == 'Ok,

check_alt_tab_ok =
keymap_ncl.tri_state.key_validator K.tri_state.alt_tab == 'Ok,

check_alt_tab_json = {
actual = keymap_ncl.tri_state.to_json_value K.tri_state.alt_tab,
expected = {
hold = { key_modifiers = 4 },
tap = { key_code = { Keyboard = 43 } },
},
},
},
}
2 changes: 2 additions & 0 deletions smart-keymap-core/src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ pub mod sticky;
pub mod tap_dance;
/// Tap-Hold keys.
pub mod tap_hold;
/// Tri-state keys (start / continue / interrupt; e.g. Alt-Tab swapper).
pub mod tri_state;

/// The maximum number of key events that are emitted by [crate::key::System] implementations.
pub const MAX_KEY_EVENTS: usize = 4;
Expand Down
Loading
Loading