# Hammerspoon Cheatsheet

`Hyper` = `⌘ ⌃ ⇧ ⌥` (Cmd + Ctrl + Shift + Alt), bound via Karabiner.

Source of truth: `init.fnl`. When you add or change a binding there, update the
matching row in this file **and** the matching group in `whichkey.fnl`.

A floating dark-mode overlay (Catppuccin Mocha) shows the current group of
bindings after a short delay. See [WhichKey panels](#whichkey-panels).

## System

| Shortcut    | Action                                                 |
| :---------- | :----------------------------------------------------- |
| `Hyper + r` | Reload Hammerspoon config manually (saves auto-reload after ~0.5s) |
| `Hyper + ;` | Debug focused window (id, title, app, bundleid, frame) |
| `Ctrl × 2`  | Toggle Claude Desktop (Quake-style double-tap Control) |

## App focus

| Shortcut        | Action                              |
| :-------------- | :---------------------------------- |
| `Hyper + h`     | Scratchpad                          |
| `Hyper + 1`     | Toggle BrowserOS ↔ Cursor           |
| `Hyper + 2`     | Focus Reflect (quick capture)       |
| `Hyper + 3`     | Obsidian                            |
| `Hyper + 5`     | Cycle Terminals (WezTerm → Ghostty → kitty) |
| `Hyper + d`     | Media modal: p/n/b/l/r/s -> running player (Apple Music wins if both open); esc exits |
| `Hyper + 6`     | Apple Music: toggle repeat (bypasses modal player resolution) |
| `Hyper + 0`     | Focus previous window               |
| `Hyper + space` | All-windows fuzzy picker            |
| `Hyper + x`     | Current-app windows fuzzy picker    |

## Browsers

| Shortcut    | Action                                        |
| :---------- | :-------------------------------------------- |
| `Hyper + f`     | Focus Comet                                   |
| `Hyper + e`     | Toggle BrowserOS ↔ Chrome                     |
| `Hyper + [`     | Open Google AI Mode (google.com/aimode) in default browser |
| `Hyper + /`     | Open DevDocs (devdocs.io) in Comet            |

## App cycles & toggles

| Shortcut    | Action                                       |
| :---------- | :------------------------------------------- |
| `Hyper + g` | Cycle Cursor → BrowserOS → Sublime           |
| `` Hyper + ` `` | Cycle Claude → Sublime → Cursor → BrowserOS |
| `Hyper + 8` | Cycle Browsers group (BrowserOS/Chrome)      |
| `Hyper + 9` | Cycle Notes group (Obsidian)                 |
| `Hyper + =` | Cycle Editors group (Cursor/Sublime/Script Debugger, open apps only) |
| `Hyper + a` | Cycle Social group (Telegram, Cardhop, Calendar, Signal) |

## Layout modal — `Hyper + Return`

Press `Hyper + Return`, then a group key, then a layout digit. `Esc` cancels at any step.
The whichkey panels `layout-groups` and `layout-choice` mirror these on-screen.

**Group keys**

| Key | Group                  | Apps                                |
| :-- | :--------------------- | :---------------------------------- |
| `e` | Editors                | Cursor / Sublime / Script Debugger  |
| `b` | Browsers               | BrowserOS / Chrome                  |
| `n` | Notes                  | Obsidian                            |
| `h` | HTTP Toolkit + Sublime | (uses Keyboard Maestro for halves)  |
| `c` | Crypto                 | Trezor Suite / Exodus               |
| `s` | Social                 | BusyContacts / Calendar             |

**Layout keys**

| Key | Layout              |
| :-- | :------------------ |
| `1` | 1/3 left ∥ 2/3 right |
| `2` | 2/3 left ∥ 1/3 right |
| `3` | 1/2 ∥ 1/2 (halves)  |

## Window management — `Ctrl + Alt`

Raycast Window Management commands, opened in the background so focus stays put.
Mirrored by the whichkey `window` group.

| Shortcut             | Action              |
| :------------------- | :------------------ |
| `Ctrl + Alt + ←`     | Left half           |
| `Ctrl + Alt + →`     | Right half          |
| `Ctrl + Alt + ↑`     | Top half            |
| `Ctrl + Alt + ↓`     | Bottom half         |
| `Ctrl + Alt + ⏎`     | Maximize            |
| `Ctrl + Alt + Space` | Maximize            |
| `Ctrl + Alt + c`     | Center              |
| `Ctrl + Alt + u`     | Top-left quarter    |
| `Ctrl + Alt + i`     | Top-right quarter   |
| `Ctrl + Alt + j`     | Bottom-left quarter |
| `Ctrl + Alt + k`     | Bottom-right quarter|
| `Ctrl + Alt + f`     | Almost-maximize     |
| `Ctrl + Alt + d`     | Restore             |

## Raycast / search / AI

| Shortcut    | Action                                      |
| :---------- | :------------------------------------------ |
| `Hyper + q` | Raycast: create note                        |
| `Hyper + n` | Raycast: search notes                       |
| `Hyper + o` | Raycast: explore snippets                   |
| `Hyper + i` | Raycast: Cursor recent projects             |
| `Hyper + z` | Raycast: Zed recent projects                |
| `Hyper + j` | Raycast: browser tabs                       |
| `Hyper + Tab` | Raycast: Kitty tabs                       |
| `Hyper + y` | Raycast: Comet search history               |
| `Hyper + w` | Raycast: Comet search tabs                  |
| `Hyper + 4` | Raycast: browser history search             |
| `Hyper + k` | Raycast: app shortcuts                      |
| `Hyper + u` | Raycast: Bunch search                       |
| `Hyper + m` | Raycast: Keyboard Maestro macros            |
| `Hyper + s` | Raycast: Dictate                            |
| `Hyper + \` | Raycast: Dictate to Note                   |
| `Hyper + 7` | Raycast: send selected text to AI chat      |
| `Hyper + l` | Raycast: Search Snippets                    |
| `Hyper + '` | Raycast: Create Snippet                     |

## Clipboard, typing, lookups

| Shortcut    | Action                                                                  |
| :---------- | :---------------------------------------------------------------------- |
| `Hyper + v` | Raycast clipboard history                                               |
| `Hyper + t` | Human-type clipboard (default cadence) — bypasses paste-disabled fields |
| `Hyper + p` | Human-type clipboard (fast cadence, low typo rate)                      |
| `Hyper + ,` | Paste random docker-style name (fetched from frightanic.com)            |
| `Hyper + .` | Magic-IP: open canonical `ip.undetect.io` base URL                      |
| `Hyper + c` | Magic-IP: clipboard IPv4 if present, else fetch external IP → same URL  |

### Magic-IP (`lib/ip.fnl`)

Implementation lives in `~/dotfiles/.config/hammerspoon/lib/ip.fnl` (not ad-hoc
in `init.fnl`). Keep in sync with Fish `checksocks` / `checkhttps` / `ipqs` and
the zsh twins when changing probe order or endpoints.

| Piece | Behaviour |
| --- | --- |
| Magic URL | `https://ip.undetect.io/?key=…` (+ `&ip=` when an address is known) |
| External IP | free `ifconfig.me` through the proxy when probing; `api.ipapi.is` fallback |
| IPQS | free `secretapi.stalker.to/magicip/…`, paid `api.ipapi.is/?key=…&q=` |
| Proxy monitor | 1s pasteboard watcher; SOCKS5 then HTTP dual-probe via `api.ipapi.is` |
| Overlay font | `GeistMonoNFM` PostScript name (`hs.styledtext`; plain “Geist Mono” is not installed) |

## WhichKey panels

A delayed (0.45 s) floating Catppuccin-Mocha overlay summarising the current
group. Lives in `whichkey.fnl` and is driven by URL events:

```
hammerspoon://whichkey?action=show&group=<group-name>
hammerspoon://whichkey?action=hide&group=<group-name>
hammerspoon://whichkey?action=hide
```

Quick presses are debounced: if you release the trigger before the delay
elapses, no panel is shown.

All groups defined in `whichkey.fnl`, with the trigger that shows each panel.
`hyper-cycle` and `layout-groups` are derived at runtime from the workspace
table in `init.fnl`; the rest are the hand-curated static companion table.

**Hammerspoon-triggered**

| Group           | Trigger              | Panel title          |
| :-------------- | :------------------- | :------------------- |
| `hyper`         | Hold Hyper           | Hyper                |
| `apps`          | Hyper (app keys)     | Hyper · Apps         |
| `hyper-cycle`   | Hyper (cycle keys)   | Hyper · Cycle        |
| `raycast`       | Hyper (Raycast keys) | Hyper · Raycast      |
| `utility`       | Hyper (utility keys) | Hyper · Utility      |
| `media`         | `Hyper + d` modal    | Hyper D · Media      |
| `window`        | `Ctrl + Alt`         | Ctrl+Alt · Window    |
| `layout-groups` | `Hyper + Return`     | Layout · Pick Group  |
| `layout-choice` | `Hyper + Return, <group>` | Layout · Pick Split |

**Karabiner-triggered** (duoLayers and the A-Mode sim layer — bindings live in
the karabiner-config-macos cheatsheet, not here)

| Group              | Trigger        | Panel title         |
| :----------------- | :------------- | :------------------ |
| `karabiner-window` | `[` + `]`      | [+] · Window Mode   |
| `search`           | `]` + `'`      | ] ' · Search        |
| `snippet`          | `q` + `w`      | Q W · Snippet       |
| `editing`          | `m` + `,`      | M , · Editing       |
| `launch`           | `[` + `p`      | [ P · Launch        |
| `toggle`           | `]` + `p`      | ] P · Toggle        |
| `quick`            | `.` + `/`      | . / · Quick Actions |
| `macro`            | `f` + `d`      | F D · Macro         |
| `emoji`            | `z` + `x`      | Z X · Emoji         |
| `a-mode`           | `a` (sim layer) | A · Movement       |
| `comma-period`     | `,` + `.`      | ,. · Experimental   |

