Svelte Example: Create Hotkey Sequence Recorder

<script lang="ts">
  import {
    createHotkeySequences,
    createHotkeySequenceRecorder,
    formatForDisplay,
    getHotkeyRegistrations,
  } from '@tanstack/svelte-hotkeys'
  import type { HotkeySequence } from '@tanstack/svelte-hotkeys'
  import ShortcutListItem from './ShortcutListItem.svelte'

  interface Shortcut {
    id: string
    name: string
    description: string
    sequence: HotkeySequence
  }

  let nextId = 0
  function createId(): string {
    return `shortcut_${++nextId}`
  }

  let shortcuts = $state<Array<Shortcut>>([
    {
      id: createId(),
      name: 'Save',
      description: 'Save the current document',
      sequence: ['Mod+S'],
    },
    {
      id: createId(),
      name: 'Open (gg)',
      description: 'Open the file browser',
      sequence: ['G', 'G'],
    },
    {
      id: createId(),
      name: 'New (dd)',
      description: 'Create a new document',
      sequence: ['D', 'D'],
    },
    {
      id: createId(),
      name: 'Close',
      description: 'Close the current tab',
      sequence: ['Mod+Shift+K'],
    },
    {
      id: createId(),
      name: 'Undo (yy)',
      description: 'Undo the last action',
      sequence: ['Y', 'Y'],
    },
    {
      id: createId(),
      name: 'Redo',
      description: 'Redo the last undone action',
      sequence: ['Mod+Shift+G'],
    },
  ])

  let editingId = $state<string | null>(null)
  let draftName = $state('')
  let draftDescription = $state('')

  const recorder = createHotkeySequenceRecorder({
    onRecord: (sequence: HotkeySequence) => {
      if (editingId) {
        shortcuts = shortcuts.map((s) =>
          s.id === editingId
            ? {
                ...s,
                sequence,
                name: draftName,
                description: draftDescription,
              }
            : s,
        )
        editingId = null
      }
    },
    onCancel: () => {
      if (editingId) {
        const shortcut = shortcuts.find((s) => s.id === editingId)
        if (shortcut && shortcut.sequence.length === 0) {
          shortcuts = shortcuts.filter((s) => s.id !== editingId)
        }
      }
      editingId = null
    },
    onClear: () => {
      if (editingId) {
        shortcuts = shortcuts.map((s) =>
          s.id === editingId
            ? {
                ...s,
                sequence: [],
                name: draftName,
                description: draftDescription,
              }
            : s,
        )
        editingId = null
      }
    },
  })

  let isRecording = $derived(recorder.isRecording)

  // Register all sequences with meta
  createHotkeySequences(() =>
    shortcuts
      .filter((s) => s.sequence.length > 0)
      .map((s) => ({
        sequence: s.sequence,
        callback: () => {
          console.log(`${s.name} triggered:`, s.sequence)
        },
        options: {
          enabled: !isRecording,
          meta: {
            name: s.name,
            description: s.description,
          },
        },
      })),
  )

  function handleEdit(id: string) {
    const shortcut = shortcuts.find((s) => s.id === id)
    if (!shortcut) return
    editingId = id
    draftName = shortcut.name
    draftDescription = shortcut.description
    recorder.startRecording()
  }

  function handleSaveEditing() {
    if (editingId) {
      shortcuts = shortcuts.map((s) =>
        s.id === editingId
          ? { ...s, name: draftName, description: draftDescription }
          : s,
      )
      recorder.stopRecording()
      editingId = null
    }
  }

  function handleCancel() {
    recorder.cancelRecording()
  }

  function handleDelete(id: string) {
    shortcuts = shortcuts.filter((s) => s.id !== id)
  }

  function handleCreateNew() {
    const newShortcut: Shortcut = {
      id: createId(),
      name: '',
      description: '',
      sequence: [],
    }
    shortcuts = [...shortcuts, newShortcut]
    editingId = newShortcut.id
    draftName = ''
    draftDescription = ''
    recorder.startRecording()
  }

  // Registrations viewer
  const registrations = getHotkeyRegistrations()
