Skip to content

About

My hammerspoon config

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

69 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Hammerspoon Config

Personal Hammerspoon configuration for macOS automation — window management, Slack status, encrypted scratchpad, GIF search, and more.

Modules

Module Purpose Hotkey
window_manager Rectangle-style window tiling with fraction cycling (1/2, 1/3, 2/3) Ctrl+Alt+Cmd + arrows/F/Home/End
scratchpad Encrypted markdown editor synced via iCloud Ctrl+Alt+S
gif_finder GIF search via Klipy API with favorites and recents, copies URL to clipboard Ctrl+Alt+G
slack_status Auto-updates Slack status based on WiFi network; manual overrides and custom status via menu —
hyperduck Monitors iCloud file for URLs sent from iPhone, opens them on Mac —
battery_indicator Shows remaining battery time in menu bar —
screen_blur Full-screen blur overlay for privacy (downsample trick via sips) Ctrl+Alt+B
stt Local speech-to-text via a daemon with 2 backends (parakeet-mlx or Apple SpeechTranscriber), optional LLM post-processing, audio tones, media pause/resume, transcription history viewer, and a settings panel for vocabulary and per-app LLM styles fn+Space (toggle) / fn+Shift (hold; +Ctrl also presses Return) / Ctrl+Alt+H (history) / Ctrl+Alt+W (settings)
clipboard_history Clipboard history with search, auto-skips password manager entries, 30-day retention Ctrl+Alt+V
mouse_grid Keyboard-driven mouse (Mouseless-style): full-screen hint grid for click, right/double click, drag & drop, and scrolling; element hints mode (Shortcat-style, via the Accessibility API); free mode for smooth relative cursor movement Tap left Cmd (grid) / Double-tap left Cmd (hints) / Tap left Alt (free)
unified_menu Combines Slack Status, Hyperduck, Scratchpad, Screen Blur, Clipboard History, and Keyboard Lock into a single menubar item —
eject_lock Locks the screen when the Magic Keyboard's Eject key is double-tapped; holding Eject for 4 seconds runs the "Eject TimeMachine Disk" macOS Shortcut (modifier combos like Cmd+Alt+Eject pass through untouched) Double-tap Eject / Hold Eject 4s
whatcable Menubar view of USB-C/Thunderbolt port data (link speed, cable capabilities, charging, displays, connected device tree, cable trust warnings) via the whatcable CLI; refreshes on USB/power/display changes —
keyboard_lock Locks the keyboard so you can clean it: a full-screen overlay plus an event tap that swallows keys, modifiers and media keys; a hold of Esc or the mouse button for 3 seconds unlocks Ctrl+Alt+K
voice_mode Voice conversations with Claude Code: reads Claude's replies aloud with the default system voice and sends your push-to-talk dictation to the Claude Code session (straight into its iTerm or Orca terminal without moving focus, otherwise to its window). Pressing fn+Space while it talks stops the speech, and cancels Claude's turn if it is still working. Turned on and off by the voice-mode Claude Code skill fn+Space / fn+Shift (while on)
stall_log Writes keyboard delay data to ~/Library/Logs/hammerspoon-stall.log: each time the Hammerspoon main thread blocks for more than 0.5 s (with the last 1000 characters of the console), and each key event that arrives more than 150 ms late —

Hotkeys

Shortcut Action
Ctrl+Alt+Cmd+Left Tile window left (cycles 1/2 → 1/3 → 2/3)
Ctrl+Alt+Cmd+Right Tile window right (cycles 1/2 → 1/3 → 2/3)
Ctrl+Alt+Cmd+Up Tile window top (cycles 1/2 → 1/3 → 2/3)
Ctrl+Alt+Cmd+Down Tile window bottom (cycles 1/2 → 1/3 → 2/3)
Ctrl+Alt+Cmd+F Maximize window
Ctrl+Alt+Cmd+Home Move window to previous display
Ctrl+Alt+Cmd+End Move window to next display
Ctrl+Alt+S Toggle scratchpad
Ctrl+Alt+G Toggle GIF finder
Ctrl+Alt+B Toggle screen blur overlay (also dismisses on click or any keypress)
fn+Space Toggle speech-to-text recording (press to start, press again to stop and paste)
fn+Shift Hold-to-talk speech-to-text (hold both to record, release to stop and paste). Hold Ctrl too to also press Return after the paste: the pill shows ↵ while Ctrl is held, and letting go of Ctrl more than 200 ms before fn or Shift cancels it. With the Apple backend and 2 or more locales, a tap (less than 250 ms) switches to the next language
Ctrl+Alt+H Toggle STT transcription history viewer
Ctrl+Alt+W Toggle STT settings (vocabulary, per-app styles)
Ctrl+Alt+V Toggle clipboard history viewer
Tap left Cmd Toggle mouse grid overlay (quick press+release of left Cmd alone)
Double-tap left Cmd Element hints mode (Shortcat-style: labels on clickable UI elements)
Tap left Alt Toggle free mouse mode (relative cursor movement, no overlay)
Double-tap Eject Lock screen
Hold Eject (4s) Run the "Eject TimeMachine Disk" macOS Shortcut
Ctrl+Alt+K Lock the keyboard for a clean (hold Esc or the mouse button for 3 seconds, then release, to unlock)

