Alpine Example: Kitchen Sink

import Alpine from 'alpinejs'
import { createHotkeysScope, formatForDisplay } from '@tanstack/alpine-hotkeys'
import type {
  AlpineHotkeyRecorder,
  AlpineHotkeySequenceRecorder,
  AlpineHotkeyState,
  Hotkey,
  HotkeyRegistrationsResult,
  HotkeySequence,
  RecorderKeyMode,
  RegisterableHotkey,
} from '@tanstack/alpine-hotkeys'
import './index.css'

const pages = [
  { to: '/', label: 'Tickets', shortcut: 'Alt+[Digit1]' },
  { to: '/editor', label: 'Editor', shortcut: 'Alt+[Digit2]' },
  { to: '/sequences', label: 'Sequences', shortcut: 'Alt+[Digit3]' },
  { to: '/recording', label: 'Recording', shortcut: 'Alt+[Digit4]' },
  { to: '/formatting', label: 'Formatting', shortcut: 'Alt+[Digit5]' },
] as const

class Hint {
  private hotkeysScope = createHotkeysScope({ hotkey: { requireReset: true } })

  formatForDisplay = formatForDisplay
  visible!: AlpineHotkeyState<boolean>
  constructor(
    public props: () => {
      hotkey: RegisterableHotkey
      enabled?: boolean
    },
  ) {}
  init() {
    this.visible = this.hotkeysScope.createHotkeyHint(() => this.props().hotkey)
  }
  destroy() {
    this.hotkeysScope.destroy()
  }
}

class Layout {
  private hotkeysScope = createHotkeysScope({ hotkey: { requireReset: true } })
  private cleanups: Array<() => void> = []
  formatForDisplay = formatForDisplay
  pages = pages
  currentPath: string = window.location.pathname
  showShortcuts = false
  activity: Array<string> = []
  keys!: AlpineHotkeyState<Array<string>>
  codes!: AlpineHotkeyState<Record<string, string>>
  shift!: AlpineHotkeyState<boolean>
  registrationState0!: HotkeyRegistrationsResult
  navigate({ to }: { to: string }) {
    window.history.pushState({}, '', to)
    this.currentPath = to
  }
  log(message: string) {
    this.activity = [message, ...this.activity].slice(0, 6)
  }
  get hotkeys() {
    return this.registrationState0.hotkeys
  }
  get sequences() {
    return this.registrationState0.sequences
  }
  get registrations() {
    return [...this.hotkeys, ...this.sequences]
  }
  get groups() {
    return [
      ...new Set(
        this.registrations.map((reg) => reg.options.meta?.group ?? 'Other'),
      ),
    ]
  }

  init() {
    this.navigate = this.navigate.bind(this)
    this.log = this.log.bind(this)
    this.keys = this.hotkeysScope.createHeldKeys()
    this.codes = this.hotkeysScope.createHeldKeyCodes()
    this.shift = this.hotkeysScope.createKeyHold('Shift')
    this.registrationState0 = this.hotkeysScope.createHotkeyRegistrations()
    this.hotkeysScope.createHotkeys(
      () =>
        pages.map((page) => ({
          hotkey: page.shortcut,
          callback: () => {
            void this.navigate({ to: page.to })
          },
          options: {
            meta: { name: `Open ${page.label}`, group: 'Navigation' },
          },
        })),
      { ignoreInputs: true },
    )
    this.hotkeysScope.createHotkey(
      'Alt+Shift+[KeyK]',
      () => (this.showShortcuts = !this.showShortcuts),
      {
        meta: { name: 'Show shortcuts', group: 'Navigation' },
      },
    )
    const onPopState = () => {
      this.currentPath = window.location.pathname
    }
    window.addEventListener('popstate', onPopState)
    this.cleanups.push(() => window.removeEventListener('popstate', onPopState))
  }
  destroy() {
    for (const cleanup of this.cleanups) cleanup()
    this.hotkeysScope.destroy()
  }
}

class Tickets {
  private hotkeysScope = createHotkeysScope({ hotkey: { requireReset: true } })

