Initial commit of Hammerspoon config
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,359 @@
|
||||
--- === 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 <cmsj@tenshu.net>"
|
||||
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.
|
||||
@@ -0,0 +1,236 @@
|
||||
--- === 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
|
||||
@@ -0,0 +1,47 @@
|
||||
local obj = {}
|
||||
obj.__index = obj
|
||||
obj.__name = "seal_calc"
|
||||
obj.icon = hs.image.imageFromAppBundle("com.apple.Calculator")
|
||||
|
||||
function obj:commands()
|
||||
return {}
|
||||
end
|
||||
|
||||
function obj:bare()
|
||||
return self.bareCalc
|
||||
end
|
||||
|
||||
function obj.bareCalc(query)
|
||||
local choices = {}
|
||||
if query == nil or query == "" then
|
||||
return choices
|
||||
end
|
||||
|
||||
-- Filter out commas and dollar signs
|
||||
query, _ = query:gsub("[%,%$]", "")
|
||||
|
||||
-- We need to determine if the query only contains mathematical calculations
|
||||
-- To do this we'll see if it matches the inverse of that set of characters
|
||||
if string.match(query, "[^%d^%.^%+^%-^/^%*^%^^ ^%(^%)]") == nil then
|
||||
local choice = {}
|
||||
local compile_result, fn = load("return " .. query)
|
||||
if type(compile_result) == "function" then
|
||||
local result = compile_result()
|
||||
choice["text"] = result
|
||||
choice["subText"] = "Copy result to clipboard"
|
||||
choice["image"] = obj.icon
|
||||
choice["plugin"] = obj.__name
|
||||
choice["type"] = "copyToClipboard"
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
end
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.completionCallback(rowInfo)
|
||||
if rowInfo["type"] == "copyToClipboard" then
|
||||
hs.pasteboard.setContents(rowInfo["text"])
|
||||
end
|
||||
end
|
||||
|
||||
return obj
|
||||
@@ -0,0 +1,168 @@
|
||||
--- === 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
|
||||
@@ -0,0 +1,182 @@
|
||||
--- ==== Seal.plugins.pasteboard ====
|
||||
---
|
||||
--- Visual, searchable pasteboard (ie clipboard) history
|
||||
local obj = {}
|
||||
obj.__index = obj
|
||||
obj.__name = "seal_pasteboard"
|
||||
obj.timer = nil
|
||||
obj.lastItem = nil
|
||||
obj.itemBuffer = {}
|
||||
obj.choices = {}
|
||||
|
||||
--- Seal.plugins.pasteboard.historySize
|
||||
--- Variable
|
||||
---
|
||||
--- The number of history items to keep. Defaults to 50
|
||||
obj.historySize = 50
|
||||
|
||||
--- Seal.plugins.pasteboard.saveHistory
|
||||
--- Variable
|
||||
---
|
||||
--- A boolean, true if Seal should automatically load/save clipboard history. Defaults to true
|
||||
obj.saveHistory = true
|
||||
|
||||
--- Seal.plugins.pasteboard.skipUTIs
|
||||
--- Variable
|
||||
---
|
||||
--- An array of UTIs to skip when saving to the history. Defaults to:
|
||||
--- ```
|
||||
--- {
|
||||
--- "de.petermaurer.TransientPasteboardType",
|
||||
--- "com.typeit4me.clipping",
|
||||
--- "Pasteboard generator type",
|
||||
--- "com.agilebits.onepassword",
|
||||
--- "org.nspasteboard.TransientType",
|
||||
--- "org.nspasteboard.ConcealedType",
|
||||
--- "org.nspasteboard.AutoGeneratedType"
|
||||
--- }
|
||||
--- ```
|
||||
obj.skipUTIs = {
|
||||
"de.petermaurer.TransientPasteboardType",
|
||||
"com.typeit4me.clipping",
|
||||
"Pasteboard generator type",
|
||||
"com.agilebits.onepassword",
|
||||
"org.nspasteboard.TransientType",
|
||||
"org.nspasteboard.ConcealedType",
|
||||
"org.nspasteboard.AutoGeneratedType"
|
||||
}
|
||||
|
||||
function obj:commands()
|
||||
return {
|
||||
pb = {
|
||||
cmd = "pb",
|
||||
fn = obj.choicesPasteboardCommand,
|
||||
name = "Pasteboard",
|
||||
description = "Pasteboard history",
|
||||
plugin = obj.__name
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function obj:bare()
|
||||
return nil
|
||||
end
|
||||
|
||||
function obj.choicesPasteboardCommand(query)
|
||||
-- Return the choices that match the query
|
||||
return hs.fnutils.filter(obj.choices, function(choice)
|
||||
return string.find(string.lower(choice["text"]), string.lower(query))
|
||||
end)
|
||||
end
|
||||
|
||||
function obj.pasteboardToChoice(item)
|
||||
local choice = {}
|
||||
|
||||
choice["uuid"] = item["uuid"]
|
||||
choice["name"] = item["text"]
|
||||
choice["text"] = item["text"]
|
||||
choice["kind"] = kind
|
||||
choice["plugin"] = obj.__name
|
||||
choice["type"] = "copy"
|
||||
choice["subText"] = ""
|
||||
|
||||
if item["uti"] then
|
||||
choice["subText"] = item["uti"]
|
||||
if hs.application.defaultAppForUTI then
|
||||
local bundleID = hs.application.defaultAppForUTI(item["uti"])
|
||||
print("Default app for " .. item["uti"] .. " :: " .. (bundleID or "(null)"))
|
||||
if bundleID then
|
||||
choice["image"] = hs.image.imageFromAppBundle(bundleID)
|
||||
end
|
||||
end
|
||||
end
|
||||
if item["dateTime"] then
|
||||
choice["subText"] = choice["subText"] .. " :: " .. item["dateTime"]
|
||||
end
|
||||
|
||||
return choice
|
||||
end
|
||||
|
||||
function obj.completionCallback(rowInfo)
|
||||
if rowInfo["type"] == "copy" then
|
||||
hs.pasteboard.setContents(rowInfo["name"])
|
||||
end
|
||||
end
|
||||
|
||||
function obj.checkPasteboard()
|
||||
local pasteboard = hs.pasteboard.getContents()
|
||||
local shouldSave = false
|
||||
|
||||
if pasteboard == nil then
|
||||
return
|
||||
end
|
||||
|
||||
if (#obj.itemBuffer == 0) or (pasteboard ~= obj.itemBuffer[#obj.itemBuffer]["text"]) then
|
||||
local currentTypes = hs.pasteboard.allContentTypes()[1]
|
||||
if currentTypes == nil then
|
||||
print("ERROR: NO PASTEBOARD CURRENT TYPES. Please file a bug so we can understand this:")
|
||||
print(hs.inspect(pasteboard))
|
||||
return
|
||||
end
|
||||
for _, aType in pairs(currentTypes) do
|
||||
for _, uti in pairs(obj.skipUTIs) do
|
||||
if uti == aType then
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
local item = {}
|
||||
item["uuid"] = hs.host.uuid()
|
||||
item["text"] = pasteboard
|
||||
item["uti"] = currentTypes[1]
|
||||
item["dateTime"] = os.date()
|
||||
|
||||
table.insert(obj.itemBuffer, item)
|
||||
table.insert(obj.choices, obj.pasteboardToChoice(item))
|
||||
|
||||
shouldSave = true
|
||||
end
|
||||
if #obj.itemBuffer > obj.historySize then
|
||||
table.remove(obj.itemBuffer, 1)
|
||||
table.remove(obj.choices, 1)
|
||||
|
||||
shouldSave = true
|
||||
end
|
||||
|
||||
if shouldSave then
|
||||
obj.save()
|
||||
end
|
||||
end
|
||||
|
||||
function obj.save()
|
||||
local json = hs.json.encode(obj.itemBuffer)
|
||||
local file = io.open(os.getenv("HOME") .. "/.hammerspoon/pasteboard_history.json", "w")
|
||||
if file then
|
||||
file:write(json)
|
||||
file:close()
|
||||
end
|
||||
end
|
||||
|
||||
function obj.load()
|
||||
local file = io.open(os.getenv("HOME") .. "/.hammerspoon/pasteboard_history.json", "r")
|
||||
if file then
|
||||
local json = hs.json.decode(file:read())
|
||||
if json then
|
||||
obj.itemBuffer = json
|
||||
|
||||
-- Convert all the items to the choice buffer
|
||||
for _, v in ipairs(obj.itemBuffer) do
|
||||
table.insert(obj.choices, obj.pasteboardToChoice(v))
|
||||
end
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
end
|
||||
|
||||
obj.load()
|
||||
if obj.timer == nil then
|
||||
obj.timer = hs.timer.doEvery(1, function() obj.checkPasteboard(obj) end)
|
||||
obj.timer:start()
|
||||
end
|
||||
|
||||
return obj
|
||||
@@ -0,0 +1,48 @@
|
||||
local obj = {}
|
||||
obj.__index = obj
|
||||
obj.__name = "seal_rot13"
|
||||
|
||||
function obj:commands()
|
||||
return {
|
||||
rot13 = {
|
||||
cmd = "rot13",
|
||||
fn = obj.rot13,
|
||||
name = "ROT13",
|
||||
description = "Apply ROT13 substitution cipher"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function obj:bare()
|
||||
return nil
|
||||
end
|
||||
|
||||
function obj.rot13(query)
|
||||
-- ROT13 implementation taken from https://rosettacode.org/wiki/Rot-13#Lua
|
||||
local a = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
local b = "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm"
|
||||
local rot13Text =
|
||||
query:gsub(
|
||||
"%a",
|
||||
function(c)
|
||||
return b:sub(a:find(c))
|
||||
end
|
||||
)
|
||||
|
||||
return {
|
||||
{
|
||||
text = rot13Text,
|
||||
subText = "Copy result to clipboard",
|
||||
plugin = obj.__name,
|
||||
type = "copyToClipboard"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function obj.completionCallback(rowInfo)
|
||||
if rowInfo["type"] == "copyToClipboard" then
|
||||
hs.pasteboard.setContents(rowInfo["text"])
|
||||
end
|
||||
end
|
||||
|
||||
return obj
|
||||
@@ -0,0 +1,90 @@
|
||||
--- === Seal.plugins.safari_bookmarks ===
|
||||
--- Access Safari bookmarks from Seal
|
||||
---
|
||||
--- Note: Apple has changed the way Safari stores bookmarks and this plugin no longer works on recent macOS releases.
|
||||
local obj = {}
|
||||
obj.__index = obj
|
||||
obj.__name = "seal_safari_bookmarks"
|
||||
obj.bookmarkCache = {}
|
||||
obj.icon = hs.image.iconForFileType("com.apple.safari.bookmark")
|
||||
|
||||
--- Seal.plugins.safari_bookmarks.always_open_with_safari
|
||||
--- Variable
|
||||
--- If `true` (default), bookmarks are always opened with Safari, otherwise they are opened with the default application using the `/usr/bin/open` command.
|
||||
obj.always_open_with_safari = true
|
||||
|
||||
local modifyNameMap = function(info, add)
|
||||
local name
|
||||
for _, item in ipairs(info) do
|
||||
name = item.kMDItemDisplayName
|
||||
if name ~= nil then
|
||||
if add then
|
||||
obj.bookmarkCache[name] = {
|
||||
url = item.kMDItemURL,
|
||||
}
|
||||
else
|
||||
obj.bookmarkCache[name] = nil
|
||||
end
|
||||
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
|
||||
|
||||
obj.spotlight = hs.spotlight.new():queryString([[ kMDItemContentType = "com.apple.safari.bookmark" ]])
|
||||
:callbackMessages("didUpdate", "inProgress")
|
||||
:setCallback(updateNameMap)
|
||||
:start()
|
||||
|
||||
function obj:commands()
|
||||
return {}
|
||||
end
|
||||
|
||||
function obj:bare()
|
||||
return self.choicesBookmarks
|
||||
end
|
||||
|
||||
function obj.choicesBookmarks(query)
|
||||
local choices = {}
|
||||
if query == nil or query == "" then
|
||||
return choices
|
||||
end
|
||||
for name,bookmark in pairs(obj.bookmarkCache) do
|
||||
url = bookmark["url"]
|
||||
if url and (string.match(name:lower(), query:lower()) or string.match(url:lower(), query:lower())) then
|
||||
local choice = {}
|
||||
local instances = {}
|
||||
choice["text"] = name
|
||||
choice["subText"] = url
|
||||
choice["url"] = url
|
||||
choice["image"] = obj.icon
|
||||
choice["uuid"] = obj.__name.."__"..name.."__"..url
|
||||
choice["plugin"] = obj.__name
|
||||
choice["type"] = "openURL"
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
end
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.completionCallback(rowInfo)
|
||||
if rowInfo["type"] == "openURL" then
|
||||
if obj.always_open_with_safari then
|
||||
hs.urlevent.openURLWithBundle(rowInfo["url"], "com.apple.Safari")
|
||||
else
|
||||
hs.execute(string.format("/usr/bin/open '%s'", rowInfo["url"]))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return obj
|
||||
@@ -0,0 +1,98 @@
|
||||
--- === Seal.plugins.screencapture ===
|
||||
---
|
||||
--- A plugin to capture the screen in various ways
|
||||
local obj = {}
|
||||
obj.__index = obj
|
||||
obj.__name = "seal_screencapture"
|
||||
|
||||
--- Seal.plugins.screencapture.showPostUI
|
||||
--- Variable
|
||||
--- Whether or not to show the screen capture UI in macOS 10.14 or later
|
||||
obj.showPostUI = true
|
||||
|
||||
local static_choices = {
|
||||
{
|
||||
text = "Capture menu",
|
||||
subText = "Show macOS screen capture menu",
|
||||
plugin = obj.__name,
|
||||
type = "screenUI"
|
||||
},
|
||||
{
|
||||
text = "Capture Screen",
|
||||
subText = "Capture the current screen",
|
||||
plugin = obj.__name,
|
||||
type = "screen"
|
||||
},
|
||||
{
|
||||
text = "Capture Screen to Clipboard",
|
||||
subText = "Capture the current screen to the clipboard",
|
||||
plugin = obj.__name,
|
||||
type = "screen_clipboard"
|
||||
},
|
||||
{
|
||||
text = "Capture Interactive",
|
||||
subText = "Draw a rectangle to capture",
|
||||
plugin = obj.__name,
|
||||
type = "interactive"
|
||||
},
|
||||
{
|
||||
text = "Capture Interactive to Clipboard",
|
||||
subText = "Draw a rectangle to capture to the clipboard",
|
||||
plugin = obj.__name,
|
||||
type = "interactive_clipboard"
|
||||
}
|
||||
}
|
||||
|
||||
function obj:commands()
|
||||
return {sc = {
|
||||
cmd = "sc",
|
||||
fn = obj.choicesScreenCaptureCommand,
|
||||
name = "Screencapture",
|
||||
description = "Capture the screen",
|
||||
plugin = obj.__name
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function obj:bare()
|
||||
return nil
|
||||
end
|
||||
|
||||
function obj.choicesScreenCaptureCommand(query)
|
||||
local choices = {}
|
||||
|
||||
for k,choice in pairs(static_choices) do
|
||||
if string.match(choice["text"]:lower(), query:lower()) then
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
end
|
||||
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.completionCallback(rowInfo)
|
||||
local filename = hs.fs.pathToAbsolute("~").."/Desktop/Screen Capture at "..os.date("!%Y-%m-%d-%T")..".png"
|
||||
local args = ""
|
||||
local scType = rowInfo["type"]
|
||||
|
||||
if scType == "screen" then
|
||||
-- Nothing required here
|
||||
elseif scType == "screen_clipboard" then
|
||||
args = "-c"
|
||||
elseif scType == "interactive" then
|
||||
args = "-i"
|
||||
elseif scType == "screenUI" then
|
||||
args = "-iU"
|
||||
elseif scType == "interactive_clipboard" then
|
||||
args = "-ci"
|
||||
end
|
||||
|
||||
if obj.showPostUI then
|
||||
args = args .. "u"
|
||||
end
|
||||
|
||||
print(hs.inspect(args))
|
||||
hs.task.new("/usr/sbin/screencapture", nil, {args, filename}):start()
|
||||
end
|
||||
|
||||
return obj
|
||||
@@ -0,0 +1,125 @@
|
||||
--- === Seal.plugins.urlformats ===
|
||||
---
|
||||
--- A plugin to quickly open URLs containing a search/query term
|
||||
--- This plugin is invoked with the `uf` keyword and requires some configuration, see `:providersTable()`
|
||||
---
|
||||
--- The way this works is by defining a set of providers, each of which contains a URL with a `%s` somewhere insert it.
|
||||
--- When the user types `uf` in Seal, followed by some more characters, those characters will be inserted into the string at the point where the `%s` is.
|
||||
---
|
||||
--- By way of an example, you could define a provider with a url like `http://bugs.mycorp.com/showBug?id=%s`, and just need to type `uf 123456` in Seal to get a quick shortcut to open the full URL.
|
||||
local obj = {}
|
||||
obj.__index = obj
|
||||
obj.__name = "seal_urlformats"
|
||||
|
||||
obj.providers = {}
|
||||
|
||||
-- Example format for providers table
|
||||
-- {
|
||||
-- rhbz = {
|
||||
-- name = "Red Hat Bugzilla",
|
||||
-- url = "https://bugzilla.redhat.com/show_bug.cgi?id=%s",
|
||||
-- },
|
||||
-- lp = {
|
||||
-- name = "Launchpad Bug",
|
||||
-- url = "https://launchpad.net/bugs/%s",
|
||||
-- },
|
||||
-- }
|
||||
|
||||
function obj:commands()
|
||||
return {uf = {
|
||||
cmd = "uf",
|
||||
fn = obj.choicesURLPart,
|
||||
name = "URL Formats",
|
||||
description = "Open a full URL with a search term",
|
||||
plugin = obj.__name
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function obj:bare()
|
||||
return obj.choicesBareURL
|
||||
end
|
||||
|
||||
function obj.choicesBareURL(query)
|
||||
local choices = {}
|
||||
if string.find(query, "://") ~= nil then
|
||||
local scheme = string.sub(query, 1, string.find(query, "://") - 1)
|
||||
local handlers = hs.urlevent.getAllHandlersForScheme(scheme)
|
||||
for _,bundleID in pairs(handlers) do
|
||||
local choice = {}
|
||||
local bundleInfo = hs.application.infoForBundleID(bundleID)
|
||||
if bundleInfo and bundleInfo["CFBundleName"] then
|
||||
choice["text"] = "Open URI with "..bundleInfo["CFBundleName"]
|
||||
choice["handler"] = bundleID
|
||||
choice["scheme"] = scheme
|
||||
choice["type"] = "launch"
|
||||
choice["url"] = query
|
||||
choice["plugin"] = obj.__name
|
||||
choice["image"] = hs.image.imageFromAppBundle(bundleID)
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
end
|
||||
end
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.choicesURLPart(query)
|
||||
--print("choicesURLPart for: "..query)
|
||||
local choices = {}
|
||||
for name,data in pairs(obj.providers) do
|
||||
local data_url = data["url"]:gsub("([^%%])%%([^s])", "%1%%%%%2")
|
||||
local full_url = string.format(data_url, query)
|
||||
local url_scheme = string.sub(full_url, 1, string.find(full_url, "://") - 1)
|
||||
local choice = {}
|
||||
choice["text"] = data["name"]
|
||||
choice["subText"] = full_url
|
||||
choice["plugin"] = obj.__name
|
||||
choice["type"] = "launch"
|
||||
choice["url"] = full_url
|
||||
choice["scheme"] = url_scheme
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.completionCallback(rowInfo)
|
||||
if rowInfo["type"] == "launch" then
|
||||
local handler = nil
|
||||
if rowInfo["handler"] == nil then
|
||||
handler = hs.urlevent.getDefaultHandler(rowInfo["scheme"])
|
||||
else
|
||||
handler = rowInfo["handler"]
|
||||
end
|
||||
hs.urlevent.openURLWithBundle(rowInfo["url"], handler)
|
||||
end
|
||||
end
|
||||
|
||||
--- Seal.plugins.urlformats:providersTable(aTable)
|
||||
--- Method
|
||||
--- Gets or sets the current providers table
|
||||
---
|
||||
--- Parameters:
|
||||
--- * aTable - An optional table of providers, which must contain the following keys:
|
||||
--- * name - A string naming the provider, which will be shown in the Seal results
|
||||
--- * url - A string containing the URL to insert the user's query into. This should contain one and only one `%s`
|
||||
---
|
||||
--- Returns:
|
||||
--- * Either a table of current providers, if no parameter was passed, or nothing if a parmameter was passed.
|
||||
---
|
||||
--- Notes:
|
||||
--- * An example table might look like:
|
||||
--- ```lua
|
||||
--- {
|
||||
--- rhbz = { name = "Red Hat Bugzilla", url = "https://bugzilla.redhat.com/show_bug.cgi?id=%s", },
|
||||
--- lp = { name = "Launchpad Bug", url = "https://launchpad.net/bugs/%s", },
|
||||
--- }
|
||||
--- ```
|
||||
function obj:providersTable(aTable)
|
||||
if aTable then
|
||||
self.providers = aTable
|
||||
else
|
||||
return self.providers
|
||||
end
|
||||
end
|
||||
|
||||
return obj
|
||||
@@ -0,0 +1,286 @@
|
||||
--- ==== 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
|
||||
@@ -0,0 +1,266 @@
|
||||
local obj = {}
|
||||
obj.__index = obj
|
||||
obj.__name = "seal_vpn"
|
||||
|
||||
function obj:commands()
|
||||
return {vpn = {
|
||||
cmd = "vpn",
|
||||
fn = obj.choicesVPNCommand,
|
||||
name = "VPN",
|
||||
description = "Manage VPN connections",
|
||||
plugin = obj.__name
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function obj:bare()
|
||||
return nil
|
||||
end
|
||||
|
||||
function obj.getVPNConnections()
|
||||
connections = {}
|
||||
code, output, descriptor = hs.osascript.applescript([[
|
||||
set output to ""
|
||||
|
||||
on join_list(the_list, delimiter)
|
||||
set the_string to ""
|
||||
set old_delims to AppleScript's text item delimiters
|
||||
repeat with the_item in the_list
|
||||
if the_string is equal to "" then
|
||||
set the_string to the_string & the_item
|
||||
else
|
||||
set the_string to the_string & delimiter & the_item
|
||||
end if
|
||||
end repeat
|
||||
set AppleScript's text item delimiters to old_delims
|
||||
return the_string
|
||||
end join_list
|
||||
|
||||
set vpn_connections to {}
|
||||
|
||||
tell application "Viscosity"
|
||||
repeat with the_connection in connections
|
||||
set the end of vpn_connections to (name of the_connection) & tab & (state of the_connection) & tab & "Viscosity"
|
||||
end repeat
|
||||
end tell
|
||||
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
repeat with vpn in (every service whose (kind is greater than 10 and kind is less than 17))
|
||||
set state to "Disconnected"
|
||||
if connected of current configuration of vpn is equal to true then
|
||||
set state to "Connected"
|
||||
end if
|
||||
set the end of vpn_connections to (name of vpn) & tab & state & tab & "macOS"
|
||||
end repeat
|
||||
end tell
|
||||
end tell
|
||||
|
||||
return my join_list(vpn_connections, linefeed)
|
||||
]])
|
||||
|
||||
if code == false or output == nil or output == "" then
|
||||
return connections
|
||||
end
|
||||
|
||||
for line in output:gmatch("[^\r\n]+") do
|
||||
parts = {}
|
||||
for part in line:gmatch("%S+") do
|
||||
table.insert(parts, part)
|
||||
end
|
||||
kind = parts[#parts]
|
||||
table.remove(parts, #parts)
|
||||
state = parts[#parts]
|
||||
table.remove(parts, #parts)
|
||||
name = table.concat(parts, " ")
|
||||
table.insert(connections, {name=name, state=state, kind=kind})
|
||||
end
|
||||
return connections
|
||||
end
|
||||
|
||||
function obj.disconnectVPN(name, kind)
|
||||
if kind == "Viscosity" then
|
||||
obj.disconnectViscosity(name)
|
||||
elseif kind == "macOS" then
|
||||
obj.disconnectMacOS(name)
|
||||
end
|
||||
end
|
||||
|
||||
function obj.disconnectViscosity(name)
|
||||
code, output, descriptor = hs.osascript.applescript(string.format([[
|
||||
-- Return true if VPN is active
|
||||
on vpn_is_active(vpn_name)
|
||||
tell application "Viscosity"
|
||||
repeat with the_connection in connections
|
||||
if (name of the_connection) is equal to vpn_name then
|
||||
if (state of the_connection) is equal to "Connected" then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end if
|
||||
end if
|
||||
end repeat
|
||||
end tell
|
||||
return false
|
||||
end vpn_is_active
|
||||
|
||||
on run argv
|
||||
set vpn_name to "%s"
|
||||
if my vpn_is_active(vpn_name) then
|
||||
tell application "Viscosity"
|
||||
disconnect vpn_name
|
||||
end tell
|
||||
end if
|
||||
end run
|
||||
]], name))
|
||||
end
|
||||
|
||||
function obj.disconnectMacOS(name)
|
||||
code, output, descriptor = hs.osascript.applescript(string.format([[
|
||||
-- Return true if VPN is active
|
||||
on vpn_is_active(vpn_name)
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
return connected of current configuration of service vpn_name
|
||||
end tell
|
||||
end tell
|
||||
end vpn_is_active
|
||||
|
||||
on connect_vpn(vpn_name)
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
connect service vpn_name
|
||||
end tell
|
||||
end tell
|
||||
end connect_vpn
|
||||
|
||||
on run argv
|
||||
set vpn_name to "%s"
|
||||
if my vpn_is_active(vpn_name) then
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
disconnect service vpn_name
|
||||
end tell
|
||||
end tell
|
||||
end if
|
||||
end run
|
||||
]], name))
|
||||
end
|
||||
|
||||
function obj.connectVPN(name, kind)
|
||||
if kind == "Viscosity" then
|
||||
obj.connectViscosity(name)
|
||||
elseif kind == "macOS" then
|
||||
obj.connectMacOS(name)
|
||||
end
|
||||
end
|
||||
|
||||
function obj.connectViscosity(name)
|
||||
code, output, descriptor = hs.osascript.applescript(string.format([[
|
||||
-- Return true if VPN is active
|
||||
on vpn_is_active(vpn_name)
|
||||
tell application "Viscosity"
|
||||
repeat with the_connection in connections
|
||||
if (name of the_connection) is equal to vpn_name then
|
||||
if (state of the_connection) is equal to "Connected" then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end if
|
||||
end if
|
||||
end repeat
|
||||
end tell
|
||||
return false
|
||||
end vpn_is_active
|
||||
|
||||
-- Connect to specified VPN
|
||||
on connect_vpn(vpn_name)
|
||||
tell application "Viscosity"
|
||||
connect vpn_name
|
||||
end tell
|
||||
end connect_vpn
|
||||
|
||||
on run argv
|
||||
set vpn_name to "%s"
|
||||
if my vpn_is_active(vpn_name) then
|
||||
log "VPN " & quote & vpn_name & quote & " is already active."
|
||||
return
|
||||
end if
|
||||
connect_vpn(vpn_name)
|
||||
end run
|
||||
]], name))
|
||||
end
|
||||
|
||||
function obj.connectMacOS(name)
|
||||
code, output, descriptor = hs.osascript.applescript(string.format([[
|
||||
-- Return true if VPN is active
|
||||
on vpn_is_active(vpn_name)
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
return connected of current configuration of service vpn_name
|
||||
end tell
|
||||
end tell
|
||||
end vpn_is_active
|
||||
|
||||
-- Connect to specified VPN
|
||||
on connect_vpn(vpn_name)
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
connect service vpn_name
|
||||
end tell
|
||||
end tell
|
||||
end connect_vpn
|
||||
|
||||
on run argv
|
||||
set vpn_name to "%s"
|
||||
if my vpn_is_active(vpn_name) then
|
||||
log "VPN " & quote & vpn_name & quote & " is already active."
|
||||
return
|
||||
end if
|
||||
connect_vpn(vpn_name)
|
||||
end run
|
||||
]], name))
|
||||
end
|
||||
|
||||
|
||||
function obj.choicesVPNCommand(query)
|
||||
local choices = {}
|
||||
local connections = obj.getVPNConnections()
|
||||
local img_connected = hs.image.imageFromPath(obj.seal.spoonPath.."/viscosity_locked.png")
|
||||
local img_disconnected = hs.image.imageFromPath(obj.seal.spoonPath.."/viscosity_unlocked.png")
|
||||
|
||||
for k,v in pairs(connections) do
|
||||
name = v["name"]
|
||||
if string.match(name:lower(), query:lower()) then
|
||||
state = v["state"]
|
||||
kind = v["kind"]
|
||||
local choice = {}
|
||||
choice["text"] = name
|
||||
choice["subText"] = state .. " (" .. kind .. ")"
|
||||
if state == "Connected" then
|
||||
choice["image"] = img_connected
|
||||
else
|
||||
choice["image"] = img_disconnected
|
||||
end
|
||||
choice["name"] = name
|
||||
choice["state"] = state
|
||||
choice["kind"] = kind
|
||||
choice["plugin"] = obj.__name
|
||||
choice["type"] = "toggle"
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
end
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.completionCallback(rowInfo)
|
||||
if rowInfo["type"] == "toggle" then
|
||||
if rowInfo["state"] == "Connected" then
|
||||
obj.disconnectVPN(rowInfo["name"], rowInfo["kind"])
|
||||
else
|
||||
obj.connectVPN(rowInfo["name"], rowInfo["kind"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return obj
|
||||
@@ -0,0 +1,266 @@
|
||||
local obj = {}
|
||||
obj.__index = obj
|
||||
obj.__name = "seal_vpn"
|
||||
|
||||
function obj:commands()
|
||||
return {vpn = {
|
||||
cmd = "vpn",
|
||||
fn = obj.choicesVPNCommand,
|
||||
name = "VPN",
|
||||
description = "Manage VPN connections",
|
||||
plugin = obj.__name
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function obj:bare()
|
||||
return nil
|
||||
end
|
||||
|
||||
function obj.getVPNConnections()
|
||||
connections = {}
|
||||
code, output, descriptor = hs.osascript.applescript([[
|
||||
set output to ""
|
||||
|
||||
on join_list(the_list, delimiter)
|
||||
set the_string to ""
|
||||
set old_delims to AppleScript's text item delimiters
|
||||
repeat with the_item in the_list
|
||||
if the_string is equal to "" then
|
||||
set the_string to the_string & the_item
|
||||
else
|
||||
set the_string to the_string & delimiter & the_item
|
||||
end if
|
||||
end repeat
|
||||
set AppleScript's text item delimiters to old_delims
|
||||
return the_string
|
||||
end join_list
|
||||
|
||||
set vpn_connections to {}
|
||||
|
||||
tell application "Viscosity"
|
||||
repeat with the_connection in connections
|
||||
set the end of vpn_connections to (name of the_connection) & tab & (state of the_connection) & tab & "Viscosity"
|
||||
end repeat
|
||||
end tell
|
||||
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
repeat with vpn in (every service whose (kind is greater than 10 and kind is less than 17))
|
||||
set state to "Disconnected"
|
||||
if connected of current configuration of vpn is equal to true then
|
||||
set state to "Connected"
|
||||
end if
|
||||
set the end of vpn_connections to (name of vpn) & tab & state & tab & "macOS"
|
||||
end repeat
|
||||
end tell
|
||||
end tell
|
||||
|
||||
return my join_list(vpn_connections, linefeed)
|
||||
]])
|
||||
|
||||
if code == false or output == nil or output == "" then
|
||||
return connections
|
||||
end
|
||||
|
||||
for line in output:gmatch("[^\r\n]+") do
|
||||
parts = {}
|
||||
for part in line:gmatch("%S+") do
|
||||
table.insert(parts, part)
|
||||
end
|
||||
kind = parts[#parts]
|
||||
table.remove(parts, #parts)
|
||||
state = parts[#parts]
|
||||
table.remove(parts, #parts)
|
||||
name = table.concat(parts, " ")
|
||||
table.insert(connections, {name=name, state=state, kind=kind})
|
||||
end
|
||||
return connections
|
||||
end
|
||||
|
||||
function obj.disconnectVPN(name, kind)
|
||||
if kind == "Viscosity" then
|
||||
obj.disconnectViscosity(name)
|
||||
elseif kind == "macOS" then
|
||||
obj.disconnectMacOS(name)
|
||||
end
|
||||
end
|
||||
|
||||
function obj.disconnectViscosity(name)
|
||||
code, output, descriptor = hs.osascript.applescript(string.format([[
|
||||
-- Return true if VPN is active
|
||||
on vpn_is_active(vpn_name)
|
||||
tell application "Viscosity"
|
||||
repeat with the_connection in connections
|
||||
if (name of the_connection) is equal to vpn_name then
|
||||
if (state of the_connection) is equal to "Connected" then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end if
|
||||
end if
|
||||
end repeat
|
||||
end tell
|
||||
return false
|
||||
end vpn_is_active
|
||||
|
||||
on run argv
|
||||
set vpn_name to "%s"
|
||||
if my vpn_is_active(vpn_name) then
|
||||
tell application "Viscosity"
|
||||
disconnect vpn_name
|
||||
end tell
|
||||
end if
|
||||
end run
|
||||
]], name))
|
||||
end
|
||||
|
||||
function obj.disconnectMacOS(name)
|
||||
code, output, descriptor = hs.osascript.applescript(string.format([[
|
||||
-- Return true if VPN is active
|
||||
on vpn_is_active(vpn_name)
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
return connected of current configuration of service vpn_name
|
||||
end tell
|
||||
end tell
|
||||
end vpn_is_active
|
||||
|
||||
on connect_vpn(vpn_name)
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
connect service vpn_name
|
||||
end tell
|
||||
end tell
|
||||
end connect_vpn
|
||||
|
||||
on run argv
|
||||
set vpn_name to "%s"
|
||||
if my vpn_is_active(vpn_name) then
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
disconnect service vpn_name
|
||||
end tell
|
||||
end tell
|
||||
end if
|
||||
end run
|
||||
]], name))
|
||||
end
|
||||
|
||||
function obj.connectVPN(name, kind)
|
||||
if kind == "Viscosity" then
|
||||
obj.connectViscosity(name)
|
||||
elseif kind == "macOS" then
|
||||
obj.connectMacOS(name)
|
||||
end
|
||||
end
|
||||
|
||||
function obj.connectViscosity(name)
|
||||
code, output, descriptor = hs.osascript.applescript(string.format([[
|
||||
-- Return true if VPN is active
|
||||
on vpn_is_active(vpn_name)
|
||||
tell application "Viscosity"
|
||||
repeat with the_connection in connections
|
||||
if (name of the_connection) is equal to vpn_name then
|
||||
if (state of the_connection) is equal to "Connected" then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end if
|
||||
end if
|
||||
end repeat
|
||||
end tell
|
||||
return false
|
||||
end vpn_is_active
|
||||
|
||||
-- Connect to specified VPN
|
||||
on connect_vpn(vpn_name)
|
||||
tell application "Viscosity"
|
||||
connect vpn_name
|
||||
end tell
|
||||
end connect_vpn
|
||||
|
||||
on run argv
|
||||
set vpn_name to "%s"
|
||||
if my vpn_is_active(vpn_name) then
|
||||
log "VPN " & quote & vpn_name & quote & " is already active."
|
||||
return
|
||||
end if
|
||||
connect_vpn(vpn_name)
|
||||
end run
|
||||
]], name))
|
||||
end
|
||||
|
||||
function obj.connectMacOS(name)
|
||||
code, output, descriptor = hs.osascript.applescript(string.format([[
|
||||
-- Return true if VPN is active
|
||||
on vpn_is_active(vpn_name)
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
return connected of current configuration of service vpn_name
|
||||
end tell
|
||||
end tell
|
||||
end vpn_is_active
|
||||
|
||||
-- Connect to specified VPN
|
||||
on connect_vpn(vpn_name)
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
connect service vpn_name
|
||||
end tell
|
||||
end tell
|
||||
end connect_vpn
|
||||
|
||||
on run argv
|
||||
set vpn_name to "%s"
|
||||
if my vpn_is_active(vpn_name) then
|
||||
log "VPN " & quote & vpn_name & quote & " is already active."
|
||||
return
|
||||
end if
|
||||
connect_vpn(vpn_name)
|
||||
end run
|
||||
]], name))
|
||||
end
|
||||
|
||||
|
||||
function obj.choicesVPNCommand(query)
|
||||
local choices = {}
|
||||
local connections = obj.getVPNConnections()
|
||||
local img_connected = hs.image.imageFromPath(obj.seal.spoonPath.."/viscosity_locked.png")
|
||||
local img_disconnected = hs.image.imageFromPath(obj.seal.spoonPath.."/viscosity_unlocked.png")
|
||||
|
||||
for k,v in pairs(connections) do
|
||||
name = v["name"]
|
||||
if string.match(name:lower(), query:lower()) then
|
||||
state = v["state"]
|
||||
kind = v["kind"]
|
||||
local choice = {}
|
||||
choice["text"] = name
|
||||
choice["subText"] = state .. " (" .. kind .. ")"
|
||||
if state == "Connected" then
|
||||
choice["image"] = img_connected
|
||||
else
|
||||
choice["image"] = img_disconnected
|
||||
end
|
||||
choice["name"] = name
|
||||
choice["state"] = state
|
||||
choice["kind"] = kind
|
||||
choice["plugin"] = obj.__name
|
||||
choice["type"] = "toggle"
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
end
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.completionCallback(rowInfo)
|
||||
if rowInfo["type"] == "toggle" then
|
||||
if rowInfo["state"] == "Connected" then
|
||||
obj.disconnectVPN(rowInfo["name"], rowInfo["kind"])
|
||||
else
|
||||
obj.connectVPN(rowInfo["name"], rowInfo["kind"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return obj
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Reference in New Issue
Block a user