mirror of
https://github.com/jimeh/dotfiles.git
synced 2026-02-19 06:26:39 +00:00
feat(hammerspoon/app_toggle): enable multi-app toggles
A multi-app toggle is a keybinding which is configured to toggle 2 or more applications. This is intended as a context-ish-aware toggle, as it will only toggle the most recently focused application. This essentially enables you to bind a category/class of applications to a single hotkey, and whichever of the apps that's running and was most recently focused is the one that will be toggled.
This commit is contained in:
@@ -1,32 +1,14 @@
|
||||
-- luacheck: read_globals hs
|
||||
--- === app_toggle ===
|
||||
---
|
||||
--- A Hammerspoon module for toggling between specified applications using
|
||||
--- hotkeys.
|
||||
---
|
||||
--- This module allows you to bind a hotkey to switch focus between specific
|
||||
--- applications and show/hide them.
|
||||
|
||||
local obj = {}
|
||||
|
||||
function obj:bind (mods, key, name, path)
|
||||
hs.hotkey.bind(mods, key, self:toggleFn(name, path))
|
||||
end
|
||||
|
||||
function obj:toggleFn (name, path)
|
||||
return function ()
|
||||
self:toggle(name, path)
|
||||
end
|
||||
end
|
||||
|
||||
function obj:toggle (name, path)
|
||||
local app = self.findRunningApp(name, path)
|
||||
|
||||
if app == nil then
|
||||
return hs.application.open(path or name)
|
||||
end
|
||||
|
||||
if app == hs.application.frontmostApplication() then
|
||||
return app:hide()
|
||||
end
|
||||
|
||||
return app:activate()
|
||||
end
|
||||
|
||||
function obj.findRunningApp (name, path)
|
||||
local function findRunningApp(name, path)
|
||||
for _, app in ipairs(hs.application.runningApplications()) do
|
||||
if app:name() == name and (path == nil or path == app:path()) then
|
||||
return app
|
||||
@@ -34,5 +16,127 @@ function obj.findRunningApp (name, path)
|
||||
end
|
||||
end
|
||||
|
||||
local focusTimes = {}
|
||||
|
||||
local function focusWatcher(_, eventType, appObject)
|
||||
if eventType == hs.application.watcher.activated then
|
||||
focusTimes[appObject:bundleID()] = hs.timer.secondsSinceEpoch()
|
||||
end
|
||||
end
|
||||
|
||||
local appWatcher = hs.application.watcher.new(focusWatcher)
|
||||
obj.started = false
|
||||
|
||||
function obj:start()
|
||||
if obj.started then
|
||||
return
|
||||
end
|
||||
|
||||
appWatcher:start()
|
||||
obj.started = true
|
||||
end
|
||||
|
||||
function obj:stop()
|
||||
if not obj.started then
|
||||
return
|
||||
end
|
||||
|
||||
appWatcher:stop()
|
||||
obj.started = false
|
||||
end
|
||||
|
||||
--- app_toggle:bind(mods, key, ...)
|
||||
--- Method
|
||||
--- Binds a hotkey to toggle between the specified applications.
|
||||
---
|
||||
--- Parameters:
|
||||
--- * mods - A table with the modifiers for the hotkey
|
||||
--- * key - A string with the key for the hotkey
|
||||
--- * ... - A list of tables, each containing an application name and an
|
||||
--- optional path
|
||||
function obj:bind(mods, key, ...)
|
||||
local apps = { ... }
|
||||
if #apps > 1 then
|
||||
self:start()
|
||||
end
|
||||
|
||||
hs.hotkey.bind(mods, key, self:toggleFn(apps))
|
||||
end
|
||||
|
||||
--- app_toggle:toggleFn(apps)
|
||||
--- Method
|
||||
--- Creates and returns a function that toggles between the specified
|
||||
--- applications via app_toggle:toggle() when called.
|
||||
---
|
||||
--- Parameters:
|
||||
--- * apps - A table containing application configurations. Each configuration
|
||||
--- is a table with an application name at the first index and an
|
||||
--- optional path at the second index.
|
||||
---
|
||||
--- Returns:
|
||||
--- * A function that, when called, toggles between the specified applications.
|
||||
---
|
||||
--- Example:
|
||||
--- local toggleApps = obj:toggleFn({{"Firefox"}, {"Safari"}})
|
||||
--- hs.hotkey.bind({"cmd", "ctrl"}, "b", toggleApps)
|
||||
---
|
||||
--- Notes:
|
||||
--- * The returned function can be used as a callback for hotkey bindings or
|
||||
--- other event-driven scenarios.
|
||||
function obj:toggleFn(apps)
|
||||
return function()
|
||||
self:toggle(apps)
|
||||
end
|
||||
end
|
||||
|
||||
--- app_toggle:toggle(apps)
|
||||
--- Method
|
||||
--- Toggles focus/visibility specified applications.
|
||||
---
|
||||
--- Parameters:
|
||||
--- * apps - A table containing application configurations. Each configuration
|
||||
--- is a table with an application name at the first index and an
|
||||
--- optional path at the second index.
|
||||
---
|
||||
--- Notes:
|
||||
--- * If none of the specified applications are running, the function attempts
|
||||
--- to launch the first application in the list.
|
||||
--- * If the most recently focused application in the list is the current
|
||||
--- frontmost application, it will be hidden. Otherwise, the most recently
|
||||
--- focused application will be brought to the front.
|
||||
function obj:toggle(apps)
|
||||
local runningApps = {}
|
||||
local mostRecentApp = nil
|
||||
local mostRecentTime = -1
|
||||
|
||||
for _, appInfo in ipairs(apps) do
|
||||
local name, path = appInfo[1], appInfo[2]
|
||||
local app = findRunningApp(name, path)
|
||||
if app then
|
||||
table.insert(runningApps, app)
|
||||
local focusTime = focusTimes[app:bundleID()] or 0
|
||||
if focusTime > mostRecentTime then
|
||||
mostRecentTime = focusTime
|
||||
mostRecentApp = app
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if #runningApps == 0 then
|
||||
local app = apps[1]
|
||||
local status, err = pcall(hs.application.open, app[2] or app[1])
|
||||
if not status then
|
||||
hs.alert.show('Failed to open ' .. (app[2] or app[1]) .. ': ' .. err)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if mostRecentApp == hs.application.frontmostApplication() then
|
||||
return mostRecentApp:hide()
|
||||
end
|
||||
|
||||
return mostRecentApp:activate()
|
||||
end
|
||||
|
||||
-- the end
|
||||
return obj
|
||||
|
||||
@@ -3,20 +3,32 @@ local obj = {}
|
||||
function obj.init()
|
||||
local apptoggle = require('app_toggle')
|
||||
|
||||
apptoggle:bind({'cmd', 'alt', 'ctrl'}, 'A', 'Activity Monitor')
|
||||
apptoggle:bind({'cmd', 'ctrl'}, '2', 'ChatGPT')
|
||||
apptoggle:bind({'cmd', 'ctrl'}, '4', 'Microsoft Edge')
|
||||
apptoggle:bind({'cmd', 'ctrl'}, 'A', 'Messages')
|
||||
apptoggle:bind({'cmd', 'ctrl'}, 'B', 'TablePlus')
|
||||
apptoggle:bind({'cmd', 'ctrl'}, 'C', 'Calendar')
|
||||
apptoggle:bind({'cmd', 'ctrl'}, 'D', 'Mailplane')
|
||||
apptoggle:bind({'cmd', 'ctrl'}, 'E', 'Emacs', '/Applications/Emacs.app')
|
||||
apptoggle:bind({'cmd', 'ctrl'}, 'F', 'Element Nightly')
|
||||
apptoggle:bind({'cmd', 'ctrl'}, 'S', 'Music')
|
||||
apptoggle:bind({'cmd', 'ctrl'}, 'T', 'Discord PTB')
|
||||
apptoggle:bind({'cmd', 'ctrl'}, 'W', 'WhatsApp')
|
||||
apptoggle:bind({'cmd', 'ctrl'}, 'X', 'Notion')
|
||||
apptoggle:bind({'cmd', 'ctrl'}, 'Z', 'Slack')
|
||||
apptoggle:bind({ 'cmd', 'alt', 'ctrl' }, 'A', { 'Activity Monitor' })
|
||||
apptoggle:bind({ 'cmd', 'ctrl' }, '4', { 'Microsoft Edge' })
|
||||
apptoggle:bind({ 'cmd', 'ctrl' }, 'A', { 'Messages' })
|
||||
apptoggle:bind({ 'cmd', 'ctrl' }, 'C', { 'Calendar' })
|
||||
apptoggle:bind({ 'cmd', 'ctrl' }, 'D', { 'Mailplane' })
|
||||
apptoggle:bind({ 'cmd', 'ctrl' }, 'F', { 'Element Nightly' })
|
||||
apptoggle:bind({ 'cmd', 'ctrl' }, 'S', { 'Music' })
|
||||
apptoggle:bind({ 'cmd', 'ctrl' }, 'T', { 'Discord PTB' })
|
||||
apptoggle:bind({ 'cmd', 'ctrl' }, 'X', { 'Notion' })
|
||||
apptoggle:bind({ 'cmd', 'ctrl' }, 'Z', { 'Slack' })
|
||||
|
||||
apptoggle:bind({ 'cmd', 'ctrl' }, '2',
|
||||
{ 'ChatGPT X' },
|
||||
{ 'ChatGPT' }
|
||||
)
|
||||
apptoggle:bind({ 'cmd', 'ctrl' }, 'B',
|
||||
{ 'TablePlus' },
|
||||
{ 'Lens' }
|
||||
)
|
||||
|
||||
apptoggle:bind({ 'cmd', 'ctrl' }, 'E',
|
||||
{ 'Emacs', '/Applications/Emacs.app' }
|
||||
)
|
||||
apptoggle:bind({ 'cmd', 'ctrl' }, 'W',
|
||||
{ 'Code', '/Applications/Visual Studio Code.app' }
|
||||
)
|
||||
end
|
||||
|
||||
return obj
|
||||
|
||||
Reference in New Issue
Block a user