  count = 0
  enabled = true
  create() {
    this.count = this.count + 1
    this.props().log('Ticket created')
  }
  save() {
    return this.props().log('Pending ticket saved')
  }
  constructor(public props: () => { log: (message: string) => void }) {}
  init() {
    this.create = this.create.bind(this)
    this.save = this.save.bind(this)
    this.hotkeysScope.createHotkey('Alt+[KeyC]', this.create, () => ({
      enabled: this.enabled,
      ignoreInputs: false,
      meta: {
        name: 'Create ticket',
        description: 'Also works while entering a ticket note',
        group: 'Tickets',
      },
    }))
    this.hotkeysScope.createHotkey('Alt+[KeyS]', this.save, () => ({
      enabled: this.enabled,
      meta: { name: 'Save pending ticket', group: 'Tickets' },
    }))
  }
  destroy() {
    this.hotkeysScope.destroy()
  }
}

class EditorPane {
  private hotkeysScope = createHotkeysScope({ hotkey: { requireReset: true } })

  formatForDisplay = formatForDisplay
  target: HTMLFieldSetElement | null = null
  text = 'One shortcut, two independent editors.'
  saves = 0

  constructor(
    public props: () => { name: string; log: (message: string) => void },
  ) {}
  init() {
    this.hotkeysScope.createHotkeys(
      [
        {
          hotkey: 'Mod+[KeyS]',
          callback: () => {
            this.saves = this.saves + 1
            this.props().log(`${this.props().name} saved`)
          },
          options: {
            meta: {
              name: `Save ${this.props().name}`,
              group: 'Editor',
              description: 'Scoped to this editor',
            },
          },
        },
        {
          hotkey: 'Alt+[Backspace]',
          callback: () => {
            this.text = ''
            this.props().log(`${this.props().name} cleared`)
          },
          options: {
            meta: { name: `Clear ${this.props().name}`, group: 'Editor' },
          },
        },
      ],
      () => ({
        target: this.target,
        ignoreInputs: false,
      }),
    )
  }
  destroy() {
    this.hotkeysScope.destroy()
  }
}

class Editor {
  private hotkeysScope = createHotkeysScope({ hotkey: { requireReset: true } })

  formatForDisplay = formatForDisplay
  repeat = false
  count = 0
  bubble = false
  scope: HTMLFieldSetElement | null = null

  constructor(public props: () => { log: (message: string) => void }) {}
  init() {
    this.hotkeysScope.createHotkey(
      'Alt+[ArrowRight]',
      () => (this.count = this.count + 1),
      () => ({
        requireReset: !this.repeat,
        meta: { name: 'Advance counter', group: 'Editor' },
      }),
    )
    this.hotkeysScope.createHotkey(
      'Alt+[KeyB]',
      () => this.props().log('Scoped B handler'),
      () => ({
        target: this.scope,
        stopPropagation: !this.bubble,
        preventDefault: !this.bubble,
        meta: { name: 'Scoped propagation demo', group: 'Editor' },
      }),
    )
    this.hotkeysScope.createHotkey(
      'Alt+[KeyB]',
      () => this.props().log('Document B handler received the bubbled event'),
      {
        meta: { name: 'Document propagation demo', group: 'Editor' },
      },
    )
    this.hotkeysScope.createHotkey(
      'Alt+[KeyU]',
      () => this.props().log('Key released: keyup handler'),
      {
        eventType: 'keyup',
        meta: { name: 'Run on release', group: 'Editor' },
      },
    )
  }
  destroy() {
    this.hotkeysScope.destroy()
  }
}

class Sequences {
  private hotkeysScope = createHotkeysScope({ hotkey: { requireReset: true } })

  formatForDisplay = formatForDisplay
  timeout = 1000
  registrationState2!: HotkeyRegistrationsResult
  get sequences() {
    return this.registrationState2.sequences
  }
  constructor(public props: () => { log: (message: string) => void }) {}
  init() {
    this.hotkeysScope.createHotkeySequence(
      ['[KeyG]', '[KeyG]'],
      () => this.props().log('Sequence: go to top'),
      () => ({
        timeout: this.timeout,
        meta: { name: 'Go to top', group: 'Sequences' },
      }),
    )
    this.hotkeysScope.createHotkeySequences(
      [
        {
          sequence: ['[KeyG]', '[KeyI]'],
          callback: () => this.props().log('Sequence: inbox'),
          options: { meta: { name: 'Open inbox', group: 'Sequences' } },
        },
        {
          sequence: ['Shift+[KeyR]', 'Shift+[KeyT]'],
          callback: () => this.props().log('Sequence: shifted chord chain'),
          options: { meta: { name: 'Shifted chain', group: 'Sequences' } },
        },
      ],
      () => ({ timeout: this.timeout }),
    )
    this.registrationState2 = this.hotkeysScope.createHotkeyRegistrations()
  }
  destroy() {
    this.hotkeysScope.destroy()
  }
}

