Initial commit of Hammerspoon config

This commit is contained in:
Franco Pellicciotti
2026-05-14 18:59:23 -04:00
commit 8a9f5c37ff
683 changed files with 180195 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+359
View File
@@ -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.
+236
View File
@@ -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
+47
View File
@@ -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
+168
View File
@@ -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
+182
View File
@@ -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
+48
View File
@@ -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
+98
View File
@@ -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
+125
View File
@@ -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
+286
View File
@@ -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
+266
View File
@@ -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
+266
View File
@@ -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