Files
2026-05-14 18:59:23 -04:00

237 lines
6.8 KiB
Lua

--- === Seal.plugins.apps ===
---
--- A plugin to add launchable apps/scripts, making Seal act as a launch bar
local obj = {}
obj.__index = obj
obj.__name = "seal_apps"
obj.appCache = {}
--- Seal.plugins.apps.appSearchPaths
--- Variable
--- Table containing the paths to search for launchable items
---
--- Notes:
--- * If you change this, you will need to call `spoon.Seal.plugins.apps:restart()` to force Spotlight to search for new items.
obj.appSearchPaths = {
"/Applications",
"/System/Applications",
"~/Applications",
"/Developer/Applications",
"/Applications/Xcode.app/Contents/Applications",
"/System/Library/PreferencePanes",
"/Library/PreferencePanes",
"~/Library/PreferencePanes",
"/System/Library/CoreServices/Applications",
"/System/Library/CoreServices/",
"/usr/local/Cellar",
"/Library/Scripts",
"~/Library/Scripts"
}
local modifyNameMap = function(info, add)
for _, item in ipairs(info) do
icon = nil
local displayname = item.kMDItemDisplayName or hs.fs.displayName(item.kMDItemPath)
displayname = displayname:gsub("%.app$", "", 1)
if string.find(item.kMDItemPath, "%.prefPane$") then
displayname = displayname .. " preferences"
if add then
icon = hs.image.iconForFile(item.kMDItemPath)
end
end
if add then
bundleID = item.kMDItemCFBundleIdentifier
if (not icon) and (bundleID) then
icon = hs.image.imageFromAppBundle(bundleID)
end
obj.appCache[displayname] = {
path = item.kMDItemPath,
bundleID = bundleID,
icon = icon
}
else
obj.appCache[displayname] = nil
end
end
end
local updateNameMap = function(obj, msg, info)
if info then
-- all three can occur in either message, so check them all!
if info.kMDQueryUpdateAddedItems then modifyNameMap(info.kMDQueryUpdateAddedItems, true) end
if info.kMDQueryUpdateChangedItems then modifyNameMap(info.kMDQueryUpdateChangedItems, true) end
if info.kMDQueryUpdateRemovedItems then modifyNameMap(info.kMDQueryUpdateRemovedItems, false) end
else
-- shouldn't happen for didUpdate or inProgress
print("~~~ userInfo from SpotLight was empty for " .. msg)
end
end
--- Seal.plugins.apps:start()
--- Method
--- Starts the Spotlight app searcher
---
--- Parameters:
--- * None
---
--- Returns:
--- * None
---
--- Notes:
--- * This is called automatically when the plugin is loaded
function obj:start()
obj.spotlight = hs.spotlight.new():queryString([[ (kMDItemContentType = "com.apple.application-bundle") || (kMDItemContentType = "com.apple.systempreference.prefpane") || (kMDItemContentType = "com.apple.applescript.text") || (kMDItemContentType = "com.apple.applescript.script") ]])
:callbackMessages("didUpdate", "inProgress")
:setCallback(updateNameMap)
:searchScopes(obj.appSearchPaths)
:start()
end
--- Seal.plugins.apps:stop()
--- Method
--- Stops the Spotlight app searcher
---
--- Parameters:
--- * None
---
--- Returns:
--- * None
function obj:stop()
obj.spotlight:stop()
obj.spotlight = nil
obj.appCache = {}
end
--- Seal.plugins.apps:restart()
--- Method
--- Restarts the Spotlight app searcher
---
--- Parameters:
--- * None
---
--- Returns:
--- * None
function obj:restart()
self:stop()
self:start()
end
hs.application.enableSpotlightForNameSearches(true)
obj:start()
function obj:commands()
return {kill = {
cmd = "kill",
fn = obj.choicesKillCommand,
plugin = obj.__name,
name = "Kill",
description = "Kill an application"
},
reveal = {
cmd = "reveal",
fn = obj.choicesRevealCommand,
plugin = obj.__name,
name = "Reveal",
description = "Reveal an application in the Finder"
}
}
end
function obj:bare()
return self.choicesApps
end
function obj.choicesApps(query)
local choices = {}
if query == nil or query == "" then
return choices
end
for name,app in pairs(obj.appCache) do
if string.match(name:lower(), query:lower()) then
local choice = {}
local instances = {}
if app["bundleID"] then
instances = hs.application.applicationsForBundleID(app["bundleID"])
end
if #instances > 0 then
choice["text"] = name .. " (Running)"
else
choice["text"] = name
end
choice["subText"] = app["path"]
if app["icon"] then
choice["image"] = app["icon"]
end
choice["path"] = app["path"]
choice["uuid"] = obj.__name.."__"..(app["bundleID"] or name)
choice["plugin"] = obj.__name
choice["type"] = "launchOrFocus"
table.insert(choices, choice)
end
end
return choices
end
function obj.choicesKillCommand(query)
local choices = {}
if query == nil then
return choices
end
local apps = hs.application.runningApplications()
for k, app in pairs(apps) do
local name = app:name()
if string.match(name:lower(), query:lower()) and app:mainWindow() then
local choice = {}
choice["text"] = "Kill "..name
choice["subText"] = app:path().." PID: "..app:pid()
choice["pid"] = app:pid()
choice["plugin"] = obj.__name
choice["type"] = "kill"
choice["image"] = hs.image.imageFromAppBundle(app:bundleID())
table.insert(choices, choice)
end
end
return choices
end
function obj.choicesRevealCommand(query)
local choices = {}
if query == nil then
return choices
end
local apps = obj.choicesApps(query)
for k, app in pairs(apps) do
local name = app.text
if string.match(name:lower(), query:lower()) then
local choice = {}
choice["text"] = "Reveal "..name
choice["path"] = app.path
choice["subText"] = app.path
choice["plugin"] = obj.__name
choice["type"] = "reveal"
if app.image then
choice["image"] = app.image
end
table.insert(choices, choice)
end
end
return choices
end
function obj.completionCallback(rowInfo)
if rowInfo["type"] == "launchOrFocus" then
if string.find(rowInfo["path"], "%.applescript$") or string.find(rowInfo["path"], "%.scpt$") then
hs.task.new("/usr/bin/osascript", nil, { rowInfo["path"] }):start()
else
hs.task.new("/usr/bin/open", nil, { rowInfo["path"] }):start()
end
elseif rowInfo["type"] == "kill" then
hs.application.get(rowInfo["pid"]):kill()
elseif rowInfo["type"] == "reveal" then
hs.osascript.applescript(string.format([[tell application "Finder" to reveal (POSIX file "%s")]], rowInfo["path"]))
hs.application.launchOrFocus("Finder")
end
end
return obj