UNPKG

karabiner.ts-greg-mods

Version:
361 lines (360 loc) 13.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.HoldTapLayerBuilder = void 0; exports.holdTapLayer = holdTapLayer; // A hold-tap layer is a modified karabiner.ts layer that mimics QMK hold-tap // behavior. // // A hold-tap layer comes with the following features on top of the regular // layer: // // - Permissive hold and hold on other key press behavior. // - Replaying typed keys when the hold-behavior doesn’t trigger, e.g., roll // overs involving the layer key during normal typing do not lose keys. // - hold-tap strategy can be configured individually per key. // // Some known flaws and drawbacks are as follows: // // - To avoid losing keys, you must configure each key that can be used // together with the layer key (e.g., during a roll over). // // ## Implementation // // The hold-tap layer is implemented as a Karabiner.ts layer but it adds a // start variable to indicate that the hold-tap key has just been pressed. This // variable is unset after the tapping term is over or when another key is // pressed. const assert_1 = __importDefault(require("assert")); const karabiner_ts_1 = require("karabiner.ts"); const karabiner_extra_1 = require("./karabiner-extra"); function applyConditionsToToEvent(e, ...conds) { const originalConditions = e.conditions ?? []; return { ...e, conditions: conds.concat(originalConditions), }; } function applyConditionToToEvents(m, cond) { if (m.to) { m.to = m.to.map((e) => applyConditionsToToEvent(e, cond)); } if (m.to_after_key_up) { m.to_after_key_up = m.to_after_key_up.map((e) => applyConditionsToToEvent(e, cond)); } if (m.to_if_alone) { m.to_if_alone = m.to_if_alone.map((e) => applyConditionsToToEvent(e, cond)); } if (m.to_if_held_down) { m.to_if_held_down = m.to_if_held_down.map((e) => applyConditionsToToEvent(e, cond)); } } function isSlowManipulator(manipulator) { return (typeof manipulator.from === "object" && manipulator.from !== null && "key_code" in manipulator.from && (0, karabiner_extra_1.isFromAndToKeyCode)(manipulator.from.key_code)); } /** * The hold-tap layer builder. */ class HoldTapLayerBuilder { key; layer_builder; ruleDescription; configKeyOptionalMods; tappingTermMs; constructor(key) { this.key = (0, karabiner_ts_1.getKeyWithAlias)(key); this.layer_builder = (0, karabiner_ts_1.layer)(key).configKey((v) => v // Set the start variable to prevent slow keys from acting with the modifier. // Add the replay token. .to([ (0, karabiner_ts_1.toSetVar)(this.startVariable, 1), (0, karabiner_ts_1.toSetVar)(this.replayVariable, 1), ]) // Unset the start variable on hold. .toIfHeldDown((0, karabiner_ts_1.toSetVar)(this.startVariable, 0)) .toAfterKeyUp([ (0, karabiner_ts_1.toSetVar)(this.startVariable, 0), // Do not remove the replay token, so we can replay the layer key // during flowing chords: // // LayerKey ↓ // OtherKey ↓ // LayerKey ↑ // OtherKey ↑ ]), /*replaceToIfAlone*/ false); } /** * Adds an event for the layer key is pressed alone. */ onAlone(toEvent) { this.layer_builder.configKey((v) => v.toIfAlone(toEvent)); return this; } /** * Adds an event for the layer key is held down. * * It's useful for home-row mods. */ onHold(key, modifiers, options) { this.layer_builder.configKey((v) => v.toIfHeldDown(key, modifiers, options)); return this; } /** * Sets optional modifiers for the layer key. */ optionalModifiers(mods) { this.configKeyOptionalMods = (0, karabiner_ts_1.parseModifierParam)(mods); return this; } /** * Allows any modifiers to be used with the layer key. */ allowAnyModifiers() { this.configKeyOptionalMods = ["any"]; return this; } /** * Sets the description for the rule. */ description(description) { this.ruleDescription = description; return this; } /** * Sets the tapping term in milliseconds. * * The tapping term is the time in milliseconds that the key must be held * down in order to be fully active. */ tappingTerm(tappingTermMs) { this.tappingTermMs = tappingTermMs; return this; } build() { if (this.ruleDescription) { this.layer_builder.description(this.ruleDescription); } let rule = this.layer_builder.build(); (0, assert_1.default)(rule.manipulators.length > 0, "HoldTapLayerBuilder should have at least one manipulator."); let configManipulator = rule .manipulators[0]; if (this.configKeyOptionalMods) { this.setOptionalConfigKeyModifiers(configManipulator.from, this.configKeyOptionalMods); } this.setTappingTerm(configManipulator); return rule; } /** * Adds a regular manipulator to the layer. * * A regular manipulator always triggers when the layer key is pressed. * * This form of hold-tap decision making is called "hold on other key press" * in QMK, hence the name. */ holdOnOtherKeyPressManipulator(manipulator) { this.layer_builder.manipulators([manipulator]); return this; } holdOnOtherKeyPressManipulators(manipulators) { this.layer_builder.manipulators(manipulators); return this; } /** * Adds an echo key. * * An echo key is just replayed back together with the layer key during the * tapping term. * * It's useful for keys that might be rolled over often. */ echoKey(echoKey) { this.holdOnOtherKeyPressManipulator((0, karabiner_ts_1.map)(echoKey) .condition((0, karabiner_ts_1.ifVar)(this.startVariable)) .to((0, karabiner_ts_1.toSetVar)(this.startVariable, 0)) .to(this.key) .to(echoKey)); return this; } echoKeys(...echoKeys) { for (const echoKey of echoKeys) { this.echoKey(echoKey); } return this; } /** * Adds a permissive hold manipulator. * * A permissive hold manipulator triggers either when the tapping term has * elapsed or the manipulator's trigger has been tapped while holding the * layer key. * * If neither of those things happened, the layer key and and the manipulator * trigger are replayed as normal keys. */ permissiveHoldManipulator(manipulator) { // Validate input parameters. if ("build" in manipulator) { let manipulators = manipulator.build(); if (manipulators.length !== 1) { throw new Error("permissiveHoldManipulator expects a single manipulator."); } manipulator = manipulators[0]; } (0, assert_1.default)("from" in manipulator && "key_code" in manipulator.from, "permissiveHoldManipulator expects a BasicManipulator with a 'from' key code."); const fromKeyCode = (0, karabiner_extra_1.getFromKeyCodeFromBasicManipulator)(manipulator); if (fromKeyCode === null) { throw new Error(`A hold-tap 'manipulator' doesn't have a simple from key, but ${JSON.stringify(manipulator.from)}.`); } if (!(0, karabiner_extra_1.isFromAndToKeyCode)(fromKeyCode)) { throw new Error("The 'from' key code must be a FromAndToKeyCode but is: " + fromKeyCode); } if (!(0, karabiner_extra_1.isFromAndToKeyCode)(this.key)) { throw new Error("The layer key must be a FromAndToKeyCode but is: " + this.key); } // Encode the permissive hold logic inside a manipulator. let ifLayerVariable = (0, karabiner_ts_1.ifVar)(this.layerVariable).build(); let unlessLayerVariable = (0, karabiner_ts_1.ifVar)(this.layerVariable).unless().build(); let ifReplayVariable = (0, karabiner_ts_1.ifVar)(this.replayVariable).build(); let phManipulator = { ...manipulator, }; // Only run the original logic on tap. applyConditionToToEvents(phManipulator, ifLayerVariable); let toEvents = phManipulator.to ?? []; phManipulator.to_after_key_up = toEvents.concat(phManipulator.to_after_key_up ?? []); phManipulator.to = undefined; // If the layer became inactive (interleaved taps), replay the keys. phManipulator.to_after_key_up.push({ conditions: [unlessLayerVariable, ifReplayVariable], key_code: this.key, }, { conditions: [unlessLayerVariable, ifReplayVariable], ...(0, karabiner_ts_1.toSetVar)(this.replayVariable, 0), }, { conditions: [unlessLayerVariable], key_code: fromKeyCode, }, // Unset the start variable. We have triggered the hold action, so we are // in. { ...(0, karabiner_ts_1.toSetVar)(this.startVariable, 0), halt: true, }); phManipulator.to_delayed_action = { to_if_invoked: [], // If we chord with another key that means we are typing, so // we should replay. to_if_canceled: [ { conditions: [unlessLayerVariable, ifReplayVariable], key_code: this.key, }, { conditions: [unlessLayerVariable, ifReplayVariable], ...(0, karabiner_ts_1.toSetVar)(this.replayVariable, 0), }, { conditions: [unlessLayerVariable], key_code: fromKeyCode, // Cancel the after key up action. We've already replayed the keys. halt: true, }, ], }; let ifStartVariable = (0, karabiner_ts_1.ifVar)(this.startVariable); let unlessStartVariable = (0, karabiner_ts_1.ifVar)(this.startVariable).unless(); let manipulators = []; manipulators.push( // If hold is active, run the manipulator. ...(0, karabiner_ts_1.withCondition)(unlessStartVariable)([manipulator]).build(), // Otherwise, run the permissive hold logic. ...(0, karabiner_ts_1.withCondition)(ifStartVariable)([phManipulator]).build()); this.layer_builder.manipulators(manipulators); return this; } permissiveHoldManipulators(...manipulators) { for (const manipulator of manipulators) { this.permissiveHoldManipulator(manipulator); } return this; } /** * Adds a slow manipulator to the layer. * * A slow manipulator is a manipulator that only triggers when * the tapping term has elapsed. */ slowManipulator(manipulator) { // Validate input parameters. const fromKeyCode = manipulator.from.key_code; if (!(0, karabiner_extra_1.isFromAndToKeyCode)(this.key)) { throw new Error("The layer key must be a FromAndToKeyCode but is: " + this.key); } let ifStartVariable = (0, karabiner_ts_1.ifVar)(this.startVariable); let unlessStartVariable = (0, karabiner_ts_1.ifVar)(this.startVariable).unless(); let unsetStartVariable = (0, karabiner_ts_1.toSetVar)(this.startVariable, 0); let manipulators = []; manipulators.push( // If hold is active, run the manipulator. ...(0, karabiner_ts_1.withCondition)(unlessStartVariable)([manipulator]).build(), ...(0, karabiner_ts_1.withCondition)(ifStartVariable)([ (0, karabiner_ts_1.map)(fromKeyCode).to(this.key).to(fromKeyCode).to(unsetStartVariable), ]).build()); this.layer_builder.manipulators(manipulators); return this; } slowManipulators(...manipulators) { let newManipulators = manipulators.flatMap((manipulatorOrBuilder) => { if ("build" in manipulatorOrBuilder) { return manipulatorOrBuilder.build(); } return [manipulatorOrBuilder]; }); for (const manipulator of newManipulators) { if (isSlowManipulator(manipulator)) { this.slowManipulator(manipulator); } else { throw new Error("slowManipulators expects a SlowManipulator but got: " + JSON.stringify(manipulator)); } } return this; } get layerVariable() { // This is internally used by Karabiner.ts’ layer. return "layer-" + this.key; } get startVariable() { // Purposefully using a suffix to make sure this variable is grouped // together in Karabiner Event Viewer. return this.layerVariable + "-start"; } get replayVariable() { return this.layerVariable + "-replay"; } setOptionalConfigKeyModifiers(layerKey, mods) { layerKey.modifiers = { optional: mods, }; } setTappingTerm(configManipulator) { if (this.tappingTermMs) { configManipulator.parameters = configManipulator.parameters ?? {}; configManipulator.parameters["basic.to_if_held_down_threshold_milliseconds"] = this.tappingTermMs; } } } exports.HoldTapLayerBuilder = HoldTapLayerBuilder; /** * Creates a builder for the hold-tap layer. */ function holdTapLayer(key) { return new HoldTapLayerBuilder(key); }