Note: Home = Fn+Left and End = Fn+Right on Mac keyboards.

Mouse Grid (while overlay is up)

Type a cell's two characters (first char = row, a–z top-to-bottom; second char = column, keyboard rows qwert/asdfg/zxcvb left-to-right). Cells are wide horizontal rectangles (26 rows × 15 columns). Then pick a precision point in the subgrid shown inside the cell (qwert / asdfg / zxcvb, laid out spatially) or press Space for the cell center. A hint toast at the bottom of the screen shows the available keys at every step. Modifiers held on the final key choose the action:

Key Action
subgrid key / Space Left click
Shift + final key Right click
Ctrl + final key Double click
Alt + final key Move cursor only (no click)
Cmd + final key Arm drag (mouse down; overlay stays up — next selection drops)
Hold final key Nudge: cursor jumps to the point and arrows/h/j/k/l move it in small steps (Shift = bigger); releasing the key performs the action (modifiers apply at release)
Backspace Undo one selection level (or cancel a nudge)
Tab Move overlay to next screen
, Scroll mode: h/j/k/l or arrows scroll, Shift = faster, Esc exits
Esc Dismiss overlay (cancels an armed drag)

Hints Mode (double-tap left Cmd)

Shortcat-style element hints: the focused window's accessibility tree is scanned for actionable elements (buttons, links, text fields, checkboxes, menu items…) and each one gets a short yellow label. Typing a label's characters filters the hints live (non-matching ones dim out); completing a label performs the action at that element's center. No screenshots involved — element positions come straight from the macOS Accessibility API.

Key Action
label chars Filter hints / act when a label is completed
Space Search mode: type the element's text (title/label/value) to find it
Shift + final char Right click
Ctrl + final char Double click
Alt + final char Move cursor only (no click)
Cmd + final char Arm drag (mouse down; hints reappear — next label drops)
Backspace Un-type one label character
Esc Exit hints mode (cancels an armed drag)

While searching, matching elements are outlined instead of labelled and typing goes to the query:

Key Action
any text Filter elements by their accessibility text (case-insensitive substring)
Tab / Shift+Tab Select next / previous match (red outline)
Enter Act on the selected match — same modifiers as above (Shift right, Ctrl double, Alt move, Cmd drag)
Backspace Delete a query character (on an empty query: back to labels)
Esc Back to label mode (Esc again exits hints)

Notes:

  • Scans the focused window of the frontmost app. A "scanning…" toast shows while the (asynchronous) traversal runs; very large windows are capped at 400 hints.
  • Chromium browsers (Chrome, Arc, Brave, Edge, Vivaldi) and Electron apps (Slack, Discord, VS Code, Notion) hide web/page content from the accessibility tree by default. The module temporarily enables the relevant accessibility attribute (AXEnhancedUserInterface / AXManualAccessibility) while hints are up and restores it on exit, since leaving it on can make window snapping glitchy. The first scan in these apps may take one extra ~200ms rescan while the tree populates. The app lists are configurable (enhancedUIApps / electronApps).
  • Single-tap left Cmd while hints are up switches to the grid; tap left Alt to switch to free mode.

Free Mode (tap left Alt)

Moves the real cursor with the keyboard — no grid overlay, just a hint toast and a soft glow around the screen edges so it's obvious the mode is active (the glow follows the cursor across monitors). Exits on Esc or after 10s of inactivity. Tapping left Cmd switches to the grid; tapping left Alt while the grid is up switches to free mode.

