Repository: evan-liu/karabiner.ts
Pull request #66 (2023-05-03 08:51:17)

✨ Add layer().configKey()


Patch:
diff --git a/src/config/layer.ts b/src/config/layer.ts
index 9e99451..2cdb8ad 100644
--- a/src/config/layer.ts
+++ b/src/config/layer.ts
@@ -1,7 +1,7 @@
 import {
   FromKeyCode,
   FromOnlyKeyCode,
-  ModifierKeyCode,
+  StickyModifierKeyCode,
 } from '../karabiner/key-code'
 import {
   BasicManipulator,
@@ -16,15 +16,18 @@ import { buildCondition, ifVar } from './condition'
 import { BasicRuleBuilder } from './rule'
 import { toArray } from '../utils/to-array'
 import { BuildContext } from '../utils/build-context'
+import { BasicManipulatorBuilder } from './manipulator'
 
 export type LayerKeyCode = Exclude<
   FromKeyCode,
-  FromOnlyKeyCode | ModifierKeyCode
->
-export type LayerKeyParam = Exclude<
-  FromKeyParam,
-  FromOnlyKeyCode | ModifierKeyCode | ModifierKeyAlias
+  FromOnlyKeyCode | StickyModifierKeyCode
 >
+export type LayerKeyParam =
+  | Exclude<
+      FromKeyParam,
+      FromOnlyKeyCode | StickyModifierKeyCode | ModifierKeyAlias
+    >
+  | '⇪'
 
 /** @see https://github.com/yqrashawn/GokuRakuJoudo/blob/master/tutorial.md#advance3 */
 export function layer(
@@ -39,6 +42,8 @@ export function layer(
 export class LayerRuleBuilder extends BasicRuleBuilder {
   protected readonly keys: LayerKeyCode[]
   protected layerCondition = ifVar(this.varName, this.onValue)
+  protected layerKeyManipulator?: BasicManipulatorBuilder
+  protected replaceLayerKeyToIfAlone = false
 
   constructor(
     key: LayerKeyParam | LayerKeyParam[],
@@ -49,6 +54,25 @@ export class LayerRuleBuilder extends BasicRuleBuilder {
     super(`Layer - ${varName}`)
     this.keys = toArray(key).map((v) => getKeyWithAlias(v) as LayerKeyCode)
     this.condition(this.layerCondition)
+    this.allowEmptyManipulators = true
+  }
+
+  /** Config the layer key. */
+  public configKey(
+    config: (
+      manipulator: Omit<
+        BasicManipulatorBuilder,
+        'description' | 'condition' | 'parameters' | 'build'
+      >,
+    ) => void,
+    replaceToIfAlone = false,
+  ): this {
+    if (!this.layerKeyManipulator) {
+      this.layerKeyManipulator = map('fn')
+    }
+    config(this.layerKeyManipulator)
+    this.replaceLayerKeyToIfAlone = replaceToIfAlone
+    return this
   }
 
   public build(context?: BuildContext): Rule {
@@ -66,6 +90,8 @@ export class LayerRuleBuilder extends BasicRuleBuilder {
           this.offValue,
           conditions,
           context,
+          this.layerKeyManipulator,
+          this.replaceLayerKeyToIfAlone,
         ),
         ...rule.manipulators,
       ]
@@ -82,13 +108,54 @@ export function layerToggleManipulator(
   offValue: ToVariable['value'],
   conditions?: Condition[],
   context?: BuildContext,
+  layerKeyManipulator?: BasicManipulatorBuilder,
+  replaceLayerKeyToIfAlone?: boolean,
 ) {
+  function mergeManipulator<T extends BasicManipulator | BasicManipulator[]>(
+    to: T,
+  ): T {
+    if (!layerKeyManipulator) return to
+    const fromItem = layerKeyManipulator.build()[0]
+    const toItem = toArray(to)[0]
+
+    const keys = [
+      'to',
+      'to_if_alone',
+      'to_if_held_down',
+      'to_after_key_up',
+    ] satisfies Array<keyof BasicManipulator>
+    keys.forEach((key) =>
+      fromItem[key]?.forEach(
+        (v) => (toItem[key] = [...(toItem[key] || []), v]),
+      ),
+    )
+    if (fromItem.to_delayed_action) {
+      toItem.to_delayed_action = toItem.to_delayed_action || {
+        to_if_invoked: [],
+        to_if_canceled: [],
+      }
+      for (const key of ['to_if_invoked', 'to_if_canceled'] as const) {
+        fromItem.to_delayed_action[key].forEach((v) =>
+          toItem.to_delayed_action?.[key].push(v),
+        )
+      }
+    }
+
+    if (replaceLayerKeyToIfAlone) {
+      toItem.to_if_alone = toItem.to_if_alone?.filter(
+        (v) => !('key_code' in v && v.key_code === key_code),
+      )
+    }
+
+    return to
+  }
+
   const manipulator = map(key_code)
     .toVar(varName, onValue)
     .toAfterKeyUp(toSetVar(varName, offValue))
     .toIfAlone({ key_code })
   if (conditions?.length) manipulator.condition(...conditions)
-  if (!context) return manipulator.build()
+  if (!context) return mergeManipulator(manipulator.build())
 
   const key = [
     `layer_${key_code}`,
@@ -103,10 +170,11 @@ export function layerToggleManipulator(
       exiting.to.push(toSetVar(varName, onValue))
       exiting.to_after_key_up.push(toSetVar(varName, offValue))
     }
+    mergeManipulator(exiting)
     return [] // Already added
   }
 
   const result = manipulator.build(context)
   context.setCache(key, result[0])
-  return result
+  return mergeManipulator(result)
 }
diff --git a/src/config/rule.ts b/src/config/rule.ts
index 095460c..3148e89 100644
--- a/src/config/rule.ts
+++ b/src/config/rule.ts
@@ -15,6 +15,7 @@ export class BasicRuleBuilder implements RuleBuilder {
   protected readonly manipulatorSources: Array<
     Manipulator | ManipulatorBuilder
   > = []
+  protected allowEmptyManipulators = false
 
   constructor(
     protected ruleDescription: string,
@@ -47,7 +48,7 @@ export class BasicRuleBuilder implements RuleBuilder {
       ),
     }
 
-    if (rule.manipulators.length === 0) {
+    if (!this.allowEmptyManipulators && rule.manipulators.length === 0) {
       throw new Error(`"manipulators" is empty in "${rule.description}"`)
     }
 


Test patch:
diff --git a/src/config/layer.test.ts b/src/config/layer.test.ts
index 0069842..0aeddbd 100644
--- a/src/config/layer.test.ts
+++ b/src/config/layer.test.ts
@@ -5,6 +5,7 @@ import { BasicManipulator } from '../karabiner/karabiner-config'
 import { ifVar } from './condition'
 import { complexModifications } from './complex-modifications'
 import { simlayer } from './simlayer'
+import { toKey } from './to'
 
 test('layer()', () => {
   const rule = layer('a', 'b-mode', 2, -1)
@@ -54,8 +55,8 @@ test('layer() conditions', () => {
   ])
 })
 
-test('layer() empty manipulators error', () => {
-  expect(() => layer('a', '').manipulators([]).build()).toThrow(
+test('layer() allows empty manipulators', () => {
+  expect(() => layer('a', '').manipulators([]).build()).not.toThrow(
     /manipulators.*empty/,
   )
 })
@@ -90,3 +91,40 @@ test('multiple layer() by same key ', () => {
   expect(rules[2].manipulators.length).toBe(2)
   expect(rules[3].manipulators.length).toBe(1)
 })
+
+test('layer().configKey()', () => {
+  const rule = layer('a', 'v1')
+    .configKey((v) =>
+      v.to('b').toIfHeldDown('c').toDelayedAction(toKey('x'), toKey('y')),
+    )
+    .build()
+  const manipulators = rule.manipulators as BasicManipulator[]
+  expect(manipulators.length).toBe(1)
+  expect(manipulators[0]).toEqual({
+    type: 'basic',
+    from: { key_code: 'a' },
+    to: [{ set_variable: { name: 'v1', value: 1 } }, { key_code: 'b' }],
+    to_after_key_up: [{ set_variable: { name: 'v1', value: 0 } }],
+    to_if_alone: [{ key_code: 'a' }],
+    to_if_held_down: [{ key_code: 'c' }],
+    to_delayed_action: {
+      to_if_invoked: [{ key_code: 'x' }],
+      to_if_canceled: [{ key_code: 'y' }],
+    },
+  })
+})
+
+test('layer().configKey() replaceToIfAlone', () => {
+  const rule = layer('⇪', 'v1')
+    .configKey((v) => v.toIfAlone('b', '⌘'), true)
+    .build()
+  const manipulators = rule.manipulators as BasicManipulator[]
+  expect(manipulators.length).toBe(1)
+  expect(manipulators[0]).toEqual({
+    type: 'basic',
+    from: { key_code: 'caps_lock' },
+    to: [{ set_variable: { name: 'v1', value: 1 } }],
+    to_after_key_up: [{ set_variable: { name: 'v1', value: 0 } }],
+    to_if_alone: [{ key_code: 'b', modifiers: ['command'] }],
+  })
+})