Repository: evan-liu/karabiner.ts
Pull request #83 (2023-05-15 07:40:18)

✨ Add manipulator map to rule and utils params


Patch:
diff --git a/src/config/manipulator.ts b/src/config/manipulator.ts
index 4ce1b0c..8ac1dd9 100644
--- a/src/config/manipulator.ts
+++ b/src/config/manipulator.ts
@@ -41,11 +41,16 @@ import { StickyModifierKeyCode } from '../karabiner/key-code'
 import { toArray } from '../utils/to-array'
 import { BuildContext } from '../utils/build-context'
 import { toTypeSequence } from './to-type-sequence'
+import { FromKeyParam, map } from './from'
 
 export interface ManipulatorBuilder {
   build(context?: BuildContext): Manipulator[]
 }
 
+// TODO Make the type FromKeyParam.
+//  Current TypeScript version gives errors for some keys, like numbers.
+export type ManipulatorMap = Partial<Record<string | number, ToEvent>>
+
 export class BasicManipulatorBuilder implements ManipulatorBuilder {
   protected readonly manipulator: BasicManipulator
 
@@ -57,6 +62,7 @@ export class BasicManipulatorBuilder implements ManipulatorBuilder {
   get from(): FromEvent {
     return this.manipulator.from
   }
+
   /** Set or add ToEvent(s) to the Manipulator. */
 
   to(event: ToEvent | ToEvent[]): this
@@ -313,14 +319,21 @@ export class BasicManipulatorBuilder implements ManipulatorBuilder {
 }
 
 export function isManipulatorBuilder(
-  src: Manipulator | ManipulatorBuilder,
+  src: Manipulator | ManipulatorBuilder | ManipulatorMap,
 ): src is ManipulatorBuilder {
   return typeof (src as ManipulatorBuilder).build === 'function'
 }
 
 export function buildManipulators(
-  src: Manipulator | ManipulatorBuilder,
+  src: Manipulator | ManipulatorBuilder | ManipulatorMap,
   context?: BuildContext,
 ): Manipulator[] {
-  return isManipulatorBuilder(src) ? src.build(context) : [src]
+  if (isManipulatorBuilder(src)) return src.build(context)
+  if ('type' in src) return [src as Manipulator]
+
+  const entries = Object.entries(src) as Array<[FromKeyParam, ToEvent]>
+  return entries.reduce(
+    (r, [k, v]) => [...r, ...map(k).to(v).build(context)],
+    [] as Manipulator[],
+  )
 }
diff --git a/src/config/rule.ts b/src/config/rule.ts
index 3148e89..b2f09ff 100644
--- a/src/config/rule.ts
+++ b/src/config/rule.ts
@@ -1,5 +1,9 @@
 import { Condition, Manipulator, Rule } from '../karabiner/karabiner-config'
-import { buildManipulators, ManipulatorBuilder } from './manipulator'
+import {
+  buildManipulators,
+  ManipulatorBuilder,
+  ManipulatorMap,
+} from './manipulator'
 import { buildCondition, ConditionBuilder } from './condition'
 import { BuildContext } from '../utils/build-context'
 
@@ -13,7 +17,7 @@ export function rule(
 export class BasicRuleBuilder implements RuleBuilder {
   protected readonly conditions: Array<Condition | ConditionBuilder>
   protected readonly manipulatorSources: Array<
-    Manipulator | ManipulatorBuilder
+    Manipulator | ManipulatorBuilder | ManipulatorMap
   > = []
   protected allowEmptyManipulators = false
 
@@ -24,8 +28,16 @@ export class BasicRuleBuilder implements RuleBuilder {
     this.conditions = conditions
   }
 
-  manipulators(src: Array<Manipulator | ManipulatorBuilder>): this {
-    src.forEach((v) => this.manipulatorSources.push(v))
+  manipulators(
+    src:
+      | ManipulatorMap
+      | Array<Manipulator | ManipulatorBuilder | ManipulatorMap>,
+  ): this {
+    if (Array.isArray(src)) {
+      src.forEach((v) => this.manipulatorSources.push(v))
+    } else {
+      this.manipulatorSources.push(src)
+    }
     return this
   }
 
diff --git a/src/index.ts b/src/index.ts
index 9a9ef95..3d24ab5 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -12,7 +12,6 @@ export type {
   ModifierParam,
   FromModifierParam,
 } from './config/modifier'
-export type { ModificationParameters } from './config/complex-modifications'
 export type { MultiModifierAlias } from './utils/multi-modifier'
 export type {
   ModifierKeyAlias,
@@ -21,6 +20,7 @@ export type {
   KeyAlias,
   NumberKeyValue,
 } from './utils/key-alias'
+export type { ManipulatorMap } from './config/manipulator'
 
 // Utils
 export { withCondition } from './utils/with-condition'
@@ -48,5 +48,12 @@ export { layer, hyperLayer, modifierLayer } from './config/layer'
 export { simlayer } from './config/simlayer'
 export { duoLayer } from './config/duo-layer'
 
+// Configs
+export type { ModificationParameters } from './config/complex-modifications'
+export { defaultComplexModificationsParameters } from './config/complex-modifications'
+export { defaultDoubleTapParameters } from './config/double-tap'
+export { defaultSimlayerParameters } from './config/simlayer'
+export { defaultDuoLayerParameters } from './config/duo-layer'
+
 // Output
 export { writeToProfile } from './output'
diff --git a/src/utils/with-condition.ts b/src/utils/with-condition.ts
index bd07e5f..1f30306 100644
--- a/src/utils/with-condition.ts
+++ b/src/utils/with-condition.ts
@@ -1,5 +1,9 @@
 import { Condition, Manipulator } from '../karabiner/karabiner-config'
-import { buildManipulators, ManipulatorBuilder } from '../config/manipulator'
+import {
+  buildManipulators,
+  ManipulatorBuilder,
+  ManipulatorMap,
+} from '../config/manipulator'
 import { buildCondition, ConditionBuilder } from '../config/condition'
 
 /**
@@ -10,9 +14,11 @@ import { buildCondition, ConditionBuilder } from '../config/condition'
 export function withCondition(
   ...conditions: Array<Condition | ConditionBuilder>
 ): (
-  manipulators: Array<Manipulator | ManipulatorBuilder>,
+  manipulators:
+    | ManipulatorMap
+    | Array<Manipulator | ManipulatorBuilder | ManipulatorMap>,
 ) => Manipulator[] & ManipulatorBuilder {
-  return (manipulators: Array<Manipulator | ManipulatorBuilder>) => {
+  return (manipulators) => {
     const sharedConditions = conditions.map(buildCondition)
     function addSharedConditions(manipulator: Manipulator) {
       if (manipulator.type !== 'basic') return manipulator
@@ -22,7 +28,10 @@ export function withCondition(
       }
     }
 
-    const result = manipulators.reduce(
+    const src = Array.isArray(manipulators)
+      ? manipulators
+      : buildManipulators(manipulators)
+    const result = src.reduce(
       (r, v) => [...r, ...buildManipulators(v).map(addSharedConditions)],
       [] as Manipulator[],
     )
diff --git a/src/utils/with-modifier.ts b/src/utils/with-modifier.ts
index c702955..e836e27 100644
--- a/src/utils/with-modifier.ts
+++ b/src/utils/with-modifier.ts
@@ -1,6 +1,10 @@
 import { Manipulator, Modifier } from '../karabiner/karabiner-config'
 import { FromModifierParam } from '../config/modifier'
-import { buildManipulators, ManipulatorBuilder } from '../config/manipulator'
+import {
+  buildManipulators,
+  ManipulatorBuilder,
+  ManipulatorMap,
+} from '../config/manipulator'
 import { BuildContext } from './build-context'
 import { parseFromModifierOverload } from './from-modifier-overload'
 import { FromOptionalModifierParam } from './optional-modifiers'
@@ -17,7 +21,11 @@ import { FromOptionalModifierParam } from './optional-modifiers'
 export function withModifier(
   mandatoryModifiers: FromModifierParam,
   optionalModifiers?: FromModifierParam,
-): (manipulators: Array<Manipulator | ManipulatorBuilder>) => ManipulatorBuilder
+): (
+  manipulators:
+    | ManipulatorMap
+    | Array<Manipulator | ManipulatorBuilder | ManipulatorMap>,
+) => ManipulatorBuilder
 /**
  * A high-order function to add optional modifiers to a group of manipulators
  *
@@ -29,12 +37,18 @@ export function withModifier(
  */
 export function withModifier(
   modifiers: FromOptionalModifierParam,
-): (manipulators: Array<Manipulator | ManipulatorBuilder>) => ManipulatorBuilder
+): (
+  manipulators:
+    | ManipulatorMap
+    | Array<Manipulator | ManipulatorBuilder | ManipulatorMap>,
+) => ManipulatorBuilder
 export function withModifier(
   mandatoryModifiers: FromModifierParam | FromOptionalModifierParam,
   optionalModifiers?: FromModifierParam,
 ): (
-  manipulators: Array<Manipulator | ManipulatorBuilder>,
+  manipulators:
+    | ManipulatorMap
+    | Array<Manipulator | ManipulatorBuilder | ManipulatorMap>,
 ) => ManipulatorBuilder {
   return (manipulators) => ({
     build: (context?: BuildContext) => {
@@ -42,7 +56,11 @@ export function withModifier(
         mandatoryModifiers,
         optionalModifiers,
       )!
-      return manipulators
+
+      const src = Array.isArray(manipulators)
+        ? manipulators
+        : buildManipulators(manipulators)
+      return src
         .map((v) => buildManipulators(v, context))
         .reduce((result, manipulator) => result.concat(manipulator), [])
         .map((src) => {


Test patch:
diff --git a/src/config/rule.test.ts b/src/config/rule.test.ts
index 1eeae4f..fc2b1e4 100644
--- a/src/config/rule.test.ts
+++ b/src/config/rule.test.ts
@@ -2,6 +2,7 @@ import { expect, test } from 'vitest'
 import { BasicRuleBuilder, isRuleBuilder, rule } from './rule'
 import { ifVar } from './condition'
 import { map } from './from'
+import { toKey } from './to'
 
 test('rule()', () => {
   const condition1 = ifVar('v1').build()
@@ -34,3 +35,26 @@ test('isRuleBuilder()', () => {
   expect(isRuleBuilder({ description: 'a', manipulators: [] })).toBe(false)
   expect(isRuleBuilder(new BasicRuleBuilder('b'))).toBe(true)
 })
+
+test('manipulators map', () => {
+  expect(
+    rule('')
+      .manipulators({ '›⌘': toKey('b') })
+      .build(),
+  ).toEqual({
+    description: '',
+    manipulators: [
+      {
+        type: 'basic',
+        from: { key_code: 'right_command' },
+        to: [{ key_code: 'b' }],
+      },
+    ],
+  })
+
+  expect(() =>
+    rule('')
+      .manipulators({ xyz: toKey(1) })
+      .build(),
+  ).toThrow(/key_code/)
+})
diff --git a/src/utils/with-condition.test.ts b/src/utils/with-condition.test.ts
index a57e03c..5585ae4 100644
--- a/src/utils/with-condition.test.ts
+++ b/src/utils/with-condition.test.ts
@@ -3,6 +3,8 @@ import { withCondition } from './with-condition'
 import { ifVar } from '../config/condition'
 import { map } from '../config/from'
 import { BasicManipulator } from '../karabiner/karabiner-config'
+import { rule } from '../config/rule'
+import { toKey } from '../config/to'
 
 test('withCondition()', () => {
   const ifA = ifVar('a').build()
@@ -20,3 +22,21 @@ test('withCondition()', () => {
   // FIXME Cleanup in 2.0 after remove the array return type
   expect(manipulators.length).toBe(3)
 })
+
+test('manipulators map', () => {
+  expect(
+    rule('')
+      .manipulators([withCondition(ifVar('b'))({ a: toKey('b') })])
+      .build(),
+  ).toEqual({
+    description: '',
+    manipulators: [
+      {
+        type: 'basic',
+        from: { key_code: 'a' },
+        to: [{ key_code: 'b' }],
+        conditions: [{ name: 'b', type: 'variable_if', value: 1 }],
+      },
+    ],
+  })
+})
diff --git a/src/utils/with-modifier.test.ts b/src/utils/with-modifier.test.ts
index 19fade6..f37806e 100644
--- a/src/utils/with-modifier.test.ts
+++ b/src/utils/with-modifier.test.ts
@@ -2,6 +2,8 @@ import { describe, expect, test } from 'vitest'
 import { withModifier } from './with-modifier'
 import { map } from '../config/from'
 import { BasicManipulator } from '../karabiner/karabiner-config'
+import { rule } from '../config/rule'
+import { toKey } from '../config/to'
 
 describe('withModifier()', () => {
   test('Add the shared modifiers to `from` but not `to`', () => {
@@ -69,3 +71,20 @@ describe('withModifier()', () => {
     ).toEqual({ mandatory: ['any'] })
   })
 })
+
+test('manipulators map', () => {
+  expect(
+    rule('')
+      .manipulators([withModifier('⌘')({ 1: toKey('b') })])
+      .build(),
+  ).toEqual({
+    description: '',
+    manipulators: [
+      {
+        type: 'basic',
+        from: { key_code: '1', modifiers: { mandatory: ['command'] } },
+        to: [{ key_code: 'b' }],
+      },
+    ],
+  })
+})