Key Action
h / j / k / l Move cursor left/down/up/right (hold; diagonals work)
Shift (held) Move faster (4×)
Ctrl (held) Move slower (0.25×, precision)
Space Left click (mode stays active)
Shift+Space Right click
Ctrl+Space Double click
Cmd+Space Drag toggle — press to grab, move, press Space again to drop
The 4 keys right of N Scroll left/up/down/right (Shift = faster) — matched by physical position, so m , . / on US, m , . - on Spanish ISO; the toast shows the keys for your layout
Esc Exit free mode (releases a held drag)

Keyboard Lock (Ctrl+Alt+K)

Ctrl+Alt+K covers every screen with a dark overlay and swallows all keyboard input. You can then wipe the keyboard. No key reaches an app and no shortcut runs. The hotkey only locks; it does not unlock. To unlock, hold Esc or the mouse button for 3 seconds and release. The lock also releases itself after 5 minutes.

The mouse and the trackpad stay live on purpose, but a short click does nothing. The mouse button needs the same 3-second hold as Esc, because a palm on the trackpad is easy during a clean.

macOS handles 3 inputs below the event tap, and the lock cannot block them: Caps Lock, the power button, and Touch ID. Trackpad gestures such as Mission Control are not keyboard events, and the lock does not block them either. A double-tap of the fn key can still start dictation; this is not verified.

URL Actions

Launchers such as Tinycast can trigger actions with a custom shell command, for example open -g hammerspoon://scratchpad. The -g flag keeps Hammerspoon in the background.

URL Action
hammerspoon://scratchpad Toggle the scratchpad
hammerspoon://gif Toggle the GIF finder
hammerspoon://clipboard Toggle clipboard history
hammerspoon://blur Toggle the screen blur
hammerspoon://stt Start or stop dictation
hammerspoon://stt-history Show the STT history
hammerspoon://stt-settings Toggle the STT settings
hammerspoon://keyboard-lock Lock the keyboard

File Structure

Webview modules (gif_finder, slack_status, scratchpad, stt, clipboard_history) store their HTML, CSS, and JS in separate files under html/:

html/
  gif_finder/    — GIF search UI
  slack_status/  — Custom status form
  scratchpad/    — CodeMirror markdown editor
  stt_history/        — Transcription history viewer
  stt_settings/       — Dictation settings (vocabulary, per-app styles)
  clipboard_history/  — Clipboard history viewer

Each directory contains index.html, style.css, and script.js. At runtime, html_loader.lua reads these files and inlines the CSS/JS into the HTML before passing it to hs.webview:html(). It puts the shared html/panel.css in front of every module's style.css.

All 5 webviews are borderless, transparent launcher panels made by panel.lua. They close on Esc, which returns focus to the previous app, and hide when you click elsewhere. Each panel lists its keys in its footer:

Panel Keys
Clipboard history ↵ / ⇧↵ paste into the previous app, ⌘C copy, ⌘⌫ delete, ↑↓ move
GIF finder Searches as you type. ↵ copy URL, ⌘↵ copy HTML, ⌘D favorite, ⇥ next tab, arrows move
STT history ↵ copy polished, ⌥↵ copy original, ↑↓ move (the selected entry shows the diff)
Scratchpad ⌘↵ turn the cursor's line into a task, or check and uncheck it, Esc save and close
Slack custom status Type to filter presets or set free text (:emoji: text sets a custom emoji), ↵ set, ⇥ emoji grid, ⌥←→ duration, ⌘⌫ clear status

Alerts from stt, eject_lock, and whatcable use the canvas toasts in toast.lua.

Setup

Prerequisites

  1. Install Hammerspoon
  2. Grant Accessibility permissions when prompted (System Settings → Privacy & Security → Accessibility)

Keychain Secrets

Store secrets in the macOS Keychain — they are never saved in code.

# Slack API token (xoxp-...)
security add-generic-password -a "$USER" -s "slack-status-token" -w "YOUR_TOKEN"

# Klipy GIF search API key (https://partner.klipy.com)
security add-generic-password -a "$USER" -s "klipy-api-key" -w "YOUR_API_KEY"

# Mistral API key for STT post-processing (optional — omit to disable)
security add-generic-password -a "$USER" -s "mistral-api-key" -w "YOUR_API_KEY"

STT Daemon

The speech-to-text module requires a local Python daemon. The default backend is parakeet-mlx:

cd ~/.hammerspoon/stt-daemon
uv sync
uv run stt_daemon.py

To run as a background service via launchd:

cp ~/.hammerspoon/stt-daemon/com.local.stt-daemon.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.local.stt-daemon.plist

Logs are written to ~/Library/Logs/stt-daemon.log.

Apple SpeechTranscriber Backend (Optional)