class Recording {
  private hotkeysScope = createHotkeysScope({ hotkey: { requireReset: true } })

  formatForDisplay = formatForDisplay
  hotkey: Hotkey = this.initial
  mode: RecorderKeyMode = 'code'
  problem = ''
  sequence: HotkeySequence = ['[KeyX]', '[KeyY]']
  sequenceProblem = ''
  idle = false
  commitOnEnter = true
  recorder!: AlpineHotkeyRecorder
  sequenceRecorder!: AlpineHotkeySequenceRecorder
  get initial(): Hotkey {
    return 'Alt+[KeyR]'
  }
  constructor(public props: () => { log: (message: string) => void }) {}
  startRecording() {
    this.problem = ''
    this.recorder.startRecording()
  }
  resetShortcut() {
    this.recorder.stopRecording()
    this.hotkey = this.initial
    this.problem = ''
  }
  startSequenceRecording() {
    this.sequenceProblem = ''
    this.sequenceRecorder.startRecording()
  }
  init() {
    this.hotkeysScope.createHotkey(
      () => this.hotkey,
      () => this.props().log('Your recorded shortcut fired'),
      {
        meta: { name: 'Recorded action', group: 'Recording' },
      },
    )
    this.hotkeysScope.createHotkeySequence(
      () => this.sequence,
      () => this.props().log('Your recorded sequence fired'),
      {
        meta: { name: 'Recorded sequence', group: 'Recording' },
      },
    )
    this.recorder = this.hotkeysScope.createHotkeyRecorder(() => ({
      recordBy: this.mode,
      ignoreInputs: false,
      detectConflicts: {
        exclude: (reg) => reg.options.meta?.name === 'Recorded action',
      },
      validate: (_, { parsedHotkey }) =>
        parsedHotkey.modifiers.length > 0 ||
        'Include a modifier for this action.',
      onReject: ({ message }) => (this.problem = message),
      onRecord: (value) => {
        this.hotkey = value
        this.problem = ''
        this.props().log(`Recorded ${value}`)
      },
      onClear: () => {
        this.hotkey = this.initial
        this.problem = ''
        this.props().log('Restored default shortcut')
      },
      onCancel: () =>
        (this.problem = 'Recording cancelled. Binding unchanged.'),
    }))
    this.sequenceRecorder = this.hotkeysScope.createHotkeySequenceRecorder(
      () => ({
        recordBy: this.mode,
        ignoreInputs: false,
        idleTimeoutMs: this.idle ? 1500 : undefined,
        commitKeys: this.commitOnEnter ? 'enter' : 'none',
        detectConflicts: {
          exclude: (reg) => reg.options.meta?.name === 'Recorded sequence',
        },
        validate: (steps) => steps.length >= 2 || 'Record at least two chords.',
        onRecord: (value) => {
          this.sequence = value
          this.sequenceProblem = ''
          this.props().log(`Recorded sequence ${value.join(' → ')}`)
        },
        onReject: (reason) => (this.sequenceProblem = reason.message),
        onClear: () => {
          this.sequence = ['[KeyX]', '[KeyY]']
          this.sequenceProblem = 'Restored default sequence.'
        },
        onCancel: () => (this.sequenceProblem = 'Recording cancelled.'),
      }),
    )
  }
  destroy() {
    this.hotkeysScope.destroy()
  }
}

class Formatting {
  formatForDisplay = formatForDisplay
  platform: 'mac' | 'windows' | 'linux' = 'mac'
  modifierSymbols = false
  keySymbols = true
  get options() {
    return {
      platform: this.platform,
      useSymbols: { modifiers: this.modifierSymbols, keys: this.keySymbols },
    }
  }
}
Alpine.data(
  'hint',
  (
    props: () => {
      hotkey: RegisterableHotkey
      enabled?: boolean
    },
  ) => new Hint(props),
)
Alpine.data('layout', () => new Layout())
Alpine.data(
  'tickets',
  (props: () => { log: (message: string) => void }) => new Tickets(props),
)
Alpine.data(
  'editorPane',
  (props: () => { name: string; log: (message: string) => void }) =>
    new EditorPane(props),
)
Alpine.data(
  'editor',
  (props: () => { log: (message: string) => void }) => new Editor(props),
)
Alpine.data(
  'sequences',
  (props: () => { log: (message: string) => void }) => new Sequences(props),
)
Alpine.data(
  'recording',
  (props: () => { log: (message: string) => void }) => new Recording(props),
)
Alpine.data('formatting', () => new Formatting())
Alpine.start()
import Alpine from 'alpinejs'
import { createHotkeysScope, formatForDisplay } from '@tanstack/alpine-hotkeys'
import type {
  AlpineHotkeyRecorder,
  AlpineHotkeySequenceRecorder,
  AlpineHotkeyState,
  Hotkey,
  HotkeyRegistrationsResult,
  HotkeySequence,
  RecorderKeyMode,
  RegisterableHotkey,
} from '@tanstack/alpine-hotkeys'
import './index.css'

