Halcyon Keyboard
Halcyon/Halcyon.Keyboard is the keyboard as data and behavior, with no rendering and no operating system. It holds the halcyon.keyboard/1 layout format, the built-in layouts, the importers, and KeyboardState, which turns presses into key events. The XR overlay’s keyboard panel draws from it, and the per-OS injectors behind IKeyInjector (in Halcyon.Desktop) replay what it emits. It references only Halcyon.Desktop, for HidUsage.
The layout format
Section titled “The layout format”A layout separates geometry from semantics, the way QMK does. Keys are physical positions with absolute geometry. Layers map key ids to actions.
{ "format": "halcyon.keyboard/1", "id": "preonic", "name": "Preonic", "grid": "ortho", // "staggered" | "ortho", informational only "pitch": {"x": 28, "y": 28}, // millimeters per key unit, per axis "source": {"kind": "qmk", "keyboard": "preonic/rev3", "layout": "LAYOUT_ortho_5x12"}, "keys": [ {"id": "KeyQ", "x": 1, "y": 1}, {"id": "SpaceLeft", "x": 5, "y": 4}, {"id": "Enter", "x": 13.75, "y": 1, "w": 1.25, "h": 2, "x2": -0.25, "y2": 0, "w2": 1.5, "h2": 1}, {"id": "KeyY", "x": 7, "y": 1.5, "r": -15, "rx": 9, "ry": 2, "hand": "right"} ], "layers": [ {"id": "base", "name": "Base", "keys": {"KeyQ": "key:KeyQ", "SpaceLeft": "key:Space", "Lower": "layer:lower"}}, {"id": "lower", "name": "Lower", "activation": "oneShot", "keys": {"Digit1": "key:shift+Digit1"}} ], "combos": [{"when": ["lower", "raise"], "activate": "adjust"}], "legends": {"key:Backspace": "⌫"}}| Field | Meaning |
|---|---|
keys[].id | The physical position: its W3C KeyboardEvent.code name where one exists (KeyQ, ShiftRight), otherwise a name (SpaceLeft, Lower, R3C0). A split space bar is two keys, SpaceLeft and SpaceRight. |
keys[].x, y, w, h | The top-left corner and size in key units. A stagger is a fractional x. w and h default to 1. |
keys[].r, rx, ry | Rotation in degrees, clockwise, about (rx, ry): the KLE and QMK convention. |
keys[].x2, y2, w2, h2 | An optional second rectangle relative to the key (an ISO Enter). |
keys[].hand | left or right on a split board. |
layers | Bottom to top; the first is the base. A layer’s keys is sparse: a key it leaves out falls through to the next active layer down. |
layers[].activation | How the layer’s keys turn it on: oneShot (the default) or toggle. |
combos | A layer that is on whenever all of when are (QMK’s tri-layer: Lower and Raise give Adjust). |
legends | Cap legend overrides by action string. Legends are otherwise derived from the action and the host’s keyboard layout. |
Unknown fields are refused, and so is a layer that maps a key that does not exist or names a layer that does not exist.
Actions
Section titled “Actions”| Action | Meaning |
|---|---|
key:KeyA, key:shift+Digit1 | Press a physical key, wrapped in modifiers. The key games and shortcuts see. Codes are W3C names, or a usage in hex (key:0x0782) when there is none. |
char:é | Produce a character: typed as text, or, with Control, Alt or Meta latched, as the key that types it. |
text:¯\_(ツ)_/¯ | Commit a string as-is. It reaches text fields, never games. |
mod:shift, mod:rctrl, mod:ctrl+alt, mod:hyper, mod:meh | A modifier key. |
layer:lower, layer:lower/toggle, layer:base/to | A layer key, with the layer’s own activation unless a suffix overrides it. to clears every layer and turns its own on; layer:base returns to the base. |
{"tap": "key:Space", "hold": "layer:nav"} | A tap-hold key (QMK LT, MT): the hold while held as another key is pressed, the tap otherwise. |
command:keyboard.hide | A command for the keyboard’s host, never sent to the system. |
none, transparent | Do nothing; fall through. |
Behavior
Section titled “Behavior”KeyboardState takes presses and releases per pointer id (each laser and each fingertip is its own pointer) and returns KeyboardOutputs: key presses and releases by HidUsage, text, and commands. GetLegend(keyId) says what each cap shows: its label, the layer it comes from, whether it fell through, its latch state and whether it is pressed.
- Modifiers latch. A tap latches one for the next key, a second tap inside
DoubleTapWindowlocks it, and a tap while latched or locked clears it. Latched modifiers are wrapped around the next key when it is emitted (modifier downs, key down, then on release key up and modifier ups), so nothing stays down at the system between presses. - A held modifier is a real hold. A modifier held by one pointer while another pointer presses a key goes down at the system and stays down until it is released, and its release latches nothing. With
HoldThresholdset, a modifier held that long on its own becomes a hold too (Shift-click with a real mouse). - One-shot layers follow the same rules: tap for the next key, double-tap to lock, tap to clear, and held by one pointer while another types they stay on for exactly as long as they are held. Toggle layers stay on until tapped again.
- Caps Lock toggles the keyboard’s Shift lock, the same state a double-tapped Shift reaches (
CapsLockLocksShift, on by default; off sends the real Caps Lock key). - Repeat is the host’s timing:
Repeat(pointer)presses the held key again, as a physical key’s auto-repeat does. Modifier, layer and command keys do not repeat. - ReleaseAll lets go of everything and clears every latch, for when the keyboard hides or loses its target.
Legends follow the host’s keyboard layout through IKeyTextMap; UsKeyTextMap is the default. An injector can supply one that reads the active OS layout.
Built-in layouts
Section titled “Built-in layouts”BuiltInLayouts embeds these as halcyon.keyboard/1 files. Every keyboard layout uses key: actions, so symbols on Lower are Shift chords that games see; the phone’s symbol and emoji pages use char: and text:.
| Id | Keys | Layers | Notes |
|---|---|---|---|
preonic (the default) | 60 | base, lower, raise, adjust | QMK LAYOUT_ortho_5x12: a 5×12 ortho with a number row and a split space |
planck | 48 | base, lower, raise, adjust | QMK LAYOUT_ortho_4x12 |
planckSplitSpace | 46 | base, lower, raise, adjust | QMK LAYOUT_planck_2x2u: two 2u space keys. Up is on Raise (E). |
ortho5x13 | 65 | base, lower, raise, adjust | QMK LAYOUT_ortho_5x13, with a center column |
keychronQ15Max | 66 | base, fn1, fn2 | QMK LAYOUT_ansi_66, the two knob presses included; it is 14 units wide, not 13 |
alt65 | 67 | base, fn | The Drop ALT, QMK drop/alt/v2 LAYOUT_65_ansi_blocker |
phone | 33 | base, num, sym, emoji | SwiftKey’s rows; 30 × 38 mm keys |
The ortho layouts’ Adjust is on whenever Lower and Raise both are. The geometry of the Preonic, both Plancks, the ALT and the Q15 Max is tested against the boards’ own QMK files.
Importers
Section titled “Importers”Each importer returns a KeyboardImport: a validated layout and a list of warnings, one per thing it could not carry over. Key ids come from the base layer’s actions, so an imported board gets the same ids as the built-ins (KeyQ, SpaceLeft/SpaceRight); a key with nothing nameable takes its matrix position (R3C0) or its index (Pos12).
| Importer | Reads | Supported | Limits |
|---|---|---|---|
QmkImporter | info.json/keyboard.json (several, merged outermost first) and an optional keymap.json | Every LAYOUT_*, with layout_aliases; x, y, w, h, r, rx, ry, hand. Every QMK basic keycode and alias (keyboard, system, media, modifiers), the US shifted keycodes (KC_EXLM), S()/LCTL()-style wrappers, MO, OSL, TT (one-shot), TG (toggle), TO/DF/PDF (to), OSM(MOD_…), LT, MT, *_T(), KC_HYPR/KC_MEH, TL_LOWR/TL_UPPR and config.features.tri_layer (a combo of layers 1 and 2 into 3), KC_TRNS/_______ and KC_NO/XXXXXXX. Without a keymap, actions are guessed from the info file’s labels. | Firmware-only keycodes (QK_BOOT, RGB, audio, mouse keys, CW_TOGG, macros) do nothing and are listed. A tri-layer set up in keymap.c is invisible to keymap.json. LM is not read. keymap.c is not read; convert it with qmk c2json. |
KleImporter | keyboard-layout-editor raw data (bare names, no outer brackets) or downloaded JSON | Geometry including x2/y2/w2/h2 and rotation, following the KLE editor: setting rx or ry moves the cursor to the rotation origin (kle-serial does not; its issue #7). Legends become actions: the unshifted half of a pair (! over 1 is Digit1), letters, F-keys, named keys (Caps Lock, PgUp, ←); a blank key 3u or wider is Space; modifiers right of center are the right-hand ones. | KLE has no semantics, so anything unrecognized does nothing and is listed. One layer only (a legend naming Fn, Lower or Raise gets an empty layer). Decals are skipped. Stepped keys are plain. |
ViaImporter | A VIA v3 or Vial definition, with an optional VIA saved layout (layers, flat in matrix order) or Vial .vil (layout, layers of rows) | The KLE geometry, matrix positions from the top-left legends, layout options from the bottom-right legends (LayoutOptions lists them; a choice other than the first moves to where the first sits, as VIA draws it). Keycodes as in QMK, plus Vial’s LTn(kc). | A QMK keymap.json is refused here: its order follows the LAYOUT macro, not the matrix, so import it with QMK’s info file. Encoders are left out. Without a keymap every key does nothing. |
KaleidoscopeImporter | A Chrysalis layout export (keymaps of {keyCode}), the keymap.custom entry of its deviceConfiguration or a backup, or a bare keymap.custom string | Keyboard keys with their held-modifier flags, consumer and system keys, LockLayer (toggle), ShiftToLayer (one-shot), MoveToLayer (to), one-shot modifiers and layers, and dual-use keys (tap-hold). Geometry is built in for the Model 100 (and Model 01), the Atreus and the Keyboardio Preonic, recognized from a layer’s size or named. | Kaleidoscope files carry no geometry. The Model 100’s is measured from Chrysalis’s drawing, key centers and angles, with every key drawn as a 1u square. LED, mouse, macro, tap-dance, leader and other plugin keys do nothing and are listed. Sketches (KEYMAP_STACKED in a .ino) are not read; flash the sketch and export from Chrysalis. A flat keymap.custom whose length fits more than one device needs the device named. |
Every keycode table lives in one place, KeyCodes: W3C names, QMK keycodes, KLE legend words, Kaleidoscope codes and the US text layout.