Repository: evan-liu/karabiner.ts
Pull request #74 (2023-05-09 07:10:17)

✨ Add ? + modifier alias for setting optional modifiers


Patch:
diff --git a/src/config/double-tap.ts b/src/config/double-tap.ts
index 9fe6c1e..460d7ff 100644
--- a/src/config/double-tap.ts
+++ b/src/config/double-tap.ts
@@ -13,9 +13,9 @@ import { BuildContext } from '../utils/build-context'
 import { FromModifierParam } from './modifier'
 import {
   FromModifierOverloadParam,
-  FromOptionalModifierParam,
   parseFromModifierOverload,
 } from '../utils/from-modifier-overload'
+import { FromOptionalModifierParam } from '../utils/optional-modifiers'
 
 export const defaultDoubleTapParameters = {
   'double_tap.delay_milliseconds': 200,
diff --git a/src/config/from.ts b/src/config/from.ts
index 51cad72..2a604f6 100644
--- a/src/config/from.ts
+++ b/src/config/from.ts
@@ -7,9 +7,9 @@ import { FromConsumerKeyCode } from '../karabiner/consumer-key-code'
 import { PointingButton } from '../karabiner/pointing-button'
 import {
   FromModifierOverloadParam,
-  FromOptionalModifierParam,
   parseFromModifierOverload,
 } from '../utils/from-modifier-overload'
+import { FromOptionalModifierParam } from '../utils/optional-modifiers'
 
 export type FromKeyParam = FromKeyCode | KeyAlias | NumberKeyValue
 
diff --git a/src/config/simlayer.ts b/src/config/simlayer.ts
index 18ad38e..d50f8bd 100644
--- a/src/config/simlayer.ts
+++ b/src/config/simlayer.ts
@@ -14,10 +14,10 @@ import { LayerKeyCode, LayerKeyParam, layerToggleManipulator } from './layer'
 import { BuildContext } from '../utils/build-context'
 import {
   FromModifierOverloadParam,
-  FromOptionalModifierParam,
   parseFromModifierOverload,
 } from '../utils/from-modifier-overload'
 import { FromModifierParam } from './modifier'
+import { FromOptionalModifierParam } from '../utils/optional-modifiers'
 
 export const defaultSimlayerParameters = {
   'simlayer.threshold_milliseconds': 200,
diff --git a/src/config/simultaneous.ts b/src/config/simultaneous.ts
index 7f6678c..aff4e95 100644
--- a/src/config/simultaneous.ts
+++ b/src/config/simultaneous.ts
@@ -9,10 +9,10 @@ import { FromKeyCode } from '../karabiner/key-code'
 import { FromKeyParam } from './from'
 import {
   FromModifierOverloadParam,
-  FromOptionalModifierParam,
   parseFromModifierOverload,
 } from '../utils/from-modifier-overload'
 import { FromModifierParam } from './modifier'
+import { FromOptionalModifierParam } from '../utils/optional-modifiers'
 
 /** Start a manipulator with from.simultaneous */
 export function mapSimultaneous(
diff --git a/src/utils/from-modifier-overload.ts b/src/utils/from-modifier-overload.ts
index 47a362d..5ab0b77 100644
--- a/src/utils/from-modifier-overload.ts
+++ b/src/utils/from-modifier-overload.ts
@@ -1,8 +1,14 @@
-import { FromModifierParam, parseFromModifierParams } from '../config/modifier'
-
-export type FromOptionalModifierParam =
-  | 'optionalAny'
-  | { optional: FromModifierParam }
+import {
+  FromModifierParam,
+  ModifierParam,
+  parseFromModifierParams,
+  parseModifierParam,
+} from '../config/modifier'
+import {
+  FromOptionalModifierParam,
+  isOptionalAnyAlias,
+  isOptionalModifierAlias,
+} from './optional-modifiers'
 
 export type FromModifierOverloadParam =
   | FromModifierParam
@@ -16,14 +22,29 @@ export function parseFromModifierOverload(
 ) {
   if (!mandatoryModifiers) {
     return parseFromModifierParams(mandatoryModifiers, optionalModifiers)
-  } else if (mandatoryModifiers === 'optionalAny') {
+  }
+
+  if (isOptionalAnyAlias(mandatoryModifiers)) {
     return parseFromModifierParams('', 'any')
-  } else if (
+  }
+
+  if (typeof mandatoryModifiers === 'string') {
+    if (isOptionalModifierAlias(mandatoryModifiers)) {
+      return parseFromModifierParams(
+        '',
+        parseModifierParam(mandatoryModifiers.slice(1) as ModifierParam),
+      )
+    } else if (mandatoryModifiers.startsWith('?')) {
+      throw new Error(`${mandatoryModifiers} is not valid optional alias`)
+    }
+  }
+
+  if (
     typeof mandatoryModifiers === 'object' &&
     'optional' in mandatoryModifiers
   ) {
     return parseFromModifierParams('', mandatoryModifiers.optional)
-  } else {
-    return parseFromModifierParams(mandatoryModifiers, optionalModifiers)
   }
+
+  return parseFromModifierParams(mandatoryModifiers, optionalModifiers)
 }
diff --git a/src/utils/optional-modifiers.ts b/src/utils/optional-modifiers.ts
new file mode 100644
index 0000000..7d7c5a8
--- /dev/null
+++ b/src/utils/optional-modifiers.ts
@@ -0,0 +1,29 @@
+import { FromModifierParam } from '../config/modifier'
+import { ModifierKeyAlias, modifierKeyAliases } from './key-alias'
+import { MultiModifierAlias, multiModifierAliases } from './multi-modifier'
+
+const optionalAnyAliases = ['optionalAny', '?any', '??'] as const
+export type OptionalAnyAlias = (typeof optionalAnyAliases)[number]
+
+export type OptionalModifierAlias = `?${ModifierKeyAlias | MultiModifierAlias}`
+
+export function isOptionalAnyAlias(src: any): src is OptionalAnyAlias {
+  if (!src || typeof src !== 'string') return false
+  return optionalAnyAliases.includes(src as OptionalAnyAlias)
+}
+
+export type FromOptionalModifierParam =
+  | OptionalAnyAlias
+  | { optional: FromModifierParam }
+  | OptionalModifierAlias
+
+const optionalModifierAliases = [
+  ...Object.keys(modifierKeyAliases),
+  ...Object.keys(multiModifierAliases),
+].map((v) => `?${v}`)
+
+export function isOptionalModifierAlias(
+  src: string,
+): src is OptionalModifierAlias {
+  return optionalModifierAliases.includes(src)
+}
diff --git a/src/utils/with-modifier.ts b/src/utils/with-modifier.ts
index f2ebbbb..c702955 100644
--- a/src/utils/with-modifier.ts
+++ b/src/utils/with-modifier.ts
@@ -2,10 +2,8 @@ import { Manipulator, Modifier } from '../karabiner/karabiner-config'
 import { FromModifierParam } from '../config/modifier'
 import { buildManipulators, ManipulatorBuilder } from '../config/manipulator'
 import { BuildContext } from './build-context'
-import {
-  FromOptionalModifierParam,
-  parseFromModifierOverload,
-} from './from-modifier-overload'
+import { parseFromModifierOverload } from './from-modifier-overload'
+import { FromOptionalModifierParam } from './optional-modifiers'
 
 /**
  * A high-order function to add modifiers to a group of manipulators


Test patch:
diff --git a/src/config/from.test.ts b/src/config/from.test.ts
index f5b174d..1a03b98 100644
--- a/src/config/from.test.ts
+++ b/src/config/from.test.ts
@@ -14,10 +14,12 @@ test('map()', () => {
     modifiers: { optional: ['command'] },
   })
 
-  expect(map(1, 'optionalAny').to(2).build()[0].from).toEqual({
-    key_code: '1',
-    modifiers: { optional: ['any'] },
-  })
+  for (const optionalAny of ['optionalAny', '?any', '??'] as const) {
+    expect(map(1, optionalAny).to(2).build()[0].from).toEqual({
+      key_code: '1',
+      modifiers: { optional: ['any'] },
+    })
+  }
 
   expect(map(1, { optional: 'any' }).to(2).build()[0].from).toEqual({
     key_code: '1',
@@ -28,6 +30,13 @@ test('map()', () => {
     key_code: '1',
     modifiers: { optional: ['command', 'option'] },
   })
+
+  expect(map(1, '?⌘⌥').to(2).build()[0].from).toEqual({
+    key_code: '1',
+    modifiers: { optional: ['command', 'option'] },
+  })
+
+  expect(() => map(1, '?⌘⌥x' as any)).toThrow()
 })
 
 test('mapConsumerKey()', () => {