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

287 lines
9.2 KiB
Lua

--- ==== Seal.plugins.useractions ====
---
--- Allow accessing user-defined bookmarks and arbitrary actions from Seal.
---
local obj = {}
obj.__index = obj
obj.__basename = "useractions"
obj.__name = "seal_" .. obj.__basename
obj.default_icon = hs.image.imageFromName(hs.image.systemImageNames.ActionTemplate)
--- Seal.plugins.useractions.actions
--- Variable
---
--- Notes:
--- * A table containing the definitions of static user-defined actions. Each entry is indexed by the name of the entry as it will be shown in the chooser. Its value is a table which can have the following keys (one of `fn` or `url` is required. If both are provided, `url` is ignored):
--- * fn - A function which will be called when the entry is selected. The function receives no arguments.
--- * url - A URL which will be opened when the entry is selected. Can also be non-HTTP URLs, such as `mailto:` or other app-specific URLs.
--- * description - (optional) A string or `hs.styledtext` object that will be shown underneath the main text of the choice.
--- * icon - (optional) An `hs.image` object that will be shown next to the entry in the chooser. If not provided, `Seal.plugins.useractions.default_icon` is used. For `url` bookmarks, it can be set to `"favicon"` to fetch and use the website's favicon.
--- * keyword - (optional) A command by which this action will be invoked, effectively turning it into a Seal command. Any arguments passed to the command will be handled as follows:
--- * For `fn` actions, passed as an argument to the function
--- * For `url` actions, substituted into the URL, taking the place of any occurrences of `${query}`.
--- * hotkey - (optional) A hotkey specification in the form `{ modifiers, key }` by which this action can be invoked.
--- * Example configuration:
--- ```
--- spoon.Seal:loadPlugins({"useractions"})
--- spoon.Seal.plugins.useractions.actions =
--- {
--- ["Hammerspoon docs webpage"] = {
--- url = "http://hammerspoon.org/docs/",
--- icon = hs.image.imageFromName(hs.image.systemImageNames.ApplicationIcon),
--- description = "Open Hammerspoon documentation",
--- hotkey = { hyper, "h" },
--- },
--- ["Leave corpnet"] = {
--- fn = function()
--- spoon.WiFiTransitions:processTransition('foo', 'corpnet01')
--- end,
--- },
--- ["Arrive in corpnet"] = {
--- fn = function()
--- spoon.WiFiTransitions:processTransition('corpnet01', 'foo')
--- end,
--- },
--- ["Translate using Leo"] = {
--- url = "http://dict.leo.org/ende/index_de.html#/search=${query}",
--- icon = 'favicon',
--- keyword = "leo",
--- },
--- ["Tell me something"] = {
--- keyword = "tellme",
--- fn = function(str) hs.alert.show(str) end,
--- }
--- ```
obj.actions = {}
--- Seal.plugins.useractions.get_favicon
--- Variable
---
--- If `true`, attempt to obtain the favicon for URLs added through the `add` command, and use it in the chooser. Defaults to `true`
obj.get_favicon = true
-- Internal functions for storing/retrieving bookmarks in the settings database.
local getSetting = function(label, default) return hs.settings.get(obj.__name.."."..label) or default end
local setSetting = function(label, value) hs.settings.set(obj.__name.."."..label, value); return value end
-- Internal variable where the dynamically-added bookmarks are kept
obj.stored_actions = getSetting('stored_actions', {})
-- Internal variable where the merged list of bookmarks/actions is kept
obj.all_actions = nil
function update_all_actions()
if (obj.all_actions == nil) then
obj.all_actions = {}
for k,v in pairs(obj.actions) do obj.all_actions[k] = hs.fnutils.copy(v) end
for k,v in pairs(obj.stored_actions) do
obj.all_actions[k] = hs.fnutils.copy(v)
if v.encoded_icon then
obj.all_actions[k].icon = hs.image.imageFromURL(v.encoded_icon)
end
end
end
end
function obj:commands()
local cmds={
add = {
cmd = "add",
fn = obj.choicesAddURLCommand,
name = "Add URL",
description = "Add URL to bookmarks",
plugin = obj.__name
},
del = {
cmd = "del",
fn = obj.choicesDelURLCommand,
name = "Delete URL",
description = "Delete URL from bookmarks",
plugin = obj.__name
}
}
local hotkeys_def = {}
local hotkeys_map = {}
local any_hotkeys = false
for k,v in pairs(self.actions or {}) do
if v.keyword and (not cmds[v.keyword]) then
if v.url ~= nil and v.icon == 'favicon' then
v.icon = obj.favIcon(v.url)
end
cmds[v.keyword] = {
cmd = v.keyword,
fn = hs.fnutils.partial(obj.choicesActionKeyword, k, v),
name = k,
icon = v.icon,
plugin = obj.__name
}
end
if v.hotkey then
local choice = obj.buildChoice(k,v)
hotkeys_def[k] = hs.fnutils.partial(obj.completionCallback, choice)
hotkeys_map[k] = v.hotkey
any_hotkeys = true
end
end
if any_hotkeys then
hs.spoons.bindHotkeysToSpec(hotkeys_def, hotkeys_map)
end
return cmds
end
function obj:bare()
return self.bareActions
end
function obj.buildChoice(action, v)
local icon, kind
local choice=nil
if type(v) == 'table' then
if v.fn then
kind = 'runFunction'
elseif v.url then
kind = 'openURL'
if v.icon == 'favicon' then
v.icon = obj.favIcon(v.url)
end
end
icon = v.icon or obj.default_icon
choice = {}
choice.text = action
choice.type = kind
choice.plugin = obj.__name
choice.image = icon
if v.description then
choice.subText = v.description
end
end
return choice
end
function obj.bareActions(query)
local choices = {}
if query == nil or query == "" then
return choices
end
update_all_actions()
obj.seal:refreshCommandsForPlugin(obj.__basename)
for action,v in pairs(obj.all_actions) do
if string.match(action:lower(), query:lower()) then
local choice = obj.buildChoice(action, v)
if choice then
table.insert(choices, choice)
end
end
end
return choices
end
function obj.favIcon(url)
local query=string.format("http://www.google.com/s2/favicons?sz=64&domain_url=%s", hs.http.encodeForQuery(url))
return hs.image.imageFromURL(query)
end
function obj.choicesAddURLCommand(query)
local choices = {}
if query == ".*" then
query = "<url> <name>"
end
local url,name = string.match(query, "([^%s]+)%s+(.*)")
local subtext = ""
if url then
subtext = string.format("New bookmark '%s' pointing to %s", name,url)
end
local choice = {
text = "add " .. query,
subText = subtext,
url = url,
name = name,
plugin = obj.__name,
type = 'addURL',
}
table.insert(choices, choice)
return choices
end
function obj.choicesDelURLCommand(query)
local choices = {}
for k,v in pairs(obj.stored_actions) do
if string.match(k:lower(), query:lower()) or string.match(v.url:lower(), query:lower()) then
local choice = {
text = string.format("delete '%s'", k),
subText = v.url,
delKey = k,
plugin = obj.__name,
type = 'delURL',
}
if v.encoded_icon then
choice.image = hs.image.imageFromURL(v.encoded_icon)
end
table.insert(choices, choice)
end
end
return choices
end
function obj.choicesActionKeyword(action, def, query)
local choices = {}
if query == ".*" then
query = ""
end
local choice = {
text = def.keyword .. " " .. query,
subText = def.description or action,
actionname = action,
arg = query,
plugin = obj.__name,
image = def.icon,
type = 'invokeKeyword',
}
table.insert(choices, choice)
return choices
end
function obj.openURL(url)
hs.execute(string.format("/usr/bin/open '%s'", url))
end
function obj.completionCallback(row)
update_all_actions()
if row.type == 'runFunction' then
local fn = obj.all_actions[row.text].fn
fn()
elseif row.type == 'openURL' then
local url = obj.all_actions[row.text].url
obj.openURL(url)
elseif row.type == 'addURL' then
obj.stored_actions[row.name] = { url = row.url }
obj.all_actions = nil
if obj.get_favicon then
local ico=obj.favIcon(row.url)
if ico then
obj.stored_actions[row.name]['encoded_icon'] = ico:encodeAsURLString()
end
end
setSetting('stored_actions', obj.stored_actions)
elseif row.type == 'delURL' then
obj.stored_actions[row.delKey] = nil
obj.all_actions = nil
setSetting('stored_actions', obj.stored_actions)
elseif row.type == 'invokeKeyword' then
if obj.actions[row.actionname].fn then
obj.actions[row.actionname].fn(row.arg)
elseif obj.actions[row.actionname].url then
row.arg = hs.http.encodeForQuery(row.arg)
local query = row.arg:gsub("%%", "%%%%")
local url = string.gsub(obj.actions[row.actionname].url, '${query}', query)
obj.openURL(url)
end
end
end
return obj