On macOS 26 or later, the daemon can use the on-device Apple SpeechTranscriber in place of parakeet-mlx. The daemon records the audio as before and sends it to the apple-stt Swift CLI while you speak. The pill shows as many of the newest words of the live transcript as fit, and updates approximately once each second. When you stop, Hammerspoon pastes the final text. The Parakeet model does not load with this backend. The backend does not detect the language. It uses the first locale in the locales option (default en-US).

  1. Build the CLI:
    cd ~/.hammerspoon/stt-daemon
    swiftc -O -target arm64-apple-macos26.0 apple_stt.swift -o apple-stt
  2. Run the CLI once with no audio. If the en-US model is not installed, the CLI downloads it now:
    ./apple-stt < /dev/null
  3. Set backend = "apple" in the stt.init call in init.lua.
  4. Reload the Hammerspoon config.

To use parakeet-mlx again, set backend = "parakeet" (the default) and reload the config.

To dictate in more than 1 language:

  1. Download the model for each locale, for example Chilean Spanish:
    ./apple-stt es-CL < /dev/null
  2. Set locales = { "en-US", "es-CL" } in the stt.init call in init.lua.
  3. Reload the Hammerspoon config.

A tap of fn+Shift (less than 250 ms) switches to the next locale and shows it in a toast. The locale stays until the next tap, and fn+Space uses it too. While recording, the pill shows a 2-letter badge for the current locale.

LLM Post-Processing (Optional)

When a Mistral API key is present in the keychain, transcribed text is sent through the Mistral API to remove filler words, fix punctuation/capitalization, and apply light grammar corrections. The pill overlay shows a purple "Polishing" spinner during this step. If the API call fails or times out (10s), the raw transcription is pasted instead. With the Apple backend, the whole transcript so far is also polished while you speak (one request at a time, the next one starts when the reply arrives), and a panel above the pill shows the latest polished version. When the last live polish covers the final transcript exactly, it is pasted without another request. With the Apple backend, the request also gives the locale of the recording (for example es-CL), so that Mistral does not guess the language.

Configuration options in init.lua:

stt.init({
    llm_api_key = mistralApiKey,
    -- llm_model = "mistral-small-latest",           -- model to use
    -- llm_api_url = "https://api.mistral.ai/v1/chat/completions",  -- API endpoint
    -- llm_timeout = 10,                             -- seconds before fallback
})

The API uses the OpenAI-compatible chat completions format, so other providers (OpenRouter, Groq, Together, etc.) work by changing llm_api_url, llm_model, and llm_api_key.

Voice Mode (Claude Code)

Ask Claude Code for "voice mode" (or run /voice-mode). The skill runs open -g "hammerspoon://voice-mode?state=on&session=...&pid=...&iterm=...&orca=...", passing $ITERM_SESSION_ID (left out inside tmux) and $ORCA_TERMINAL_HANDLE. While voice mode is on:

  • fn+Space and fn+Shift send the transcript to Claude from any app. In Orca (orca terminal send) or iTerm (AppleScript write text to that session, as a bracketed paste and then Return), it goes straight into Claude's terminal and focus never moves, so you can keep working in a browser. In any other terminal, Hammerspoon saves the frontmost window when voice mode turns on and sends to it (focus, paste, Return, focus back); if that window has several tabs or panes, the text goes to the active one. If a send fails, or Claude doesn't start a turn within 1.5 seconds, the transcript is copied to the clipboard and a toast says so. The first send to iTerm asks for Automation permission.
  • Claude's replies are spoken line by line as they appear. Code blocks, table rows, and URLs are skipped.
  • Starting a recording stops the speech. If Claude is still working when you send, Hammerspoon sends Esc first to cancel that turn.
  • The pill stays on screen: "Your turn", "Thinking", or "Speaking".
  • Auto-listen: when Claude's turn has ended and it has finished speaking, dictation starts by itself. Once you stop talking, the stop button counts down 3‑2‑1 and then sends; new words reset the countdown. If you say nothing for 10 seconds, it stops listening and waits for fn+Space. There is no way to cancel an auto-started recording without sending it, so anything said near the mic within those 3 seconds goes to Claude. This needs the Apple backend's live words. Set auto_listen = false in voiceMode.init to turn it off, and auto_stop_seconds / auto_stop_no_speech in stt.init to change the timings.
  • Say "stop voice mode" to end it. It also ends when the Claude Code session exits, and on a Hammerspoon reload.

