dot-hammerspoon is my personal configuration for Hammerspoon, and you can modify to suit your needs and preferences.
- Application quick launch or hide.
- Application window manipulation, such as moving, resizing, changing position, etc.
- System management, such as lock screen, restart system, etc.
- Keep the Mac awake for uninterrupted work with a menubar status icon.
- Auto switch input method according to the application.
- Switch to the specified input method.
- Open the specified website directly.
- Clipboard history with a menubar and chooser UI.
- Translate selected text with an OpenAI-compatible model and show the result in a popup.
- Toggle the keybindings cheatsheet.
- Keep the desktop wallpaper the same as the bing daily picture.
- Force a configurable break reminder, with support for soft or hard mode.
- Gamify break reminders with daily focus stats, streaks, skip penalties, and skinnable menubar icons.
- Visualize pressed keys during recording or demos with an on-screen overlay.
- Auto reload configuration when lua files changes.
- The code structure is clear and easy to customize into your own configuration.
-
Install Hammerspoon first:
brew install hammerspoon --cask -
Run
Hammerspoon.appand follow the prompts to enable Accessibility access for the app.
If Accessibility is not granted, startup will show a warning alert and modules that depend on input or window control may not work correctly, such as Window Manipulation, Break Reminder hard mode, and Key Caster.
git clone --depth 1 https://github.com/windvalley/dot-hammerspoon.git ~/.hammerspoon
Keep update:
cd ~/.hammerspoon && git pull⌥ + /
- ⌥ + 1: ABC
- ⌥ + 2: Pinyin
- ⌃⌥ + P: Toggle Prevent Sleep
- ⌥ + Q: Lock Screen
- ⌥ + S: Start Screensaver
- ⌃⌥⌘⇧ + R: Restart Computer
- ⌃⌥⌘⇧ + S: Shutdown Computer
Use the menubar icon or ⌃⌥ + P to prevent idle sleep during long-running tasks.
enabled: default on/off state for the prevent-sleep mode.show_menubar: show the menubar icon for quick status checks and toggling.keep_display_awake: whentrue, prevent both system sleep and display sleep. Whenfalse, only prevent system idle sleep so the display can still turn off.- The menubar menu can switch
keep_display_awakeat runtime. - The menubar menu can also update the toggle hotkey at runtime.
- The menubar menu can temporarily hide the menubar icon for the current Hammerspoon session. After
hs.reload(), it returns to the config default. - You can also restore or toggle it from the Hammerspoon console with
package.loaded.keep_awake.show_menubar()orpackage.loaded.keep_awake.toggle_menubar_visibility(). - The current on/off state resets to the config default after Hammerspoon reloads.
keep_display_awakemode and the toggle hotkey are still restored after Hammerspoon reloads viahs.settings.
- ⌥ + 8: github.com
- ⌥ + 9: google.com
- ⌥ + 7: bing.com
The wallpaper module periodically fetches Bing Daily Picture metadata and applies the selected image to all screens.
You can customize it in ~/.hammerspoon/keybindings_config.lua:
_M.bing_daily_wallpaper = {
enabled = true,
refresh_interval_seconds = 60 * 60,
picture_width = 3072,
picture_height = 1920,
history_count = 1,
metadata_base_url = "https://cn.bing.com",
image_base_url = "https://www.bing.com",
cache_dir = "",
}When cache_dir is empty, Bing Daily Wallpaper will automatically use:
~/Library/Caches/<current Hammerspoon bundle id>/bing_daily_wallpaper
When history_count is greater than 1, the module will randomly choose one image from the most recent Bing wallpaper entries returned by the metadata API.
Use the menubar item or ⌥⇧ + C to open a searchable chooser for:
- Clipboard history
- Clipboard images
You can customize it in ~/.hammerspoon/keybindings_config.lua:
_M.clipboard = {
enabled = true,
show_menubar = true,
history_size = 80,
menu_history_size = 12,
max_item_length = 30000,
capture_images = true,
image_cache_dir = "",
preview_enabled = true,
preview_width = 420,
preview_height = 320,
image_menu_thumbnail_size = 80,
chooser_rows = 12,
chooser_width = 40,
auto_paste = false,
prefix = { "Option", "Shift" },
key = "C",
message = "Clipboard Center",
}When image_cache_dir is empty, Clipboard Center will automatically use:
~/Library/Caches/<current Hammerspoon bundle id>/clipboard_center_images
When a chooser item is highlighted, Clipboard Center will show a larger preview panel next to the chooser. You can tune it with preview_enabled, preview_width, and preview_height. Older image_preview_* keys are still supported for compatibility.
Image thumbnails inside hs.chooser use a fixed small icon size because the native chooser row height is effectively fixed. If you need a larger view, use the preview panel on the right.
History items can be removed individually: right click a history row in the chooser, or hold ⌘ while clicking an item shown directly in the menubar menu.
When the menubar menu is open, the first nine recent history items can also be restored directly with number keys 1 to 9.
menu_history_size controls how many recent history items are shown directly in the menubar menu. You can also change it at runtime from the menubar via 最近历史显示数量, and the override will be persisted via hs.settings.
The menubar menu can also update the Clipboard Center hotkey at runtime. The override is persisted via hs.settings, and you can restore the file-configured hotkey from the same menu.
The menubar menu also provides an 自动粘贴 toggle. It is off by default; when enabled, selecting a clipboard history item will restore it and then immediately send ⌘ + V to the previously focused app. The runtime override is also persisted via hs.settings.
chooser_rows controls how many rows are shown in the chooser, and chooser_width controls the chooser width as a percentage of the current screen width.
Use ⌥⇧ + S to open a searchable chooser for stored text snippets. The first two chooser rows are built-in actions:
- Create a blank snippet in the built-in multiline editor
- Save the current clipboard text as a new snippet
Selecting a snippet writes it to the pasteboard, reactivates the previously focused app, and sends ⌘ + V. By default, the original clipboard contents are restored after the paste completes.
Use ⌥⇧⌘ + S to quick-save the current clipboard text without opening the chooser. Duplicate content is rejected.
Snippets have an optional title. When the title is left empty, the chooser automatically uses the first non-empty content line as the display title.
Right click a snippet row in the chooser to edit, rename, pin, copy, or delete it.
When the chooser is open, Snippet Center also shows a preview panel for the currently selected row. For stored snippets, the preview includes the full body so you can inspect multi-line content before inserting it. You can tune it with preview_enabled, preview_width, preview_height, preview_poll_interval, and preview_body_max_chars.
Snippets are stored in a JSON file instead of hs.settings. You can customize the file location with storage_path, which makes migration across Macs straightforward. Writes use a temp file followed by an atomic rename so normal saves do not leave half-written data behind.
If you enable show_menubar, Snippet Center also adds a lightweight menubar entry for:
- Opening the chooser
- Creating an empty snippet
- Saving the current clipboard text
- Toggling
auto_paste - Managing a small set of common snippets from submenus
The menubar only shows a limited number of snippets, controlled by menu_items. Those entries follow the same ranking as the chooser: pinned items first, then recent and frequently used items. Runtime changes to auto_paste made from the menubar are persisted via hs.settings.
The menubar also shows the current hotkeys for opening the chooser and quick-saving the clipboard, and lets you change or restore both shortcuts at runtime. Those hotkey overrides are also persisted via hs.settings.
You can customize it in ~/.hammerspoon/keybindings_config.lua:
_M.snippets = {
enabled = true,
storage_path = "~/.hammerspoon/data/snippets.json",
max_items = 200,
max_content_length = 20000,
chooser_rows = 12,
chooser_width = 40,
auto_paste = true,
restore_clipboard_after_paste = true,
preview_enabled = true,
preview_width = 420,
preview_height = 320,
preview_poll_interval = 0.08,
preview_body_max_chars = 6000,
show_menubar = true,
menu_items = 8,
auto_title_length = 36,
editor = {
width = 620,
height = 480,
},
prefix = { "Option", "Shift" },
key = "S",
message = "Snippet Center",
quick_save = {
prefix = { "Option", "Shift", "Command" },
key = "S",
message = "Quick Save Snippet",
},
}You can also toggle the menubar entry from the Hammerspoon console with package.loaded.snippet_center.show_menubar(), package.loaded.snippet_center.hide_menubar(), and package.loaded.snippet_center.toggle_menubar_visibility().
Select any text and press ⌥ + R to translate it through an OpenAI-compatible chat/completions API. By default, non-Chinese text is translated into Simplified Chinese, while text containing Chinese characters is translated into English. The translated text is shown in a popup, and the popup also lets you copy the result back to the clipboard.
If the source itself is a single English word, or if the final translation result is a single English word, the popup switches to a word-card layout and always shows the fields in this order: the English word, American IPA, British IPA, and the Chinese meaning. Phrases and full sentences continue to use the normal translation layout without extra phonetic fields.
The same module can also translate text from screenshots. Press a separate hotkey such as ⌥ + ⇧ + R, draw a screenshot region, and the captured image will be sent to the configured multimodal model. If the current model rejects image input, the module shows a direct unsupported-image warning.
The module first tries to read the current accessibility selection directly. If that fails, it can either read the current clipboard directly for apps that auto-copy selections, or simulate a copy shortcut and then restore the previous clipboard contents. The default fallback shortcut is ⌘ + C. When Clipboard Center is enabled, the temporary copy/restore sequence is also suppressed from clipboard history.
It also adds a menubar entry, so you can adjust the main settings at runtime and persist them through hs.settings, including the selection hotkey, screenshot hotkey, translation direction, target languages, popup theme, popup timing, and provider-grouped model service settings (api_url, model, and a locally saved API key). Long translation results are automatically split into pages inside the popup, with previous/next controls. A 恢复默认 action clears these menu overrides and goes back to keybindings_config.lua.
You can customize it in ~/.hammerspoon/keybindings_config.lua:
_M.selected_text_translate = {
enabled = true,
show_menubar = true,
prefix = { "Option" },
key = "R",
message = "Translate Selection",
screenshot_hotkey = {
prefix = { "Option", "Shift" },
key = "R",
message = "Translate Screenshot",
},
translation_direction = "auto",
target_language = "简体中文",
chinese_target_language = "英文",
popup_duration_seconds = 10,
popup_theme = "paper",
popup_background_alpha = 0.88,
clipboard_poll_interval_seconds = 0.05,
clipboard_max_wait_seconds = 0.4,
selection_auto_copy_by_bundle_id = {
["com.mitchellh.ghostty"] = true,
},
model_service = {
provider = "ollama",
request_timeout_seconds = 20,
ollama = {
api_url = "http://localhost:11434/api/chat",
model = "qwen3.5:35b",
supports_image_input = true,
enable_warmup = true,
keep_alive = "30m",
disable_thinking = true,
},
openai_compatible = {
api_url = "https://api.openai.com/v1/chat/completions",
model = "gpt-4o-mini",
supports_image_input = true,
api_key_env = "OPENAI_API_KEY",
api_key = "",
},
gemini = {
api_url = "https://generativelanguage.googleapis.com/v1beta/models",
model = "gemini-2.0-flash",
supports_image_input = true,
api_key_env = "GEMINI_API_KEY",
api_key = "",
},
anthropic = {
api_url = "https://api.anthropic.com/v1/messages",
model = "claude-3-5-haiku-latest",
supports_image_input = true,
api_key_env = "ANTHROPIC_API_KEY",
api_key = "",
},
},
}The translation popup supports a lightweight floating-card style. By default it auto-hides after popup_duration_seconds; when the mouse hovers over the popup, auto-hide pauses and resumes after the mouse leaves.
selection_auto_copy_by_bundle_id tells the module to trust the current clipboard directly for specific apps. This is useful for terminals such as Ghostty when selected text is already copied automatically, including common zellij setups.
If an app does not auto-copy, but uses a nonstandard copy shortcut, you can still configure copy_shortcuts_by_bundle_id to override the fallback keystroke per bundle ID.
popup_theme now uses preset themes, and popup_background_alpha controls transparency separately. Built-in themes: paper, mist, graphite, slate, ocean, forest, amber, rose, cocoa, mint.
The older popup_background = "#RRGGBB" / "#RRGGBBAA" and popup_background_color fields still work for compatibility, but the preset theme approach is recommended because it keeps background, border, text, and copy-button colors coordinated.
translation_direction = "auto" enables bidirectional translation: when the selection contains Chinese characters, it targets chinese_target_language; otherwise it targets target_language. If you want the old fixed behavior, set translation_direction = "to_target".
In the menubar presets, 中文目标语言 intentionally excludes 简体中文 to avoid a no-op same-language translation. If you still need a special case, you can enter it manually via the custom option.
model_service.provider supports ollama, openai_compatible, gemini, and anthropic. The currently selected provider decides which sub-config block supplies the active api_url, model, and API credential fields. In the menubar, these settings are grouped under each provider so you can inspect or edit another provider’s configuration without switching away from the current one first.
supports_image_input controls whether the screenshot flow should attempt to send image input through that provider. It defaults to true for the built-in providers because their APIs can carry images, but the actual model still needs vision capability. If the current model is text-only and rejects screenshots, the module will show a specific unsupported-image warning.
For local Ollama models, model_service.ollama.enable_warmup = true will silently send one lightweight warmup request a few seconds after startup, which helps reduce the first translation latency. model_service.ollama.keep_alive = "30m" attaches Ollama’s keep_alive option to both the warmup request and normal translation requests so the model stays loaded longer after use. model_service.ollama.disable_thinking = true also sends think = false by default for faster responses.
For Gemini, the default api_url can stay at the base /models path. The module will automatically expand it to /{model}:generateContent for the active model. Anthropic uses /v1/messages directly.
For providers that require an API key, you can leave api_key empty in the file config and save the key from the menubar instead. That value will persist locally via hs.settings and continue working after reboot. If you prefer environment variables, practical examples for GUI-launched Hammerspoon are:
launchctl setenv OPENAI_API_KEY "your-api-key"
launchctl setenv GEMINI_API_KEY "your-api-key"
launchctl setenv ANTHROPIC_API_KEY "your-api-key"Then restart Hammerspoon so the app can read the variable.
Key Caster is designed for screen recording or live demos. When enabled, it listens for keyboard events and shows the latest key combination as an overlay on the active screen.
It now supports lightweight runtime control: by default, ⌃⌘K toggles it for the current Hammerspoon session, and the menubar entry can follow auto, true, or false visibility modes.
You can enable or tune it in ~/.hammerspoon/keybindings_config.lua:
_M.key_caster = {
enabled = false,
show_menubar = "auto",
position = {
anchor = "bottom_center",
offset_x = 0,
offset_y = 140,
},
toggle_hotkey = {
prefix = { "Command", "Ctrl" },
key = "K",
message = "Toggle Key Caster",
},
font = {
name = "Menlo Bold",
size = 44,
},
text_color = {
hex = "#F8FAFC",
alpha = 1,
},
background_color = {
hex = "#111827",
alpha = 0.78,
},
display_mode = "single",
sequence_window_seconds = 0.4,
duration_seconds = 1.2,
}enabled: whether the overlay starts enabled after Hammerspoon reload. It is disabled by default to avoid visual noise during normal daily use.show_menubar: supportsauto,true, orfalse.autoonly shows the menubar entry while Key Caster is enabled;truekeeps it visible all the time; runtime changes are session-only and reset afterhs.reload().toggle_hotkey: configure the runtime on/off shortcut. Setkey = ""to disable the hotkey entirely.position.anchor: one oftop_left,top_center,top_right,center,bottom_left,bottom_center,bottom_right.position.offset_xandposition.offset_y: fine tune the overlay position relative to the selected anchor.font: configure the displayed font family and size.text_colorandbackground_color: configure overlay colors and alpha.display_mode:singlekeeps the existing behavior and always shows only the latest key;sequencemerges consecutive plain-letter input into a short text run.sequence_window_seconds: only used insequencemode; letters typed within this interval are appended into the same overlay.duration_seconds: how long each key overlay stays visible before it disappears.- The menubar menu provides a runtime UI for enabling/disabling Key Caster, adjusting the icon visibility mode, switching between
singleandsequencedisplay modes, and changing the overlay position, font size, and duration. - Position, font size, duration, display mode, and menubar visibility changes made from the Key Caster menubar are persisted via
hs.settings, and the same menu also provides恢复默认to clear those saved overrides and return tokeybindings_config.lua. - You can also manage it from the Hammerspoon console with
package.loaded.key_caster.toggle(),package.loaded.key_caster.show_menubar(),package.loaded.key_caster.auto_menubar(),package.loaded.key_caster.single_display_mode(), andpackage.loaded.key_caster.sequence_display_mode(). - Accessibility permission is required. If it is missing, startup will show a warning and the module may fail to capture key events.
- ⌥ + H: Hammerspoon Console
- ⌥ + F: Finder
- ⌥ + I: Ghostty
- ⌥ + C: Chrome
- ⌥ + N: Antigravity
- ⌥ + D: WPS
- ⌥ + O: Obsidian
- ⌥ + M: Mail
- ⌥ + P: Postman
- ⌥ + E: Excel
- ⌥ + V: VSCode
- ⌥ + K: Cursor
- ⌥ + J: Tuitui
- ⌥ + W: WeChat
The configuration will enforce scheduled breaks on all screens based on your configured work and rest durations.
- A menubar icon is available for runtime control and quick configuration.
soft: show a translucent fullscreen overlay, keep the current app usable.hard: show the overlay and block keyboard/mouse input during the break.show_menubar: show the menubar icon for quick actions and UI-based configuration.start_next_cycle: controls how the next work cycle starts after a break.autostarts immediately,on_inputwaits for the first keyboard or mouse input.overlay_opacity: overlay opacity in the range0to1. Defaults to0.32insoftmode and0.96inhardmode.minimal_display: when enabled, the overlay only shows a coffee icon☕️.friendly_reminder_message: customize the reminder message template.friendly_reminder_duration_seconds: how long the reminder stays visible. Set0to keep it open until manually closed with×.friendly_reminder_seconds: how many seconds before the break to show a light reminder. Set0to disable it.- Track daily focus time, completed breaks, skipped breaks, streak days, and a simple score directly from the menubar menu.
menubar_skin: switch betweencoffee,hourglass, andbars.focus_goal_minutes: the daily focus goal used for streak tracking.break_goal_count: the daily break goal for completed breaks. Set0to disable it.- Break adherence is shown as completed breaks divided by completed plus skipped breaks.
strict_mode_after_skips: after skipping this many breaks in a day, reminders are automatically upgraded tohardmode. Set0to disable.rest_penalty_seconds_per_skip: extra rest time added to every later break after each skip.max_rest_penalty_seconds: cap for the accumulated skip penalty.- The menubar menu provides
Skip Current Break, which immediately ends the current break and applies the configured penalties. rest_seconds: break duration in seconds.- Changes made from the menubar are persisted via
hs.settings. They override the file config until you choose恢复默认from the menu. - Choosing
恢复默认will ask for confirmation, then clear the runtime overrides and fall back to the defaults defined inkeybindings_config.lua. - Locked-screen, display-sleep, and system-sleep time are not counted as work time. After the session becomes active again, the work timer restarts from a new cycle.
Supported reminder placeholders:
{{remaining}}: human-readable time such as1 分钟 30 秒{{remaining_seconds}}: remaining seconds as an integer{{remaining_mmss}}: remaining time inMM:SS{{rest}}: break duration in human-readable form{{rest_seconds}}: break duration in seconds{{rest_mmss}}: break duration inMM:SS
You can change or disable it in ~/.hammerspoon/keybindings_config.lua:
_M.break_reminder = {
enabled = true,
show_menubar = true,
menubar_skin = "hourglass",
start_next_cycle = "on_input",
mode = "soft",
overlay_opacity = 0.32,
minimal_display = true,
focus_goal_minutes = 240,
break_goal_count = 8,
strict_mode_after_skips = 2,
rest_penalty_seconds_per_skip = 30,
max_rest_penalty_seconds = 300,
friendly_reminder_message = "还有 {{remaining}} 开始休息",
friendly_reminder_duration_seconds = 10,
friendly_reminder_seconds = 120,
work_minutes = 28,
rest_seconds = 120,
}Window move and resize actions keep the focused window within the current screen's visible frame, and prevent repeated shrink operations from reducing it below a minimum usable size.
-
⌃⌥ + C: Center Window
-
⌃⌥ + K: Up Half of Screen
-
⌃⌥ + J: Down Half of Screen
-
⌃⌥ + H: Left Half of Screen
-
⌃⌥ + L: Right Half of Screen
-
⌃⌥ + Y: Top Left Corner
-
⌃⌥ + O: Top Right Corner
-
⌃⌥ + U: Bottom Left Corner
-
⌃⌥ + I: Bottom Right Corner
-
⌃⌥ + Q: Left or Top 1/3
-
⌃⌥ + W: Right or Bottom 1/3
-
⌃⌥ + E: Left or Top 2/3
-
⌃⌥ + R: Right or Bottom 2/3
-
⌃⌥ + M: Max Window
-
⌃⌥ + =: Stretch Outward
-
⌃⌥ + -: Shrink Inward
-
⌃⌥⌘⇧ + K: Bottom Side Stretch Upward
-
⌃⌥⌘⇧ + J: Bottom Side Stretch Downward
-
⌃⌥⌘⇧ + H: Right Side Stretch Leftward
-
⌃⌥⌘⇧ + L: Right Side Stretch Rightward
- ⌃⌥⌘ + K: Move Upward
- ⌃⌥⌘ + J: Move Downward
- ⌃⌥⌘ + H: Move Leftward
- ⌃⌥⌘ + L: Move Rightward
- ⌃⌥ + UP: Move to Above Monitor
- ⌃⌥ + DOWN: Move to Below Monitor
- ⌃⌥ + LEFT: Move to Left Monitor
- ⌃⌥ + RIGHT: Move to Right Monitor
- ⌃⌥ + SPACE: Move to Next Monitor
- ⌃⌥⌘ + M: Minimize All Windows
- ⌃⌥⌘ + U: Unminimize All Windows
- ⌃⌥⌘ + Q: Close All Windows
Modify the file ~/.hammerspoon/keybindings_config.lua according to your keystroke habits.
More details
- ⌃ + RIGHT: Switch to right desktop
- ⌃ + LEFT: Switch to left desktop
- ⌃ + UP: Toggle tiling windows
- ⌥⌘ + D: Toggle dock
- ⌘ + Q: Close app
- ⌘ + ,: Open the app's preferences
- ⌘⇧ + /: Toggle help
- ⌘ + H: Hide window
- ⌘ + M: Minimize window
- ⌘ + N: New window
- ⌘ + W: Close window
- ⌘ + `: Switch between windows of the same application
- ⌃⌘ + F: Toggle window fullscreen
- ⌃⌘ + H: Hide all windows except the current one
- ⌘⇧ + [: Switch to the left tab
- ⌘⇧ + ]: Switch to the right tab
- ⌘ + NUMBER: Switch to the specified tab
- ⌘ + 9: Switch to the last tab
- ⌃ + P: Move the cursor up
- ⌃ + N: Move the cursor down
- ⌃ + B: Move the cursor back/left
- ⌃ + F: Move the cursor forward/right
- ⌃ + A: Move the cursor to the beginning of the line
- ⌃ + E: Move the cursor to the end of the line
- ⌘ + BACKSPACE: Delete the selected file
- ⌘ + DOWN: Go to a directory or open a file
- ⌘ + UP: Back to the upper level directory
- ⌘⇧ + BACKSPACE: Clear the Trash
-
⌘ + +: Expand font size
-
⌘ + -: Shrink font size
-
⌘ + 0: Reset font size
-
⌘ + Z: Undo
-
⌘⇧ + Z: Redo
-
⌘ + C: Copy
-
⌘ + V: Paste
-
⌘⌥ + V: Paste and delete the original object
This project is under the MIT License. See the LICENSE file for the full license text.









