Skip to content

eventtap: setProperty choose CGEventField setter based on value type#3859

Open
mogenson wants to merge 1 commit intoHammerspoon:masterfrom
mogenson:eventtap_event_set_hidden_property
Open

eventtap: setProperty choose CGEventField setter based on value type#3859
mogenson wants to merge 1 commit intoHammerspoon:masterfrom
mogenson:eventtap_event_set_hidden_property

Conversation

@mogenson
Copy link
Copy Markdown
Contributor

eventtap event's setProperty() method uses CGEventSetDoubleValueField for a defined list of CGEventField properties, and CGEventSetIntegerValueField for all others.

Keep the list of double type CGEventField properties, but make a change to check the type of the value parameter for the all others case.

If the value is a Lua integer, use CGEventSetIntegerValueField. If the value is a Lua number (aka double), use the CGEventSetDoubleValueField.

The motivation for this change is to use setProperty() to set private / hidden event fields. These event field constants are not defined in any public headers from Apple so it doesn't make sense to add a mapping from string to constant value in the hs.eventtap.event.types table. However, a spoon or Hammerspoon config can set these event fields with setProperty(property, value) by passing an integer for the property and integer or number for the value.

Also add a doc note clarifying which CGEvent API is used depending on the Lua value type passed.

eventtap event's setProperty() method uses CGEventSetDoubleValueField
for a defined list of CGEventField properties, and
CGEventSetIntegerValueField for all others.

Keep the list of double type CGEventField properties, but make a change
to check the type of the value parameter for the all others case.

If the value is a Lua integer, use CGEventSetIntegerValueField. If the
value is a Lua number (aka double), use the CGEventSetDoubleValueField.

The motivation for this change is to use setProperty() to set private /
hidden event fields. These event field constants are not defined in any
public headers from Apple so it doesn't make sense to add a mapping from
string to constant value in the hs.eventtap.event.types table.
However, a spoon or Hammerspoon config can set these event fields with
setProperty(property, value) by passing an integer for the property and
integer or number for the value.

Also add a doc note clarifying which CGEvent API is used depending
on the Lua value type passed.
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request does not contain a valid label. Please add one of the following labels: ['pr-fix', 'pr-change', 'pr-feature', 'pr-maintenance']

@mogenson
Copy link
Copy Markdown
Contributor Author

Some people have discovered a very cool way to switch spaces without the animation by posting a swipe gesture with a very fast velocity: https://github.com/joshuarli/iss

It would be great if we could use this technique in Hammerspoon. However, it requires setting some undocumented / hidden CGEventFields. I'm not sure about the best way to go about this. The fast swipe gesture only works on spaces on the same screen, so it's not a suitable replacement for hs.spaces.gotoSpace(). The CGEventField values are not defined any any public Apple header and are subject to change / break so I'm hesitant to manually define them in the hs.eventtap.event.types table. I settled on making a small modification to hs.eventtap.event:setProperty() to select either the CGEventSetIntegerValueField or CGEventSetDoubleValueField setter based on the Lua type of the value parameter.

With this change we can create a fast swipe gesture to switch spaces without an animation by doing the following:

local FLT_TRUE_MIN = 2 ^ -149
local kCGEventGestureHIDType = 110
local kCGEventGesturePhase = 132
local kCGEventGestureScrollY = 119
local kCGEventGestureSwipeMotion = 123
local kCGEventGestureSwipeProgress = 124
local kCGEventGestureSwipeVelocityX = 129
local kCGEventGestureSwipeVelocityY = 130
local kCGEventGestureZoomDeltaX = 139
local kCGEventScrollGestureFlagBits = 135
local kCGGestureMotionHorizontal = 1
local kCGSEventDockControl = 30
local kCGSEventGesture = 29
local kCGSEventTypeField = 55
local kCGSGesturePhaseBegan = 1
local kCGSGesturePhaseEnded = 4
local kIOHIDEventTypeDockSwipe = 23

function fast_switch_space(direction_right)
  -- begin gesture
  local ev = hs.eventtap.event.newEvent()
  ev:setProperty(kCGSEventTypeField, kCGSEventDockControl)
  ev:setProperty(kCGEventGestureHIDType, kIOHIDEventTypeDockSwipe)
  ev:setProperty(kCGEventGesturePhase, kCGSGesturePhaseBegan)
  ev:setProperty(kCGEventScrollGestureFlagBits, direction_right and 1 or 0)
  ev:setProperty(kCGEventGestureSwipeMotion, kCGGestureMotionHorizontal)
  ev:setProperty(kCGEventGestureScrollY, 0.0)             -- must be a double value
  ev:setProperty(kCGEventGestureZoomDeltaX, FLT_TRUE_MIN) -- must be a double value
  ev:post()

  hs.eventtap.event.newEvent():setProperty(kCGSEventTypeField, kCGSEventGesture):post()

  -- end gesture
  ev = hs.eventtap.event.newEvent()
  ev:setProperty(kCGSEventTypeField, kCGSEventDockControl)
  ev:setProperty(kCGEventGestureHIDType, kIOHIDEventTypeDockSwipe)
  ev:setProperty(kCGEventGesturePhase, kCGSGesturePhaseEnded)
  ev:setProperty(kCGEventGestureSwipeProgress, direction_right and 2.0 or -2.0) -- double value
  ev:setProperty(kCGEventScrollGestureFlagBits, direction_right and 1 or 0)
  ev:setProperty(kCGEventGestureSwipeMotion, kCGGestureMotionHorizontal)
  ev:setProperty(kCGEventGestureScrollY, 0.0)                                        -- must be a double value
  ev:setProperty(kCGEventGestureSwipeVelocityX, direction_right and 400.0 or -400.0) -- must be a double value
  ev:setProperty(kCGEventGestureSwipeVelocityY, 0.0)                                 -- must be a double value
  ev:setProperty(kCGEventGestureZoomDeltaX, FLT_TRUE_MIN)                            -- must be a double value
  ev:post()

  hs.eventtap.event.newEvent():setProperty(kCGSEventTypeField, kCGSEventGesture):post()
end

As far as I know, this change preserves previous behavior because all the existing "(N)" CGEventField properties still explicitly call CGEventSetDoubleValueField (even if the value passed is an integer type). And, if any spoon or user was passing a number (double) value to any of the "(I)" CGEventField properties, this would have failed the luaL_checkinteger() getter (so hopefully spoons were not doing that)!

However, let me know if all CGEventField types should be defined in libeventtap_event.m (even private ones) or if there should be a new Lua API method for setting raw event properties.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 30, 2026

Codecov Report

❌ Patch coverage is 0% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 27.37%. Comparing base (08e93f6) to head (9654e14).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3859      +/-   ##
==========================================
- Coverage   27.39%   27.37%   -0.03%     
==========================================
  Files         191      191              
  Lines       51537    51542       +5     
==========================================
- Hits        14119    14108      -11     
- Misses      37418    37434      +16     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@asmagill
Copy link
Copy Markdown
Member

asmagill commented Apr 1, 2026

Generally yes, even private properties are added to the appropriate tables (in this case, in libeventtap_event.m) -- it encourages experimentation and someone might find a sequence or set of values to perform something that we haven't thought of yet.

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