## Notify from a shell script

Scripts reach Hammerspoon's notification layer through
`~/dotfiles/scripts/hs-notify`, which tries four transports in order and never
fails the caller:

```bash
hs-notify "Build" "green" 5
hs-notify --title Deploy --text "prod live" --subtitle web --alert
```

| # | Transport | Notes |
| --- | --- | --- |
| 1 | `hs -c "notifyB64('…')"` | mach IPC, ~20–50 ms. Needs HS running and the CLI installed. |
| 2 | `open -g hammerspoon://notify?title=&text=` | LaunchServices. No return value, but `-g` keeps focus. |
| 3 | `osascript display notification` | Works with Hammerspoon closed. |
| 4 | stderr line | Last resort, so a script never dies for want of a toast. |

`--alert` additionally draws an `hs.alert` overlay, which bypasses Notification
Center and Focus modes.

### Why the payload is base64

`hs -c` `load()`s its argument as Lua **source** inside Hammerspoon, so
interpolating untrusted text into it is remote code execution. `notifyB64`
therefore takes base64-encoded JSON — `{title, subtitle, text, timeout, sound,
alert}` — and decodes it inside HS, where it is inert data.

### The `hs` CLI

`init.fnl` installs it on load, preferring the Apple-Silicon prefix so
`which hs` resolves, with a one-second fallback to the default path:

```fennel
(local ipc (require :hs.ipc))
(when (not (ipc.cliStatus "/opt/homebrew"))
  (pcall ipc.cliInstall "/opt/homebrew"))
```

Added in `2307ddbb` / `4d366eab`; the URL handler had a parse error in its first
form, fixed by the latter.

## Notes

- `` Hyper + ` `` is used for the app cycle (Claude/Sublime/Cursor/BrowserOS). The
  cheatsheet-overlay binding on the same chord is intentionally commented out
  in `init.fnl` — the whichkey panels cover that role now.
- Bindings are defined in `/Users/nico/.config/hammerspoon/init.fnl`.
- Style rules for whichkey rows are enforced in `whichkey.fnl`:
  one accent per row, verb-then-noun, no parentheticals, `A/B` for aliased
  keys, comma-separated cycle members.
