← Back to Index
Architecture

Rule Collections & Config Generation

How user remappings become a running Kanata config.

Collect. Generate. Reload.

Every user interaction follows the same pipeline: gather rules, write the config file, and tell Kanata to reload via TCP.

Collect

RuleCollectionsManager gathers enabled rules and custom rules.

.kbd

Generate

ConfigurationService writes the keypath.kbd file.

Reload

ConfigReloadCoordinator sends TCP reload to Kanata.

The Complete Pipeline

Rules flow one way: from the UI model through config generation to the running Kanata process. The config file is never read back.
USER ACTION toggle / edit / keymap MANAGER RuleCollectionsManager SOURCE OF TRUTH RuleCollections.json CustomRules.json [RuleCollection] GENERATOR ConfigurationService keypath.kbd CONFIG FILE ~/.config/keypath/keypath.kbd TCP reload RUNTIME Kanata (LaunchDaemon) OVERLAY KeycapOverlay layer state ONE-WAY FLOW Blue arrows show the data flow. Config is never read back.
Core invariant. The .kbd config file is the single artifact. Every rule, keymap, layer, and launcher binding is expressed as Kanata configuration. KeyPath never reads the config back — it regenerates from its own model every time.

Five Types, One Pipeline

Each type owns one step. Together they turn a UI toggle into a running config.
RuleCollection Model
A group of related key mappings that can be toggled together. Each collection has a configuration (discriminated union) controlling its display style, a targetLayer, and an array of KeyMapping values. Uses a type-safe enum for style-specific data: .list, .singleKeyPicker, .homeRowMods, .tapHoldPicker, .layerPresetPicker.
public struct RuleCollection: Identifiable, Codable {
    let id: UUID
    var name: String
    var isEnabled: Bool
    var mappings: [KeyMapping]
    var targetLayer: RuleCollectionLayer
    var configuration: RuleCollectionConfiguration
}
RuleCollectionsManager Manager
The central coordinator for rule state. Holds the arrays of [RuleCollection] and [CustomRule], manages the active keymap, detects conflicts between rules, and triggers config regeneration via its onRulesChanged callback. All config writes go through regenerateConfigFromCollections().
@MainActor final class RuleCollectionsManager {
    var ruleCollections: [RuleCollection]
    var customRules: [CustomRule]
    var activeKeymapId: String

    // Single write path for all config changes
    var onRulesChanged: (() async -> Void)?
}
ConfigurationService Infrastructure
Handles all config file I/O. Generates the keypath.kbd file from mappings, manages file watching for external edits, and provides the current KanataConfiguration. Preserves user-authored sections outside the KP:BEGIN/KP:END sentinel blocks.
public final class ConfigurationService: FileConfigurationProviding {
    let configurationPath: String  // ~/.config/keypath/keypath.kbd

    func current() async -> KanataConfiguration
    func reload() async throws -> KanataConfiguration
    func saveConfiguration(_: [RuleCollection], _: [CustomRule]) async throws
}
ConfigReloadCoordinator Coordinator
The only entry point for sending TCP reload commands to Kanata. Checks service health and permission gates before sending. Handles safety monitoring, rollback on failure, and diagnostics clearing on success.
@MainActor final class ConfigReloadCoordinator {
    func triggerConfigReload() async -> ReloadResult
    var onReloadSuccess: (() -> Void)?
}
CustomRule Model
A single user-authored remapping: input key to output key. Supports advanced behaviors (tap-hold, tap-dance, macro), shifted outputs, per-device overrides, and target layer assignment. Managed alongside collections but stored in a separate CustomRules.json.
public struct CustomRule: Identifiable, Sendable {
    var input: String
    var output: String
    var behavior: MappingBehavior?     // tap-hold, macro, etc.
    var targetLayer: RuleCollectionLayer
    var deviceOverrides: [DeviceKeyOverride]?
}

Every Change, Same Path

No alternate write paths. No direct config edits from UI code.
User adds a remap
RuleCollectionsManager updates the collection, fires onRulesChanged
Keymap changes
activeKeymapId updated, full regeneration triggered
Config regenerated
ConfigurationService.saveConfiguration() writes keypath.kbd
TCP reload sent
ConfigReloadCoordinator.triggerConfigReload() tells Kanata to pick up the new config
Kanata confirms
Reload success clears stale diagnostics; failure triggers rollback
Overlay updates
Keycap overlay reads layer state from running Kanata via TCP, showing new mappings

Design Rules

Four invariants that keep the config pipeline reliable.
1
Never parse Kanata configs
Use TCP and the simulator for runtime state and key mapping queries. Kanata syntax is too complex to shadow-implement. See ADR-023.
2
Don't skip TCP reload after config changes
Writing the .kbd file is not enough. Without a TCP reload, Kanata runs stale config. Every write must be followed by triggerConfigReload().
3
Don't send TCP reload directly
Always go through ConfigReloadCoordinator. It checks service health, permission gates, and handles rollback. Raw TCP calls bypass safety.
4
Config generation is one-way
UI model → .kbd file → Kanata. Never reverse. JSON stores are the source of truth; the config file is a derived artifact regenerated on every change. See ADR-025.

Cross-References