Repository: evan-liu/karabiner.ts (ts)

Issue:
Abstraction mapDoubleTap()
```
mapDoubleTap(1).to('q', '⌘')
```


Pull request description:
✨ Add mapDoubleTap()
Resolve #41 

Patch:
diff --git a/src/config/condition.ts b/src/config/condition.ts
index 0969f77..404b47d 100644
--- a/src/config/condition.ts
+++ b/src/config/condition.ts
@@ -129,6 +129,10 @@ export function isConditionBuilder(
   return typeof (src as ConditionBuilder).build === 'function'
 }
 
+export function buildCondition(src: Condition | ConditionBuilder): Condition {
+  return isConditionBuilder(src) ? src.build() : src
+}
+
 function formatRegExp(v: string | RegExp) {
   return typeof v === 'string' ? v : v.toString().slice(1, -1)
 }
diff --git a/src/config/double-tap.ts b/src/config/double-tap.ts
new file mode 100644
index 0000000..4afd4ed
--- /dev/null
+++ b/src/config/double-tap.ts
@@ -0,0 +1,55 @@
+import { BasicManipulatorBuilder } from './manipulator'
+import { BasicManipulator } from '../karabiner/karabiner-config'
+import { FromKeyParam } from './from'
+import { getKeyWithAlias, KeyAlias, NumberKeyValue } from '../utils/key-alias'
+import { FromAndToKeyCode } from '../karabiner/key-code'
+import { ifVar } from './condition'
+import { setVar } from './to'
+
+export type DoubleTapParam = FromAndToKeyCode | KeyAlias | NumberKeyValue
+
+export function mapDoubleTap(key: DoubleTapParam, delay?: number) {
+  return new DoubleTapManipulatorBuilder(key, delay)
+}
+
+export class DoubleTapManipulatorBuilder extends BasicManipulatorBuilder {
+  private readonly keyCode: FromAndToKeyCode
+  private readonly varName: string
+
+  constructor(key: FromKeyParam, private readonly delay?: number) {
+    const keyCode = getKeyWithAlias(key) as FromAndToKeyCode
+    super({ key_code: keyCode })
+
+    this.keyCode = keyCode
+    this.varName = `double-tap-${keyCode}`
+  }
+
+  public build(): BasicManipulator[] {
+    const onCondition = ifVar(this.varName).build()
+    const offCondition = ifVar(this.varName).unless().build()
+
+    const baseManipulator: BasicManipulator = {
+      ...this.manipulator,
+      conditions: [...(this.manipulator.conditions || []), onCondition],
+      to: [...(this.manipulator.to || []), setVar(this.varName)],
+    }
+
+    const toggleManipulator: BasicManipulator = {
+      type: 'basic',
+      from: { key_code: this.keyCode },
+      to: [setVar(this.varName, 1)],
+      conditions: [offCondition],
+      to_delayed_action: {
+        to_if_invoked: [{ key_code: this.keyCode }, setVar(this.varName, 0)],
+        to_if_canceled: [setVar(this.varName, 0)],
+      },
+    }
+    if (this.delay && this.delay > 0) {
+      toggleManipulator.parameters = {
+        'basic.to_delayed_action_delay_milliseconds': this.delay,
+      }
+    }
+
+    return [baseManipulator, toggleManipulator]
+  }
+}
diff --git a/src/config/layer.ts b/src/config/layer.ts
index 9308a72..296efb9 100644
--- a/src/config/layer.ts
+++ b/src/config/layer.ts
@@ -27,7 +27,7 @@ export function layer(
 }
 
 export class LayerRuleBuilder extends BasicRuleBuilder {
-  protected readonly layerManipulator: Manipulator
+  protected readonly layerManipulators: Manipulator[]
 
   constructor(
     protected readonly key: LayerKeyParam,
@@ -38,7 +38,7 @@ export class LayerRuleBuilder extends BasicRuleBuilder {
     super(`Layer - ${varName}`, ifVar(varName, onValue))
 
     const key_code = getKeyWithAlias(key) as LayerKeyCode
-    this.layerManipulator = map(key_code)
+    this.layerManipulators = map(key_code)
       .toVar(varName, onValue)
       .toAfterKeyUp(setVar(varName, offValue))
       .toIfAlone({ key_code })
@@ -47,7 +47,7 @@ export class LayerRuleBuilder extends BasicRuleBuilder {
 
   public build(): Rule {
     const rule = super.build()
-    rule.manipulators = [this.layerManipulator, ...rule.manipulators]
+    rule.manipulators = [...this.layerManipulators, ...rule.manipulators]
     return rule
   }
 }
diff --git a/src/config/manipulator.ts b/src/config/manipulator.ts
index 6af13c2..2a88a24 100644
--- a/src/config/manipulator.ts
+++ b/src/config/manipulator.ts
@@ -12,16 +12,16 @@ import {
 } from '../karabiner/karabiner-config'
 import { ModifierParam, parseModifierParam } from './modifier'
 import { toKey, ToKeyParam } from './to'
-import { ConditionBuilder, isConditionBuilder } from './condition'
+import { buildCondition, ConditionBuilder } from './condition'
 import { ToConsumerKeyCode } from '../karabiner/consumer-key-code'
 import { PointingButton } from '../karabiner/pointing-button'
 
 export interface ManipulatorBuilder {
-  build(): Manipulator
+  build(): Manipulator[]
 }
 
 export class BasicManipulatorBuilder implements ManipulatorBuilder {
-  private readonly manipulator: BasicManipulator
+  protected readonly manipulator: BasicManipulator
 
   constructor(from: FromEvent) {
     this.manipulator = { type: 'basic', from }
@@ -207,8 +207,7 @@ set the clipboard to prev'`)
 
   condition(...v: Array<Condition | ConditionBuilder>): this {
     const { conditions = [] } = this.manipulator
-    v.forEach((c) => conditions.push(isConditionBuilder(c) ? c.build() : c))
-    this.manipulator.conditions = conditions
+    this.manipulator.conditions = [...conditions, ...v.map(buildCondition)]
     return this
   }
 
@@ -217,15 +216,15 @@ set the clipboard to prev'`)
     return this
   }
 
-  build(): BasicManipulator {
-    return { ...this.manipulator }
+  build(): BasicManipulator[] {
+    return [{ ...this.manipulator }]
   }
 
-  private addToEvent(event: ToEvent) {
+  protected addToEvent(event: ToEvent) {
     this.pushOrCreateList(this.manipulator, 'to', event)
   }
 
-  private pushOrCreateList<T extends {}>(obj: T, key: keyof T, item: any) {
+  protected pushOrCreateList<T extends {}>(obj: T, key: keyof T, item: any) {
     const list = (obj[key] || []) as any[]
     list.push(item)
     Object.assign(obj, { [key]: list })
@@ -237,3 +236,9 @@ export function isManipulatorBuilder(
 ): src is ManipulatorBuilder {
   return typeof (src as ManipulatorBuilder).build === 'function'
 }
+
+export function buildManipulators(
+  src: Manipulator | ManipulatorBuilder,
+): Manipulator[] {
+  return isManipulatorBuilder(src) ? src.build() : [src]
+}
diff --git a/src/config/mouse-motion-to-scroll.ts b/src/config/mouse-motion-to-scroll.ts
index 8ca53df..302c9f0 100644
--- a/src/config/mouse-motion-to-scroll.ts
+++ b/src/config/mouse-motion-to-scroll.ts
@@ -6,7 +6,7 @@ import {
   MouseMotionToScrollOptions,
 } from '../karabiner/karabiner-config'
 import { FromModifierParam, parseFromModifierParams } from './modifier'
-import { ConditionBuilder, isConditionBuilder } from './condition'
+import { buildCondition, ConditionBuilder } from './condition'
 
 export function mouseMotionToScroll() {
   return new MouseMotionToScrollManipulatorBuilder()
@@ -31,8 +31,7 @@ export class MouseMotionToScrollManipulatorBuilder
 
   condition(...v: Array<Condition | ConditionBuilder>): this {
     const { conditions = [] } = this.manipulator
-    v.forEach((c) => conditions.push(isConditionBuilder(c) ? c.build() : c))
-    this.manipulator.conditions = conditions
+    this.manipulator.conditions = [...conditions, ...v.map(buildCondition)]
     return this
   }
 
@@ -41,7 +40,7 @@ export class MouseMotionToScrollManipulatorBuilder
     return this
   }
 
-  public build(): Manipulator {
-    return { ...this.manipulator }
+  public build(): Manipulator[] {
+    return [{ ...this.manipulator }]
   }
 }
diff --git a/src/config/rule.ts b/src/config/rule.ts
index 369b022..e9c293d 100644
--- a/src/config/rule.ts
+++ b/src/config/rule.ts
@@ -1,6 +1,6 @@
 import { Condition, Manipulator, Rule } from '../karabiner/karabiner-config'
-import { isManipulatorBuilder, ManipulatorBuilder } from './manipulator'
-import { ConditionBuilder, isConditionBuilder } from './condition'
+import { buildManipulators, ManipulatorBuilder } from './manipulator'
+import { buildCondition, ConditionBuilder } from './condition'
 
 export function rule(
   description: string,
@@ -23,7 +23,7 @@ export class BasicRuleBuilder implements RuleBuilder {
 
   manipulators(src: Array<Manipulator | ManipulatorBuilder>): this {
     src.forEach((v) =>
-      this.rule.manipulators.push(isManipulatorBuilder(v) ? v.build() : v),
+      buildManipulators(v).forEach((m) => this.rule.manipulators.push(m)),
     )
     return this
   }
@@ -41,9 +41,7 @@ export class BasicRuleBuilder implements RuleBuilder {
 
     if (this.conditions.length === 0) return rule
 
-    const conditions = this.conditions.map((condition) =>
-      isConditionBuilder(condition) ? condition.build() : condition,
-    )
+    const conditions = this.conditions.map(buildCondition)
     rule.manipulators = rule.manipulators.map((v) =>
       v.type === 'basic'
         ? { ...v, conditions: [...(v.conditions || []), ...conditions] }
@@ -60,3 +58,7 @@ export interface RuleBuilder {
 export function isRuleBuilder(src: Rule | RuleBuilder): src is RuleBuilder {
   return typeof (src as RuleBuilder).build === 'function'
 }
+
+export function buildRule(src: Rule | RuleBuilder): Rule {
+  return isRuleBuilder(src) ? src.build() : src
+}
diff --git a/src/index.ts b/src/index.ts
index 5377597..3288a0c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -6,6 +6,7 @@ export * from './utils/multi-modifier'
 export * from './utils/with-condition'
 
 export * from './config/condition'
+export * from './config/double-tap'
 export * from './config/from'
 export * from './config/layer'
 export * from './config/manipulator'
diff --git a/src/output.ts b/src/output.ts
index 898ed37..2210857 100644
--- a/src/output.ts
+++ b/src/output.ts
@@ -6,7 +6,7 @@ import {
   KarabinerConfig,
   Rule,
 } from './karabiner/karabiner-config'
-import { isRuleBuilder, RuleBuilder } from './config/rule'
+import { buildRule, RuleBuilder } from './config/rule'
 
 export const karabinerConfigDir = join(homedir(), '.config/karabiner')
 export const karabinerConfigFile = join(karabinerConfigDir, 'karabiner.json')
@@ -40,9 +40,7 @@ export function writeToProfile(
  `)
 
   try {
-    profile.complex_modifications.rules = rules.map((v) =>
-      isRuleBuilder(v) ? v.build() : v,
-    )
+    profile.complex_modifications.rules = rules.map(buildRule)
   } catch (e) {
     exitWithError(e)
   }
diff --git a/src/utils/with-condition.ts b/src/utils/with-condition.ts
index f19fb6d..aca33a0 100644
--- a/src/utils/with-condition.ts
+++ b/src/utils/with-condition.ts
@@ -1,6 +1,6 @@
 import { Condition, Manipulator } from '../karabiner/karabiner-config'
-import { isManipulatorBuilder, ManipulatorBuilder } from '../config/manipulator'
-import { ConditionBuilder, isConditionBuilder } from '../config/condition'
+import { buildManipulators, ManipulatorBuilder } from '../config/manipulator'
+import { buildCondition, ConditionBuilder } from '../config/condition'
 
 /** A high-order function to add condition to a group of manipulators */
 export function withCondition(
@@ -9,16 +9,18 @@ export function withCondition(
   return (
     manipulators: Array<Manipulator | ManipulatorBuilder>,
   ): Manipulator[] => {
-    const newConditions = conditions.map((v) =>
-      isConditionBuilder(v) ? v.build() : v,
-    )
-    return manipulators.map((v) => {
-      const manipulator = isManipulatorBuilder(v) ? v.build() : v
+    const sharedConditions = conditions.map(buildCondition)
+    function addSharedConditions(manipulator: Manipulator) {
       if (manipulator.type !== 'basic') return manipulator
       return {
         ...manipulator,
-        conditions: [...(manipulator.conditions || []), ...newConditions],
+        conditions: [...(manipulator.conditions || []), ...sharedConditions],
       }
-    })
+    }
+
+    return manipulators.reduce(
+      (r, v) => [...r, ...buildManipulators(v).map(addSharedConditions)],
+      [] as Manipulator[],
+    )
   }
 }


Test patch:
diff --git a/src/config/double-tap.test.ts b/src/config/double-tap.test.ts
new file mode 100644
index 0000000..26a039b
--- /dev/null
+++ b/src/config/double-tap.test.ts
@@ -0,0 +1,40 @@
+import { expect, test } from 'vitest'
+import { mapDoubleTap } from './double-tap'
+
+test('mapDoubleTap()', () => {
+  expect(mapDoubleTap(1).to(2).build()).toEqual([
+    {
+      type: 'basic',
+      from: { key_code: '1' },
+      to: [
+        { key_code: '2' },
+        { set_variable: { name: 'double-tap-1', value: 1 } },
+      ],
+      conditions: [{ name: 'double-tap-1', type: 'variable_if', value: 1 }],
+    },
+    {
+      type: 'basic',
+      from: { key_code: '1' },
+      to: [{ set_variable: { name: 'double-tap-1', value: 1 } }],
+      conditions: [
+        {
+          description: undefined,
+          name: 'double-tap-1',
+          type: 'variable_unless',
+          value: 1,
+        },
+      ],
+      to_delayed_action: {
+        to_if_invoked: [
+          { key_code: '1' },
+          { set_variable: { name: 'double-tap-1', value: 0 } },
+        ],
+        to_if_canceled: [{ set_variable: { name: 'double-tap-1', value: 0 } }],
+      },
+    },
+  ])
+
+  expect(mapDoubleTap(1, 11).to(2).build()[1].parameters).toEqual({
+    'basic.to_delayed_action_delay_milliseconds': 11,
+  })
+})
diff --git a/src/config/from.test.ts b/src/config/from.test.ts
index 8c4c2f1..ada3c05 100644
--- a/src/config/from.test.ts
+++ b/src/config/from.test.ts
@@ -2,7 +2,7 @@ import { expect, test } from 'vitest'
 import { map, mapConsumerKey, mapPointingButton, mapSimultaneous } from './from'
 
 test('map()', () => {
-  expect(map('/', '⌘⌥', '⌃').build().from).toEqual({
+  expect(map('/', '⌘⌥', '⌃').build()[0].from).toEqual({
     key_code: 'slash',
     modifiers: { mandatory: ['command', 'option'], optional: ['control'] },
   })
@@ -13,7 +13,7 @@ test('mapSimultaneous()', () => {
     ['a', '←', 2],
     { key_up_order: 'strict' },
     123,
-  ).build()
+  ).build()[0]
   expect(from).toEqual({
     simultaneous: [
       { key_code: 'a' },
@@ -26,18 +26,18 @@ test('mapSimultaneous()', () => {
     'basic.simultaneous_threshold_milliseconds': 123,
   })
 
-  expect(mapSimultaneous([1, 2]).build().parameters).toBeUndefined()
+  expect(mapSimultaneous([1, 2]).build()[0].parameters).toBeUndefined()
 })
 
 test('mapConsumerKey()', () => {
-  expect(mapConsumerKey('menu', '⌘⌥', '⌃').build().from).toEqual({
+  expect(mapConsumerKey('menu', '⌘⌥', '⌃').build()[0].from).toEqual({
     consumer_key_code: 'menu',
     modifiers: { mandatory: ['command', 'option'], optional: ['control'] },
   })
 })
 
 test('mapPointingButton()', () => {
-  expect(mapPointingButton('button2', '⌘⌥', '⌃').build().from).toEqual({
+  expect(mapPointingButton('button2', '⌘⌥', '⌃').build()[0].from).toEqual({
     pointing_button: 'button2',
     modifiers: { mandatory: ['command', 'option'], optional: ['control'] },
   })
diff --git a/src/config/manipulator.test.ts b/src/config/manipulator.test.ts
index 0ab048e..91668d8 100644
--- a/src/config/manipulator.test.ts
+++ b/src/config/manipulator.test.ts
@@ -8,7 +8,7 @@ describe('ManipulatorBuilder', () => {
   const toEvent: ToEvent = { key_code: 'b', modifiers: ['command'], lazy: true }
 
   test('type and from', () => {
-    expect(new BasicManipulatorBuilder(from).build()).toEqual({
+    expect(new BasicManipulatorBuilder(from).build()[0]).toEqual({
       type: 'basic',
       from,
     })
@@ -19,15 +19,15 @@ describe('ManipulatorBuilder', () => {
       new BasicManipulatorBuilder(from)
         .to(toEvent)
         .to('b', '⌘', { lazy: true })
-        .build().to,
+        .build()[0].to,
     ).toEqual([toEvent, toEvent])
   })
 
   test('toHyper(), toMeh()', () => {
-    expect(new BasicManipulatorBuilder(from).toHyper().build().to).toEqual([
+    expect(new BasicManipulatorBuilder(from).toHyper().build()[0].to).toEqual([
       { key_code: 'left_command', modifiers: ['option', 'control', 'shift'] },
     ])
-    expect(new BasicManipulatorBuilder(from).toMeh().build().to).toEqual([
+    expect(new BasicManipulatorBuilder(from).toMeh().build()[0].to).toEqual([
       { key_code: 'left_option', modifiers: ['control', 'shift'] },
     ])
   })
@@ -37,7 +37,7 @@ describe('ManipulatorBuilder', () => {
       new BasicManipulatorBuilder(from)
         .toConsumerKey('rewind')
         .toConsumerKey('mute', '⌘', { lazy: true })
-        .build().to,
+        .build()[0].to,
     ).toEqual([
       { consumer_key_code: 'rewind' },
       { consumer_key_code: 'mute', modifiers: ['command'], lazy: true },
@@ -49,7 +49,7 @@ describe('ManipulatorBuilder', () => {
       new BasicManipulatorBuilder(from)
         .toPointingButton('button2')
         .toPointingButton('button3', '⌘', { lazy: true })
-        .build().to,
+        .build()[0].to,
     ).toEqual([
       { pointing_button: 'button2' },
       { pointing_button: 'button3', modifiers: ['command'], lazy: true },
@@ -62,7 +62,7 @@ describe('ManipulatorBuilder', () => {
         .toMouseKey({ x: 100, y: -100 })
         .toMouseKey({ horizontal_wheel: -100 })
         .toMouseKey({ speed_multiplier: 2 })
-        .build().to,
+        .build()[0].to,
     ).toEqual([
       { mouse_key: { x: 100, y: -100 } },
       { mouse_key: { horizontal_wheel: -100 } },
@@ -76,7 +76,7 @@ describe('ManipulatorBuilder', () => {
       .toApp('Finder')
       .toApp('Xcode.app')
       .toPaste('test')
-      .build().to as Array<{ shell_command: string }>
+      .build()[0].to as Array<{ shell_command: string }>
     expect(to[0]).toEqual({ shell_command: 'cd' })
     expect(to[1]).toEqual({ shell_command: 'open -a "Finder".app' })
     expect(to[2]).toEqual({ shell_command: 'open -a "Xcode".app' })
@@ -90,7 +90,7 @@ describe('ManipulatorBuilder', () => {
         .toApp('System Settings.app')
         .toApp('"System Settings"')
         .toApp('"System Settings.app"')
-        .build().to as Array<{ shell_command: string }>,
+        .build()[0].to as Array<{ shell_command: string }>,
     ).toEqual([
       { shell_command: 'open -a "System Settings".app' },
       { shell_command: 'open -a "System Settings".app' },
@@ -101,7 +101,8 @@ describe('ManipulatorBuilder', () => {
 
   test('toVar()', () => {
     expect(
-      new BasicManipulatorBuilder(from).toVar('a', 1).toVar('b', 2).build().to,
+      new BasicManipulatorBuilder(from).toVar('a', 1).toVar('b', 2).build()[0]
+        .to,
     ).toEqual([
       { set_variable: { name: 'a', value: 1 } },
       { set_variable: { name: 'b', value: 2 } },
@@ -113,7 +114,7 @@ describe('ManipulatorBuilder', () => {
       new BasicManipulatorBuilder(from)
         .toMouseCursorPosition({ x: 0, y: 0 })
         .toMouseCursorPosition({ x: '50%', y: '50%', screen: 1 })
-        .build().to,
+        .build()[0].to,
     ).toEqual([
       { software_function: { set_mouse_cursor_position: { x: 0, y: 0 } } },
       {
@@ -129,7 +130,7 @@ describe('ManipulatorBuilder', () => {
       new BasicManipulatorBuilder(from)
         .toIfAlone(toEvent)
         .toIfAlone('b', '⌘', { lazy: true })
-        .build().to_if_alone,
+        .build()[0].to_if_alone,
     ).toEqual([toEvent, toEvent])
   })
 
@@ -138,7 +139,7 @@ describe('ManipulatorBuilder', () => {
       new BasicManipulatorBuilder(from)
         .toIfHeldDown(toEvent)
         .toIfHeldDown('b', '⌘', { lazy: true })
-        .build().to_if_held_down,
+        .build()[0].to_if_held_down,
     ).toEqual([toEvent, toEvent])
   })
 
@@ -147,7 +148,7 @@ describe('ManipulatorBuilder', () => {
       new BasicManipulatorBuilder(from)
         .toAfterKeyUp(toEvent)
         .toAfterKeyUp('b', '⌘', { lazy: true })
-        .build().to_after_key_up,
+        .build()[0].to_after_key_up,
     ).toEqual([toEvent, toEvent])
   })
 
@@ -156,7 +157,7 @@ describe('ManipulatorBuilder', () => {
       new BasicManipulatorBuilder(from)
         .toDelayedAction([{ key_code: 'a' }], [{ key_code: 'b' }])
         .toDelayedAction([{ key_code: 'c' }], [{ key_code: 'd' }])
-        .build().to_delayed_action,
+        .build()[0].to_delayed_action,
     ).toEqual({
       to_if_invoked: [{ key_code: 'a' }, { key_code: 'c' }],
       to_if_canceled: [{ key_code: 'b' }, { key_code: 'd' }],
@@ -165,11 +166,11 @@ describe('ManipulatorBuilder', () => {
 
   test('description()', () => {
     const manipulatorBuilder = new BasicManipulatorBuilder(from)
-    expect(manipulatorBuilder.build().description).toBeUndefined()
+    expect(manipulatorBuilder.build()[0].description).toBeUndefined()
     manipulatorBuilder.description('a')
-    expect(manipulatorBuilder.build().description).toBe('a')
+    expect(manipulatorBuilder.build()[0].description).toBe('a')
     manipulatorBuilder.description('b')
-    expect(manipulatorBuilder.build().description).toBe('b')
+    expect(manipulatorBuilder.build()[0].description).toBe('b')
   })
 
   test('condition()', () => {
@@ -177,7 +178,7 @@ describe('ManipulatorBuilder', () => {
       new BasicManipulatorBuilder(from)
         .condition(ifEventChanged())
         .condition(ifEventChanged(false), ifEventChanged())
-        .build().conditions,
+        .build()[0].conditions,
     ).toEqual([
       { type: 'event_changed_if', value: true },
       { type: 'event_changed_if', value: false },
@@ -195,7 +196,7 @@ describe('ManipulatorBuilder', () => {
         .parameters({
           'basic.simultaneous_threshold_milliseconds': 3,
         })
-        .build().parameters,
+        .build()[0].parameters,
     ).toEqual({
       'basic.simultaneous_threshold_milliseconds': 3,
       'basic.to_if_alone_timeout_milliseconds': 2,
diff --git a/src/config/mouse-motion-to-scroll.test.ts b/src/config/mouse-motion-to-scroll.test.ts
index 7de5a83..2f6e957 100644
--- a/src/config/mouse-motion-to-scroll.test.ts
+++ b/src/config/mouse-motion-to-scroll.test.ts
@@ -8,7 +8,7 @@ test('mouseMotionToScroll()', () => {
       .modifiers('⌘⌥', '⌃')
       .condition(ifVar('a'))
       .options({ speed_multiplier: 2 })
-      .build(),
+      .build()[0],
   ).toEqual({
     type: 'mouse_motion_to_scroll',
     from: {
diff --git a/src/config/rule.test.ts b/src/config/rule.test.ts
index dd2f31c..01157b5 100644
--- a/src/config/rule.test.ts
+++ b/src/config/rule.test.ts
@@ -7,8 +7,8 @@ test('rule()', () => {
   const condition1 = ifVar('v1').build()
   const condition2 = ifVar('v2').build()
   const condition3 = ifVar('v3').build()
-  const manipulator1 = map('a').to('b').build()
-  const manipulator2 = map('c').to('d').condition(condition3).build()
+  const manipulator1 = map('a').to('b').build()[0]
+  const manipulator2 = map('c').to('d').condition(condition3).build()[0]
   expect(
     rule('test', condition1)
       .condition(condition2)