const pages = [
  { to: '/', label: 'Tickets', shortcut: 'Alt+[Digit1]' },
  { to: '/editor', label: 'Editor', shortcut: 'Alt+[Digit2]' },
  { to: '/sequences', label: 'Sequences', shortcut: 'Alt+[Digit3]' },
  { to: '/recording', label: 'Recording', shortcut: 'Alt+[Digit4]' },
  { to: '/formatting', label: 'Formatting', shortcut: 'Alt+[Digit5]' },
] as const

class Hint {
  private hotkeysScope = createHotkeysScope({ hotkey: { requireReset: true } })

  formatForDisplay = formatForDisplay
  visible!: AlpineHotkeyState<boolean>
  constructor(
    public props: () => {
      hotkey: RegisterableHotkey
      enabled?: boolean
    },
  ) {}
  init() {
    this.visible = this.hotkeysScope.createHotkeyHint(() => this.props().hotkey)
  }
  destroy() {
    this.hotkeysScope.destroy()
  }
}

class Layout {
  private hotkeysScope = createHotkeysScope({ hotkey: { requireReset: true } })
  private cleanups: Array<() => void> = []
  formatForDisplay = formatForDisplay
  pages = pages
  currentPath: string = window.location.pathname
  showShortcuts = false
  activity: Array<string> = []
  keys!: AlpineHotkeyState<Array<string>>
  codes!: AlpineHotkeyState<Record<string, string>>
  shift!: AlpineHotkeyState<boolean>
  registrationState0!: HotkeyRegistrationsResult
  navigate({ to }: { to: string }) {
    window.history.pushState({}, '', to)
    this.currentPath = to
  }
  log(message: string) {
    this.activity = [message, ...this.activity].slice(0, 6)
  }
  get hotkeys() {
    return this.registrationState0.hotkeys
  }
  get sequences() {
    return this.registrationState0.sequences
  }
  get registrations() {
    return [...this.hotkeys, ...this.sequences]
  }
  get groups() {
    return [
      ...new Set(
        this.registrations.map((reg) => reg.options.meta?.group ?? 'Other'),
      ),
    ]
  }

  init() {
    this.navigate = this.navigate.bind(this)
    this.log = this.log.bind(this)
    this.keys = this.hotkeysScope.createHeldKeys()
    this.codes = this.hotkeysScope.createHeldKeyCodes()
    this.shift = this.hotkeysScope.createKeyHold('Shift')
    this.registrationState0 = this.hotkeysScope.createHotkeyRegistrations()
    this.hotkeysScope.createHotkeys(
      () =>
        pages.map((page) => ({
          hotkey: page.shortcut,
          callback: () => {
            void this.navigate({ to: page.to })
          },
          options: {
            meta: { name: `Open ${page.label}`, group: 'Navigation' },
          },
        })),
      { ignoreInputs: true },
    )
    this.hotkeysScope.createHotkey(
      'Alt+Shift+[KeyK]',
      () => (this.showShortcuts = !this.showShortcuts),
      {
        meta: { name: 'Show shortcuts', group: 'Navigation' },
      },
    )
    const onPopState = () => {
      this.currentPath = window.location.pathname
    }
    window.addEventListener('popstate', onPopState)
    this.cleanups.push(() => window.removeEventListener('popstate', onPopState))
  }
  destroy() {
    for (const cleanup of this.cleanups) cleanup()
    this.hotkeysScope.destroy()
  }
}

class Tickets {
  private hotkeysScope = createHotkeysScope({ hotkey: { requireReset: true } })

  count = 0
  enabled = true
  create() {
    this.count = this.count + 1
    this.props().log('Ticket created')
  }
  save() {
    return this.props().log('Pending ticket saved')
  }
  constructor(public props: () => { log: (message: string) => void }) {}
  init() {
    this.create = this.create.bind(this)
    this.save = this.save.bind(this)
    this.hotkeysScope.createHotkey('Alt+[KeyC]', this.create, () => ({
      enabled: this.enabled,
      ignoreInputs: false,
      meta: {
        name: 'Create ticket',
        description: 'Also works while entering a ticket note',
        group: 'Tickets',
      },
    }))
    this.hotkeysScope.createHotkey('Alt+[KeyS]', this.save, () => ({
      enabled: this.enabled,
      meta: { name: 'Save pending ticket', group: 'Tickets' },
    }))
  }
  destroy() {
    this.hotkeysScope.destroy()
  }
}

