karabiner.ts-greg-mods
Version:
Powerful mods for karabiner.ts.
361 lines (360 loc) • 13.9 kB
JavaScript
;
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);
}