[MacOS Hammerspoon] HoldDown Multi-Button Script with In-Game Toggle

Hi, I would like to share with you my first Hammerspoon script that I developed for a few days as a prototype to fulfill the following simple requirements. It’s currently written in a very simple and straightforward way and it’s also the first time I’m writing a script in LUA, so bear with me. :smiley:

  • The script may only actively capture and execute keyboard strokes when WOW is in the foreground.
  • The script should be able to be switched on and off in-game so that, for example, simple number keys also behave normally, e.g. in chats or the auction house, when the script is switched off.
  • Any number of trigger keys (with possible modifiers) should be easily definable in the script without writing new methods for each key or creating redundant code.
  • As long as a trigger key is pressed, the script should execute a defined key (with possible modifiers) repeatedly with a certain click rate without blocking other keystrokes or causing the game to lag.

I hope that the code blocks are sufficiently commented so that the code can be easily understood.

The whole script as well as parts of it can be used freely but on your own responsibility as well as adapted or changed.

So the following script only turns on when WoW comes to the foreground and all inputs can be registered by the game, which is indicated by a system message on the screen.

The functionality of the script is switched off by default (but can also be configured with on by default) and can be switched on or off with the key combination “cmd+q” defined as an example. This toggle switch is also displayed on the screen via a sysem message.

The keys “1” and “2” without modifier are defined as triggers in this script as an example. For each of these keys, as long as they are pressed, the script automatically executes the key combination repeatedly as defined (press “1” results in spam “cmd+1” or/and press “2” results in spam “cmd+2”) at the interval of 100ms defined as an example.

Other keystrokes are possible while pressing a trigger key without interrupting the “spam”.

The configuration of the default toggle, click rate, toggle key or new trigger keys and their execute keys is relatively simple, whereby the modifier must always be specified as a table with 0 or more modifiers. Please note that you should not specify the same key combination of key and modifier as trigger key and execute key. This would be mutually exclusive.

If you have any questions or problems with understanding, please check the Hammerspoon Docs or post them here.

local wf = hs.window.filter
local applicationName = "Wow"
local statusTablePath = "keyPressedStatus"
local statusKeyPrefix = "keyPressed#"
local watcherPrefix = "watcher#"
local statusTable = hs.watchable.new(statusTablePath)

-- defaults
local scriptToggle = false
local clickRate = "100ms"
local clickDelay = "0s"

-- trigger keys
-- attention: the combination of trigger button and mods must not be identical to the combination of macro button and mods.
local triggerKeys = {
	["1"] = {
		mods = {};
		executeKey = {
			key = "1";
			mods = {"cmd"}
		}
	};
	["2"] = {
		mods = {};
		executeKey = {
			key = "2";
			mods = {"cmd"}
		}
	}
}

-- script toggle key
local scriptToggleKey = {["q"] = {mods = {"cmd"}}}

-- set key press state
local setKeyPressStatus = function(triggerKey, isPressed)
	statusTable[statusKeyPrefix .. triggerKey] = isPressed or false
end

-- hit macro key once
local hitExecuteKey = function(triggerKey)
	hs.eventtap.keyStroke(triggerKeys[triggerKey].executeKey.mods, triggerKeys[triggerKey].executeKey.key, hs.timer.seconds(clickDelay))
end

-- spam macro key with given clickrate as long as the trigger key is pressed
local spamExecuteKey = function(triggerKey, watcher)
	hs.timer.doWhile(function() return watcher:value() end, function() hitExecuteKey(triggerKey) end, hs.timer.seconds(clickRate))
end

-- create watcher
local setKeyPressWatcher = function(triggerKey)
	statusTable[watcherPrefix .. triggerKey] = hs.watchable.watch(statusTablePath, statusKeyPrefix .. triggerKey, function(watcher, path, key, old, new)
		if new == true then
			-- hit executeKey once instead of the original trigger key event, the propagation of which is suppressed in the eventhandler
			-- so that you have the feeling of pressing the execute key immediately
			hitExecuteKey(triggerKey)
			spamExecuteKey(triggerKey, watcher)
		end
	end)
end

-- reset states of all triggerKeys
local resetAllKeyPressStatus = function()
	for triggerKey, value in pairs(triggerKeys) do
		setKeyPressStatus(triggerKey)
	end
end

-- set up key status and watcher
local init = function()
	for triggerKey, value in pairs(triggerKeys) do
		setKeyPressStatus(triggerKey)
		setKeyPressWatcher(triggerKey)
	end
end

init()

-- keyDown / keyUp eventhandler
local wowKeyPressEventHandler = function(event)
  local keyPressed = tostring(hs.keycodes.map[event:getKeyCode()])
  local flags = event:getFlags()

  -- ingame toggle to activate or deactivate the script functionality
  if (scriptToggleKey[keyPressed] ~= nil) and (flags:containExactly(scriptToggleKey[keyPressed].mods)) and (event:getType() == 10) then
  	scriptToggle = not scriptToggle
  	hs.alert.show('Script Toggle ' .. tostring(scriptToggle))
  end

  if (scriptToggle) then
  	-- just set keypress state depending on event
  	-- only catch defined trigger key/mod combination and let pass through anything else include the macro key/mod combination
  	if (triggerKeys[keyPressed] ~= nil) and (flags:containExactly(triggerKeys[keyPressed].mods)) and (not flags:containExactly(triggerKeys[keyPressed].executeKey.mods)) then
  		setKeyPressStatus(keyPressed, event:getType() == 10)
      -- prevent event propagation of the trigger key event
      return true
  	end
	end
end

wowKeyPressEventListener = hs.eventtap.new({hs.eventtap.event.types.keyDown, hs.eventtap.event.types.keyUp}, wowKeyPressEventHandler):stop()

wowWindowFilter = wf.new(function(win)
  return win:application():name() == applicationName
end)

wowWindowFilter:subscribe(wf.windowFocused, function(win)
	resetAllKeyPressStatus()
	wowKeyPressEventListener:start()
	hs.alert.show('Script enabled.')
end)

wowWindowFilter:subscribe(wf.windowUnfocused, function(win)
  wowKeyPressEventListener:stop()
  resetAllKeyPressStatus()
  hs.alert.show('Script disabled.')
end)