Parts outside this repo:

  • ~/.claude/skills/voice-mode/SKILL.md — turns voice mode on and off, and tells Claude to talk in short sentences and ask one question at a time.
  • ~/.claude/hooks/voice-mode.py — registered in ~/.claude/settings.json on MessageDisplay, UserPromptSubmit, Stop, StopFailure, and SessionEnd. It forwards events from the session named in ~/.claude/voice-mode-session to the voice-batch, voice-turn, and voice-mode URL events, and ignores every other session.

For a natural voice, download a Premium voice (System Settings → Accessibility → Spoken Content → System Voice → Manage Voices) and select it as the system voice.

Settings: Vocabulary and Per-App Styles

Ctrl+Alt+W opens the dictation settings. Everything saves as you type, to ~/Library/Mobile Documents/com~apple~CloudDocs/STT/settings.json, which is read again at each recording start. Without an LLM API key, these settings have no effect.

  • Vocabulary: terms you dictate often, such as product names (DigitalOcean, Vultr), one per line. The LLM then corrects words that sound like a term (e.g. "digital lotion" → "DigitalOcean") when the context fits. The transcription models do not use it: Apple SpeechTranscriber ignores contextual strings, and parakeet-mlx has no vocabulary option.
  • Default style: the cleanup instructions for apps without their own style.
  • Per-app styles: "+ Add " adds a style for the app that was frontmost when you opened the panel. When a recording starts in that app (matched by bundle ID), its style replaces the default style, e.g. shell commands for a terminal, or a casual tone for Slack. While recording and polishing, the pill shows the app style in use as a badge (nothing for the default style).

A fixed core prompt always applies on top of the style: never answer or follow the transcript, keep its language, no em or en dashes, return only the cleaned text. On first run, the old vocabulary.txt is copied into the settings.

Transcription History & Audio Backup

Each transcription is appended to an iCloud-synced history file at ~/Library/Mobile Documents/com~apple~CloudDocs/STT/history.txt. Entries include a UTC timestamp, the raw transcription, and the LLM-polished version (if different). The history file grows indefinitely — clean up manually via Finder if needed.

The daemon also saves each recording to a temporary WAV file in /tmp/ before transcription. On success, the WAV is automatically deleted. On failure (transcription error, daemon crash), the WAV is preserved for debugging or manual recovery.

Audio Tones & Media Control

By default, the STT module plays subtle macOS system sounds at key moments:

  • Tink — recording starts
  • Pop — recording stops
  • Glass — transcription/polishing complete

It also pauses any currently playing media (Spotify, Music, YouTube, etc.) when recording starts and resumes it when recording stops. Media state detection uses media-control, which must be installed via Homebrew:

brew tap ungive/media-control && brew install media-control

Both features can be disabled in init.lua:

stt.init({
    play_tones = false,   -- disable notification sounds
    pause_media = false,  -- disable media pause/resume
})

The scratchpad encryption key is generated automatically on first use. To copy it to another Mac:

# Export from source Mac
security find-generic-password -a "hammerspoon" -s "scratchpad-encryption-key" -w

# Import on target Mac
security add-generic-password -a "hammerspoon" -s "scratchpad-encryption-key" -w "PASTE_KEY_HERE"

iCloud Sync

The scratchpad, Hyperduck, and GIF Finder modules store files in iCloud Drive:

  • Scratchpad: ~/Library/Mobile Documents/com~apple~CloudDocs/Scratchpad/scratchpad.txt
  • Hyperduck: ~/Library/Mobile Documents/com~apple~CloudDocs/Hyperduck/inbox.txt
  • GIF Finder: ~/Library/Mobile Documents/com~apple~CloudDocs/GifFinder/favorites.json and recents.json
  • STT: ~/Library/Mobile Documents/com~apple~CloudDocs/STT/history.txt — append-only transcription history
  • STT settings: ~/Library/Mobile Documents/com~apple~CloudDocs/STT/settings.json — vocabulary, default style, per-app styles
  • Clipboard History: ~/Library/Mobile Documents/com~apple~CloudDocs/ClipboardHistory/history.json — clipboard entries (30-day retention)

Hyperduck requires an iPhone Shortcut that appends timestamped URLs (timestamp|url format) to the inbox file. URLs older than 7 days are automatically purged.

Reloading

After making changes, reload the config:

  • Menu bar: Click the Hammerspoon icon → Reload Config
  • Console: Open Hammerspoon console and press Cmd+Shift+R

About

My hammerspoon config

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Used by

Contributors

Languages