hammerspoon/extensions/eventtap/eventtap.lua

303 lines
14 KiB
Lua

--- === hs.eventtap ===
---
--- Tap into input events (mouse, keyboard, trackpad) for observation and possibly overriding them
--- It also provides convenience wrappers for sending mouse and keyboard events. If you need to construct finely controlled mouse/keyboard events, see hs.eventtap.event
---
--- This module is based primarily on code from the previous incarnation of Mjolnir by [Steven Degutis](https://github.com/sdegutis/).
--- === hs.eventtap.event ===
---
--- Create, modify and inspect events for `hs.eventtap`
---
--- This module is based primarily on code from the previous incarnation of Mjolnir by [Steven Degutis](https://github.com/sdegutis/).
---
--- `hs.eventtap.event.newGesture` uses an external library by Calf Trail Software, LLC.
---
--- Touch
--- Copyright (C) 2010 Calf Trail Software, LLC
---
--- This program is free software; you can redistribute it and/or
--- modify it under the terms of the GNU General Public License
--- as published by the Free Software Foundation; either version 2
--- of the License, or (at your option) any later version.
---
--- This program is distributed in the hope that it will be useful,
--- but WITHOUT ANY WARRANTY; without even the implied warranty of
--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--- GNU General Public License for more details.
---
--- You should have received a copy of the GNU General Public License
--- along with this program; if not, write to the Free Software
--- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
local module = require("hs.libeventtap")
module.event = require("hs.libeventtapevent")
local fnutils = require("hs.fnutils")
local keycodes = require("hs.keycodes")
local timer = require("hs.timer")
-- private variables and methods -----------------------------------------
local function getKeycode(s)
local n
if type(s)=='number' then n=s
elseif type(s)~='string' then error('key must be a string or a number',3)
elseif (s:sub(1, 1) == '#') then n=tonumber(s:sub(2))
else n=keycodes.map[string.lower(s)] end
if not n then error('Invalid key: '..s..' - this may mean that the key requested does not exist in your keymap (particularly if you switch keyboard layouts frequently)',3) end
return n
end
local function getMods(mods)
local r={}
if not mods then return r end
if type(mods)=='table' then mods=table.concat(mods,'-') end
if type(mods)~='string' then error('mods must be a string or a table of strings',3) end
-- super simple substring search for mod names in a string
mods=string.lower(mods)
local function find(ps)
for _,s in ipairs(ps) do
if string.find(mods,s,1,true) then r[#r+1]=ps[#ps] return end
end
end
find{'cmd','command',''} find{'ctrl','control',''}
find{'alt','option',''} find{'shift',''}
find{'fn'}
return r
end
module.event.types = ls.makeConstantsTable(module.event.types)
module.event.properties = ls.makeConstantsTable(module.event.properties)
module.event.rawFlagMasks = ls.makeConstantsTable(module.event.rawFlagMasks)
-- Public interface ------------------------------------------------------
local originalNewKeyEvent = module.event.newKeyEvent
module.event.newKeyEvent = function(mods, key, isDown)
if type(mods) == "nil" then mods = {} end
if (type(mods) == "number" or type(mods) == "string") and type(key) == "boolean" then
mods, key, isDown = nil, mods, key
end
local keycode = getKeycode(key)
local modifiers = mods and getMods(mods) or nil
-- print(finspect(table.pack(modifiers, keycode, isDown)))
return originalNewKeyEvent(modifiers, keycode, isDown)
end
--- hs.eventtap.event.newKeyEventSequence(modifiers, character) -> table
--- Function
--- Generates a table containing the keydown and keyup events to generate the keystroke with the specified modifiers.
---
--- Parameters:
--- * modifiers - A table containing the keyboard modifiers to apply ("cmd", "alt", "shift", "ctrl", "rightCmd", "rightAlt", "rightShift", "rightCtrl", or "fn")
--- * character - A string containing a character to be emitted
---
--- Returns:
--- * a table with events which contains the individual events that Apple recommends for building up a keystroke combination (see [hs.eventtap.event.newKeyEvent](#newKeyEvents)) in the order that they should be posted (i.e. the first half will contain keyDown events and the second half will contain keyUp events)
---
--- Notes:
--- * The `modifiers` table must contain the full name of the modifiers you wish used for the keystroke as defined in `hs.keycodes.map` -- the Unicode equivalents are not supported by this function.
--- * The returned table will always contain an even number of events -- the first half will be the keyDown events and the second half will be the keyUp events.
--- * The events have not been posted; the table can be used without change as the return value for a callback to a watcher defined with [hs.eventtap.new](#new).
function module.event.newKeyEventSequence(modifiers, character)
local codes = fnutils.map({table.unpack(modifiers), character}, getKeycode)
local n = #codes
local events = {}
for i, code in ipairs(codes) do
events[i] = module.event.newKeyEvent(code, true)
events[2*n+1-i] = module.event.newKeyEvent(code, false)
end
return events
end
--- hs.eventtap.event.newMouseEvent(eventtype, point[, modifiers) -> event
--- Constructor
--- Creates a new mouse event
---
--- Parameters:
--- * eventtype - One of the mouse related values from `hs.eventtap.event.types`
--- * point - An hs.geometry point table (i.e. of the form `{x=123, y=456}`) indicating the location where the mouse event should occur
--- * modifiers - An optional table (e.g. {"cmd", "alt"}) containing zero or more of the following keys:
--- * cmd
--- * alt
--- * shift
--- * ctrl
--- * fn
---
--- Returns:
--- * An `hs.eventtap` object
function module.event.newMouseEvent(eventtype, point, modifiers)
local types = module.event.types
local button
if eventtype == types["leftMouseDown"] or eventtype == types["leftMouseUp"] or eventtype == types["leftMouseDragged"] then
button = "left"
elseif eventtype == types["rightMouseDown"] or eventtype == types["rightMouseUp"] or eventtype == types["rightMouseDragged"] then
button = "right"
elseif eventtype == types["otherMouseDown"] or eventtype == types["otherMouseUp"] or eventtype == types["otherMouseDragged"] then
button = "other"
elseif eventtype == types["mouseMoved"] then
button = "none"
else
print("Error: unrecognised mouse button eventtype: " .. tostring(eventtype))
return nil
end
return module.event._newMouseEvent(eventtype, point, button, modifiers)
end
--- hs.eventtap.leftClick(point[, delay])
--- Function
--- Generates a left mouse click event at the specified point
---
--- Parameters:
--- * point - A table with keys `{x, y}` indicating the location where the mouse event should occur
--- * delay - An optional delay (in microseconds) between mouse down and up event. Defaults to 200000 (i.e. 200ms)
---
--- Returns:
--- * None
---
--- Notes:
--- * This is a wrapper around `hs.eventtap.event.newMouseEvent` that sends `leftmousedown` and `leftmouseup` events)
function module.leftClick(point, delay)
if delay==nil then
delay=200000
end
module.event.newMouseEvent(module.event.types["leftMouseDown"], point):post()
timer.usleep(delay)
module.event.newMouseEvent(module.event.types["leftMouseUp"], point):post()
end
--- hs.eventtap.rightClick(point[, delay])
--- Function
--- Generates a right mouse click event at the specified point
---
--- Parameters:
--- * point - A table with keys `{x, y}` indicating the location where the mouse event should occur
--- * delay - An optional delay (in microseconds) between mouse down and up event. Defaults to 200000 (i.e. 200ms)
---
--- Returns:
--- * None
---
--- Notes:
--- * This is a wrapper around `hs.eventtap.event.newMouseEvent` that sends `rightmousedown` and `rightmouseup` events)
function module.rightClick(point, delay)
if delay==nil then
delay=200000
end
module.event.newMouseEvent(module.event.types["rightMouseDown"], point):post()
timer.usleep(delay)
module.event.newMouseEvent(module.event.types["rightMouseUp"], point):post()
end
--- hs.eventtap.otherClick(point[, delay][, button])
--- Function
--- Generates an "other" mouse click event at the specified point
---
--- Parameters:
--- * point - A table with keys `{x, y}` indicating the location where the mouse event should occur
--- * delay - An optional delay (in microseconds) between mouse down and up event. Defaults to 200000 (i.e. 200ms)
--- * button - An optional integer, default 2, between 2 and 31 specifying the button number to be pressed. If this parameter is specified then `delay` must also be specified, though you may specify it as `nil` to use the default.
---
--- Returns:
--- * None
---
--- Notes:
--- * This is a wrapper around `hs.eventtap.event.newMouseEvent` that sends `otherMouseDown` and `otherMouseUp` events)
--- * macOS recognizes up to 32 distinct mouse buttons, though few mouse devices have more than 3. The left mouse button corresponds to button number 0 and the right mouse button corresponds to 1; distinct events are used for these mouse buttons, so you should use `hs.eventtap.leftClick` and `hs.eventtap.rightClick` respectively. All other mouse buttons are coalesced into the `otherMouse` events and are distinguished by specifying the specific button with the `mouseEventButtonNumber` property, which this function does for you.
--- * The specific purpose of mouse buttons greater than 2 varies by hardware and application (typically they are not present on a mouse and have no effect in an application)
function module.otherClick(point, delay, button)
if delay==nil then
delay=200000
end
if button==nil then
button = 2
end
if button < 2 or button > 31 then
error("button number must be between 2 and 31 inclusive", 2)
end
module.event.newMouseEvent(module.event.types["otherMouseDown"], point):setProperty(module.event.properties["mouseEventButtonNumber"], button):post()
hs.timer.usleep(delay)
module.event.newMouseEvent(module.event.types["otherMouseUp"], point):setProperty(module.event.properties["mouseEventButtonNumber"], button):post()
end
--- hs.eventtap.middleClick(point[, delay])
--- Function
--- Generates a middle mouse click event at the specified point
---
--- Parameters:
--- * point - A table with keys `{x, y}` indicating the location where the mouse event should occur
--- * delay - An optional delay (in microseconds) between mouse down and up event. Defaults to 200000 (i.e. 200ms)
---
--- Returns:
--- * None
---
--- Notes:
--- * This function is just a wrapper which calls `hs.eventtap.otherClick(point, delay, 2)` and is included solely for backwards compatibility.
module.middleClick = function(point, delay)
module.otherClick(point, delay, 2)
end
--- hs.eventtap.keyStroke(modifiers, character[, delay, application])
--- Function
--- Generates and emits a single keystroke event pair for the supplied keyboard modifiers and character
---
--- Parameters:
--- * modifiers - A table containing the keyboard modifiers to apply ("fn", "ctrl", "alt", "cmd", "shift", or their Unicode equivalents)
--- * character - A string containing a character to be emitted
--- * delay - An optional delay (in microseconds) between key down and up event. Defaults to 200000 (i.e. 200ms)
--- * application - An optional hs.application object to send the keystroke to
---
--- Returns:
--- * None
---
--- Notes:
--- * This function is ideal for sending single keystrokes with a modifier applied (e.g. sending ⌘-v to paste, with `hs.eventtap.keyStroke({"cmd"}, "v")`). If you want to emit multiple keystrokes for typing strings of text, see `hs.eventtap.keyStrokes()`
--- * Note that invoking this function with a table (empty or otherwise) for the `modifiers` argument will force the release of any modifier keys which have been explicitly created by [hs.eventtap.event.newKeyEvent](#newKeyEvent) and posted that are still in the "down" state. An explicit `nil` for this argument will not (i.e. the keystroke will inherit any currently "down" modifiers)
function module.keyStroke(modifiers, character, delay, application)
local targetApp = nil
local keyDelay = 200000
if type(delay) == "userdata" then
targetApp = delay
else
targetApp = application
end
if type(delay) == "number" then
keyDelay = delay
end
--print("targetApp: "..tostring(targetApp))
--print("keyDelay: "..tostring(keyDelay))
module.event.newKeyEvent(modifiers, character, true):post(targetApp)
timer.usleep(keyDelay)
module.event.newKeyEvent(modifiers, character, false):post(targetApp)
end
--- hs.eventtap.scrollWheel(offsets, modifiers, unit) -> event
--- Function
--- Generates and emits a scroll wheel event
---
--- Parameters:
--- * offsets - A table containing the {horizontal, vertical} amount to scroll. Positive values scroll up or left, negative values scroll down or right.
--- * mods - A table containing zero or more of the following:
--- * cmd
--- * alt
--- * shift
--- * ctrl
--- * fn
--- * unit - An optional string containing the name of the unit for scrolling. Either "line" (the default) or "pixel"
---
--- Returns:
--- * None
function module.scrollWheel(offsets, modifiers, unit)
module.event.newScrollEvent(offsets, modifiers, unit):post()
end
-- Return Module Object --------------------------------------------------
return module