{"id":37598744,"url":"https://github.com/dhruvinsh/zmk-tri-state","last_synced_at":"2026-04-04T18:59:44.155Z","repository":{"id":220736642,"uuid":"752456144","full_name":"dhruvinsh/zmk-tri-state","owner":"dhruvinsh","description":"Proof of concept where swapper implemented as external module","archived":false,"fork":false,"pushed_at":"2025-03-16T19:01:18.000Z","size":25,"stargazers_count":18,"open_issues_count":2,"forks_count":14,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-24T07:08:51.935Z","etag":null,"topics":["zephyr","zmk","zmk-config","zmk-firmware"],"latest_commit_sha":null,"homepage":"","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dhruvinsh.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-02-03T22:22:38.000Z","updated_at":"2026-01-07T02:13:08.000Z","dependencies_parsed_at":"2025-03-16T19:38:59.792Z","dependency_job_id":"72934f96-04a2-43cc-8240-2ee00f47933f","html_url":"https://github.com/dhruvinsh/zmk-tri-state","commit_stats":null,"previous_names":["dhruvinsh/zmk-swapper","dhruvinsh/zmk-tri-state"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/dhruvinsh/zmk-tri-state","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dhruvinsh%2Fzmk-tri-state","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dhruvinsh%2Fzmk-tri-state/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dhruvinsh%2Fzmk-tri-state/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dhruvinsh%2Fzmk-tri-state/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dhruvinsh","download_url":"https://codeload.github.com/dhruvinsh/zmk-tri-state/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dhruvinsh%2Fzmk-tri-state/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31409470,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T10:20:44.708Z","status":"ssl_error","status_checked_at":"2026-04-04T10:20:06.846Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["zephyr","zmk","zmk-config","zmk-firmware"],"created_at":"2026-01-16T10:00:29.478Z","updated_at":"2026-04-04T18:59:44.151Z","avatar_url":"https://github.com/dhruvinsh.png","language":"C","funding_links":[],"categories":["Community firmware Modules and Behaviors","Behaviors"],"sub_categories":["Custom Behaviors"],"readme":"# ZMK-TRI-STATE\n\nA ZMK implementation of [Nick Conway's](https://github.com/nickconway) tri-state behavior for the\n[ZMK Firmware](https://github.com/zmkfirmware/zmk).\n\nThis project demonstrates how to implement custom behaviors without maintaining\na separate ZMK fork. It's fully compatible with the latest Zephyr 3.5 release.\n\n## Installation\n\nAdd this module to your ZMK config by modifying your `config/west.yml` file. Add\nthe necessary `remotes` and `projects` sections as shown below:\n\n```yaml\nmanifest:\n  remotes:\n    - name: zmkfirmware\n      url-base: https://github.com/zmkfirmware\n    - name: dhruvinsh\n      url-base: https://github.com/dhruvinsh\n  projects:\n    - name: zmk\n      remote: zmkfirmware\n      revision: main\n      import: app/west.yml\n    - name: zmk-tri-state\n      remote: dhruvinsh\n      revision: main\n  self:\n    path: config\n```\n\nFor a complete example configuration, see [my ZMK config](https://github.com/dhruvinsh/zmk-config/blob/0e4919ba45f08714bf1cc053c103ae1977bacd76/config/west.yml).\n\n**NOTE: Common use-cases have not yet been defined for this behavior in the DTSI format. See [ZMK documentation](https://zmk.dev/docs/development/new-behavior#defining-common-use-cases-for-the-behavior-dtsi-optional) for more information.**\n\n## Basic Usage\n\nAdd the tri-state behavior to your keymap:\n\n```devicetree\n#include \u003cdt-bindings/zmk/keys.h\u003e\n\n/ {\n  behaviors {\n    swapper: swapper {\n      compatible = \"zmk,behavior-tri-state\";\n      #binding-cells = \u003c0\u003e;\n      bindings = \u003c\u0026kp A\u003e, \u003c\u0026kp B\u003e, \u003c\u0026kt C\u003e;\n    };\n  };\n};\n```\n\nAfter defining it, you can use `\u0026swapper` anywhere in your keymap. See the detailed documentation below for more information and check out a [working example here](https://github.com/dhruvinsh/zmk-config/blob/0ed6b525a5834dc423eb600b4552b9afb9c621ee/config/include/behaviors.dtsi#L52-L61).\n\n## Summary: Tri-State\n\nTri-States are a way to have something persist while other behaviors occur.\n\nThe tri-state key will fire the 'start' behavior when the key is pressed for\nthe first time. Subsequent presses of the same key will output the second,\n'continue' behavior, and any key position or layer state change that is not\nspecified (see below) will trigger the 'interrupt behavior'.\n\n### Basic Usage\n\nThe following is a basic definition of a tri-state:\n\n```devicetree\n/ {\n    behaviors {\n        tri-state: tri-state {\n            compatible = \"zmk,behavior-tri-state\";\n            label = \"TRI-STATE\";\n            #binding-cells = \u003c0\u003e;\n            bindings = \u003c\u0026kp A\u003e, \u003c\u0026kp B\u003e, \u003c\u0026kt C\u003e;\n        };\n    };\n\n    keymap {\n        compatible = \"zmk,keymap\";\n        label =\"Default keymap\";\n\n        default_layer {\n            bindings = \u003c\n                \u0026tri-state  \u0026kp D\n                \u0026kp E       \u0026kp F\u003e;\n        };\n    };\n};\n```\n\nPressing `tri-state` will fire the first behavior, and output `A`, as well as\nthe second behavior, outputting `B`. Subsequent presses of `tri-state` will\noutput `B`. When another key is pressed or a layer change occurs, the third,\n'interrupt' behavior will fire.\n\n### Advanced Configuration\n\n#### `timeout-ms`\n\nSetting `timeout-ms` will cause the deactivation behavior to fire when the time\nhas elapsed after releasing the Tri-State or a ignored key.\n\n#### `ignored-key-positions`\n\n- Including `ignored-key-positions` in your tri-state definition will let the\n  key positions specified NOT trigger the interrupt behavior when a tri-state\n  is active.\n- Pressing any key **NOT** listed in `ignored-key-positions` will cause the\n  interrupt behavior to fire.\n- Note that `ignored-key-positions` is an array of key position indexes. Key\n  positions are numbered according to your keymap, starting with 0. So if the\n  first key in your keymap is Q, this key is in position 0. The next key\n  (probably W) will be in position 1, et cetera.\n- See the following example, which is an implementation of the popular [Swapper](https://github.com/callum-oakley/qmk_firmware/tree/master/users/callum)\n  from Callum Oakley:\n\n```devicetree\n/ {\n    behaviors {\n        swap: swapper {\n            compatible = \"zmk,behavior-tri-state\";\n            label = \"SWAPPER\";\n            #binding-cells = \u003c0\u003e;\n            bindings = \u003c\u0026kt LALT\u003e, \u003c\u0026kp TAB\u003e, \u003c\u0026kt LALT\u003e;\n            ignored-key-positions = \u003c1\u003e;\n        };\n    };\n\n    keymap {\n        compatible = \"zmk,keymap\";\n        label =\"Default keymap\";\n\n        default_layer {\n            bindings = \u003c\n                \u0026swap    \u0026kp LS(TAB)\n                \u0026kp B    \u0026kp C\u003e;\n        };\n    };\n};\n```\n\n- The sequence `(swap, swap, LS(TAB))` produces `(LA(TAB), LA(TAB), LA(LS(TAB)))`.\n  The LS(TAB) behavior does not fire the interrupt behavior, because it is\n  included in `ignored-key-positions`.\n- The sequence `(swap, swap, B)` produces `(LA(TAB), LA(TAB), B)`. The B\n  behavior **does** fire the interrupt behavior, because it is **not** included\n  in `ignored-key-positions`.\n\n#### `ignored-layers`\n\n- By default, any layer change will trigger the end behavior.\n- Including `ignored-layers` in your tri-state definition will let the specified\n  layers NOT trigger the end behavior when they become active (include the\n  layer the behavior is on to accommodate for layer toggling).\n- Activating any layer **NOT** listed in `ignored-layers` will cause the\n  interrupt behavior to fire.\n- Note that `ignored-layers` is an array of layer indexes. Layers are numbered\n  according to your keymap, starting with 0. The first layer in your keymap is\n  layer 0. The next layer will be layer 1, et cetera.\n- Looking back at the swapper implementation, we can see how `ignored-layers`\n  can affect things\n\n```devicetree\n/ {\n    behaviors {\n        swap: swapper {\n            compatible = \"zmk,behavior-tri-state\";\n            label = \"SWAPPER\";\n            #binding-cells = \u003c0\u003e;\n            bindings = \u003c\u0026kt LALT\u003e, \u003c\u0026kp TAB\u003e, \u003c\u0026kt LALT\u003e;\n            ignored-key-positions = \u003c1 2 3\u003e;\n            ignored-layers = \u003c1\u003e;\n        };\n    };\n\n    keymap {\n        compatible = \"zmk,keymap\";\n        label =\"Default keymap\";\n\n        default_layer {\n            bindings = \u003c\n                \u0026swap    \u0026kp LS(TAB)\n                \u0026kp B    \u0026tog 1\u003e;\n        };\n\n        layer2 {\n            bindings = \u003c\n                \u0026kp DOWN    \u0026kp B\n                \u0026tog 2    \u0026trans\u003e;\n        };\n\n        layer3 {\n            bindings = \u003c\n                \u0026kp LEFT  \u0026kp N2\n                \u0026trans    \u0026kp N3\u003e;\n        };\n    };\n};\n```\n\n- The sequence `(swap, tog 1, DOWN)` produces `(LA(TAB), LA(DOWN))`. The change\n  to layer 1 does not fire the interrupt behavior, because it is included in\n  `ignored-layers`, and DOWN is in the same position as the tri-state, also not\n  firing the interrupt behavior.\n- The sequence `(swap, tog 1, tog 2, LEFT)` produces `(LA(TAB), LEFT`. The\n  change to layer 2 **does** fire the interrupt behavior, because it is not\n  included in `ignored-layers`.\n\n## Credits\n\n- [Pete Johanson](https://github.com/petejohanson)\n- [Nick Conway](https://github.com/nickconway)\n- [Cem Aksoylar](https://github.com/caksoylar)\n- ZMK and Zephyr Teams\n\n## Related Projects\n\n- [ZMK-NUM-WORD](https://github.com/dhruvinsh/zmk-num-word)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdhruvinsh%2Fzmk-tri-state","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdhruvinsh%2Fzmk-tri-state","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdhruvinsh%2Fzmk-tri-state/lists"}