class EditorPane {
  private hotkeysScope = createHotkeysScope({ hotkey: { requireReset: true } })

  formatForDisplay = formatForDisplay
  target: HTMLFieldSetElement | null = null
  text = 'One shortcut, two independent editors.'
  saves = 0

  constructor(
    public props: () => { name: string; log: (message: string) => void },
  ) {}
  init() {
    this.hotkeysScope.createHotkeys(
      [
        {
          hotkey: 'Mod+[KeyS]',
          callback: () => {
            this.saves = this.saves + 1
            this.props().log(`${this.props().name} saved`)
          },
          options: {
            meta: {
              name: `Save ${this.props().name}`,
              group: 'Editor',
              description: 'Scoped to this editor',
            },
          },
        },
        {
          hotkey: 'Alt+[Backspace]',
          callback: () => {
            this.text = ''
            this.props().log(`${this.props().name} cleared`)
          },
          options: {
            meta: { name: `Clear ${this.props().name}`, group: 'Editor' },
          },
        },
      ],
      () => ({
        target: this.target,
        ignoreInputs: false,
      }),
    )
  }
  destroy() {
    this.hotkeysScope.destroy()
  }
}

class Editor {
  private hotkeysScope = createHotkeysScope({ hotkey: { requireReset: true } })

  formatForDisplay = formatForDisplay
  repeat = false
  count = 0
  bubble = false
  scope: HTMLFieldSetElement | null = null

  constructor(public props: () => { log: (message: string) => void }) {}
  init() {
    this.hotkeysScope.createHotkey(
      'Alt+[ArrowRight]',
      () => (this.count = this.count + 1),
      () => ({
        requireReset: !this.repeat,
        meta: { name: 'Advance counter', group: 'Editor' },
      }),
    )
    this.hotkeysScope.createHotkey(
      'Alt+[KeyB]',
      () => this.props().log('Scoped B handler'),
      () => ({
        target: this.scope,
        stopPropagation: !this.bubble,
        preventDefault: !this.bubble,
        meta: { name: 'Scoped propagation demo', group: 'Editor' },
      }),
    )
    this.hotkeysScope.createHotkey(
      'Alt+[KeyB]',
      () => this.props().log('Document B handler received the bubbled event'),
      {
        meta: { name: 'Document propagation demo', group: 'Editor' },
      },
    )
    this.hotkeysScope.createHotkey(
      'Alt+[KeyU]',
      () => this.props().log('Key released: keyup handler'),
      {
        eventType: 'keyup',
        meta: { name: 'Run on release', group: 'Editor' },
      },
    )
  }
  destroy() {
    this.hotkeysScope.destroy()
  }
}

class Sequences {
  private hotkeysScope = createHotkeysScope({ hotkey: { requireReset: true } })

  formatForDisplay = formatForDisplay
  timeout = 1000
  registrationState2!: HotkeyRegistrationsResult
  get sequences() {
    return this.registrationState2.sequences
  }
  constructor(public props: () => { log: (message: string) => void }) {}
  init() {
    this.hotkeysScope.createHotkeySequence(
      ['[KeyG]', '[KeyG]'],
      () => this.props().log('Sequence: go to top'),
      () => ({
        timeout: this.timeout,
        meta: { name: 'Go to top', group: 'Sequences' },
      }),
    )
    this.hotkeysScope.createHotkeySequences(
      [
        {
          sequence: ['[KeyG]', '[KeyI]'],
          callback: () => this.props().log('Sequence: inbox'),
          options: { meta: { name: 'Open inbox', group: 'Sequences' } },
        },
        {
          sequence: ['Shift+[KeyR]', 'Shift+[KeyT]'],
          callback: () => this.props().log('Sequence: shifted chord chain'),
          options: { meta: { name: 'Shifted chain', group: 'Sequences' } },
        },
      ],
      () => ({ timeout: this.timeout }),
    )
    this.registrationState2 = this.hotkeysScope.createHotkeyRegistrations()
  }
  destroy() {
    this.hotkeysScope.destroy()
  }
}

