Repository: evan-liu/karabiner.ts
Pull request #96 (2023-07-18 09:43:06)

✨ Add notification to duoLayer


Patch:
diff --git a/src/config/complex-modifications.ts b/src/config/complex-modifications.ts
index b8dbd10..a6339b2 100644
--- a/src/config/complex-modifications.ts
+++ b/src/config/complex-modifications.ts
@@ -30,6 +30,8 @@ export function complexModifications(
   const {
     'double_tap.delay_milliseconds': doubleTapDelay,
     'simlayer.threshold_milliseconds': simlayerThreshold,
+    'duo_layer.threshold_milliseconds': duoLayerThreshold,
+    'duo_layer.notification': duoLayerNotification,
     ...complexModificationsParameters
   } = parameters
 
@@ -40,6 +42,10 @@ export function complexModifications(
   context.setParameters<typeof defaultSimlayerParameters>({
     'simlayer.threshold_milliseconds': simlayerThreshold,
   })
+  context.setParameters<typeof defaultDuoLayerParameters>({
+    'duo_layer.threshold_milliseconds': duoLayerThreshold,
+    'duo_layer.notification': duoLayerNotification,
+  })
 
   const builtRules = rules.map((v) => buildRule(v, context))
   const modifications: ComplexModifications = {
diff --git a/src/config/duo-layer.ts b/src/config/duo-layer.ts
index 6eb9c6d..864cb57 100644
--- a/src/config/duo-layer.ts
+++ b/src/config/duo-layer.ts
@@ -10,10 +10,15 @@ import { BasicRuleBuilder } from './rule.ts'
 import { buildCondition, ConditionBuilder, ifVar } from './condition.ts'
 import { BuildContext } from '../utils/build-context.ts'
 import { mapSimultaneous } from './simultaneous.ts'
-import { toSetVar } from './to.ts'
+import {
+  toNotificationMessage,
+  toRemoveNotificationMessage,
+  toSetVar,
+} from './to.ts'
 
 export const defaultDuoLayerParameters = {
   'duo_layer.threshold_milliseconds': 200,
+  'duo_layer.notification': false as boolean | string,
 }
 
 export function duoLayer(
@@ -32,6 +37,8 @@ export class DuoLayerRuleBuilder extends BasicRuleBuilder {
   private readonly simultaneousOptions: SimultaneousOptions = {}
   private simultaneousThreshold?: number
 
+  private layerNotification?: boolean | string
+
   constructor(
     private readonly key1: LayerKeyParam,
     private readonly key2: LayerKeyParam,
@@ -39,11 +46,12 @@ export class DuoLayerRuleBuilder extends BasicRuleBuilder {
     private readonly onValue: ToVariable['value'] = 1,
     private readonly offValue: ToVariable['value'] = 0,
   ) {
+    const desc = `DuoLayer ${varName || `${key1} ${key2}`}`
     if (!varName) {
       varName = `duo-layer-${key1}-${key2}`
     }
 
-    super(`Layer - ${varName}`)
+    super(desc)
     this.varName = varName
     this.layerCondition = ifVar(this.varName, this.onValue)
     this.condition(this.layerCondition)
@@ -60,6 +68,12 @@ export class DuoLayerRuleBuilder extends BasicRuleBuilder {
     return this
   }
 
+  /** Set the notification when the layer is active. */
+  public notification(v: boolean | string) {
+    this.layerNotification = v
+    return this
+  }
+
   public build(context?: BuildContext): Rule {
     const rule = super.build(context)
 
@@ -68,25 +82,36 @@ export class DuoLayerRuleBuilder extends BasicRuleBuilder {
       defaultDuoLayerParameters
     const threshold =
       this.simultaneousThreshold || params['duo_layer.threshold_milliseconds']
+    const notification =
+      this.layerNotification ?? params['duo_layer.notification']
 
     const conditions = this.conditions
       .filter((v) => v !== this.layerCondition)
       .map(buildCondition)
 
     // Layer toggle
+    const toAfterKeyUp = [
+      ...(this.simultaneousOptions.to_after_key_up || []),
+      toSetVar(this.varName, this.offValue),
+    ]
+    const to = [toSetVar(this.varName, this.onValue)]
+    if (notification) {
+      const id = `duo-layer-${this.varName}`
+      const message =
+        notification === true ? this.ruleDescription : notification
+      to.push(toNotificationMessage(id, message))
+      toAfterKeyUp.push(toRemoveNotificationMessage(id))
+    }
     const manipulator = mapSimultaneous(
       [this.key1, this.key2],
       {
         ...this.simultaneousOptions,
-        to_after_key_up: [
-          ...(this.simultaneousOptions.to_after_key_up || []),
-          toSetVar(this.varName, this.offValue),
-        ],
+        to_after_key_up: toAfterKeyUp,
       },
       threshold,
     )
       .modifiers('??')
-      .to(toSetVar(this.varName, this.onValue))
+      .to(to)
     if (conditions.length) {
       manipulator.condition(...conditions)
     }
diff --git a/src/index.ts b/src/index.ts
index 9bdc3b1..3fe7add 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -4,6 +4,11 @@ export * from './karabiner/consumer-key-code.ts'
 export * from './karabiner/pointing-button.ts'
 export * from './karabiner/karabiner-config.ts'
 
+// Key alias
+export * from './config/modifier.ts'
+export * from './utils/key-alias.ts'
+export * from './utils/multi-modifier.ts'
+
 // Types
 export type {
   LeftModifierFlag,
@@ -32,8 +37,6 @@ export { withCondition } from './utils/with-condition.ts'
 export { withMapper } from './utils/with-mapper.ts'
 export { withModifier } from './utils/with-modifier.ts'
 
-export { namedMultiModifierAliases } from './utils/multi-modifier.ts'
-
 // From
 export * from './config/from.ts'
 export { mapSimultaneous } from './config/simultaneous.ts'


Test patch:
diff --git a/src/config/duo-layer.test.ts b/src/config/duo-layer.test.ts
index cdd9a95..592247c 100644
--- a/src/config/duo-layer.test.ts
+++ b/src/config/duo-layer.test.ts
@@ -3,16 +3,19 @@ import { defaultDuoLayerParameters, duoLayer } from './duo-layer'
 import { map } from './from'
 import {
   BasicManipulator,
+  FromEvent,
+  FromKeyType,
   FromSimultaneousEvent,
 } from '../karabiner/karabiner-config'
 import { toSetVar } from './to'
 import { ifVar } from './condition'
 import { complexModifications } from './complex-modifications'
+import { BuildContext } from '../utils/build-context.ts'
 
 test('duoLayer()', () => {
   const layer = duoLayer(1, 2).manipulators([map(3).to(4)])
   const rule = layer.build()
-  expect(rule.description).toBe('Layer - duo-layer-1-2')
+  expect(rule.description).toBe('DuoLayer 1 2')
   expect(layer.description('test').build().description).toBe('test')
 
   const manipulators = rule.manipulators as BasicManipulator[]
@@ -39,7 +42,7 @@ test('duoLayer()', () => {
 
 test('duoLayer() varName and values', () => {
   const rule = duoLayer(1, 2, 'var12', 2, -1).build()
-  expect(rule.description).toBe(`Layer - var12`)
+  expect(rule.description).toBe(`DuoLayer var12`)
   const manipulator = rule.manipulators[0] as BasicManipulator
   expect(manipulator.to).toEqual([
     { set_variable: { name: 'var12', value: 2 } },
@@ -100,3 +103,59 @@ test('duoLayer() with same keys', () => {
       .simultaneous_options?.to_after_key_up,
   ).toEqual([toSetVar('duo-layer-1-2', 0)])
 })
+
+test('duoLayer() notification', () => {
+  const rule = duoLayer('a', 'b').notification(true).build()
+  const manipulators = rule.manipulators as BasicManipulator[]
+  expect(manipulators.length).toBe(1)
+  expect(manipulators[0].to?.[1]).toEqual({
+    set_notification_message: {
+      id: 'duo-layer-duo-layer-a-b',
+      text: 'DuoLayer a b',
+    },
+  })
+  const from = manipulators[0].from as Extract<
+    FromEvent,
+    { simultaneous: FromKeyType[] }
+  >
+  expect(from.simultaneous_options?.to_after_key_up?.[1]).toEqual({
+    set_notification_message: { id: 'duo-layer-duo-layer-a-b', text: '' },
+  })
+
+  const ruleB = duoLayer('a', 'b').notification('test-b').build()
+  const manipulatorB = ruleB.manipulators[0] as BasicManipulator
+  expect(manipulatorB.to?.[1]).toEqual({
+    set_notification_message: {
+      id: 'duo-layer-duo-layer-a-b',
+      text: 'test-b',
+    },
+  })
+})
+
+test('duoLayer() notification parameters', () => {
+  defaultDuoLayerParameters['duo_layer.notification'] = 'ab'
+
+  const rule = duoLayer('a', 'b').build()
+  const manipulator = rule.manipulators[0] as BasicManipulator
+  expect(manipulator.to?.[1]).toEqual({
+    set_notification_message: {
+      id: 'duo-layer-duo-layer-a-b',
+      text: 'ab',
+    },
+  })
+})
+
+test('duoLayer() parameters in BuildContext', () => {
+  const context = new BuildContext()
+  context.setParameters<typeof defaultDuoLayerParameters>({
+    'duo_layer.notification': 'bc',
+  })
+  const rule = duoLayer('a', 'b').build(context)
+  const manipulator = rule.manipulators[0] as BasicManipulator
+  expect(manipulator.to?.[1]).toEqual({
+    set_notification_message: {
+      id: 'duo-layer-duo-layer-a-b',
+      text: 'bc',
+    },
+  })
+})