← Back to Index
Architecture

Keyboard Layouts & Device Detection

Geometry follows layout. Labels follow keymap.

Define. Label. Detect.

Three concerns, cleanly separated. A physical layout describes where keys sit. A logical keymap describes what goes on each key. A device monitor discovers what keyboards are connected.

Define

PhysicalLayout describes key positions and sizes

A S D Q W E

Label

LogicalKeymap maps key codes to display labels

Detect

HIDDeviceMonitor discovers connected keyboards

The Complete Picture

Physical geometry and logical labels are independent concerns that combine at render time in the overlay view. Device detection feeds per-device config.
GEOMETRY PhysicalLayout LABELS LogicalKeymap positions labels SWIFTUI VIEW OverlayKeyboardView Device Detection Layer IOKIT OBSERVER HIDDeviceMonitor USB keyboards Bluetooth keyboards events PARSER DeviceEnumerationService KANATA CONFIG (switch ((device N)) ...) Blue arrows show the render path. Gray arrows show the device detection path.
Core invariant. A physical layout is pure geometry — it has no opinion about what label goes on each key. A logical keymap is pure labeling — it has no opinion about key positions. The two combine at render time in the overlay view.

Five Types, Three Layers

Geometry, labeling, and device detection. Each type has a single responsibility.
PhysicalLayout Model
Describes every key's position, size, and optional rotation for a specific keyboard form factor. Pure geometry: x, y, width, height, and totalWidth/totalHeight for aspect ratio. Ships with US (ANSI), International (ISO/JIS/ABNT2/Korean), and Ergonomic (Kinesis, Corne, Sofle, Ferris Sweep) registries. Supports custom user layouts.
struct PhysicalLayout: Identifiable {
    let id: String          // "macbook-us", "kinesis-360"
    let name: String        // "MacBook US"
    let keys: [PhysicalKey]
    let totalWidth: Double
    let totalHeight: Double
}
Ab
LogicalKeymap Model
Maps UInt16 key codes to display strings. Splits labels into coreLabels (30-key letter block, always shown) and extraLabels (number row and punctuation, toggled). Includes QWERTY, Colemak, Colemak-DH, Dvorak, Workman, Graphite, AZERTY, and QWERTZ. A special "system" keymap queries UCKeyTranslate for dynamic OS-driven labels.
struct LogicalKeymap: Identifiable {
    let id: String
    let coreLabels: [UInt16: String]   // letter block
    let extraLabels: [UInt16: String]  // numbers + punctuation

    func displayLabel(for key: PhysicalKey, includeExtraKeys: Bool) -> String
}
HIDDeviceMonitor Service
Monitors USB HID keyboard connect/disconnect events via IOKit on a dedicated thread. Publishes connectedKeyboards as an @Published array. Filters out VirtualHID devices and anonymous BLE devices (VID:PID 0:0). Each device is identified by vendorID, productID, and productName.
@MainActor final class HIDDeviceMonitor: ObservableObject {
    @Published private(set) var connectedKeyboards: [HIDKeyboardEvent]
    @Published private(set) var lastConnectedKeyboard: HIDKeyboardEvent?
}
DeviceEnumerationService Parser
Parses kanata --list output into structured ConnectedDevice records. Pure parsing logic kept separate from Process execution for testability. Identifies VirtualHID devices, extracts vendor/product IDs, and deduplicates by device hash.
enum DeviceEnumerationService {
    static func parseAllDevices(
        fromKanataList output: String
    ) -> [ConnectedDevice]
}
OverlayKeyboardView Consumer
The SwiftUI overlay that renders a keyboard visualization. Receives a PhysicalLayout for key positions and sizes, and a LogicalKeymap for display labels. Combines them at render time — never stores merged data.

From Boot to Key Label

Five events, one consistent data path.
App startup
HIDDeviceMonitor starts IOKit run loop, scans for connected USB/BT keyboards
Layout selected
User picks a PhysicalLayout — key geometry flows to overlay view
Keymap applied
LogicalKeymap labels applied to layout — displayLabel(for:includeExtraKeys:)
Keyboard connected
HIDDeviceMonitor publishes event → DeviceEnumerationService identifies device
Per-device config
Config generator emits (switch ((device N)) ...) only for keys with different mappings

Design Rules

Four invariants that keep geometry, labeling, and device detection cleanly separated.
1
Geometry follows selected PhysicalLayout
The user-selected layout ID determines all key positions, sizes, and rotations. The overlay view reads the layout's keys array directly.
2
Labels follow selected LogicalKeymap
The user-selected keymap determines what text appears on each key. coreLabels for the letter block, extraLabels for number row and punctuation.
3
No UI toggle for this separation
Treat geometry-follows-layout and labels-follow-keymap as a single consistent rule. Do not expose separate toggles for choosing a layout vs. a keymap.
4
Per-device mappings use Kanata's defcfg conditional switch
Not app-level branching. Only keys with different behavior per device get a (switch ((device N)) ... break) wrapper. All other keys are emitted normally.

Cross-References