Files
hammerspoon/Spoons/Seal.spoon/seal_filesearch.lua
T
2026-05-14 18:59:23 -04:00

169 lines
5.1 KiB
Lua

--- === Seal.plugins.filesearch ===
---
--- A plugin to add file search capabilities, making Seal act as a spotlight file search
local obj = {}
obj.__index = obj
obj.__name = "seal_filesearch"
--- Seal.plugins.filesearch.fileSearchPaths
--- Variable
--- Table containing the paths to search for files
---
--- Notes:
--- * You will need to authorize hammerspoon to access the folders in this list in order for this to work.
obj.fileSearchPaths = {"~/", "~/Downloads", "~/Documents", "~/Movies", "~/Desktop", "~/Music", "~/Pictures"}
--- Seal.plugins.filesearch.maxResults
--- Variable
--- Maximum number of results to display
obj.maxQueryResults = 40
--- Seal.plugins.filesearch.displayResultsTimeout
--- Variable
--- Maximum time to wait before displaying the results
--- Defaults to 0.2s (200ms).
---
--- Notes:
--- * higher value might give you more results but will give a less snappy experience
obj.displayResultsTimeout = 0.2
-- Variables
obj.currentQuery = nil
obj.currentQueryResults = {}
obj.currentQueryResultsDisplayed = false
obj.showQueryResultsTimer = nil
obj.spotlight = hs.spotlight.new()
-- hammerspoon passes .* as empty query
EMPTY_QUERY = ".*"
-- Private functions
local stopCurrentSearch = function()
if obj.spotlight:isRunning() then
obj.spotlight:stop()
end
if obj.showQueryResultsTimer ~= nil and obj.showQueryResultsTimer:running() then
obj.showQueryResultsTimer:stop()
end
end
local displayQueryResults = function()
stopCurrentSearch()
if not obj.currentQueryResultsDisplayed then
obj.currentQueryResultsDisplayed = true
-- we force seal to refresh the choices so we can serve the real query results
obj.seal.chooser:refreshChoicesCallback()
end
end
local buildSpotlightQuery = function(query)
local queryWords = hs.fnutils.split(query, "%s+")
local searchFilters = hs.fnutils.map(queryWords, function(word)
return [[kMDItemFSName like[c] "*]] .. word .. [[*"]]
end)
local spotligthQuery = table.concat(searchFilters, [[ && ]])
return spotligthQuery
end
local convertSpotlightResultToQueryResult = function(item)
local icon = hs.image.iconForFile(item.kMDItemPath)
local bundleID = item.kMDItemCFBundleIdentifier
if (not icon) and (bundleID) then
icon = hs.image.imageFromAppBundle(bundleID)
end
return {
text = item.kMDItemDisplayName,
subText = item.kMDItemPath,
path = item.kMDItemPath,
uuid = obj.__name .. "__" .. (bundleID or item.kMDItemDisplayName),
plugin = obj.__name,
type = "open",
image = icon
}
end
local updateQueryResults = function(items)
for _, item in ipairs(items) do
if #obj.currentQueryResults >= obj.maxQueryResults then
break
end
table.insert(obj.currentQueryResults, convertSpotlightResultToQueryResult(item))
end
end
local handleSpotlightCallback = function(_, msg, info)
if msg == "inProgress" and info.kMDQueryUpdateAddedItems ~= nil then
updateQueryResults(info.kMDQueryUpdateAddedItems)
end
if msg == "didFinish" or #obj.currentQueryResults >= obj.maxQueryResults then
displayQueryResults()
end
end
-- Public methods
function obj:commands()
return {
filesearch = {
cmd = "'",
fn = obj.fileSearch,
name = "Search file",
description = "Search file",
plugin = obj.__name
}
}
end
function obj:bare()
return nil
end
function obj.completionCallback(rowInfo)
if rowInfo["type"] == "open" 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
end
end
function obj.fileSearch(query)
stopCurrentSearch()
if query == EMPTY_QUERY then
obj.currentQuery = ""
obj.currentQueryResults = {}
return {}
end
if query ~= obj.currentQuery then
-- Seal want the results synchronously, but spotlight will return then asynchronously
-- to workaround that, we launch the spotlight search in the background and
-- return the previous results (so that Seal doesn't change the current results list)
-- We force a refresh later once we have the results
local previousResults = obj.currentQueryResults
obj.currentQuery = query
obj.currentQueryResults = {}
obj.currentQueryResultsDisplayed = false
obj.spotlight:queryString(buildSpotlightQuery(query)):start()
obj.showQueryResultsTimer = hs.timer.doAfter(obj.displayResultsTimeout, displayQueryResults)
return previousResults
else
-- If we are here, it's mean the force refreshed has been triggered after receving spotlight results
-- we just return the results we accumulated from spotlight
return obj.currentQueryResults
end
end
obj.spotlight:searchScopes(obj.fileSearchPaths):callbackMessages("inProgress", "didFinish"):setCallback(
handleSpotlightCallback)
return obj