--- === Seal === --- --- Pluggable launch bar --- --- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/Seal.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/Seal.spoon.zip) --- --- Seal includes a number of plugins, which you can choose to load (see `:loadPlugins()` below): --- * apps : Launch applications by name --- * calc : Simple calculator --- * rot13 : Apply ROT13 substitution cipher --- * safari_bookmarks : Open Safari bookmarks (this is broken since at least High Sierra) --- * screencapture : Lets you take screenshots in various ways --- * urlformats : User defined URL formats to open --- * useractions : User defined custom actions --- * vpn : Connect and disconnect VPNs (currently supports Viscosity and macOS system preferences)A local obj = {} obj.__index = obj -- Metadata obj.name = "Seal" obj.version = "1.0" obj.author = "Chris Jones " obj.homepage = "https://github.com/Hammerspoon/Spoons" obj.license = "MIT - https://opensource.org/licenses/MIT" obj.chooser = nil obj.hotkeyShow = nil obj.hotkeyToggle = nil obj.plugins = {} obj.commands = {} obj.queryChangedTimer = nil obj.spoonPath = hs.spoons.scriptPath() --- Seal.queryChangedTimerDuration --- Variable --- Time between the last keystroke and the start of the recalculation of the choices to display, in seconds. --- --- Notes: --- * Defaults to 0.02s (20ms). obj.queryChangedTimerDuration = 0.02 --- Seal.plugin_search_paths --- Variable --- List of directories where Seal will look for plugins. Defaults to `~/.hammerspoon/seal_plugins/` and the Seal Spoon directory. obj.plugin_search_paths = { hs.configdir .. "/seal_plugins", obj.spoonPath } --- Seal:refreshCommandsForPlugin(plugin_name) --- Method --- Refresh the list of commands provided by the given plugin. --- --- Parameters: --- * plugin_name - the name of the plugin. Should be the name as passed to `loadPlugins()` or `loadPluginFromFile`. --- --- Returns: --- * The Seal object --- --- Notes: --- * Most Seal plugins expose a static list of commands (if any), which are registered at the time the plugin is loaded. This method is used for plugins which expose a dynamic or changing (e.g. depending on configuration) list of commands. function obj:refreshCommandsForPlugin(plugin_name) plugin = self.plugins[plugin_name] if plugin.commands then for cmd,cmdInfo in pairs(plugin:commands()) do if not self.commands[cmd] then print("-- Adding Seal command: "..cmd) self.commands[cmd] = cmdInfo end end end return self end --- Seal:refreshAllCommands() --- Method --- Refresh the list of commands provided by all the currently loaded plugins. --- --- Parameters: --- * None --- --- Returns: --- * The Seal object --- --- Notes: --- * Most Seal plugins expose a static list of commands (if any), which are registered at the time the plugin is loaded. This method is used for plugins which expose a dynamic or changing (e.g. depending on configuration) list of commands. function obj:refreshAllCommands() for p, _ in pairs(self.plugins) do self:refreshCommandsForPlugin(p) end return self end --- Seal:loadPluginFromFile(plugin_name, file) --- Method --- Loads a plugin from a given file --- --- Parameters: --- * plugin_name - the name of the plugin, without "seal_" at the beginning or ".lua" at the end --- * file - the file where the plugin code is stored. --- --- Returns: --- * The Seal object if the plugin was successfully loaded, `nil` otherwise --- --- Notes: --- * You should normally use `Seal:loadPlugins()`. This method allows you to load plugins --- from non-standard locations and is mostly a development interface. --- * Some plugins may immediately begin doing background work (e.g. Spotlight searches) function obj:loadPluginFromFile(plugin_name, file) local f,err = loadfile(file) if f~= nil then local plugin = f() plugin.seal = self self.plugins[plugin_name] = plugin self:refreshCommandsForPlugin(plugin_name) return self else return nil end end --- Seal:loadPlugins(plugins) --- Method --- Loads a list of Seal plugins --- --- Parameters: --- * plugins - A list containing the names of plugins to load --- --- Returns: --- * The Seal object --- --- Notes: --- * The plugins live inside the Seal.spoon directory --- * The plugin names in the list, should not have `seal_` at the start, or `.lua` at the end --- * Some plugins may immediately begin doing background work (e.g. Spotlight searches) function obj:loadPlugins(plugins) self.chooser = hs.chooser.new(self.completionCallback) self.chooser:choices(self.choicesCallback) self.chooser:queryChangedCallback(self.queryChangedCallback) for k,plugin_name in pairs(plugins) do local loaded=nil print("-- Loading Seal plugin: " .. plugin_name) for _,dir in ipairs(self.plugin_search_paths) do if obj.plugins[plugin_name] == nil then local file = dir .. "/seal_" .. plugin_name .. ".lua" loaded = (self:loadPluginFromFile(plugin_name, file) ~= nil) end end if (not loaded) then hs.showError(string.format("Error: could not find Seal plugin %s in any of the load paths %s", plugin_name, hs.inspect(self.plugin_search_paths))) end end return self end --- Seal:bindHotkeys(mapping) --- Method --- Binds hotkeys for Seal --- --- Parameters: --- * mapping - A table containing hotkey modifier/key details for the following (optional) items: --- * show - This will cause Seal's UI to be shown --- * toggle - This will cause Seal's UI to be shown or hidden depending on its current state --- --- Returns: --- * The Seal object function obj:bindHotkeys(mapping) if (self.hotkeyShow) then self.hotkeyShow:delete() end if (self.hotkeyToggle) then self.hotkeyToggle:delete() end if mapping["show"] ~= nil then local showMods = mapping["show"][1] local showKey = mapping["show"][2] self.hotkeyShow = hs.hotkey.new(showMods, showKey, function() self:show() end) end if mapping["toggle"] ~= nil then local toggleMods = mapping["toggle"][1] local toggleKey = mapping["toggle"][2] self.hotkeyToggle = hs.hotkey.new(toggleMods, toggleKey, function() self:toggle() end) end return self end --- Seal:start() --- Method --- Starts Seal --- --- Parameters: --- * None --- --- Returns: --- * The Seal object function obj:start() print("-- Starting Seal") if self.hotkeyShow then self.hotkeyShow:enable() end if self.hotkeyToggle then self.hotkeyToggle:enable() end return self end --- Seal:stop() --- Method --- Stops Seal --- --- Parameters: --- * None --- --- Returns: --- * The Seal object --- --- Notes: --- * Some Seal plugins will continue performing background work even after this call (e.g. Spotlight searches) function obj:stop() print("-- Stopping Seal") self.chooser:hide() if self.hotkeyShow then self.hotkeyShow:disable() end if self.hotkeyToggle then self.hotkeyToggle:disable() end return self end --- Seal:show(query) --- Method --- Shows the Seal UI --- --- Parameters: --- * query - An optional string to pre-populate the query box with --- --- Returns: --- * None --- --- Notes: --- * This may be useful if you wish to show Seal in response to something other than its hotkey function obj:show(query) self.chooser:show() if query then self.chooser:query(query) end return self end --- Seal:toggle(query) --- Method --- Shows or hides the Seal UI --- --- Parameters: --- * query - An optional string to pre-populate the query box with --- --- Returns: --- * None function obj:toggle(query) if self.chooser:isVisible() then self.chooser:hide() else self:show(query) end return self end function obj.completionCallback(rowInfo) if rowInfo == nil then return end if rowInfo["type"] == "plugin_cmd" then obj.chooser:query(rowInfo["cmd"]) return end for k,plugin in pairs(obj.plugins) do if plugin.__name == rowInfo["plugin"] then plugin.completionCallback(rowInfo) break end end end function obj.choicesCallback() -- TODO: Sort each of these clusters of choices, alphabetically choices = {} query = obj.chooser:query() cmd = nil query_words = {} if tostring(query):find("^%s*$") ~= nil then return choices end for word in string.gmatch(query, "%S+") do if cmd == nil then cmd = word else table.insert(query_words, word) end end query_words = table.concat(query_words, " ") -- First get any direct command matches for _,cmdInfo in pairs(obj.commands) do cmd_fn = cmdInfo["fn"] if cmd:lower() == cmdInfo["cmd"]:lower() then if (query_words or "") == "" then query_words = ".*" end fn_choices = cmd_fn(query_words) if fn_choices ~= nil then for j,choice in pairs(fn_choices) do table.insert(choices, choice) end end end end -- Now get any bare matches for k,plugin in pairs(obj.plugins) do bare = plugin:bare() if bare then for i,choice in pairs(bare(query)) do table.insert(choices, choice) end end end -- Now add in any matching commands -- TODO: This only makes sense to do if we can select the choice without dismissing the chooser, which requires changes to HSChooser for command,cmdInfo in pairs(obj.commands) do if string.match(command, query) and #query_words == 0 then choice = {} choice["text"] = cmdInfo["name"] choice["subText"] = cmdInfo["description"] choice["type"] = "plugin_cmd" table.insert(choices,choice) end end return choices end function obj.queryChangedCallback(query) if obj.queryChangedTimer then obj.queryChangedTimer:stop() end obj.queryChangedTimer = hs.timer.doAfter(obj.queryChangedTimerDuration, function() obj.chooser:refreshChoicesCallback() end) end return obj --- === Seal.plugins === --- --- Various APIs for Seal plugins -- This isn't really shown, but it's necessary to force Seal.plugins.html to render --- Seal.plugins --- Constant --- This is a table containing all of the loaded plugins for Seal. You should interact with it only via documented API that the plugins expose.