-- Helpers shared by init.lua and a machine's own ~/.config/hammerspoon/ -- local.lua, which reach them with `require("dotlib")`. -- -- They used to be locals of init.lua, which meant a local.lua -- a separate -- chunk -- could not see them and had to copy whatever it needed. Hammerspoon -- puts the config directory first on package.path (its Contents/Resources/ -- setup.lua), and init.lua prepends it again from its own location, so this -- file is found wherever the config is linked. -- -- link/hammerspoon.sh links this file next to init.lua. Anything added here is -- part of what a local.lua may call, so keep the names stable. local M = {} -- Bind a key. Argument order is the one init.lua has always used: key first, -- then the modifier table, then the function. function M.hotkey(key, mods, fn) hs.hotkey.bind(mods, key, fn) end -- The modifier combinations these configs bind against. M.mods = { cs = { "ctrl", "shift" }, ca = { "ctrl", "alt" }, csa = { "ctrl", "shift", "alt" }, cag = { "ctrl", "alt", "cmd" }, hyp = { "ctrl", "alt", "cmd", "shift" }, spc = "space", } -- Focus an app. Given several names, the first one installed wins, so one -- binding can cover machines that have different apps for the same job. function M.focus(...) local appNames = { ... } local selectedAppName = nil -- If only one argument is provided, use it directly without searching if #appNames == 1 then selectedAppName = appNames[1] else -- For multiple arguments, find the first app that exists on disk local appDirs = { "/Applications/", os.getenv("HOME") .. "/Applications/" } for _, appName in ipairs(appNames) do for _, dir in ipairs(appDirs) do local path = dir .. appName .. ".app" if hs.fs.attributes(path) then selectedAppName = appName break end end if selectedAppName then break end end if not selectedAppName then print("No apps found: " .. table.concat(appNames, ", ")) end end return function() if selectedAppName then print('Opening ' .. selectedAppName) hs.application.launchOrFocus(selectedAppName) else print("No apps found: " .. table.concat(appNames, ", ")) end end end -- Run a shell command. function M.execute(cmd) return function() hs.execute(cmd, true) end end -- Type today's date. function M.type_current_date() local clock = os.date('%Y-%m-%d') hs.eventtap.keyStrokes(clock) end -- Type ids from random-id.ts, through a login shell so it is on PATH. function M.type_random_ids(count) local cmd = string.format('/bin/bash --login -c "random-id.ts %s"', count) return function() local out = hs.execute(cmd, true) if count == 1 then out = out:gsub("%s+$", "") end hs.eventtap.keyStrokes(out) end end -- Type a string. With min_delay/max_delay it is typed character by character, -- at a randomised pace, for a screencast; without, all at once. function M.type_text(text, options) return function() if options and (options.min_delay or options.max_delay) then -- Type with delays between characters local min_delay = options.min_delay or 0 local max_delay = options.max_delay or min_delay local chars = {} for char in text:gmatch(".") do table.insert(chars, char) end local function typeNextChar(index) if index > #chars then return end -- Handle newlines specially by sending return key if chars[index] == "\n" then hs.eventtap.keyStroke({}, "return") else hs.eventtap.keyStrokes(chars[index]) end if index < #chars then local delay = min_delay if max_delay > min_delay then -- Use squared random for skewed distribution -- Most delays will be near min_delay, higher delays are rarer local rand_squared = math.random() ^ 2 delay = min_delay + rand_squared * (max_delay - min_delay) end hs.timer.doAfter(delay, function() typeNextChar(index + 1) end) end end typeNextChar(1) else -- Fast typing without delays hs.eventtap.keyStrokes(text) end end end return M