class Recording {
  private hotkeysScope = createHotkeysScope({ hotkey: { requireReset: true } })

  formatForDisplay = formatForDisplay
  hotkey: Hotkey = this.initial
  mode: RecorderKeyMode = 'code'
  problem = ''
  sequence: HotkeySequence = ['[KeyX]', '[KeyY]']
  sequenceProblem = ''
  idle = false
  commitOnEnter = true
  recorder!: AlpineHotkeyRecorder
  sequenceRecorder!: AlpineHotkeySequenceRecorder
  get initial(): Hotkey {
    return 'Alt+[KeyR]'
  }
  constructor(public props: () => { log: (message: string) => void }) {}
  startRecording() {
    this.problem = ''
    this.recorder.startRecording()
  }
  resetShortcut() {
    this.recorder.stopRecording()
    this.hotkey = this.initial
    this.problem = ''
  }
  startSequenceRecording() {
    this.sequenceProblem = ''
    this.sequenceRecorder.startRecording()
  }
  init() {
    this.hotkeysScope.createHotkey(
      () => this.hotkey,
      () => this.props().log('Your recorded shortcut fired'),
      {
        meta: { name: 'Recorded action', group: 'Recording' },
      },
    )
    this.hotkeysScope.createHotkeySequence(
      () => this.sequence,
      () => this.props().log('Your recorded sequence fired'),
      {
        meta: { name: 'Recorded sequence', group: 'Recording' },
      },
    )
    this.recorder = this.hotkeysScope.createHotkeyRecorder(() => ({
      recordBy: this.mode,
      ignoreInputs: false,
      detectConflicts: {
        exclude: (reg) => reg.options.meta?.name === 'Recorded action',
      },
      validate: (_, { parsedHotkey }) =>
        parsedHotkey.modifiers.length > 0 ||
        'Include a modifier for this action.',
      onReject: ({ message }) => (this.problem = message),
      onRecord: (value) => {
        this.hotkey = value
        this.problem = ''
        this.props().log(`Recorded ${value}`)
      },
      onClear: () => {
        this.hotkey = this.initial
        this.problem = ''
        this.props().log('Restored default shortcut')
      },
      onCancel: () =>
        (this.problem = 'Recording cancelled. Binding unchanged.'),
    }))
    this.sequenceRecorder = this.hotkeysScope.createHotkeySequenceRecorder(
      () => ({
        recordBy: this.mode,
        ignoreInputs: false,
        idleTimeoutMs: this.idle ? 1500 : undefined,
        commitKeys: this.commitOnEnter ? 'enter' : 'none',
        detectConflicts: {
          exclude: (reg) => reg.options.meta?.name === 'Recorded sequence',
        },
        validate: (steps) => steps.length >= 2 || 'Record at least two chords.',
        onRecord: (value) => {
          this.sequence = value
          this.sequenceProblem = ''
          this.props().log(`Recorded sequence ${value.join(' → ')}`)
        },
        onReject: (reason) => (this.sequenceProblem = reason.message),
        onClear: () => {
          this.sequence = ['[KeyX]', '[KeyY]']
          this.sequenceProblem = 'Restored default sequence.'
        },
        onCancel: () => (this.sequenceProblem = 'Recording cancelled.'),
      }),
    )
  }
  destroy() {
    this.hotkeysScope.destroy()
  }
}

class Formatting {
  formatForDisplay = formatForDisplay
  platform: 'mac' | 'windows' | 'linux' = 'mac'
  modifierSymbols = false
  keySymbols = true
  get options() {
    return {
      platform: this.platform,
      useSymbols: { modifiers: this.modifierSymbols, keys: this.keySymbols },
    }
  }
}
Alpine.data(
  'hint',
  (
    props: () => {
      hotkey: RegisterableHotkey
      enabled?: boolean
    },
  ) => new Hint(props),
)
Alpine.data('layout', () => new Layout())
Alpine.data(
  'tickets',
  (props: () => { log: (message: string) => void }) => new Tickets(props),
)
Alpine.data(
  'editorPane',
  (props: () => { name: string; log: (message: string) => void }) =>
    new EditorPane(props),
)
Alpine.data(
  'editor',
  (props: () => { log: (message: string) => void }) => new Editor(props),
)
Alpine.data(
  'sequences',
  (props: () => { log: (message: string) => void }) => new Sequences(props),
)
Alpine.data(
  'recording',
  (props: () => { log: (message: string) => void }) => new Recording(props),
)
Alpine.data('formatting', () => new Formatting())
Alpine.start()