{"id":50931402,"url":"https://github.com/ianmurrays/hammerspoon","last_synced_at":"2026-06-17T04:32:18.766Z","repository":{"id":340845885,"uuid":"1128329972","full_name":"ianmurrays/hammerspoon","owner":"ianmurrays","description":"My hammerspoon config","archived":false,"fork":false,"pushed_at":"2026-06-11T15:55:58.000Z","size":230,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-11T16:24:47.923Z","etag":null,"topics":["dotfiles","hammerspoon"],"latest_commit_sha":null,"homepage":"","language":"Lua","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ianmurrays.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-05T13:30:47.000Z","updated_at":"2026-06-11T15:56:02.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ianmurrays/hammerspoon","commit_stats":null,"previous_names":["ianmurrays/hammerspoon"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ianmurrays/hammerspoon","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ianmurrays%2Fhammerspoon","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ianmurrays%2Fhammerspoon/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ianmurrays%2Fhammerspoon/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ianmurrays%2Fhammerspoon/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ianmurrays","download_url":"https://codeload.github.com/ianmurrays/hammerspoon/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ianmurrays%2Fhammerspoon/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34434492,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-17T02:00:05.408Z","response_time":127,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dotfiles","hammerspoon"],"created_at":"2026-06-17T04:32:17.668Z","updated_at":"2026-06-17T04:32:18.754Z","avatar_url":"https://github.com/ianmurrays.png","language":"Lua","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Hammerspoon Config\n\nPersonal Hammerspoon configuration for macOS automation — window management, Slack status, encrypted scratchpad, GIF search, and more.\n\n## Modules\n\n| Module | Purpose | Hotkey |\n|---|---|---|\n| `window_manager` | Rectangle-style window tiling with fraction cycling (1/2, 1/3, 2/3) | Ctrl+Alt+Cmd + arrows/F/Home/End |\n| `scratchpad` | Encrypted markdown editor synced via iCloud | Ctrl+Alt+S |\n| `gif_finder` | GIF search via Klipy API with favorites and recents, copies URL to clipboard | Ctrl+Alt+G |\n| `slack_status` | Auto-updates Slack status based on WiFi network; manual overrides and custom status via menu | — |\n| `hyperduck` | Monitors iCloud file for URLs sent from iPhone, opens them on Mac | — |\n| `battery_indicator` | Shows remaining battery time in menu bar | — |\n| `screen_blur` | Full-screen blur overlay for privacy (downsample trick via `sips`) | Ctrl+Alt+B |\n| `stt` | Local speech-to-text via parakeet-mlx daemon with optional LLM post-processing, audio tones, media pause/resume, and transcription history viewer | fn+Space (toggle) / fn+Shift (hold) / Ctrl+Alt+H (history) |\n| `clipboard_history` | Clipboard history with search, auto-skips password manager entries, 30-day retention | Ctrl+Alt+V |\n| `mouse_grid` | Keyboard-driven mouse (Mouseless-style): full-screen hint grid for click, right/double click, drag \u0026 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) |\n| `unified_menu` | Combines Slack Status, Hyperduck, Scratchpad, Screen Blur, and Clipboard History into a single menubar item | — |\n\n## Hotkeys\n\n| Shortcut | Action |\n|---|---|\n| Ctrl+Alt+Cmd+Left | Tile window left (cycles 1/2 → 1/3 → 2/3) |\n| Ctrl+Alt+Cmd+Right | Tile window right (cycles 1/2 → 1/3 → 2/3) |\n| Ctrl+Alt+Cmd+Up | Tile window top (cycles 1/2 → 1/3 → 2/3) |\n| Ctrl+Alt+Cmd+Down | Tile window bottom (cycles 1/2 → 1/3 → 2/3) |\n| Ctrl+Alt+Cmd+F | Maximize window |\n| Ctrl+Alt+Cmd+Home | Move window to previous display |\n| Ctrl+Alt+Cmd+End | Move window to next display |\n| Ctrl+Alt+S | Toggle scratchpad |\n| Ctrl+Alt+G | Toggle GIF finder |\n| Ctrl+Alt+B | Toggle screen blur overlay (also dismisses on click or any keypress) |\n| fn+Space | Toggle speech-to-text recording (press to start, press again to stop and paste) |\n| fn+Shift | Hold-to-talk speech-to-text (hold both to record, release to stop and paste) |\n| Ctrl+Alt+H | Toggle STT transcription history viewer |\n| Ctrl+Alt+V | Toggle clipboard history viewer |\n| Tap left Cmd | Toggle mouse grid overlay (quick press+release of left Cmd alone) |\n| Double-tap left Cmd | Element hints mode (Shortcat-style: labels on clickable UI elements) |\n| Tap left Alt | Toggle free mouse mode (relative cursor movement, no overlay) |\n\n\u003e **Note:** Home = Fn+Left and End = Fn+Right on Mac keyboards.\n\n### Mouse Grid (while overlay is up)\n\nType 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:\n\n| Key | Action |\n|---|---|\n| subgrid key / Space | Left click |\n| Shift + final key | Right click |\n| Ctrl + final key | Double click |\n| Alt + final key | Move cursor only (no click) |\n| Cmd + final key | Arm drag (mouse down; overlay stays up — next selection drops) |\n| 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) |\n| Backspace | Undo one selection level (or cancel a nudge) |\n| Tab | Move overlay to next screen |\n| `,` | Scroll mode: h/j/k/l or arrows scroll, Shift = faster, Esc exits |\n| Esc | Dismiss overlay (cancels an armed drag) |\n\n### Hints Mode (double-tap left Cmd)\n\nShortcat-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.\n\n| Key | Action |\n|---|---|\n| label chars | Filter hints / act when a label is completed |\n| Space | Search mode: type the element's text (title/label/value) to find it |\n| Shift + final char | Right click |\n| Ctrl + final char | Double click |\n| Alt + final char | Move cursor only (no click) |\n| Cmd + final char | Arm drag (mouse down; hints reappear — next label drops) |\n| Backspace | Un-type one label character |\n| Esc | Exit hints mode (cancels an armed drag) |\n\nWhile searching, matching elements are outlined instead of labelled and typing goes to the query:\n\n| Key | Action |\n|---|---|\n| any text | Filter elements by their accessibility text (case-insensitive substring) |\n| Tab / Shift+Tab | Select next / previous match (red outline) |\n| Enter | Act on the selected match — same modifiers as above (Shift right, Ctrl double, Alt move, Cmd drag) |\n| Backspace | Delete a query character (on an empty query: back to labels) |\n| Esc | Back to label mode (Esc again exits hints) |\n\nNotes:\n- 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.\n- 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`).\n- Single-tap left Cmd while hints are up switches to the grid; tap left Alt to switch to free mode.\n\n### Free Mode (tap left Alt)\n\nMoves 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.\n\n| Key | Action |\n|---|---|\n| h / j / k / l | Move cursor left/down/up/right (hold; diagonals work) |\n| Shift (held) | Move faster (4×) |\n| Ctrl (held) | Move slower (0.25×, precision) |\n| Space | Left click (mode stays active) |\n| Shift+Space | Right click |\n| Ctrl+Space | Double click |\n| Cmd+Space | Drag toggle — press to grab, move, press Space again to drop |\n| 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 |\n| Esc | Exit free mode (releases a held drag) |\n\n## File Structure\n\nWebview modules (`gif_finder`, `slack_status`, `scratchpad`, `stt`, `clipboard_history`) store their HTML, CSS, and JS in separate files under `html/`:\n\n```\nhtml/\n  gif_finder/    — GIF search UI\n  slack_status/  — Custom status form\n  scratchpad/    — CodeMirror markdown editor\n  stt_history/        — Transcription history viewer\n  clipboard_history/  — Clipboard history viewer\n```\n\nEach 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()`.\n\n## Setup\n\n### Prerequisites\n\n1. Install [Hammerspoon](https://www.hammerspoon.org/)\n2. Grant Accessibility permissions when prompted (System Settings → Privacy \u0026 Security → Accessibility)\n\n### Keychain Secrets\n\nStore secrets in the macOS Keychain — they are never saved in code.\n\n```bash\n# Slack API token (xoxp-...)\nsecurity add-generic-password -a \"$USER\" -s \"slack-status-token\" -w \"YOUR_TOKEN\"\n\n# Klipy GIF search API key (https://partner.klipy.com)\nsecurity add-generic-password -a \"$USER\" -s \"klipy-api-key\" -w \"YOUR_API_KEY\"\n\n# Mistral API key for STT post-processing (optional — omit to disable)\nsecurity add-generic-password -a \"$USER\" -s \"mistral-api-key\" -w \"YOUR_API_KEY\"\n```\n\n### STT Daemon\n\nThe speech-to-text module requires a local Python daemon running `parakeet-mlx`:\n\n```bash\ncd ~/.hammerspoon/stt-daemon\nuv sync\nuv run stt_daemon.py\n```\n\nTo run as a background service via launchd:\n\n```bash\ncp ~/.hammerspoon/stt-daemon/com.local.stt-daemon.plist ~/Library/LaunchAgents/\nlaunchctl load ~/Library/LaunchAgents/com.local.stt-daemon.plist\n```\n\nLogs are written to `~/Library/Logs/stt-daemon.log`.\n\n#### LLM Post-Processing (Optional)\n\nWhen 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.\n\nConfiguration options in `init.lua`:\n\n```lua\nstt.init({\n    llm_api_key = mistralApiKey,\n    -- llm_model = \"mistral-small-latest\",           -- model to use\n    -- llm_system_prompt = \"...\",                     -- custom prompt\n    -- llm_api_url = \"https://api.mistral.ai/v1/chat/completions\",  -- API endpoint\n    -- llm_timeout = 10,                             -- seconds before fallback\n})\n```\n\nThe 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`.\n\n#### Transcription History \u0026 Audio Backup\n\nEach 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.\n\nThe 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.\n\n#### Audio Tones \u0026 Media Control\n\nBy default, the STT module plays subtle macOS system sounds at key moments:\n- **Tink** — recording starts\n- **Pop** — recording stops\n- **Glass** — transcription/polishing complete\n\nIt 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`](https://github.com/ungive/media-control), which must be installed via Homebrew:\n\n```bash\nbrew tap ungive/media-control \u0026\u0026 brew install media-control\n```\n\nBoth features can be disabled in `init.lua`:\n\n```lua\nstt.init({\n    play_tones = false,   -- disable notification sounds\n    pause_media = false,  -- disable media pause/resume\n})\n```\n\nThe scratchpad encryption key is generated automatically on first use. To copy it to another Mac:\n\n```bash\n# Export from source Mac\nsecurity find-generic-password -a \"hammerspoon\" -s \"scratchpad-encryption-key\" -w\n\n# Import on target Mac\nsecurity add-generic-password -a \"hammerspoon\" -s \"scratchpad-encryption-key\" -w \"PASTE_KEY_HERE\"\n```\n\n### iCloud Sync\n\nThe scratchpad, Hyperduck, and GIF Finder modules store files in iCloud Drive:\n\n- **Scratchpad:** `~/Library/Mobile Documents/com~apple~CloudDocs/Scratchpad/scratchpad.txt`\n- **Hyperduck:** `~/Library/Mobile Documents/com~apple~CloudDocs/Hyperduck/inbox.txt`\n- **GIF Finder:** `~/Library/Mobile Documents/com~apple~CloudDocs/GifFinder/favorites.json` and `recents.json`\n- **STT:** `~/Library/Mobile Documents/com~apple~CloudDocs/STT/history.txt` — append-only transcription history\n- **Clipboard History:** `~/Library/Mobile Documents/com~apple~CloudDocs/ClipboardHistory/history.json` — clipboard entries (30-day retention)\n\nHyperduck requires an iPhone Shortcut that appends timestamped URLs (`timestamp|url` format) to the inbox file. URLs older than 7 days are automatically purged.\n\n## Reloading\n\nAfter making changes, reload the config:\n\n- **Menu bar:** Click the Hammerspoon icon → Reload Config\n- **Console:** Open Hammerspoon console and press Cmd+Shift+R\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fianmurrays%2Fhammerspoon","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fianmurrays%2Fhammerspoon","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fianmurrays%2Fhammerspoon/lists"}