</script>

<div class="app">
  <header>
    <h1>Sequence Shortcut Settings</h1>
    <p>
      Customize Vim-style sequences. Click Edit, press each chord in order, then
      press Enter to save. Escape cancels; Backspace removes the last chord or
      clears when empty.
    </p>
  </header>

  <main>
    <section class="demo-section">
      <h2>Shortcuts</h2>
      <div class="shortcuts-list">
        {#each shortcuts as shortcut (shortcut.id)}
          <ShortcutListItem
            {shortcut}
            isEditing={editingId === shortcut.id}
            draftName={editingId === shortcut.id ? draftName : shortcut.name}
            draftDescription={editingId === shortcut.id
              ? draftDescription
              : shortcut.description}
            onDraftNameChange={(v) => (draftName = v)}
            onDraftDescriptionChange={(v) => (draftDescription = v)}
            liveSteps={recorder.steps}
            onEdit={() => handleEdit(shortcut.id)}
            onSave={handleSaveEditing}
            onCancel={handleCancel}
            onDelete={() => handleDelete(shortcut.id)}
          />
        {/each}
      </div>
      <button
        type="button"
        class="create-button"
        onclick={handleCreateNew}
        disabled={isRecording}
      >
        + Create New Shortcut
      </button>
    </section>

    {#if recorder.isRecording}
      <div class="info-box recording-notice">
        <strong>Recording sequence...</strong> Press each chord, then Enter to
        finish. Escape cancels. Backspace removes the last chord or clears.
        {#if recorder.steps.length > 0}
          <div>
            Steps:
            {#each recorder.steps as h, i}
              {#if i > 0}{' '}{/if}<kbd>{formatForDisplay(h)}</kbd>
            {/each}
          </div>
        {/if}
      </div>
    {/if}

    <!-- Registrations Viewer -->
    <section class="demo-section">
      <h2>Live Registrations</h2>
      <p>
        This table is powered by <code>getHotkeyRegistrations()</code> — trigger counts,
        names, and descriptions update in real-time as you use your sequences.
      </p>
      <table class="registrations-table">
        <thead>
          <tr>
            <th>Sequence</th>
            <th>Name</th>
            <th>Description</th>
            <th>Enabled</th>
            <th>Triggers</th>
          </tr>
        </thead>
        <tbody>
          {#each registrations.sequences as reg (reg.id)}
            <tr>
              <td>
                {#each reg.sequence as s, i}
                  {#if i > 0}{' '}{/if}<kbd>{formatForDisplay(s)}</kbd>
                {/each}
              </td>
              <td>{reg.options.meta?.name ?? '—'}</td>
              <td class="description-cell">
                {reg.options.meta?.description ?? '—'}
              </td>
              <td>
                <span
                  class={reg.options.enabled !== false
                    ? 'status-on'
                    : 'status-off'}
                >
                  {reg.options.enabled !== false ? 'yes' : 'no'}
                </span>
              </td>
              <td class="trigger-count">{reg.triggerCount}</td>
            </tr>
          {/each}
          {#if registrations.sequences.length === 0}
            <tr>
              <td colspan="5" class="empty-row">No sequences registered</td>
            </tr>
          {/if}
        </tbody>
      </table>
    </section>

    <section class="demo-section">
      <h2>Usage</h2>
      <pre class="code-block">{`import {
  createHotkeySequences,
  createHotkeySequenceRecorder,
  getHotkeyRegistrations,
} from '@tanstack/svelte-hotkeys'

// Register sequences dynamically with meta
createHotkeySequences(() =>
  shortcuts.map((s) => ({
    sequence: s.sequence,
    callback: () => handleAction(s.id),
    options: {
      enabled: !isRecording,
      meta: { name: s.name, description: s.description },
    },
  })),
)

// Read all registrations reactively
const registrations = getHotkeyRegistrations()
// registrations.sequences[0].options.meta?.name → 'Save'
// registrations.sequences[0].triggerCount → 3`}</pre>
    </section>
  </main>
</div>
<script lang="ts">
  import {
    createHotkeySequences,
    createHotkeySequenceRecorder,
    formatForDisplay,
    getHotkeyRegistrations,
  } from '@tanstack/svelte-hotkeys'
  import type { HotkeySequence } from '@tanstack/svelte-hotkeys'
  import ShortcutListItem from './ShortcutListItem.svelte'

  interface Shortcut {
    id: string
    name: string
    description: string
    sequence: HotkeySequence
  }

  let nextId = 0
  function createId(): string {
    return `shortcut_${++nextId}`
  }

  let shortcuts = $state<Array<Shortcut>>([
    {
      id: createId(),
      name: 'Save',
      description: 'Save the current document',
      sequence: ['Mod+S'],
    },
    {
      id: createId(),
      name: 'Open (gg)',
      description: 'Open the file browser',
      sequence: ['G', 'G'],
    },
    {
      id: createId(),
      name: 'New (dd)',
      description: 'Create a new document',
      sequence: ['D', 'D'],
    },
    {
      id: createId(),
      name: 'Close',
      description: 'Close the current tab',
      sequence: ['Mod+Shift+K'],
    },
    {
      id: createId(),
      name: 'Undo (yy)',
      description: 'Undo the last action',
      sequence: ['Y', 'Y'],
    },
    {
      id: createId(),
      name: 'Redo',
      description: 'Redo the last undone action',
      sequence: ['Mod+Shift+G'],
    },
  ])

  let editingId = $state<string | null>(null)
  let draftName = $state('')
  let draftDescription = $state('')

  const recorder = createHotkeySequenceRecorder({
    onRecord: (sequence: HotkeySequence) => {
      if (editingId) {
        shortcuts = shortcuts.map((s) =>
          s.id === editingId
            ? {
                ...s,
                sequence,
                name: draftName,
                description: draftDescription,
              }
            : s,
        )
        editingId = null
      }
    },
    onCancel: () => {
      if (editingId) {
        const shortcut = shortcuts.find((s) => s.id === editingId)
        if (shortcut && shortcut.sequence.length === 0) {
          shortcuts = shortcuts.filter((s) => s.id !== editingId)
        }
      }
      editingId = null
    },
    onClear: () => {
      if (editingId) {
        shortcuts = shortcuts.map((s) =>
          s.id === editingId
            ? {
                ...s,
                sequence: [],
                name: draftName,
                description: draftDescription,
              }
            : s,
        )
        editingId = null
      }
    },
  })

  let isRecording = $derived(recorder.isRecording)

  // Register all sequences with meta
  createHotkeySequences(() =>
    shortcuts
      .filter((s) => s.sequence.length > 0)
      .map((s) => ({
        sequence: s.sequence,
        callback: () => {
          console.log(`${s.name} triggered:`, s.sequence)
        },
        options: {
          enabled: !isRecording,
          meta: {
            name: s.name,
            description: s.description,
          },
        },
      })),
  )

  function handleEdit(id: string) {
    const shortcut = shortcuts.find((s) => s.id === id)
    if (!shortcut) return
    editingId = id
    draftName = shortcut.name
    draftDescription = shortcut.description
    recorder.startRecording()
  }

  function handleSaveEditing() {
    if (editingId) {
      shortcuts = shortcuts.map((s) =>
        s.id === editingId
          ? { ...s, name: draftName, description: draftDescription }
          : s,
      )
      recorder.stopRecording()
      editingId = null
    }
  }

  function handleCancel() {
    recorder.cancelRecording()
  }

  function handleDelete(id: string) {
    shortcuts = shortcuts.filter((s) => s.id !== id)
  }

  function handleCreateNew() {
    const newShortcut: Shortcut = {
      id: createId(),
      name: '',
      description: '',
      sequence: [],
    }
    shortcuts = [...shortcuts, newShortcut]
    editingId = newShortcut.id
    draftName = ''
    draftDescription = ''
    recorder.startRecording()
  }

  // Registrations viewer
  const registrations = getHotkeyRegistrations()
</script>

<div class="app">
  <header>
    <h1>Sequence Shortcut Settings</h1>
    <p>
      Customize Vim-style sequences. Click Edit, press each chord in order, then
      press Enter to save. Escape cancels; Backspace removes the last chord or
      clears when empty.
    </p>
  </header>

  <main>
    <section class="demo-section">
      <h2>Shortcuts</h2>
      <div class="shortcuts-list">
        {#each shortcuts as shortcut (shortcut.id)}
          <ShortcutListItem
            {shortcut}
            isEditing={editingId === shortcut.id}
            draftName={editingId === shortcut.id ? draftName : shortcut.name}
            draftDescription={editingId === shortcut.id
              ? draftDescription
              : shortcut.description}
            onDraftNameChange={(v) => (draftName = v)}
            onDraftDescriptionChange={(v) => (draftDescription = v)}
            liveSteps={recorder.steps}
            onEdit={() => handleEdit(shortcut.id)}
            onSave={handleSaveEditing}
            onCancel={handleCancel}
            onDelete={() => handleDelete(shortcut.id)}
          />
        {/each}
      </div>
      <button
        type="button"
        class="create-button"
        onclick={handleCreateNew}
        disabled={isRecording}
      >
        + Create New Shortcut
      </button>
    </section>

    {#if recorder.isRecording}
      <div class="info-box recording-notice">
        <strong>Recording sequence...</strong> Press each chord, then Enter to
        finish. Escape cancels. Backspace removes the last chord or clears.
        {#if recorder.steps.length > 0}
          <div>
            Steps:
            {#each recorder.steps as h, i}
              {#if i > 0}{' '}{/if}<kbd>{formatForDisplay(h)}</kbd>
            {/each}
          </div>
        {/if}
      </div>
    {/if}

    <!-- Registrations Viewer -->
    <section class="demo-section">
      <h2>Live Registrations</h2>
      <p>
        This table is powered by <code>getHotkeyRegistrations()</code> — trigger counts,
        names, and descriptions update in real-time as you use your sequences.
      </p>
      <table class="registrations-table">
        <thead>
          <tr>
            <th>Sequence</th>
            <th>Name</th>
            <th>Description</th>
            <th>Enabled</th>
            <th>Triggers</th>
          </tr>
        </thead>
        <tbody>
          {#each registrations.sequences as reg (reg.id)}
            <tr>
              <td>
                {#each reg.sequence as s, i}
                  {#if i > 0}{' '}{/if}<kbd>{formatForDisplay(s)}</kbd>
                {/each}
              </td>
              <td>{reg.options.meta?.name ?? ''}</td>
              <td class="description-cell">
                {reg.options.meta?.description ?? ''}
              </td>
              <td>
                <span
                  class={reg.options.enabled !== false
                    ? 'status-on'
                    : 'status-off'}
                >
                  {reg.options.enabled !== false ? 'yes' : 'no'}
                </span>
              </td>
              <td class="trigger-count">{reg.triggerCount}</td>
            </tr>
          {/each}
          {#if registrations.sequences.length === 0}
            <tr>
              <td colspan="5" class="empty-row">No sequences registered</td>
            </tr>
          {/if}
        </tbody>
      </table>
    </section>

    <section class="demo-section">
      <h2>Usage</h2>
      <pre class="code-block">{`import {
  createHotkeySequences,
  createHotkeySequenceRecorder,
  getHotkeyRegistrations,
} from '@tanstack/svelte-hotkeys'

// Register sequences dynamically with meta
createHotkeySequences(() =>
  shortcuts.map((s) => ({
    sequence: s.sequence,
    callback: () => handleAction(s.id),
    options: {
      enabled: !isRecording,
      meta: { name: s.name, description: s.description },
    },
  })),
)

// Read all registrations reactively
const registrations = getHotkeyRegistrations()
// registrations.sequences[0].options.meta?.name → 'Save'
// registrations.sequences[0].triggerCount → 3`}</pre>
    </section>
  </main>
</div>