karabiner.ts-greg-mods
Version:
Powerful mods for karabiner.ts.
414 lines (413 loc) • 13.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HrmBuilder = exports.qwertyLayout = exports.HrmKeyboardLayout = void 0;
exports.getSideOfKey = getSideOfKey;
exports.hrm = hrm;
// Home row mods manipulator layer(s).
//
// Flaws & Limitations:
//
// * Can’t chain modifiers to get a chord. You need to press the mod keys
// simultaneously to get two or three modifiers.
// * If you want to remap some chords such as `r⌥+r` to `⏎`, you need to
// define them as a manipulator in the HRM builder and as a standalone
// manipulator.
const karabiner_ts_1 = require("karabiner.ts");
const mod_tap_1 = require("./mod-tap");
const mod_tap_layer_1 = require("./mod-tap-layer");
const karabiner_extra_1 = require("./karabiner-extra");
/**
* Generates all unique ordered pairs from an array or iterable.
*/
function getDoubles(keys) {
const arr = Array.from(keys);
const out = [];
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr.length; j++) {
if (i !== j) {
out.push([arr[i], arr[j]]);
}
}
}
return out;
}
/**
* Generates all unique ordered triples from an array or iterable.
*/
function getTriples(keys) {
const arr = Array.from(keys);
const out = [];
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr.length; j++) {
for (let k = 0; k < arr.length; k++) {
if (i !== j && i !== k && j !== k) {
out.push([arr[i], arr[j], arr[k]]);
}
}
}
}
return out;
}
class HrmKeyboardLayout {
leftHandKeys;
rightHandKeys;
constructor(leftHandKeys, rightHandKeys) {
this.leftHandKeys = leftHandKeys.map((k) => (0, karabiner_ts_1.getKeyWithAlias)(k));
this.rightHandKeys = rightHandKeys.map((k) => (0, karabiner_ts_1.getKeyWithAlias)(k));
}
}
exports.HrmKeyboardLayout = HrmKeyboardLayout;
const qwertyLeftHandKeys = [
"q",
"w",
"e",
"r",
"t",
"a",
"s",
"d",
"f",
"g",
"z",
"x",
"c",
"v",
"b",
"escape",
"1",
"2",
"3",
"4",
"5",
"tab",
"caps_lock",
];
const qwertyRightHandKeys = [
"y",
"u",
"i",
"o",
"p",
"[",
"]",
"\\",
"h",
"j",
"k",
"l",
";",
"'",
"n",
"m",
",",
".",
"/",
"␣",
"⏎",
"6",
"7",
"8",
"9",
"0",
"hyphen",
"equal_sign",
"delete_or_backspace",
"left_arrow",
"up_arrow",
"down_arrow",
"right_arrow",
"page_up",
"page_down",
];
exports.qwertyLayout = new HrmKeyboardLayout(qwertyLeftHandKeys, qwertyRightHandKeys);
/**
* Given a key and a keyboard layout, returns which side ('left' | 'right') the key is on,
* or null if the key is not found in either hand.
*/
function getSideOfKey(key, layout) {
if (layout.leftHandKeys.includes(key)) {
return "left";
}
else if (layout.rightHandKeys.includes(key)) {
return "right";
}
else {
return null;
}
}
class SmartModifierMap {
elems = new Map();
get(modifierParam) {
let modifiers = (0, karabiner_ts_1.parseModifierParam)(modifierParam);
if (!modifiers || modifiers.length != 1) {
throw new Error(`Expected a single modifier, but got ${JSON.stringify(modifierParam)}.`);
}
let modifier = modifiers[0];
let elems = this.elems.get(modifier) ?? [];
if ((0, karabiner_extra_1.isSidedMod)(modifier)) {
const unsidedModifier = (0, karabiner_extra_1.getUnsidedMod)(modifier);
elems = elems.concat(this.elems.get(unsidedModifier) ?? []);
}
return elems;
}
add(modifierParam, elem) {
let modifiers = (0, karabiner_ts_1.parseModifierParam)(modifierParam);
if (!modifiers || modifiers.length != 1) {
throw new Error(`Expected a single modifier, but got ${JSON.stringify(modifierParam)}.`);
}
let modifier = modifiers[0];
this.elems.set(modifier, (this.elems.get(modifier) ?? []).concat([elem]));
return this;
}
}
class HrmBuilder {
// A map of keys to their modifiers, e.g., a ⇒ ⌃, s ⇒ ⌥, d ⇒ ⇧.
hrmKeys;
keyboardLayout;
// Manipulators that override the default mod-tap behavior.
smartManipulatorMap = new SmartModifierMap();
smartKeyOverrideMap = new SmartModifierMap();
isLazy = false;
chosenHoldTapStrategy = "permissive-hold";
isChordalHold = true;
hasTriples = true;
simultaneousThresholdMs;
tappingTermMs;
constructor(hrmKeys, layout = exports.qwertyLayout) {
this.hrmKeys = hrmKeys;
this.keyboardLayout = layout;
}
/**
* Sets whether the modifiers are lazy.
*/
lazy(isLazy) {
this.isLazy = isLazy;
return this;
}
/**
* Sets the hold tap strategy for the builder.
*/
holdTapStrategy(strategy) {
this.chosenHoldTapStrategy = strategy;
return this;
}
/**
* Enables or disables chordal hold.
*/
chordalHold(isChordalHold) {
this.isChordalHold = isChordalHold;
return this;
}
/**
* Sets whether the builder should generate 3-key chords.
*/
triples(hasTriples) {
this.hasTriples = hasTriples;
return this;
}
/**
* Sets the simultaneous threshold in milliseconds.
*
* Simultaneous threshold is the maximum time in milliseconds Karabiner will wait for a simultaneous
* chord to appear for doubles and triples.
*/
simultaneousThreshold(simultaneousThresholdMs) {
this.simultaneousThresholdMs = simultaneousThresholdMs;
return this;
}
/**
* Sets the tapping term in milliseconds.
*/
tappingTerm(tappingTermMs) {
this.tappingTermMs = tappingTermMs;
return this;
}
build() {
// Tuple manipulators need to come first, so that a more generic condition
// doesn't trigger first.
let manipulators = this.tupleManipulators();
manipulators = manipulators.concat(this.singleManipulators());
return manipulators;
}
smartManipulator(mod, manipulator) {
if ("build" in manipulator) {
for (const m of manipulator.build())
this.smartManipulatorMap.add(mod, m);
}
else {
this.smartManipulatorMap.add(mod, manipulator);
}
return this;
}
smartManipulators(mod, ...manipulators) {
for (const manipulator of manipulators) {
this.smartManipulator(mod, manipulator);
}
return this;
}
/**
* Adds some keys that override the default hold-tap strategy.
*/
keys(mod, keys, strategy) {
for (const key of keys) {
this.smartKeyOverrideMap.add(mod, [
(0, karabiner_ts_1.getKeyWithAlias)(key),
strategy,
]);
}
return this;
}
/**
* Creates all chorded modifiers for the home row mods.
*/
tupleManipulators() {
const hrmKeys = this.hrmKeys;
let manipulators = [];
for (const side of karabiner_extra_1.sides) {
const sideKeys = Array.from(hrmKeys.keys()).filter((k) => (0, karabiner_extra_1.getSideOfMod)(hrmKeys.get(k)) === side);
if (this.hasTriples) {
for (const [first, second, third] of getTriples(sideKeys)) {
let mt = (0, mod_tap_1.modTap)()
.from([first, second, third], [], "any")
.modifiers((0, karabiner_ts_1.toKey)(hrmKeys.get(first), [
hrmKeys.get(second),
hrmKeys.get(third),
]))
.permissive(true);
if (this.simultaneousThresholdMs) {
mt.simultaneousThreshold(this.simultaneousThresholdMs);
}
if (this.tappingTermMs) {
mt.tappingTerm(this.tappingTermMs);
}
manipulators = manipulators.concat(mt.build());
}
}
for (const [first, second] of getDoubles(sideKeys)) {
let mt = (0, mod_tap_1.modTap)()
.from([first, second], [], "any")
.modifiers((0, karabiner_ts_1.toKey)(hrmKeys.get(first), hrmKeys.get(second)))
.permissive(true);
if (this.simultaneousThresholdMs) {
mt.simultaneousThreshold(this.simultaneousThresholdMs);
}
if (this.tappingTermMs) {
mt.tappingTerm(this.tappingTermMs);
}
manipulators = manipulators.concat(mt.build());
}
}
return (0, karabiner_ts_1.withCondition)((0, karabiner_ts_1.ifVar)(karabiner_extra_1.layerVarName).unless())(manipulators);
}
singleManipulators() {
let manipulators = [];
for (const [hrmKey, hrmMod] of this.hrmKeys.entries()) {
manipulators = manipulators.concat(this.singleManipulatorLayer(hrmKey, hrmMod));
}
return manipulators;
}
singleManipulatorLayer(hrmKey, hrmMod) {
const side = (0, karabiner_extra_1.getSideOfMod)(hrmMod);
if (side === null) {
throw new Error(`Expected a sided modifier, but got ${JSON.stringify(hrmMod)}.`);
}
let smartManipulators = this.smartManipulatorMap.get(hrmMod);
let mtLayer = (0, mod_tap_layer_1.modTapLayer)(hrmKey, hrmMod)
.allowAnyModifiers()
.lazy(this.isLazy);
for (const sm of smartManipulators) {
const smFrom = (0, karabiner_extra_1.getFromKeyCodeFromBasicManipulator)(sm);
if (smFrom === null) {
throw new Error(`Expected a from key code in a smart manipulator but got ${JSON.stringify(sm)}.`);
}
if (!(0, karabiner_extra_1.isFromAndToKeyCode)(smFrom)) {
throw new Error(`Expected a from and to key code in a smart manipulator but got ${JSON.stringify(smFrom)}.`);
}
const smSide = getSideOfKey(smFrom, this.keyboardLayout);
if (smSide === null) {
throw new Error(`Could not determine the side of the smart manipulator key, ${JSON.stringify(smFrom)}.`);
}
if (this.isChordalHold && smSide === side) {
mtLayer.slowManipulators(sm);
}
else {
switch (this.chosenHoldTapStrategy) {
case "permissive-hold":
mtLayer.permissiveHoldManipulators(sm);
break;
case "hold-on-other-key-press":
mtLayer.holdOnOtherKeyPressManipulator(sm);
break;
case "slow":
mtLayer.slowManipulators(sm);
break;
default:
throw this.chosenHoldTapStrategy;
}
}
}
let keyOverrides = this.smartKeyOverrideMap.get(hrmMod);
for (const [key, strategy] of keyOverrides) {
switch (strategy) {
case "permissive-hold":
mtLayer.permissiveHoldKey(key);
break;
case "hold-on-other-key-press":
mtLayer.holdOnOtherKeyPressKeys([key]);
break;
case "slow":
mtLayer.slowKeys([key]);
break;
default:
throw strategy;
}
}
let smartKeys = side === "left"
? this.keyboardLayout.rightHandKeys
: this.keyboardLayout.leftHandKeys;
let slowKeys = side === "left"
? this.keyboardLayout.leftHandKeys
: this.keyboardLayout.rightHandKeys;
switch (this.chosenHoldTapStrategy) {
case "permissive-hold":
mtLayer.permissiveHoldKeys(...smartKeys);
break;
case "hold-on-other-key-press":
mtLayer.holdOnOtherKeyPressKeys(smartKeys);
break;
case "slow":
mtLayer.slowKeys(smartKeys);
break;
default:
throw this.chosenHoldTapStrategy;
}
if (this.isChordalHold) {
mtLayer.slowKeys(slowKeys);
}
else {
switch (this.chosenHoldTapStrategy) {
case "permissive-hold":
mtLayer.permissiveHoldKeys(...slowKeys);
break;
case "hold-on-other-key-press":
mtLayer.holdOnOtherKeyPressKeys(slowKeys);
break;
case "slow":
mtLayer.slowKeys(slowKeys);
break;
default:
throw this.chosenHoldTapStrategy;
}
}
// Let the primary mouse button benefit from modifiers.
mtLayer.holdOnOtherKeyPressManipulator((0, karabiner_ts_1.mapPointingButton)("button1").to((0, karabiner_ts_1.toPointingButton)("button1", hrmMod)));
if (this.tappingTermMs) {
mtLayer.tappingTerm(this.tappingTermMs);
}
return mtLayer.build().manipulators;
}
}
exports.HrmBuilder = HrmBuilder;
function hrm(hrmKeys, layout = exports.qwertyLayout) {
return new HrmBuilder(hrmKeys, layout);
}