;;; GNU Guix --- Functional package management for GNU ;;; ;;; Copyright © 2023 Stefan ;;; ;;; This file is not part of GNU Guix. ;;; ;;; GNU Guix is free software; you can redistribute it and/or modify it ;;; under the terms of the GNU General Public License as published by ;;; the Free Software Foundation; either version 3 of the License, or (at ;;; your option) any later version. ;;; ;;; GNU Guix is distributed in the hope that it will be useful, but ;;; WITHOUT ANY WARRANTY; without even the implied warranty of ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;;; GNU General Public License for more details. ;;; ;;; You should have received a copy of the GNU General Public License ;;; along with GNU Guix. If not, see . (define-module (zephyr zmk) #:use-module (guix build union) #:use-module (guix build utils) #:use-module (guix build-system trivial) #:use-module (guix gexp) #:use-module (guix git-download) #:use-module ((guix licenses) #:prefix license:) #:use-module (guix packages) #:use-module (ice-9 match) #:use-module (ice-9 optargs) #:use-module (srfi srfi-1) #:use-module (zephyr) #:use-module (zephyr apps) #:use-module (zephyr modules)) (define zmk-config (package (name "zmk-config") (version "0") (source #f) (build-system trivial-build-system) (arguments (list #:builder #~(mkdir #$output))) (native-search-paths (list (search-path-specification (variable "ZMK_CONFIG") (files '("zmk-config")) (separator #f) (file-type 'directory) (file-pattern "^config$")))) (home-page "https://zmk.dev/docs/config#config-file-locations") (synopsis "ZMK firmware configuration") (description "This ZMK Firmware configuration is a helper to set the ZMK_CONFIG environment varibale during a ZMK Firmware package build to its configuration input. Add a file-like object like a file-union or a package containing a zmk-config/config folder as build input to a ZMK Firmare packege.") (license license:expat))) (define*-public (make-zmk board #:key (shield "") (extra-inputs '()) (extra-name "") (patches '()) snippet) "Make a ZMK firmware package for a keyboard consisting of an Arm microcontroller BOARD with a SHIELD PCB using the list of EXTRA-INPUTS. Add an EXTRA-NAME with a trailing hyphen to customize the package name. Use PATCHES or SNIPPET to modify the ZMK sources." (make-zephyr-application-for-arm (let* ((revision "1") (commit "9d714c0b69fee2098a010d29e534051aeca26386") (underscore->hyphen (lambda (name) (string-map (lambda (char) (if (char=? char #\_) #\- char)) name))) (board-name (underscore->hyphen board)) (shield-name (underscore->hyphen shield)) (shield (if (string-null? shield) #f shield))) (package (name (string-append shield-name (if shield "-" "") extra-name board-name "-zmk")) (version (git-version "2023.06.12" revision commit)) (source (origin (method git-fetch) (uri (git-reference (url "https://github.com/zmkfirmware/zmk") (commit commit))) (file-name (git-file-name name version)) (sha256 (base32 "08mihhcdlb9hh1qa0l6limggmvy98qiq6051p9qhnh6zbs8021h7")) (patches patches) (snippet snippet))) (build-system #f) (arguments (list #:out-of-source? #t #:configure-flags #~(append (list "-S../source/app" (string-append "-DBOARD=" #$board)) (if #$shield (list (string-append "-DSHIELD=" #$shield)) '())))) (inputs (append extra-inputs (list zephyr-module-cmsis zephyr-module-lvgl-8.2.0 zephyr-module-tinycrypt zmk-config))) (home-page "https://zmk.dev") (synopsis (if shield (format #f "ZMK Firmware for a ~a keyboard with ~a" shield-name board-name) (format #f "ZMK Firmware for a ~a keyboard" board-name))) (description "ZMK Firmware is an open source (MIT) keyboard firmware built on the Zephyr™ Project Real Time Operating System (RTOS).") (license license:expat))) #:zephyr zephyr-3.2+zmk-fixes #:source-prefix "zmk")) (define*-public (make-zmk-union zmk-packages #:key name synopsis) "Make a union of several ZMK Firmware packages for left and right hand or settings-reset firmware files." (package (inherit (car zmk-packages)) (name (or name (package-name (car zmk-packages)))) (source #f) (build-system trivial-build-system) (arguments (list #:modules '((guix build union)) #:builder #~(begin (use-modules ((guix build union))) (union-build #$output (quote #$zmk-packages))))) (synopsis (or synopsis (package-synopsis (car zmk-packages)))))) (define*-public (make-nrfmicro-13-zmk shield #:key zmk-config (extra-name "")) "Make a ZMK firmware package for a keyboard consisting of the nrfmicro 1.3/1.4 board with a SHIELD PCB. Use the ZMK-CONFIG directory containing optional boards/ or dts/ directories, or .conf, .keypad, .overlay files prefixed with shield or board names." (make-zmk "nrfmicro_13" #:shield shield #:extra-name extra-name #:extra-inputs (append (list zephyr-module-hal-nordic-2.11) (if zmk-config (list zmk-config) '())) #:snippet #~(begin (use-modules (guix build utils)) (substitute* "app/CMakeLists.txt" ;; Move combo.c and behaviour_tap_dance.c above all other behaviors. ;; This fix is needed to get working layer-tap-dance. (("^ target_sources\\(app PRIVATE src/combo.c\\)\n") "") (("^ target_sources\\(app PRIVATE src/behaviors/behavior_tap_dance.c\\)\n") "") (("^ target_sources\\(app PRIVATE src/hid.c\\)\n" line) (string-append line " target_sources(app PRIVATE src/combo.c)\n" " target_sources(app PRIVATE src/behaviors/behavior_tap_dance.c)\n")))))) (define (hid-modifier modifier) "Map a symbol for a MODIFIER key into a macro symbol for a ZMK keymap file. An unknown MODIFIER symbol is just returned." (define hid-modifier->zmk-macro '((⇧ . LS) (⌃ . LC) (⌥ . LA) (⌘ . LG) (R⌘ . RG) (R⌥ . RA) (R⌃ . RC) (R⇧ . RS))) (or (assoc-ref hid-modifier->zmk-macro modifier) modifier)) (define-public (special-bindings key-label) "Map a KEY-LABEL matching a special-binding into a binding symbol for a ZMK keymap file. An unknown KEY-LABEL symbol is just returned." (define special-bindings->zmk-name '(;; A whole in the keyboard matrix without meaning to ZMK. (◌ . "") ;; No functionality. (☒ . &none) ;; Fall-through to the next active lower layer. (☐ . &trans) ;; Keypress on sensor, requires two parameters for up and down keycodes. (⟳ . &inc_dec_kp) ;; Reset and bootloader, on split keyboards this is side specific. (⎊ . &sys_reset) (↯ . &bootloader) ;; Bluetooth, requires one or two parameters. (⌔ . &bt) ;; Backlight, requires one parameter. (☼ . &bl))) (or (assoc-ref special-bindings->zmk-name key-label) key-label)) (define-public (hid key-label) "Map a HID KEY-LABEL into a macro symbol for a ZMK keymap file. Any other KEY-LABEL will be treated by 'special-bindings'." (define hid->zmk-name '((⎋ . ESC) (⎙ . PSCRN) (⤓ . SLCK) (⎉ . PAUSE_BREAK) (^ . GRAVE) (- . MINUS) (= . EQUAL) (⌫ . BSPC) (⇥ . TAB) (⟦ . LBKT) (⟧ . RBKT) (↲ . RET) (⏎ . RET) (↩ . RET) (⇪ . CAPS) (⍮ . SEMI) (⍘ . SQT) (⋕ . NUHS) (⇧ . LSHFT) (\ . NUBS) (‚ . COMMA) (· . DOT) (/ . SLASH) (R⇧ . RSHFT) (⌃ . LCTRL) (⌥ . LALT) (⌘ . LGUI) (␣ . SPC) (R⌘ . RGUI) (R⌥ . RALT) (R⌃ . RCTRL) (☰ . K_APP) (⌵ . INS) (⇱ . HOME) (↖ . HOME) (⇞ . PG_UP) (⌦ . DEL) (⇲ . END) (↘ . END) (⇟ . PG_DN) (← . LEFT) (↓ . DOWN) (↑ . UP) (→ . RIGHT) (⇠ . LEFT) (⇣ . DOWN) (⇡ . UP) (⇢ . RIGHT) (⇭ . KP_NUMLOCK) (NUM . KP_NUMLOCK) (⌧ . KP_CLEAR) (⟨ . KP_LPAR) (⟩ . KP_RPAR) (P= . KP_EQUAL) (÷ . KP_DIVIDE) (* . KP_MULTIPLY) (− . KP_MINUS) (+ . KP_PLUS) (P1 . KP_N1) (P2 . KP_N2) (P3 . KP_N3) (P4 . KP_N4) (P5 . KP_N5) (P6 . KP_N6) (P7 . KP_N7) (P8 . KP_N8) (P9 . KP_N9) (P0 . KP_N0) (P. . KP_DOT) (P, . KP_COMMA) (⌤ . ENTER) (✄ . C_AC_CUT) (◫ . C_AC_COPY) (⎀ . C_AC_PASTE) (↶ . C_AC_UNDO) (↷ . C_AC_REDO) (⌨ . C_AL_KEYBOARD_LAYOUT))) (special-bindings (or (assoc-ref hid->zmk-name key-label) key-label))) (define-public (de key-label) "Map a german KEY-LABEL based on the QWERTZ-layout into an international HID key-label, if needed, and return a symbol for a ZMK keymap file." (define de->hid '((ß . -) (´ . =) (Z . Y) (Ü . ⟦) (+ . ⟧) (Ö . ⍮) (Ä . ⍘) (< . \) (Y . Z) (- . /) (P+ . +) (P, . P.) (P. . P,))) (hid (or (assoc-ref de->hid key-label) key-label))) (define-public (neo key-label) "Map a german KEY-LABEL based on the neo-layout into the international HID key-label, if needed, and return a symbol as needed by a ZMK keymap file." (define neo->de '((T1 . ^) (X . Q) (V . W) (L . E) (C . R) (W . T) (M3 . ⇪) (U . A) (I . S) (A . D) (E . F) (O . G) (M4 . <) (Ü . Y) (Ö . X) (Ä . C) (P . V) (Z . B) (- . ß) (T2 . ´) (K . Z) (H . U) (G . I) (F . O) (Q . P) (ẞ . Ü) (T3 . +) (S . H) (N . J) (R . K) (T . L) (D . Ö) (Y . Ä) (RM3 . ⋕) (B . N) (J . -) (RM4 . R⌥) (P⇥ . ⇭))) (de (or (assoc-ref neo->de key-label) key-label))) (define*-public (zmk-keymap #:key (properties '()) (behaviors '()) (combos '()) (conditional_layers '()) (layers '()) (macros '())) "Generate the content of a keymap file for ZMK. Each layer in LAYERS has a name, a layout and multiple rows, of which each contains the key-bindings. The last row contains the bindings for sensors. The key-bindings use symbols from the layout. The BEHAVIORS, COMBOS, MACROS and CONDITIONAL-LAYERS contain lists of strings to inject own appropiate definitions for ZMK. PROPERTIES may contain properties for behaviors or even C macro definitions." (define (include file) "Return an include statement for file" (string-append "#include <" file ">")) (define (include-binding file) "Return an include statement for file defining bindings." (include (string-append "dt-bindings/zmk/" file))) (define (includes) "Return all include statements offered by ZMK for keymap files." (append (map include '("behaviors.dtsi")) (map include-binding '("backlight.h" "bt.h" "ext_power.h" "hid_usage.h" "hid_usage_pages.h" "keys.h" "kscan_mock.h" "matrix_transform.h" "modifiers.h" "outputs.h" "reset.h" "rgb.h")))) (define* (keymap-layer name layout rows) "Return a string with a keymap layer definition NAME for a ZMK keymap file, consisting of ROWS of keys with their labels based on LAYOUT." (define (zmk-name->string zmk-name) "Tansform a ZMK-NAME into a string." (cond ((string? zmk-name) zmk-name) ((number? zmk-name) (number->string zmk-name)) (else (symbol->string zmk-name)))) (define (key-label->zmk key-label) "Tansform a key-label based on a keyboard-layout into a ZMK string." (zmk-name->string (layout key-label))) (define (modified-key->zmk modified-key) "Transform a possibly MODIFIED-KEY like '(⇧ ⌥ ⎋) into the \"LS((LA(ESC))\" respresentation of ZMK." (match modified-key ((modifier modifier-or-key . rest) (string-append (zmk-name->string (hid-modifier modifier)) "(" (modified-key->zmk (cdr modified-key)) ")")) ((unmodified-key) (modified-key->zmk unmodified-key)) (key-label (key-label->zmk key-label)))) (define (behavior->zmk behavior strings-of-layers-and-modified-keys) "Join a BEHAVIOR symbol like '&mt with STRINGS-OF-LAYERS-AND-MODIFIED-KEYS as parameters like '(\"LALT\" \"ESC\") into the \"&mt LALT ESC\" respresentation of ZMK." (string-join (cons (key-label->zmk behavior) strings-of-layers-and-modified-keys))) (define (&-symbol? symbol) "Predicate to identify a symbol as a ZMK behavior prefixed with &." (string=? "&" (string-take (key-label->zmk symbol) 1))) (define (key-binding->zmk key-binding) "Transform the KEY-BINDING, which could be a key-label, a modified key, or a behavior with layer and modified key parameters, into the representation of a ZMK behavior for a keymap layer." (match key-binding (((? &-symbol? behavior) . parameters) ;; A list starting with an &-symbol is a behavior with parameters. ;; The parameters themselves may be layers or modified keys. (behavior->zmk behavior (map modified-key->zmk parameters))) (modified-key (let ((modified-key (modified-key->zmk modified-key))) (if (or (string-null? modified-key) (&-symbol? modified-key)) ;; There is nothing or a behavior is present, just use it. modified-key ;; Add a key-press behavior to the modified-key and start over. (behavior->zmk '&kp (list modified-key))))))) (define (keys->zmk key-bindings) "Transform a list of KEY-BINDINGS into ZMK behaviors for a keymap layer." (string-join (map (lambda (zmk-behavior) (string-pad-right zmk-behavior (max 12 (string-length zmk-behavior)))) (map key-binding->zmk key-bindings)))) (string-append " " name "_layer {" "\n bindings = <" (string-join (map keys->zmk (drop-right rows 1)) "\n " 'prefix) "\n >;" (if (null? (last rows)) "" (string-append "\n sensor-bindings = <" (string-join (map keys->zmk (last rows)) "\n " 'prefix) "\n >;")) "\n };")) (define (layer layer) "Return a string for a ZMK keymap file containing a layer definition." (match layer ((name layout . rows) (keymap-layer name layout rows)))) (string-join (append (includes) properties (list "/ {" " behaviors {") behaviors (list " };" " combos {" " compatible = \"zmk,combos\";") combos (list " };" " conditional_layers {" " compatible = \"zmk,conditional_layers\";") conditional_layers (list " };" " keymap {" " compatible = \"zmk,keymap\";") (map layer layers) (list " };" " macros {") macros (list " };" "};")) "\n")) ;; This is a hold-tap behavior for a key, which momentarily activates a layer, ;; if hold, or switches to that layer, if tapped. (define-public layer-hold-tap " /omit-if-no-ref/ lht: behavior_layer_hold_tap { compatible = \"zmk,behavior-hold-tap\"; label = \"LAYER_HOLD_TAP\"; #binding-cells = <2>; flavor = \"balanced\"; tapping-term-ms = <200>; bindings = <&mo>, <&to>; }; ") (define-public (layer-tap-dance n) "Give a tap-dance behavior '<dN', which counts the taps for the layer number and momentarily activates that layer on hold, or switches to that layer on tap. If the parameter N is 0, then taps select the layers 1, 2, 3. If N is 1, taps select the layers 0, 2, 3, and so on." (let ((first (if (>= n 1) "0 0" "1 1")) (second (if (>= n 2) "1 1" "2 2")) (third (if (>= n 3) "2 2" "3 3")) (n (number->string n))) (string-append " /omit-if-no-ref/ ltd" n ": behavior_layer_tap_dance" n " { compatible = \"zmk,behavior-tap-dance\"; label = \"LAYER_TAP_DANCE" n "\"; #binding-cells = <0>; tapping-term-ms = <200>; bindings = <&lht " first ">, <&lht " second ">, <&lht " third ">; }; "))) (define-public settings-reset-nrfmicro-13-zmk (package (inherit (make-nrfmicro-13-zmk "settings_reset")) (synopsis "ZMK settings reset firmware for split-keyboards with nrfmicro 1.3/1.4 boards") (description "Pairing issues of ZMK firmware split-keyboard halves can be resolved by flashing this settings reset firmware to both controllers."))) (define-public redox-left-nrfmicro-13-zmk (make-nrfmicro-13-zmk "redox_left")) (define-public redox-right-nrfmicro-13-zmk (make-nrfmicro-13-zmk "redox_right")) (define-public redox-nrfmicro-13-zmk (make-zmk-union (list settings-reset-nrfmicro-13-zmk redox-left-nrfmicro-13-zmk redox-right-nrfmicro-13-zmk) #:name "redox-nrfmicro-13-zmk" #:synopsis "ZMK firmware for a Redox shield with nrfmicro-1.3/1.4 board")) (define-public redox-neo-keymap (let* ((M3Y '(&mt RM3 Y)) (⇧- '(&mt ⇧ -)) (R⇧ẞ '(&mt R⇧ ẞ)) (⌥- '(&mt ⌥ -)) (⌥. '(&mt ⌥ ·)) (l1␣ '(< 1 ␣)) (l0 '(<d0)) ; Layer tap-dance for layer 0. (l1 '(<d1)) ; Layer tap-dance for layer 1. (l2 '(<d2)) ; Layer tap-dance for layer 2. (l3 '(<d3)) ; Layer tap-dance for layer 3. (⌔1 '(⌔ BT_SEL 0)) (⌔2 '(⌔ BT_SEL 1)) (⌔3 '(⌔ BT_SEL 2)) (⌔4 '(⌔ BT_SEL 3)) (⌔5 '(⌔ BT_SEL 4)) (⌔⌧ '(⌔ BT_CLR)) (⌔→ '(⌔ BT_NXT)) (⌔← '(⌔ BT_PRV)) (keymap (zmk-keymap #:layers `(("default" ,neo ( ⎋ N1 N2 N3 N4 N5 ◌ ◌ ◌ ◌ N6 N7 N8 N9 N0 ⌫ ) ( ⇥ X V L C W T2 ◌ ◌ ☰ K H G F Q ↲ ) ( M3 U I A E O T3 ◌ ◌ ⌵ S N R T D ,M3Y) (,⇧- Ü Ö Ä P Z ⇧ ⌘ R⌘ R⇧ B M ‚ · J ,R⇧ẞ) ( ⌃ ⌥ T1 ⌦ ,l0 ◌ ⌃ M4 RM4 R⌃ ◌ ,l1␣ ⌦ ,l0 ,⌥- R⌃ ) ()) ("cursor" ,neo ( ⎋ F1 F2 F3 F4 F5 ◌ ◌ ◌ ◌ F6 F7 F8 F9 F10 ⌫ ) ( ⇥ ⇞ ⌫ ↑ ⌦ ⇟ ⎉ ◌ ◌ ☒ ⇞ ⌫ ↑ ⌦ ⇟ ↲ ) ( ☒ ⇱ ← ↓ → ⇲ ☒ ◌ ◌ ☒ ⇱ ← ↓ → ⇲ ☒ ) (,⇧- ⎋ ⇥ ⎀ ↲ ↶ ⇧ ⌘ R⌘ ⇧ ⎋ ⇥ ⎀ ↲ ↶ R⇧ ) ( ⌃ ⌥ ⎙ ⌦ ,l1 ◌ ⌃ ⌥ ⌥ R⌃ ◌ ␣ ⌦ ,l1 ,⌥- R⌃ ) ()) ("keypad" ,neo ( ⎋ F11 F12 F13 F14 F15 ◌ ◌ ◌ ◌ ⎋ P⇥ ÷ * − ⌫ ) ( ⇥ ⇞ ⌫ ↑ ⌦ ⇟ ☒ ◌ ◌ ⇭ ☒ P7 P8 P9 P+ ↲ ) ( M3 ⇱ ← ↓ → ⇲ ☒ ◌ ◌ ☒ ☒ P4 P5 P6 P= M3 ) (,⇧- ⎋ ⇥ ⎀ ↲ ↶ ⇧ ⌘ R⌘ R⇧ ␣ P1 P2 P3 ⌤ R⇧ ) ( ⌃ ⌥ ☒ ⌦ ,l2 ◌ ⌃ M4 RM4 R⌃ ◌ P0 ⌦ P, ,⌥. R⌃ ) ()) ("zmk" ,neo ( ⎊ ,⌔1 ,⌔2 ,⌔3 ,⌔4 ,⌔5 ◌ ◌ ◌ ◌ ☒ ☒ ☒ ☒ ☒ ⎊ ) ( ↯ ☒ ☒ ☒ ☒ ☒ ☒ ◌ ◌ ☒ ☒ ☒ ☒ ☒ ☒ ↯ ) ( ☒ ☒ ,⌔← ,⌔⌧ ,⌔→ ☒ ☒ ◌ ◌ ☒ ☒ ☒ ☒ ☒ ☒ ☒ ) ( ☒ ☒ ☒ ☒ ☒ ☒ ☒ ☒ ☒ ☒ ☒ ☒ ☒ ☒ ☒ ☒ ) ( ☒ ☒ ☒ ☒ ,l3 ◌ ☒ ☒ ☒ ☒ ☒ ☒ ☒ ,l3 ☒ ☒ ) ())) #:properties (list "< {quick-tap-ms = <200>;};" "&mt {quick-tap-ms = <200>;};") #:combos (list " combo_up {" ; G F ⇒ ↑ " key-positions = <22 23>;" " bindings = <&kp UP>;" " };" " combo_left {" ; N R ⇒ ← " key-positions = <35 36>;" " bindings = <&kp LEFT>;" " };" " combo_down {" ; R T ⇒ ↓ " key-positions = <36 37>;" " bindings = <&kp DOWN>;" " };" " combo_right {" ; T D ⇒ → " key-positions = <37 38>;" " bindings = <&kp RIGHT>;" " };") #:behaviors (list layer-hold-tap (layer-tap-dance 0) (layer-tap-dance 1) (layer-tap-dance 2) (layer-tap-dance 3))))) (file-union "redox-config" (list (list "zmk-config/config/redox.keymap" (plain-file "redox-neo.keymap" keymap)))))) (define-public redox-left-neo-nrfmicro-13-zmk (make-nrfmicro-13-zmk "redox_left" #:zmk-config redox-neo-keymap #:extra-name "neo-")) (define-public redox-right-neo-nrfmicro-13-zmk (make-nrfmicro-13-zmk "redox_right" #:zmk-config redox-neo-keymap #:extra-name "neo-")) (define-public redox-neo-nrfmicro-13-zmk (make-zmk-union (list settings-reset-nrfmicro-13-zmk redox-left-neo-nrfmicro-13-zmk redox-right-neo-nrfmicro-13-zmk) #:name "redox-neo-nrfmicro-13-zmk" #:synopsis "Neo layout ZMK firmware for a Redox shield with nrfmicro-1.3/1.4 board"))