Skip to content

hs.hotkey: guard bind() against a callback-less no-op hotkey - #207

Merged
cmsj merged 1 commit into
cmsj:mainfrom
muescha:fix/hs.hotkey-require-min-one-callback
Sep 10, 2026
Merged

cmsj merged 1 commit into
cmsj:mainfrom
muescha:fix/hs.hotkey-require-min-one-callback

Conversation

@muescha

@muescha muescha commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Context / question

While reading through hs.hotkey I noticed that bind() happily creates and
enables a hotkey even when no function callback is given at all — e.g.
bind(mods, key, null, null) or bind(mods, key, null, null, null). Since bind() enables immediately, the result is a hotkey that claims the key combination
(swallowing it from other apps) but runs nothing — a silent no-op.

I'm genuinely not sure whether that's intended or just an edge case that never came up. Guarding against it in bind() seemed sensible, but I may well be missing a reason it's allowed — I'm completely fine if this PR is dismissed and rejected.

What this PR does

bind() returns null (and logs an error) unless at least one of them is a function:

  • callbackPressed,
  • callbackReleased, or
  • callbackRepeat

create() is deliberately left permissive, so the "build now, assign callbacks later via properties" pattern still works.

(Side note: the message toast is shown just before a callback fires, so a
message-only hotkey never shows its message either — which is partly why a
callback-less hotkey feels like a mistake rather than a use case.)

Example

console.log("before")
var cmdAltH = hs.hotkey.bind(["cmd", "alt"], "h", null, null)
console.log("after")

Log:

2026-09-07 02:02:33 - JavaScript: before
2026-09-07 02:02:33 - Debug: Loading module: hotkey
2026-09-07 02:02:33 - Garbage: Init of hs.hotkey: 56E59F07-19D2-4DCC-A69E-FA93690642FE
2026-09-07 02:02:33 - Error: hs.hotkey.bind: at least one of callbackPressed, callbackReleased, or callbackRepeat must be a function
2026-09-07 02:02:33 - JavaScript: after

bind() logs the error and returns null without throwing, so the surrounding
script keeps running.

Changes

  • Guard in bind() requiring ≥1 function callback.
  • Updated the two bindSpec message tests to pass a callback (a message-only spec is now rejected under this rule).
  • New tests: bind with no callbacks → null; create with no callbacks still succeeds.

Notes

  • Docs (api.json / HTML / TS) intentionally not regenerated here; the updated bind() docstring flags that a docs refresh is owed.
  • If the current behaviour is by design, closing this PR is totally fine.
  • Related: hs.hotkey: make released optional and unify callback handling #208 (also touches hs.hotkey); minor rebase possible depending on merge order.

🤖 Generated with Claude Code

bind() enables the hotkey immediately, so binding with no function
callbacks (e.g. bind(mods, key, null, null)) would silently claim the
key combination and do nothing. Guard against it and return null, while
keeping create() permissive so callers can build a hotkey and assign
callbacks later.

Update the two bindSpec message tests to pass a callback (a message-only
hotkey never fires its callback, so it is now correctly rejected), and
add tests for the new guard and for create()'s deferred-assignment path.

Docs (api.json/HTML/TS) intentionally not regenerated here; the updated
bind() docstring flags that a docs refresh is still owed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR prevents hs.hotkey.bind() from immediately enabling a hotkey that has no functional callbacks.

  • Requires at least one pressed, released, or repeat callback.
  • Leaves create() permissive for deferred callback assignment.
  • Updates bindSpec tests and adds coverage for callback-less bind() and create() calls.

Confidence Score: 4/5

The implementation appears safe to merge, with a non-blocking documentation refresh needed to expose the new API constraint.

The guard follows existing callback validation conventions, bindSpec safely handles a null result, and tests cover both rejection by bind and continued permissiveness in create; only the checked-in API documentation remains inconsistent.

Files Needing Attention: Hammerspoon 2/Modules/hs.hotkey/HSHotkeyModule.swift

Important Files Changed

Filename Overview
Hammerspoon 2/Modules/hs.hotkey/HSHotkeyModule.swift Adds the callback-presence guard and source documentation, but the corresponding checked-in API documentation remains stale.
Hammerspoon 2Tests/IntegrationTests/HSHotkeyIntegrationTests.swift Updates message tests to provide callbacks and verifies the intended distinction between bind() and create().
Prompt To Fix All With AI
### Issue 1
Hammerspoon 2/Modules/hs.hotkey/HSHotkeyModule.swift:23
**API Documentation Is Stale**

The runtime now rejects callback-less `bind()` calls, but the checked-in API documentation still permits each callback to be null and describes null only as a generic binding failure. Regenerate or update the non-HTML API artifacts so users can discover the new requirement instead of encountering an unexplained compatibility change.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "hs.hotkey: require at least one callback..." | Re-trigger Greptile

Comment thread Hammerspoon 2/Modules/hs.hotkey/HSHotkeyModule.swift
@cmsj
cmsj merged commit 94a09d1 into cmsj:main Sep 10, 2026
5 of 6 checks passed
cmsj added a commit that referenced this pull request Sep 11, 2026
PR #208 (dcee222, rebased here as e767bbd) intentionally left the docs
refresh for later. Rebasing onto main also picked up #207's callback
guard in bind(), so both land together.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLGfVh2SKZmZAh1YQfbMfn
@muescha
muescha deleted the fix/hs.hotkey-require-min-one-callback branch September 16, 2026 17:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants