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
BIN
View File
Binary file not shown.
+322
View File
@@ -0,0 +1,322 @@
[
{
"Command": [],
"Constant": [],
"Constructor": [],
"Deprecated": [],
"Field": [],
"Function": [],
"Method": [
{
"def": "BrewInfo:bindHotkeys(mapping)",
"desc": "Binds hotkeys for BrewInfo",
"doc": "Binds hotkeys for BrewInfo\n\nParameters:\n * mapping - A table containing hotkey modifier/key details for the following items:\n * show_brew_info - Show output of `brew info` using the selected text as package name\n * open_brew_url - Open the homepage of the formula whose name is currently selected\n * show_brew_cask_info - Show output of `brew cask info` using the selected text as package name\n * open_brew_cask_url - Open the homepage of the Cask whose name is currently selected",
"examples": [],
"file": "Source/BrewInfo.spoon//init.lua",
"lineno": "166",
"name": "bindHotkeys",
"notes": [],
"parameters": [
" * mapping - A table containing hotkey modifier/key details for the following items:\n * show_brew_info - Show output of `brew info` using the selected text as package name\n * open_brew_url - Open the homepage of the formula whose name is currently selected\n * show_brew_cask_info - Show output of `brew cask info` using the selected text as package name\n * open_brew_cask_url - Open the homepage of the Cask whose name is currently selected"
],
"returns": [],
"signature": "BrewInfo:bindHotkeys(mapping)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "BrewInfo:openBrewURL(pkg, subcommand)",
"desc": "Opens the homepage for package `pkg`, as obtained from the `homepage` field in `brew <subcommand> cat <pkg>`",
"doc": "Opens the homepage for package `pkg`, as obtained from the `homepage` field in `brew <subcommand> cat <pkg>`\n\nParameters:\n * pkg - name of the package to query\n * subcommand - brew subcommand to use for the `cat` command. Defaults to an empty string, which results in \"brew cat <pkg>\" being run. For example, if `subcommand` is \"cask\", the `brew cask cat <pkg>` command will be used.\n\nReturns:\n * The Spoon object",
"examples": [],
"file": "Source/BrewInfo.spoon//init.lua",
"lineno": "126",
"name": "openBrewURL",
"notes": [],
"parameters": [
" * pkg - name of the package to query",
" * subcommand - brew subcommand to use for the `cat` command. Defaults to an empty string, which results in \"brew cat <pkg>\" being run. For example, if `subcommand` is \"cask\", the `brew cask cat <pkg>` command will be used."
],
"returns": [
" * The Spoon object"
],
"signature": "BrewInfo:openBrewURL(pkg, subcommand)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "BrewInfo:openBrewURLCurSel(subcommand)",
"desc": "Opens the homepage for the currently-selected package, as obtained from the `homepage` field in `brew <subcommand> cat <pkg>`",
"doc": "Opens the homepage for the currently-selected package, as obtained from the `homepage` field in `brew <subcommand> cat <pkg>`\n\nParameters:\n * subcommand - brew subcommand to use for the `cat` command. Defaults to an empty string, which results in \"brew cat <pkg>\" being run. For example, if `subcommand` is \"cask\", the `brew cask cat <pkg>` command will be used.\n\nReturns:\n * The Spoon object",
"examples": [],
"file": "Source/BrewInfo.spoon//init.lua",
"lineno": "153",
"name": "openBrewURLCurSel",
"notes": [],
"parameters": [
" * subcommand - brew subcommand to use for the `cat` command. Defaults to an empty string, which results in \"brew cat <pkg>\" being run. For example, if `subcommand` is \"cask\", the `brew cask cat <pkg>` command will be used."
],
"returns": [
" * The Spoon object"
],
"signature": "BrewInfo:openBrewURLCurSel(subcommand)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "BrewInfo:showBrewInfo(pkg, subcommand)",
"desc": "Displays an alert with the output of `brew <subcommand> info <pkg>`",
"doc": "Displays an alert with the output of `brew <subcommand> info <pkg>`\n\nParameters:\n * pkg - name of the package to query\n * subcommand - brew subcommand to use for the `info` command. Defaults to an empty string, which results in \"brew info <pkg>\" being run. For example, if `subcommand` is \"cask\", the `brew cask info <pkg>` command will be used.\n\nReturns:\n * The Spoon object",
"examples": [],
"file": "Source/BrewInfo.spoon//init.lua",
"lineno": "89",
"name": "showBrewInfo",
"notes": [],
"parameters": [
" * pkg - name of the package to query",
" * subcommand - brew subcommand to use for the `info` command. Defaults to an empty string, which results in \"brew info <pkg>\" being run. For example, if `subcommand` is \"cask\", the `brew cask info <pkg>` command will be used."
],
"returns": [
" * The Spoon object"
],
"signature": "BrewInfo:showBrewInfo(pkg, subcommand)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "BrewInfo:showBrewInfoCurSel(subcommand)",
"desc": "Display `brew <subcommand> info` using the selected text as the package name",
"doc": "Display `brew <subcommand> info` using the selected text as the package name\n\nParameters:\n * subcommand - brew subcommand to use for the `info` command. Defaults to an empty string, which results in \"brew info\" being run. For example, if `subcommand` is \"cask\", the `brew cask info` command will be used.\n\nReturns:\n * The Spoon object",
"examples": [],
"file": "Source/BrewInfo.spoon//init.lua",
"lineno": "113",
"name": "showBrewInfoCurSel",
"notes": [],
"parameters": [
" * subcommand - brew subcommand to use for the `info` command. Defaults to an empty string, which results in \"brew info\" being run. For example, if `subcommand` is \"cask\", the `brew cask info` command will be used."
],
"returns": [
" * The Spoon object"
],
"signature": "BrewInfo:showBrewInfoCurSel(subcommand)",
"stripped_doc": "",
"type": "Method"
}
],
"Variable": [
{
"def": "BrewInfo.brew_info_delay_sec",
"desc": "An integer specifying how long the alerts generated by BrewInfo will stay onscreen",
"doc": "An integer specifying how long the alerts generated by BrewInfo will stay onscreen",
"file": "Source/BrewInfo.spoon//init.lua",
"lineno": "30",
"name": "brew_info_delay_sec",
"signature": "BrewInfo.brew_info_delay_sec",
"stripped_doc": "",
"type": "Variable"
},
{
"def": "BrewInfo.brew_info_style",
"desc": "A table in conformance with the [hs.alert.defaultStyle](http://www.hammerspoon.org/docs/hs.alert.html#defaultStyle[]) format that specifies the style used by the alerts. Default value: `{ textFont = \"Courier New\", textSize = 14, radius = 10 }`",
"doc": "A table in conformance with the [hs.alert.defaultStyle](http://www.hammerspoon.org/docs/hs.alert.html#defaultStyle[]) format that specifies the style used by the alerts. Default value: `{ textFont = \"Courier New\", textSize = 14, radius = 10 }`",
"file": "Source/BrewInfo.spoon//init.lua",
"lineno": "35",
"name": "brew_info_style",
"signature": "BrewInfo.brew_info_style",
"stripped_doc": "",
"type": "Variable"
},
{
"def": "BrewInfo.brew_path",
"desc": "A string specifying the path to the `brew` executable. Defaults to `/usr/local/bin/brew`",
"doc": "A string specifying the path to the `brew` executable. Defaults to `/usr/local/bin/brew`",
"file": "Source/BrewInfo.spoon//init.lua",
"lineno": "25",
"name": "brew_path",
"signature": "BrewInfo.brew_path",
"stripped_doc": "",
"type": "Variable"
},
{
"def": "BrewInfo.select_text_if_needed",
"desc": "If `true`, and no text is currently selected in the terminal, issue a double-click to select the text below the cursor, and use that as the input to `brew info`. See also `BrewInfo.select_text_modifiers`. Defaults to `true`.",
"doc": "If `true`, and no text is currently selected in the terminal, issue a double-click to select the text below the cursor, and use that as the input to `brew info`. See also `BrewInfo.select_text_modifiers`. Defaults to `true`.",
"file": "Source/BrewInfo.spoon//init.lua",
"lineno": "44",
"name": "select_text_if_needed",
"signature": "BrewInfo.select_text_if_needed",
"stripped_doc": "",
"type": "Variable"
},
{
"def": "BrewInfo.select_text_modifiers",
"desc": "Table containing the modifiers to be used together with a double-click when `BrewInfo.select_text_if_needed` is true. Defaults to `{cmd = true, shift = true}` to issue a Cmd-Shift-double-click, which will select a continuous non-space string in Terminal and iTerm2.",
"doc": "Table containing the modifiers to be used together with a double-click when `BrewInfo.select_text_if_needed` is true. Defaults to `{cmd = true, shift = true}` to issue a Cmd-Shift-double-click, which will select a continuous non-space string in Terminal and iTerm2.",
"file": "Source/BrewInfo.spoon//init.lua",
"lineno": "49",
"name": "select_text_modifiers",
"signature": "BrewInfo.select_text_modifiers",
"stripped_doc": "",
"type": "Variable"
}
],
"desc": "Display pop-up with Homebrew Formula info, or open their URL",
"doc": "Display pop-up with Homebrew Formula info, or open their URL\n\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/BrewInfo.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/BrewInfo.spoon.zip)\n\nYou can bind keys to automatically display the output of `brew\ninfo` of the currently-selected package name, or to open its\nhomepage. I use it to quickly explore new packages from the output\nof `brew update`.",
"items": [
{
"def": "BrewInfo:bindHotkeys(mapping)",
"desc": "Binds hotkeys for BrewInfo",
"doc": "Binds hotkeys for BrewInfo\n\nParameters:\n * mapping - A table containing hotkey modifier/key details for the following items:\n * show_brew_info - Show output of `brew info` using the selected text as package name\n * open_brew_url - Open the homepage of the formula whose name is currently selected\n * show_brew_cask_info - Show output of `brew cask info` using the selected text as package name\n * open_brew_cask_url - Open the homepage of the Cask whose name is currently selected",
"examples": [],
"file": "Source/BrewInfo.spoon//init.lua",
"lineno": "166",
"name": "bindHotkeys",
"notes": [],
"parameters": [
" * mapping - A table containing hotkey modifier/key details for the following items:\n * show_brew_info - Show output of `brew info` using the selected text as package name\n * open_brew_url - Open the homepage of the formula whose name is currently selected\n * show_brew_cask_info - Show output of `brew cask info` using the selected text as package name\n * open_brew_cask_url - Open the homepage of the Cask whose name is currently selected"
],
"returns": [],
"signature": "BrewInfo:bindHotkeys(mapping)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "BrewInfo.brew_info_delay_sec",
"desc": "An integer specifying how long the alerts generated by BrewInfo will stay onscreen",
"doc": "An integer specifying how long the alerts generated by BrewInfo will stay onscreen",
"file": "Source/BrewInfo.spoon//init.lua",
"lineno": "30",
"name": "brew_info_delay_sec",
"signature": "BrewInfo.brew_info_delay_sec",
"stripped_doc": "",
"type": "Variable"
},
{
"def": "BrewInfo.brew_info_style",
"desc": "A table in conformance with the [hs.alert.defaultStyle](http://www.hammerspoon.org/docs/hs.alert.html#defaultStyle[]) format that specifies the style used by the alerts. Default value: `{ textFont = \"Courier New\", textSize = 14, radius = 10 }`",
"doc": "A table in conformance with the [hs.alert.defaultStyle](http://www.hammerspoon.org/docs/hs.alert.html#defaultStyle[]) format that specifies the style used by the alerts. Default value: `{ textFont = \"Courier New\", textSize = 14, radius = 10 }`",
"file": "Source/BrewInfo.spoon//init.lua",
"lineno": "35",
"name": "brew_info_style",
"signature": "BrewInfo.brew_info_style",
"stripped_doc": "",
"type": "Variable"
},
{
"def": "BrewInfo.brew_path",
"desc": "A string specifying the path to the `brew` executable. Defaults to `/usr/local/bin/brew`",
"doc": "A string specifying the path to the `brew` executable. Defaults to `/usr/local/bin/brew`",
"file": "Source/BrewInfo.spoon//init.lua",
"lineno": "25",
"name": "brew_path",
"signature": "BrewInfo.brew_path",
"stripped_doc": "",
"type": "Variable"
},
{
"def": "BrewInfo:openBrewURL(pkg, subcommand)",
"desc": "Opens the homepage for package `pkg`, as obtained from the `homepage` field in `brew <subcommand> cat <pkg>`",
"doc": "Opens the homepage for package `pkg`, as obtained from the `homepage` field in `brew <subcommand> cat <pkg>`\n\nParameters:\n * pkg - name of the package to query\n * subcommand - brew subcommand to use for the `cat` command. Defaults to an empty string, which results in \"brew cat <pkg>\" being run. For example, if `subcommand` is \"cask\", the `brew cask cat <pkg>` command will be used.\n\nReturns:\n * The Spoon object",
"examples": [],
"file": "Source/BrewInfo.spoon//init.lua",
"lineno": "126",
"name": "openBrewURL",
"notes": [],
"parameters": [
" * pkg - name of the package to query",
" * subcommand - brew subcommand to use for the `cat` command. Defaults to an empty string, which results in \"brew cat <pkg>\" being run. For example, if `subcommand` is \"cask\", the `brew cask cat <pkg>` command will be used."
],
"returns": [
" * The Spoon object"
],
"signature": "BrewInfo:openBrewURL(pkg, subcommand)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "BrewInfo:openBrewURLCurSel(subcommand)",
"desc": "Opens the homepage for the currently-selected package, as obtained from the `homepage` field in `brew <subcommand> cat <pkg>`",
"doc": "Opens the homepage for the currently-selected package, as obtained from the `homepage` field in `brew <subcommand> cat <pkg>`\n\nParameters:\n * subcommand - brew subcommand to use for the `cat` command. Defaults to an empty string, which results in \"brew cat <pkg>\" being run. For example, if `subcommand` is \"cask\", the `brew cask cat <pkg>` command will be used.\n\nReturns:\n * The Spoon object",
"examples": [],
"file": "Source/BrewInfo.spoon//init.lua",
"lineno": "153",
"name": "openBrewURLCurSel",
"notes": [],
"parameters": [
" * subcommand - brew subcommand to use for the `cat` command. Defaults to an empty string, which results in \"brew cat <pkg>\" being run. For example, if `subcommand` is \"cask\", the `brew cask cat <pkg>` command will be used."
],
"returns": [
" * The Spoon object"
],
"signature": "BrewInfo:openBrewURLCurSel(subcommand)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "BrewInfo.select_text_if_needed",
"desc": "If `true`, and no text is currently selected in the terminal, issue a double-click to select the text below the cursor, and use that as the input to `brew info`. See also `BrewInfo.select_text_modifiers`. Defaults to `true`.",
"doc": "If `true`, and no text is currently selected in the terminal, issue a double-click to select the text below the cursor, and use that as the input to `brew info`. See also `BrewInfo.select_text_modifiers`. Defaults to `true`.",
"file": "Source/BrewInfo.spoon//init.lua",
"lineno": "44",
"name": "select_text_if_needed",
"signature": "BrewInfo.select_text_if_needed",
"stripped_doc": "",
"type": "Variable"
},
{
"def": "BrewInfo.select_text_modifiers",
"desc": "Table containing the modifiers to be used together with a double-click when `BrewInfo.select_text_if_needed` is true. Defaults to `{cmd = true, shift = true}` to issue a Cmd-Shift-double-click, which will select a continuous non-space string in Terminal and iTerm2.",
"doc": "Table containing the modifiers to be used together with a double-click when `BrewInfo.select_text_if_needed` is true. Defaults to `{cmd = true, shift = true}` to issue a Cmd-Shift-double-click, which will select a continuous non-space string in Terminal and iTerm2.",
"file": "Source/BrewInfo.spoon//init.lua",
"lineno": "49",
"name": "select_text_modifiers",
"signature": "BrewInfo.select_text_modifiers",
"stripped_doc": "",
"type": "Variable"
},
{
"def": "BrewInfo:showBrewInfo(pkg, subcommand)",
"desc": "Displays an alert with the output of `brew <subcommand> info <pkg>`",
"doc": "Displays an alert with the output of `brew <subcommand> info <pkg>`\n\nParameters:\n * pkg - name of the package to query\n * subcommand - brew subcommand to use for the `info` command. Defaults to an empty string, which results in \"brew info <pkg>\" being run. For example, if `subcommand` is \"cask\", the `brew cask info <pkg>` command will be used.\n\nReturns:\n * The Spoon object",
"examples": [],
"file": "Source/BrewInfo.spoon//init.lua",
"lineno": "89",
"name": "showBrewInfo",
"notes": [],
"parameters": [
" * pkg - name of the package to query",
" * subcommand - brew subcommand to use for the `info` command. Defaults to an empty string, which results in \"brew info <pkg>\" being run. For example, if `subcommand` is \"cask\", the `brew cask info <pkg>` command will be used."
],
"returns": [
" * The Spoon object"
],
"signature": "BrewInfo:showBrewInfo(pkg, subcommand)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "BrewInfo:showBrewInfoCurSel(subcommand)",
"desc": "Display `brew <subcommand> info` using the selected text as the package name",
"doc": "Display `brew <subcommand> info` using the selected text as the package name\n\nParameters:\n * subcommand - brew subcommand to use for the `info` command. Defaults to an empty string, which results in \"brew info\" being run. For example, if `subcommand` is \"cask\", the `brew cask info` command will be used.\n\nReturns:\n * The Spoon object",
"examples": [],
"file": "Source/BrewInfo.spoon//init.lua",
"lineno": "113",
"name": "showBrewInfoCurSel",
"notes": [],
"parameters": [
" * subcommand - brew subcommand to use for the `info` command. Defaults to an empty string, which results in \"brew info\" being run. For example, if `subcommand` is \"cask\", the `brew cask info` command will be used."
],
"returns": [
" * The Spoon object"
],
"signature": "BrewInfo:showBrewInfoCurSel(subcommand)",
"stripped_doc": "",
"type": "Method"
}
],
"name": "BrewInfo",
"stripped_doc": "\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/BrewInfo.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/BrewInfo.spoon.zip)\n\nYou can bind keys to automatically display the output of `brew\ninfo` of the currently-selected package name, or to open its\nhomepage. I use it to quickly explore new packages from the output\nof `brew update`.",
"submodules": [],
"type": "Module"
}
]
+195
View File
@@ -0,0 +1,195 @@
--- === BrewInfo ===
---
--- Display pop-up with Homebrew Formula info, or open their URL
---
--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/BrewInfo.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/BrewInfo.spoon.zip)
---
--- You can bind keys to automatically display the output of `brew
--- info` of the currently-selected package name, or to open its
--- homepage. I use it to quickly explore new packages from the output
--- of `brew update`.
local mod={}
mod.__index = mod
-- Conformance hack, our Travis linter expects the object to be called "obj"
local obj=mod
-- Metadata
mod.name = "BrewInfo"
obj.version = "1.1"
mod.author = "Diego Zamboni <diego@zzamboni.org>"
mod.homepage = "https://github.com/Hammerspoon/Spoons"
mod.license = "MIT - https://opensource.org/licenses/MIT"
--- BrewInfo.brew_path
--- Variable
--- A string specifying the path to the `brew` executable. Defaults to `/usr/local/bin/brew`
mod.brew_path = "/usr/local/bin/brew"
--- BrewInfo.brew_info_delay_sec
--- Variable
--- An integer specifying how long the alerts generated by BrewInfo will stay onscreen
mod.brew_info_delay_sec = 3
--- BrewInfo.brew_info_style
--- Variable
--- A table in conformance with the [hs.alert.defaultStyle](http://www.hammerspoon.org/docs/hs.alert.html#defaultStyle[]) format that specifies the style used by the alerts. Default value: `{ textFont = "Courier New", textSize = 14, radius = 10 }`
mod.brew_info_style = {
textFont = "Courier New",
textSize = 14,
radius = 10
}
--- BrewInfo.select_text_if_needed
--- Variable
--- If `true`, and no text is currently selected in the terminal, issue a double-click to select the text below the cursor, and use that as the input to `brew info`. See also `BrewInfo.select_text_modifiers`. Defaults to `true`.
mod.select_text_if_needed = true
--- BrewInfo.select_text_modifiers
--- Variable
--- Table containing the modifiers to be used together with a double-click when `BrewInfo.select_text_if_needed` is true. Defaults to `{cmd = true, shift = true}` to issue a Cmd-Shift-double-click, which will select a continuous non-space string in Terminal and iTerm2.
mod.select_text_modifiers = {cmd = true, shift = true}
-- Internal function to issue a double click with given modifiers
function leftDoubleClick(modifiers)
local pos=hs.mouse.absolutePosition()
hs.eventtap.event.newMouseEvent(hs.eventtap.event.types.leftMouseDown, pos, modifiers)
:setProperty(hs.eventtap.event.properties.mouseEventClickState, 2)
:post()
hs.eventtap.event.newMouseEvent(hs.eventtap.event.types.leftMouseUp, pos, modifiers)
:post()
end
-- Internal method to get the currently selected text
-- If `select_text_if_needed` is true and no text is selected, issue
-- a double-click to select, then use that
function mod:current_selection()
local elem=hs.uielement.focusedElement()
if elem then
local sel = elem:selectedText()
if (sel == nil or sel == "") and self.select_text_if_needed then
-- Simulate a double click to select the text under the cursor
leftDoubleClick(self.select_text_modifiers)
hs.timer.usleep(20000)
sel = elem:selectedText()
end
return sel
else
return nil
end
end
-- Internal method to show an alert in the configured style
function mod:show(text)
hs.alert.show(text, self.brew_info_style, self.brew_info_delay_sec)
return self
end
--- BrewInfo:showBrewInfo(pkg, subcommand)
--- Method
--- Displays an alert with the output of `brew <subcommand> info <pkg>`
---
--- Parameters:
--- * pkg - name of the package to query
--- * subcommand - brew subcommand to use for the `info` command. Defaults to an empty string, which results in "brew info <pkg>" being run. For example, if `subcommand` is "cask", the `brew cask info <pkg>` command will be used.
---
--- Returns:
--- * The Spoon object
function mod:showBrewInfo(pkg, subcommand)
local info = "No package selected"
local st = nil
if pkg and pkg ~= "" then
local cmd=string.format("%s %s info %s", self.brew_path, subcommand or "", pkg)
info, st=hs.execute(cmd)
if st == nil then
info = "No information found about formula '" .. pkg .. "'!"
end
end
self:show(info)
return self
end
--- BrewInfo:showBrewInfoCurSel(subcommand)
--- Method
--- Display `brew <subcommand> info` using the selected text as the package name
---
--- Parameters:
--- * subcommand - brew subcommand to use for the `info` command. Defaults to an empty string, which results in "brew info" being run. For example, if `subcommand` is "cask", the `brew cask info` command will be used.
---
--- Returns:
--- * The Spoon object
function mod:showBrewInfoCurSel(subcommand)
return self:showBrewInfo(self:current_selection(), subcommand)
end
--- BrewInfo:openBrewURL(pkg, subcommand)
--- Method
--- Opens the homepage for package `pkg`, as obtained from the `homepage` field in `brew <subcommand> cat <pkg>`
---
--- Parameters:
--- * pkg - name of the package to query
--- * subcommand - brew subcommand to use for the `cat` command. Defaults to an empty string, which results in "brew cat <pkg>" being run. For example, if `subcommand` is "cask", the `brew cask cat <pkg>` command will be used.
---
--- Returns:
--- * The Spoon object
function mod:openBrewURL(pkg, subcommand)
local msg = "No package selected"
if pkg and pkg ~= "" then
local j, st, t, rc=hs.execute(string.format("%s %s cat %s", self.brew_path, (subcommand or ""), pkg ))
if st ~= nil then
local url=string.match(j, "\n%s*homepage%s+['\"](.-)['\"]%s*\n")
if url and url ~= "" then
hs.urlevent.openURLWithBundle(url, hs.urlevent.getDefaultHandler("http"))
return self
end
end
msg = "An error occurred obtaining information about '" .. pkg .. "'"
end
self:show(msg)
return self
end
--- BrewInfo:openBrewURLCurSel(subcommand)
--- Method
--- Opens the homepage for the currently-selected package, as obtained from the `homepage` field in `brew <subcommand> cat <pkg>`
---
--- Parameters:
--- * subcommand - brew subcommand to use for the `cat` command. Defaults to an empty string, which results in "brew cat <pkg>" being run. For example, if `subcommand` is "cask", the `brew cask cat <pkg>` command will be used.
---
--- Returns:
--- * The Spoon object
function mod:openBrewURLCurSel(subcommand)
return self:openBrewURL(self:current_selection(), subcommand)
end
--- BrewInfo:bindHotkeys(mapping)
--- Method
--- Binds hotkeys for BrewInfo
---
--- Parameters:
--- * mapping - A table containing hotkey modifier/key details for the following items:
--- * show_brew_info - Show output of `brew info` using the selected text as package name
--- * open_brew_url - Open the homepage of the formula whose name is currently selected
--- * show_brew_cask_info - Show output of `brew cask info` using the selected text as package name
--- * open_brew_cask_url - Open the homepage of the Cask whose name is currently selected
function mod:bindHotkeys(mapping)
local def = {
show_brew_info = function() self:showBrewInfoCurSel() end,
open_brew_url = function() self:openBrewURLCurSel() end,
}
for action, key in pairs(mapping) do
local subcommand_show = action:match("show_brew_(.*)_info")
if subcommand_show and subcommand_show ~= "" then
def[action] = function() self:showBrewInfoCurSel(subcommand_show) end
end
local subcommand_open = action:match("open_brew_(.*)_url")
if subcommand_open and subcommand_open ~= "" then
def[action] = function() self:openBrewURLCurSel(subcommand_open) end
end
end
hs.spoons.bindHotkeysToSpec(def, mapping)
end
return mod
+123
View File
@@ -0,0 +1,123 @@
[
{
"Command": [],
"Constant": [],
"Constructor": [],
"Deprecated": [],
"Field": [],
"Function": [],
"Method": [
{
"def": "KSheet:bindHotkeys(mapping)",
"desc": "Binds hotkeys for KSheet",
"doc": "Binds hotkeys for KSheet\n\nParameters:\n * mapping - A table containing hotkey modifier/key details for the following items:\n * show - Show the keybinding view\n * hide - Hide the keybinding view\n * toggle - Show if hidden, hide if shown",
"name": "bindHotkeys",
"parameters": [
" * mapping - A table containing hotkey modifier/key details for the following items:",
" * show - Show the keybinding view",
" * hide - Hide the keybinding view",
" * toggle - Show if hidden, hide if shown"
],
"signature": "KSheet:bindHotkeys(mapping)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "KSheet:hide()",
"desc": "Hide the cheatsheet view.",
"doc": "Hide the cheatsheet view.",
"name": "hide",
"signature": "KSheet:hide()",
"stripped_doc": "",
"type": "Method"
},
{
"def": "KSheet:init()",
"desc": "Initialize the spoon",
"doc": "Initialize the spoon",
"name": "init",
"signature": "KSheet:init()",
"stripped_doc": "",
"type": "Method"
},
{
"def": "KSheet:show()",
"desc": "Show current application's keybindings in a view.",
"doc": "Show current application's keybindings in a view.",
"name": "show",
"signature": "KSheet:show()",
"stripped_doc": "",
"type": "Method"
},
{
"def": "KSheet:toggle()",
"desc": "Alternatively show/hide the cheatsheet view.",
"doc": "Alternatively show/hide the cheatsheet view.",
"name": "toggle",
"signature": "KSheet:toggle()",
"stripped_doc": "",
"type": "Method"
}
],
"Variable": [],
"desc": "Keybindings cheatsheet for current application",
"doc": "Keybindings cheatsheet for current application\n\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/KSheet.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/KSheet.spoon.zip)",
"items": [
{
"def": "KSheet:bindHotkeys(mapping)",
"desc": "Binds hotkeys for KSheet",
"doc": "Binds hotkeys for KSheet\n\nParameters:\n * mapping - A table containing hotkey modifier/key details for the following items:\n * show - Show the keybinding view\n * hide - Hide the keybinding view\n * toggle - Show if hidden, hide if shown",
"name": "bindHotkeys",
"parameters": [
" * mapping - A table containing hotkey modifier/key details for the following items:",
" * show - Show the keybinding view",
" * hide - Hide the keybinding view",
" * toggle - Show if hidden, hide if shown"
],
"signature": "KSheet:bindHotkeys(mapping)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "KSheet:hide()",
"desc": "Hide the cheatsheet view.",
"doc": "Hide the cheatsheet view.",
"name": "hide",
"signature": "KSheet:hide()",
"stripped_doc": "",
"type": "Method"
},
{
"def": "KSheet:init()",
"desc": "Initialize the spoon",
"doc": "Initialize the spoon",
"name": "init",
"signature": "KSheet:init()",
"stripped_doc": "",
"type": "Method"
},
{
"def": "KSheet:show()",
"desc": "Show current application's keybindings in a view.",
"doc": "Show current application's keybindings in a view.",
"name": "show",
"signature": "KSheet:show()",
"stripped_doc": "",
"type": "Method"
},
{
"def": "KSheet:toggle()",
"desc": "Alternatively show/hide the cheatsheet view.",
"doc": "Alternatively show/hide the cheatsheet view.",
"name": "toggle",
"signature": "KSheet:toggle()",
"stripped_doc": "",
"type": "Method"
}
],
"name": "KSheet",
"stripped_doc": "\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/KSheet.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/KSheet.spoon.zip)",
"submodules": [],
"type": "Module"
}
]
+242
View File
@@ -0,0 +1,242 @@
--- === KSheet ===
---
--- Keybindings cheatsheet for current application
---
--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/KSheet.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/KSheet.spoon.zip)
local obj={}
obj.__index = obj
-- Metadata
obj.name = "KSheet"
obj.version = "1.0"
obj.author = "ashfinal <ashfinal@gmail.com>"
obj.homepage = "https://github.com/Hammerspoon/Spoons"
obj.license = "MIT - https://opensource.org/licenses/MIT"
-- Workaround for "Dictation" menuitem
hs.application.menuGlyphs[148]="fn fn"
obj.commandEnum = {
cmd = '',
shift = '',
alt = '',
ctrl = '',
}
--- KSheet:init()
--- Method
--- Initialize the spoon
function obj:init()
self.sheetView = hs.webview.new({x=0, y=0, w=0, h=0})
self.sheetView:windowTitle("CheatSheets")
self.sheetView:windowStyle("utility")
self.sheetView:allowGestures(true)
self.sheetView:allowNewWindows(false)
self.sheetView:level(hs.drawing.windowLevels.tornOffMenu)
local cscreen = hs.screen.mainScreen()
local cres = cscreen:fullFrame()
self.sheetView:frame({
x = cres.x+cres.w*0.15/2,
y = cres.y+cres.h*0.25/2,
w = cres.w*0.85,
h = cres.h*0.75
})
end
local function processMenuItems(menustru)
local menu = ""
for pos,val in pairs(menustru) do
if type(val) == "table" then
-- TODO: Remove menubar items with no shortcuts in them
if val.AXRole == "AXMenuBarItem" and type(val.AXChildren) == "table" then
menu = menu .. "<ul class='col col" .. pos .. "'>"
menu = menu .. "<li class='title'><strong>" .. val.AXTitle .. "</strong></li>"
menu = menu .. processMenuItems(val.AXChildren[1])
menu = menu .. "</ul>"
elseif val.AXRole == "AXMenuItem" and not val.AXChildren then
if not (val.AXMenuItemCmdChar == '' and val.AXMenuItemCmdGlyph == '') then
local CmdModifiers = ''
for key, value in pairs(val.AXMenuItemCmdModifiers) do
CmdModifiers = CmdModifiers .. obj.commandEnum[value]
end
local CmdChar = val.AXMenuItemCmdChar
local CmdGlyph = hs.application.menuGlyphs[val.AXMenuItemCmdGlyph] or ''
local CmdKeys = CmdChar .. CmdGlyph
menu = menu .. "<li><div class='cmdModifiers'>" .. CmdModifiers .. " " .. CmdKeys .. "</div><div class='cmdtext'>" .. " " .. val.AXTitle .. "</div></li>"
end
elseif val.AXRole == "AXMenuItem" and type(val.AXChildren) == "table" then
menu = menu .. processMenuItems(val.AXChildren[1])
end
end
end
return menu
end
local function generateHtml(application)
local app_title = application:title()
local menuitems_tree = application:getMenuItems()
local allmenuitems = processMenuItems(menuitems_tree)
local html = [[
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
*{margin:0; padding:0;}
html, body{
background-color:#eee;
font-family: arial;
font-size: 13px;
}
a{
text-decoration:none;
color:#000;
font-size:12px;
}
li.title{ text-align:center;}
ul, li{list-style: inside none; padding: 0 0 5px;}
footer{
position: fixed;
left: 0;
right: 0;
height: 48px;
background-color:#eee;
}
header{
position: fixed;
top: 0;
left: 0;
right: 0;
height:48px;
background-color:#eee;
z-index:99;
}
footer{ bottom: 0; }
header hr,
footer hr {
border: 0;
height: 0;
border-top: 1px solid rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
}
.title{
padding: 15px;
}
li.title{padding: 0 10px 15px}
.content{
padding: 0 0 15px;
font-size:12px;
overflow:hidden;
}
.content.maincontent{
position: relative;
height: 577px;
margin-top: 46px;
}
.content > .col{
width: 23%;
padding:20px 0 20px 20px;
}
li:after{
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
.cmdModifiers{
width: 60px;
padding-right: 15px;
text-align: right;
float: left;
font-weight: bold;
}
.cmdtext{
float: left;
overflow: hidden;
width: 165px;
}
</style>
</head>
<body>
<header>
<div class="title"><strong>]] .. app_title .. [[</strong></div>
<hr />
</header>
<div class="content maincontent">]] .. allmenuitems .. [[</div>
<br>
<footer>
<hr />
<div class="content" >
<div class="col">
by <a href="https://github.com/dharmapoudel" target="_parent">dharma poudel</a>
</div>
</div>
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.isotope/2.2.2/isotope.pkgd.min.js"></script>
<script type="text/javascript">
var elem = document.querySelector('.content');
var iso = new Isotope( elem, {
// options
itemSelector: '.col',
layoutMode: 'masonry'
});
</script>
</body>
</html>
]]
return html
end
--- KSheet:show()
--- Method
--- Show current application's keybindings in a view.
function obj:show()
local capp = hs.application.frontmostApplication()
local webcontent = generateHtml(capp)
self.sheetView:html(webcontent)
self.sheetView:show()
end
--- KSheet:hide()
--- Method
--- Hide the cheatsheet view.
function obj:hide()
self.sheetView:hide()
end
--- KSheet:toggle()
--- Method
--- Alternatively show/hide the cheatsheet view.
function obj:toggle()
if self.sheetView and self.sheetView:hswindow() and self.sheetView:hswindow():isVisible() then
self:hide()
else
self:show()
end
end
--- KSheet:bindHotkeys(mapping)
--- Method
--- Binds hotkeys for KSheet
---
--- Parameters:
--- * mapping - A table containing hotkey modifier/key details for the following items:
--- * show - Show the keybinding view
--- * hide - Hide the keybinding view
--- * toggle - Show if hidden, hide if shown
function obj:bindHotkeys(mapping)
local actions = {
toggle = hs.fnutils.partial(self.toggle, self),
show = hs.fnutils.partial(self.show, self),
hide = hs.fnutils.partial(self.hide, self)
}
hs.spoons.bindHotkeysToSpec(actions, mapping)
end
return obj
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

+39
View File
@@ -0,0 +1,39 @@
[
{
"Command": [],
"Constant": [],
"Constructor": [],
"Deprecated": [],
"Field": [],
"Function": [],
"Method": [
{
"def": "SpeedMenu:rescan()",
"desc": "Redetect the active interface, darkmode …And redraw everything.",
"doc": "Redetect the active interface, darkmode …And redraw everything.\n",
"name": "rescan",
"signature": "SpeedMenu:rescan()",
"stripped_doc": "",
"type": "Method"
}
],
"Variable": [],
"desc": "Menubar netspeed meter",
"doc": "Menubar netspeed meter\n\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpeedMenu.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpeedMenu.spoon.zip)",
"items": [
{
"def": "SpeedMenu:rescan()",
"desc": "Redetect the active interface, darkmode …And redraw everything.",
"doc": "Redetect the active interface, darkmode …And redraw everything.\n",
"name": "rescan",
"signature": "SpeedMenu:rescan()",
"stripped_doc": "",
"type": "Method"
}
],
"name": "SpeedMenu",
"stripped_doc": "\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpeedMenu.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpeedMenu.spoon.zip)",
"submodules": [],
"type": "Module"
}
]
+128
View File
@@ -0,0 +1,128 @@
--- === SpeedMenu ===
---
--- Menubar netspeed meter
---
--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpeedMenu.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpeedMenu.spoon.zip)
local obj={}
obj.__index = obj
-- Metadata
obj.name = "SpeedMenu"
obj.version = "1.0"
obj.author = "ashfinal <ashfinal@gmail.com>"
obj.homepage = "https://github.com/Hammerspoon/Spoons"
obj.license = "MIT - https://opensource.org/licenses/MIT"
function obj:init()
self.menubar = hs.menubar.new(false)
end
function obj:start()
obj.menubar:returnToMenuBar()
obj:rescan()
end
function obj:stop()
obj.menubar:removeFromMenuBar()
obj.timer:stop()
end
function obj:toggle()
if obj.timer:running() then
obj:stop()
else
obj:start()
end
end
local function data_diff()
local in_seq = hs.execute(obj.instr)
local out_seq = hs.execute(obj.outstr)
local in_diff = in_seq - obj.inseq
local out_diff = out_seq - obj.outseq
if in_diff/1024 > 1024 then
obj.kbin = string.format("%6.2f", in_diff/1024/1024) .. ' mb/s'
else
obj.kbin = string.format("%6.2f", in_diff/1024) .. ' kb/s'
end
if out_diff/1024 > 1024 then
obj.kbout = string.format("%6.2f", out_diff/1024/1024) .. ' mb/s'
else
obj.kbout = string.format("%6.2f", out_diff/1024) .. ' kb/s'
end
local disp_str = '' .. obj.kbout .. '\n' .. obj.kbin
if obj.darkmode then
obj.disp_str = hs.styledtext.new(disp_str, {font={size=9.0, color={hex="#FFFFFF"}}})
else
obj.disp_str = hs.styledtext.new(disp_str, {font={size=9.0, color={hex="#000000"}}})
end
obj.menubar:setTitle(obj.disp_str)
obj.inseq = in_seq
obj.outseq = out_seq
end
--- SpeedMenu:rescan()
--- Method
--- Redetect the active interface, darkmode …And redraw everything.
---
function obj:rescan()
obj.interface = hs.network.primaryInterfaces()
obj.darkmode = hs.osascript.applescript('tell application "System Events"\nreturn dark mode of appearance preferences\nend tell')
local menuitems_table = {}
if obj.interface then
-- Inspect active interface and create menuitems
local interface_detail = hs.network.interfaceDetails(obj.interface)
if interface_detail.AirPort then
local ssid = interface_detail.AirPort.SSID
table.insert(menuitems_table, {
title = "SSID: " .. ssid,
tooltip = "Copy SSID to clipboard",
fn = function() hs.pasteboard.setContents(ssid) end
})
end
if interface_detail.IPv4 then
local ipv4 = interface_detail.IPv4.Addresses[1]
table.insert(menuitems_table, {
title = "IPv4: " .. ipv4,
tooltip = "Copy IPv4 to clipboard",
fn = function() hs.pasteboard.setContents(ipv4) end
})
end
if interface_detail.IPv6 then
local ipv6 = interface_detail.IPv6.Addresses[1]
table.insert(menuitems_table, {
title = "IPv6: " .. ipv6,
tooltip = "Copy IPv6 to clipboard",
fn = function() hs.pasteboard.setContents(ipv6) end
})
end
local macaddr = hs.execute('ifconfig ' .. obj.interface .. ' | grep ether | awk \'{print $2}\'')
table.insert(menuitems_table, {
title = "MAC Addr: " .. macaddr,
tooltip = "Copy MAC Address to clipboard",
fn = function() hs.pasteboard.setContents(macaddr) end
})
-- Start watching the netspeed delta
obj.instr = 'netstat -ibn | grep -e ' .. obj.interface .. ' -m 1 | awk \'{print $7}\''
obj.outstr = 'netstat -ibn | grep -e ' .. obj.interface .. ' -m 1 | awk \'{print $10}\''
obj.inseq = hs.execute(obj.instr)
obj.outseq = hs.execute(obj.outstr)
if obj.timer then
obj.timer:stop()
obj.timer = nil
end
obj.timer = hs.timer.doEvery(1, data_diff)
end
table.insert(menuitems_table, {
title = "Rescan Network Interfaces",
fn = function() obj:rescan() end
})
obj.menubar:setTitle("⚠︎")
obj.menubar:setMenu(menuitems_table)
end
return obj
+478
View File
@@ -0,0 +1,478 @@
[
{
"Command": [],
"Constant": [],
"Constructor": [],
"Deprecated": [],
"Field": [],
"Function": [],
"Method": [
{
"def": "SpoonInstall:andUse(name, arg)",
"desc": "Declaratively install, load and configure a Spoon",
"doc": "Declaratively install, load and configure a Spoon\n\nParameters:\n * name - the name of the Spoon to install (without the `.spoon` extension). If the Spoon is already installed, it will be loaded using `hs.loadSpoon()`. If it is not installed, it will be installed using `SpoonInstall:asyncInstallSpoonFromRepo()` and then loaded.\n * arg - if provided, can be used to specify the configuration of the Spoon. The following keys are recognized (all are optional):\n * repo - repository from where the Spoon should be installed if not present in the system, as defined in `SpoonInstall.repos`. Defaults to `\"default\"`.\n * config - a table containing variables to be stored in the Spoon object to configure it. For example, `config = { answer = 42 }` will result in `spoon.<LoadedSpoon>.answer` being set to 42.\n * hotkeys - a table containing hotkey bindings. If provided, will be passed as-is to the Spoon's `bindHotkeys()` method. The special string `\"default\"` can be given to use the Spoons `defaultHotkeys` variable, if it exists.\n * fn - a function which will be called with the freshly-loaded Spoon object as its first argument.\n * loglevel - if the Spoon has a variable called `logger`, its `setLogLevel()` method will be called with this value.\n * start - if `true`, call the Spoon's `start()` method after configuring everything else.\n * disable - if `true`, do nothing. Easier than commenting it out when you want to temporarily disable a spoon.\n\nReturns:\n * None",
"name": "andUse",
"parameters": [
" * name - the name of the Spoon to install (without the `.spoon` extension). If the Spoon is already installed, it will be loaded using `hs.loadSpoon()`. If it is not installed, it will be installed using `SpoonInstall:asyncInstallSpoonFromRepo()` and then loaded.",
" * arg - if provided, can be used to specify the configuration of the Spoon. The following keys are recognized (all are optional):",
" * repo - repository from where the Spoon should be installed if not present in the system, as defined in `SpoonInstall.repos`. Defaults to `\"default\"`.",
" * config - a table containing variables to be stored in the Spoon object to configure it. For example, `config = { answer = 42 }` will result in `spoon.<LoadedSpoon>.answer` being set to 42.",
" * hotkeys - a table containing hotkey bindings. If provided, will be passed as-is to the Spoon's `bindHotkeys()` method. The special string `\"default\"` can be given to use the Spoons `defaultHotkeys` variable, if it exists.",
" * fn - a function which will be called with the freshly-loaded Spoon object as its first argument.",
" * loglevel - if the Spoon has a variable called `logger`, its `setLogLevel()` method will be called with this value.",
" * start - if `true`, call the Spoon's `start()` method after configuring everything else.",
" * disable - if `true`, do nothing. Easier than commenting it out when you want to temporarily disable a spoon."
],
"returns": [
" * None"
],
"signature": "SpoonInstall:andUse(name, arg)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "SpoonInstall:asyncInstallSpoonFromRepo(name, repo, callback)",
"desc": "Asynchronously install a Spoon from a registered repository",
"doc": "Asynchronously install a Spoon from a registered repository\n\nParameters:\n * name - Name of the Spoon to install.\n * repo - Name of the repository to use. Defaults to `\"default\"`\n * callback - if given, a function to call after the installation finishes (also if it fails). The function receives the following arguments:\n * urlparts - Result of calling `hs.http.urlParts` on the URL of the Spoon zip file\n * success - boolean indicating whether the installation was successful\n\nReturns:\n * `true` if the installation was correctly initiated (i.e. the repo and spoon name were correct), `false` otherwise.",
"name": "asyncInstallSpoonFromRepo",
"parameters": [
" * name - Name of the Spoon to install.",
" * repo - Name of the repository to use. Defaults to `\"default\"`",
" * callback - if given, a function to call after the installation finishes (also if it fails). The function receives the following arguments:",
" * urlparts - Result of calling `hs.http.urlParts` on the URL of the Spoon zip file",
" * success - boolean indicating whether the installation was successful"
],
"returns": [
" * `true` if the installation was correctly initiated (i.e. the repo and spoon name were correct), `false` otherwise."
],
"signature": "SpoonInstall:asyncInstallSpoonFromRepo(name, repo, callback)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "SpoonInstall:asyncInstallSpoonFromZipURL(url, callback)",
"desc": "Asynchronously download a Spoon zip file and install it.",
"doc": "Asynchronously download a Spoon zip file and install it.\n\nParameters:\n * url - URL of the zip file to install.\n * callback - if given, a function to call after the installation finishes (also if it fails). The function receives the following arguments:\n * urlparts - Result of calling `hs.http.urlParts` on the URL of the Spoon zip file\n * success - boolean indicating whether the installation was successful\n\nReturns:\n * `true` if the installation was correctly initiated (i.e. the URL is valid), `false` otherwise",
"name": "asyncInstallSpoonFromZipURL",
"parameters": [
" * url - URL of the zip file to install.",
" * callback - if given, a function to call after the installation finishes (also if it fails). The function receives the following arguments:",
" * urlparts - Result of calling `hs.http.urlParts` on the URL of the Spoon zip file",
" * success - boolean indicating whether the installation was successful"
],
"returns": [
" * `true` if the installation was correctly initiated (i.e. the URL is valid), `false` otherwise"
],
"signature": "SpoonInstall:asyncInstallSpoonFromZipURL(url, callback)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "SpoonInstall:asyncUpdateAllRepos()",
"desc": "Asynchronously fetch the information about the contents of all Spoon repositories registered in `SpoonInstall.repos`",
"doc": "Asynchronously fetch the information about the contents of all Spoon repositories registered in `SpoonInstall.repos`\n\nParameters:\n * None\n\nReturns:\n * None\n\nNotes:\n * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions.",
"name": "asyncUpdateAllRepos",
"notes": [
" * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions."
],
"parameters": [
" * None"
],
"returns": [
" * None"
],
"signature": "SpoonInstall:asyncUpdateAllRepos()",
"stripped_doc": "",
"type": "Method"
},
{
"def": "SpoonInstall:asyncUpdateRepo(repo, callback)",
"desc": "Asynchronously fetch the information about the contents of a Spoon repository",
"doc": "Asynchronously fetch the information about the contents of a Spoon repository\n\nParameters:\n * repo - name of the repository to update. Defaults to `\"default\"`.\n * callback - if given, a function to be called after the update finishes (also if it fails). The function will receive the following arguments:\n * repo - name of the repository\n * success - boolean indicating whether the update succeeded\n\nReturns:\n * `true` if the update was correctly initiated (i.e. the repo name is valid), `nil` otherwise\n\nNotes:\n * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions.",
"name": "asyncUpdateRepo",
"notes": [
" * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions."
],
"parameters": [
" * repo - name of the repository to update. Defaults to `\"default\"`.",
" * callback - if given, a function to be called after the update finishes (also if it fails). The function will receive the following arguments:",
" * repo - name of the repository",
" * success - boolean indicating whether the update succeeded"
],
"returns": [
" * `true` if the update was correctly initiated (i.e. the repo name is valid), `nil` otherwise"
],
"signature": "SpoonInstall:asyncUpdateRepo(repo, callback)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "SpoonInstall:installSpoonFromRepo(name, repo)",
"desc": "Synchronously install a Spoon from a registered repository",
"doc": "Synchronously install a Spoon from a registered repository\n\nParameters:\n * name = Name of the Spoon to install.\n * repo - Name of the repository to use. Defaults to `\"default\"`\n\nReturns:\n * `true` if the installation was successful, `nil` otherwise.",
"name": "installSpoonFromRepo",
"parameters": [
" * name = Name of the Spoon to install.",
" * repo - Name of the repository to use. Defaults to `\"default\"`"
],
"returns": [
" * `true` if the installation was successful, `nil` otherwise."
],
"signature": "SpoonInstall:installSpoonFromRepo(name, repo)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "SpoonInstall:installSpoonFromZipURL(url)",
"desc": "Synchronously download a Spoon zip file and install it.",
"doc": "Synchronously download a Spoon zip file and install it.\n\nParameters:\n * url - URL of the zip file to install.\n\nReturns:\n * `true` if the installation was successful, `nil` otherwise",
"name": "installSpoonFromZipURL",
"parameters": [
" * url - URL of the zip file to install."
],
"returns": [
" * `true` if the installation was successful, `nil` otherwise"
],
"signature": "SpoonInstall:installSpoonFromZipURL(url)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "SpoonInstall:repolist()",
"desc": "Return a sorted list of registered Spoon repositories",
"doc": "Return a sorted list of registered Spoon repositories\n\nParameters:\n * None\n\nReturns:\n * Table containing a list of strings with the repository identifiers",
"name": "repolist",
"parameters": [
" * None"
],
"returns": [
" * Table containing a list of strings with the repository identifiers"
],
"signature": "SpoonInstall:repolist()",
"stripped_doc": "",
"type": "Method"
},
{
"def": "SpoonInstall:search(pat)",
"desc": "Search repositories for a pattern",
"doc": "Search repositories for a pattern\n\nParameters:\n * pat - Lua pattern that will be matched against the name and description of each spoon in the registered repositories. All text is converted to lowercase before searching it, so you can use all-lowercase in your pattern.\n\nReturns:\n * Table containing a list of matching entries. Each entry is a table with the following keys:\n * name - Spoon name\n * desc - description of the spoon\n * repo - identifier in the repository where the match was found",
"name": "search",
"parameters": [
" * pat - Lua pattern that will be matched against the name and description of each spoon in the registered repositories. All text is converted to lowercase before searching it, so you can use all-lowercase in your pattern."
],
"returns": [
" * Table containing a list of matching entries. Each entry is a table with the following keys:",
" * name - Spoon name",
" * desc - description of the spoon",
" * repo - identifier in the repository where the match was found"
],
"signature": "SpoonInstall:search(pat)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "SpoonInstall:updateAllRepos()",
"desc": "Synchronously fetch the information about the contents of all Spoon repositories registered in `SpoonInstall.repos`",
"doc": "Synchronously fetch the information about the contents of all Spoon repositories registered in `SpoonInstall.repos`\n\nParameters:\n * None\n\nReturns:\n * None\n\nNotes:\n * This is a synchronous call, which means Hammerspoon will be blocked until it finishes.\n * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions.",
"name": "updateAllRepos",
"notes": [
" * This is a synchronous call, which means Hammerspoon will be blocked until it finishes.",
" * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions."
],
"parameters": [
" * None"
],
"returns": [
" * None"
],
"signature": "SpoonInstall:updateAllRepos()",
"stripped_doc": "",
"type": "Method"
},
{
"def": "SpoonInstall:updateRepo(repo)",
"desc": "Synchronously fetch the information about the contents of a Spoon repository",
"doc": "Synchronously fetch the information about the contents of a Spoon repository\n\nParameters:\n * repo - name of the repository to update. Defaults to `\"default\"`.\n\nReturns:\n * `true` if the update was successful, `nil` otherwise\n\nNotes:\n * This is a synchronous call, which means Hammerspoon will be blocked until it finishes. For use in your configuration files, it's advisable to use `SpoonInstall.asyncUpdateRepo()` instead.\n * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions.",
"name": "updateRepo",
"notes": [
" * This is a synchronous call, which means Hammerspoon will be blocked until it finishes. For use in your configuration files, it's advisable to use `SpoonInstall.asyncUpdateRepo()` instead.",
" * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions."
],
"parameters": [
" * repo - name of the repository to update. Defaults to `\"default\"`."
],
"returns": [
" * `true` if the update was successful, `nil` otherwise"
],
"signature": "SpoonInstall:updateRepo(repo)",
"stripped_doc": "",
"type": "Method"
}
],
"Variable": [
{
"def": "SpoonInstall.logger",
"desc": "Logger object used within the Spoon. Can be accessed to set the default log level for the messages coming from the Spoon.",
"doc": "Logger object used within the Spoon. Can be accessed to set the default log level for the messages coming from the Spoon.",
"name": "logger",
"signature": "SpoonInstall.logger",
"stripped_doc": "",
"type": "Variable"
},
{
"def": "SpoonInstall.repos",
"desc": "Table containing the list of available Spoon repositories. The key",
"doc": "Table containing the list of available Spoon repositories. The key\nof each entry is an identifier for the repository, and its value\nis a table with the following entries:\n * desc - Human-readable description for the repository\n * branch - Active git branch for the Spoon files\n * url - Base URL for the repository. For now the repository is assumed to be hosted in GitHub, and the URL should be the main base URL of the repository. Repository metadata needs to be stored under `docs/docs.json`, and the Spoon zip files need to be stored under `Spoons/`.\n\nDefault value:\n```\n{\n default = {\n url = \"https://github.com/Hammerspoon/Spoons\",\n desc = \"Main Hammerspoon Spoon repository\",\n branch = \"master\",\n }\n}\n```",
"name": "repos",
"signature": "SpoonInstall.repos",
"stripped_doc": "of each entry is an identifier for the repository, and its value\nis a table with the following entries:\n * desc - Human-readable description for the repository\n * branch - Active git branch for the Spoon files\n * url - Base URL for the repository. For now the repository is assumed to be hosted in GitHub, and the URL should be the main base URL of the repository. Repository metadata needs to be stored under `docs/docs.json`, and the Spoon zip files need to be stored under `Spoons/`.\nDefault value:\n```\n{\n default = {\n url = \"https://github.com/Hammerspoon/Spoons\",\n desc = \"Main Hammerspoon Spoon repository\",\n branch = \"master\",\n }\n}\n```",
"type": "Variable"
},
{
"def": "SpoonInstall.use_syncinstall",
"desc": "If `true`, `andUse()` will update repos and install packages synchronously. Defaults to `false`.",
"doc": "If `true`, `andUse()` will update repos and install packages synchronously. Defaults to `false`.\n\nKeep in mind that if you set this to `true`, Hammerspoon will\nblock until all missing Spoons are installed, but the notifications\nwill happen at a more \"human readable\" rate.",
"name": "use_syncinstall",
"signature": "SpoonInstall.use_syncinstall",
"stripped_doc": "Keep in mind that if you set this to `true`, Hammerspoon will\nblock until all missing Spoons are installed, but the notifications\nwill happen at a more \"human readable\" rate.",
"type": "Variable"
}
],
"desc": "Install and manage Spoons and Spoon repositories",
"doc": "Install and manage Spoons and Spoon repositories\n\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpoonInstall.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpoonInstall.spoon.zip)",
"items": [
{
"def": "SpoonInstall:andUse(name, arg)",
"desc": "Declaratively install, load and configure a Spoon",
"doc": "Declaratively install, load and configure a Spoon\n\nParameters:\n * name - the name of the Spoon to install (without the `.spoon` extension). If the Spoon is already installed, it will be loaded using `hs.loadSpoon()`. If it is not installed, it will be installed using `SpoonInstall:asyncInstallSpoonFromRepo()` and then loaded.\n * arg - if provided, can be used to specify the configuration of the Spoon. The following keys are recognized (all are optional):\n * repo - repository from where the Spoon should be installed if not present in the system, as defined in `SpoonInstall.repos`. Defaults to `\"default\"`.\n * config - a table containing variables to be stored in the Spoon object to configure it. For example, `config = { answer = 42 }` will result in `spoon.<LoadedSpoon>.answer` being set to 42.\n * hotkeys - a table containing hotkey bindings. If provided, will be passed as-is to the Spoon's `bindHotkeys()` method. The special string `\"default\"` can be given to use the Spoons `defaultHotkeys` variable, if it exists.\n * fn - a function which will be called with the freshly-loaded Spoon object as its first argument.\n * loglevel - if the Spoon has a variable called `logger`, its `setLogLevel()` method will be called with this value.\n * start - if `true`, call the Spoon's `start()` method after configuring everything else.\n * disable - if `true`, do nothing. Easier than commenting it out when you want to temporarily disable a spoon.\n\nReturns:\n * None",
"name": "andUse",
"parameters": [
" * name - the name of the Spoon to install (without the `.spoon` extension). If the Spoon is already installed, it will be loaded using `hs.loadSpoon()`. If it is not installed, it will be installed using `SpoonInstall:asyncInstallSpoonFromRepo()` and then loaded.",
" * arg - if provided, can be used to specify the configuration of the Spoon. The following keys are recognized (all are optional):",
" * repo - repository from where the Spoon should be installed if not present in the system, as defined in `SpoonInstall.repos`. Defaults to `\"default\"`.",
" * config - a table containing variables to be stored in the Spoon object to configure it. For example, `config = { answer = 42 }` will result in `spoon.<LoadedSpoon>.answer` being set to 42.",
" * hotkeys - a table containing hotkey bindings. If provided, will be passed as-is to the Spoon's `bindHotkeys()` method. The special string `\"default\"` can be given to use the Spoons `defaultHotkeys` variable, if it exists.",
" * fn - a function which will be called with the freshly-loaded Spoon object as its first argument.",
" * loglevel - if the Spoon has a variable called `logger`, its `setLogLevel()` method will be called with this value.",
" * start - if `true`, call the Spoon's `start()` method after configuring everything else.",
" * disable - if `true`, do nothing. Easier than commenting it out when you want to temporarily disable a spoon."
],
"returns": [
" * None"
],
"signature": "SpoonInstall:andUse(name, arg)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "SpoonInstall:asyncInstallSpoonFromRepo(name, repo, callback)",
"desc": "Asynchronously install a Spoon from a registered repository",
"doc": "Asynchronously install a Spoon from a registered repository\n\nParameters:\n * name - Name of the Spoon to install.\n * repo - Name of the repository to use. Defaults to `\"default\"`\n * callback - if given, a function to call after the installation finishes (also if it fails). The function receives the following arguments:\n * urlparts - Result of calling `hs.http.urlParts` on the URL of the Spoon zip file\n * success - boolean indicating whether the installation was successful\n\nReturns:\n * `true` if the installation was correctly initiated (i.e. the repo and spoon name were correct), `false` otherwise.",
"name": "asyncInstallSpoonFromRepo",
"parameters": [
" * name - Name of the Spoon to install.",
" * repo - Name of the repository to use. Defaults to `\"default\"`",
" * callback - if given, a function to call after the installation finishes (also if it fails). The function receives the following arguments:",
" * urlparts - Result of calling `hs.http.urlParts` on the URL of the Spoon zip file",
" * success - boolean indicating whether the installation was successful"
],
"returns": [
" * `true` if the installation was correctly initiated (i.e. the repo and spoon name were correct), `false` otherwise."
],
"signature": "SpoonInstall:asyncInstallSpoonFromRepo(name, repo, callback)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "SpoonInstall:asyncInstallSpoonFromZipURL(url, callback)",
"desc": "Asynchronously download a Spoon zip file and install it.",
"doc": "Asynchronously download a Spoon zip file and install it.\n\nParameters:\n * url - URL of the zip file to install.\n * callback - if given, a function to call after the installation finishes (also if it fails). The function receives the following arguments:\n * urlparts - Result of calling `hs.http.urlParts` on the URL of the Spoon zip file\n * success - boolean indicating whether the installation was successful\n\nReturns:\n * `true` if the installation was correctly initiated (i.e. the URL is valid), `false` otherwise",
"name": "asyncInstallSpoonFromZipURL",
"parameters": [
" * url - URL of the zip file to install.",
" * callback - if given, a function to call after the installation finishes (also if it fails). The function receives the following arguments:",
" * urlparts - Result of calling `hs.http.urlParts` on the URL of the Spoon zip file",
" * success - boolean indicating whether the installation was successful"
],
"returns": [
" * `true` if the installation was correctly initiated (i.e. the URL is valid), `false` otherwise"
],
"signature": "SpoonInstall:asyncInstallSpoonFromZipURL(url, callback)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "SpoonInstall:asyncUpdateAllRepos()",
"desc": "Asynchronously fetch the information about the contents of all Spoon repositories registered in `SpoonInstall.repos`",
"doc": "Asynchronously fetch the information about the contents of all Spoon repositories registered in `SpoonInstall.repos`\n\nParameters:\n * None\n\nReturns:\n * None\n\nNotes:\n * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions.",
"name": "asyncUpdateAllRepos",
"notes": [
" * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions."
],
"parameters": [
" * None"
],
"returns": [
" * None"
],
"signature": "SpoonInstall:asyncUpdateAllRepos()",
"stripped_doc": "",
"type": "Method"
},
{
"def": "SpoonInstall:asyncUpdateRepo(repo, callback)",
"desc": "Asynchronously fetch the information about the contents of a Spoon repository",
"doc": "Asynchronously fetch the information about the contents of a Spoon repository\n\nParameters:\n * repo - name of the repository to update. Defaults to `\"default\"`.\n * callback - if given, a function to be called after the update finishes (also if it fails). The function will receive the following arguments:\n * repo - name of the repository\n * success - boolean indicating whether the update succeeded\n\nReturns:\n * `true` if the update was correctly initiated (i.e. the repo name is valid), `nil` otherwise\n\nNotes:\n * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions.",
"name": "asyncUpdateRepo",
"notes": [
" * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions."
],
"parameters": [
" * repo - name of the repository to update. Defaults to `\"default\"`.",
" * callback - if given, a function to be called after the update finishes (also if it fails). The function will receive the following arguments:",
" * repo - name of the repository",
" * success - boolean indicating whether the update succeeded"
],
"returns": [
" * `true` if the update was correctly initiated (i.e. the repo name is valid), `nil` otherwise"
],
"signature": "SpoonInstall:asyncUpdateRepo(repo, callback)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "SpoonInstall:installSpoonFromRepo(name, repo)",
"desc": "Synchronously install a Spoon from a registered repository",
"doc": "Synchronously install a Spoon from a registered repository\n\nParameters:\n * name = Name of the Spoon to install.\n * repo - Name of the repository to use. Defaults to `\"default\"`\n\nReturns:\n * `true` if the installation was successful, `nil` otherwise.",
"name": "installSpoonFromRepo",
"parameters": [
" * name = Name of the Spoon to install.",
" * repo - Name of the repository to use. Defaults to `\"default\"`"
],
"returns": [
" * `true` if the installation was successful, `nil` otherwise."
],
"signature": "SpoonInstall:installSpoonFromRepo(name, repo)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "SpoonInstall:installSpoonFromZipURL(url)",
"desc": "Synchronously download a Spoon zip file and install it.",
"doc": "Synchronously download a Spoon zip file and install it.\n\nParameters:\n * url - URL of the zip file to install.\n\nReturns:\n * `true` if the installation was successful, `nil` otherwise",
"name": "installSpoonFromZipURL",
"parameters": [
" * url - URL of the zip file to install."
],
"returns": [
" * `true` if the installation was successful, `nil` otherwise"
],
"signature": "SpoonInstall:installSpoonFromZipURL(url)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "SpoonInstall.logger",
"desc": "Logger object used within the Spoon. Can be accessed to set the default log level for the messages coming from the Spoon.",
"doc": "Logger object used within the Spoon. Can be accessed to set the default log level for the messages coming from the Spoon.",
"name": "logger",
"signature": "SpoonInstall.logger",
"stripped_doc": "",
"type": "Variable"
},
{
"def": "SpoonInstall:repolist()",
"desc": "Return a sorted list of registered Spoon repositories",
"doc": "Return a sorted list of registered Spoon repositories\n\nParameters:\n * None\n\nReturns:\n * Table containing a list of strings with the repository identifiers",
"name": "repolist",
"parameters": [
" * None"
],
"returns": [
" * Table containing a list of strings with the repository identifiers"
],
"signature": "SpoonInstall:repolist()",
"stripped_doc": "",
"type": "Method"
},
{
"def": "SpoonInstall.repos",
"desc": "Table containing the list of available Spoon repositories. The key",
"doc": "Table containing the list of available Spoon repositories. The key\nof each entry is an identifier for the repository, and its value\nis a table with the following entries:\n * desc - Human-readable description for the repository\n * branch - Active git branch for the Spoon files\n * url - Base URL for the repository. For now the repository is assumed to be hosted in GitHub, and the URL should be the main base URL of the repository. Repository metadata needs to be stored under `docs/docs.json`, and the Spoon zip files need to be stored under `Spoons/`.\n\nDefault value:\n```\n{\n default = {\n url = \"https://github.com/Hammerspoon/Spoons\",\n desc = \"Main Hammerspoon Spoon repository\",\n branch = \"master\",\n }\n}\n```",
"name": "repos",
"signature": "SpoonInstall.repos",
"stripped_doc": "of each entry is an identifier for the repository, and its value\nis a table with the following entries:\n * desc - Human-readable description for the repository\n * branch - Active git branch for the Spoon files\n * url - Base URL for the repository. For now the repository is assumed to be hosted in GitHub, and the URL should be the main base URL of the repository. Repository metadata needs to be stored under `docs/docs.json`, and the Spoon zip files need to be stored under `Spoons/`.\nDefault value:\n```\n{\n default = {\n url = \"https://github.com/Hammerspoon/Spoons\",\n desc = \"Main Hammerspoon Spoon repository\",\n branch = \"master\",\n }\n}\n```",
"type": "Variable"
},
{
"def": "SpoonInstall:search(pat)",
"desc": "Search repositories for a pattern",
"doc": "Search repositories for a pattern\n\nParameters:\n * pat - Lua pattern that will be matched against the name and description of each spoon in the registered repositories. All text is converted to lowercase before searching it, so you can use all-lowercase in your pattern.\n\nReturns:\n * Table containing a list of matching entries. Each entry is a table with the following keys:\n * name - Spoon name\n * desc - description of the spoon\n * repo - identifier in the repository where the match was found",
"name": "search",
"parameters": [
" * pat - Lua pattern that will be matched against the name and description of each spoon in the registered repositories. All text is converted to lowercase before searching it, so you can use all-lowercase in your pattern."
],
"returns": [
" * Table containing a list of matching entries. Each entry is a table with the following keys:",
" * name - Spoon name",
" * desc - description of the spoon",
" * repo - identifier in the repository where the match was found"
],
"signature": "SpoonInstall:search(pat)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "SpoonInstall:updateAllRepos()",
"desc": "Synchronously fetch the information about the contents of all Spoon repositories registered in `SpoonInstall.repos`",
"doc": "Synchronously fetch the information about the contents of all Spoon repositories registered in `SpoonInstall.repos`\n\nParameters:\n * None\n\nReturns:\n * None\n\nNotes:\n * This is a synchronous call, which means Hammerspoon will be blocked until it finishes.\n * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions.",
"name": "updateAllRepos",
"notes": [
" * This is a synchronous call, which means Hammerspoon will be blocked until it finishes.",
" * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions."
],
"parameters": [
" * None"
],
"returns": [
" * None"
],
"signature": "SpoonInstall:updateAllRepos()",
"stripped_doc": "",
"type": "Method"
},
{
"def": "SpoonInstall:updateRepo(repo)",
"desc": "Synchronously fetch the information about the contents of a Spoon repository",
"doc": "Synchronously fetch the information about the contents of a Spoon repository\n\nParameters:\n * repo - name of the repository to update. Defaults to `\"default\"`.\n\nReturns:\n * `true` if the update was successful, `nil` otherwise\n\nNotes:\n * This is a synchronous call, which means Hammerspoon will be blocked until it finishes. For use in your configuration files, it's advisable to use `SpoonInstall.asyncUpdateRepo()` instead.\n * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions.",
"name": "updateRepo",
"notes": [
" * This is a synchronous call, which means Hammerspoon will be blocked until it finishes. For use in your configuration files, it's advisable to use `SpoonInstall.asyncUpdateRepo()` instead.",
" * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions."
],
"parameters": [
" * repo - name of the repository to update. Defaults to `\"default\"`."
],
"returns": [
" * `true` if the update was successful, `nil` otherwise"
],
"signature": "SpoonInstall:updateRepo(repo)",
"stripped_doc": "",
"type": "Method"
},
{
"def": "SpoonInstall.use_syncinstall",
"desc": "If `true`, `andUse()` will update repos and install packages synchronously. Defaults to `false`.",
"doc": "If `true`, `andUse()` will update repos and install packages synchronously. Defaults to `false`.\n\nKeep in mind that if you set this to `true`, Hammerspoon will\nblock until all missing Spoons are installed, but the notifications\nwill happen at a more \"human readable\" rate.",
"name": "use_syncinstall",
"signature": "SpoonInstall.use_syncinstall",
"stripped_doc": "Keep in mind that if you set this to `true`, Hammerspoon will\nblock until all missing Spoons are installed, but the notifications\nwill happen at a more \"human readable\" rate.",
"type": "Variable"
}
],
"name": "SpoonInstall",
"stripped_doc": "\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpoonInstall.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpoonInstall.spoon.zip)",
"submodules": [],
"type": "Module"
}
]
+447
View File
@@ -0,0 +1,447 @@
--- === SpoonInstall ===
---
--- Install and manage Spoons and Spoon repositories
---
--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpoonInstall.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpoonInstall.spoon.zip)
local obj={}
obj.__index = obj
-- Metadata
obj.name = "SpoonInstall"
obj.version = "0.1"
obj.author = "Diego Zamboni <diego@zzamboni.org>"
obj.homepage = "https://github.com/Hammerspoon/Spoons"
obj.license = "MIT - https://opensource.org/licenses/MIT"
--- SpoonInstall.logger
--- Variable
--- Logger object used within the Spoon. Can be accessed to set the default log level for the messages coming from the Spoon.
obj.logger = hs.logger.new('SpoonInstall')
--- SpoonInstall.repos
--- Variable
--- Table containing the list of available Spoon repositories. The key
--- of each entry is an identifier for the repository, and its value
--- is a table with the following entries:
--- * desc - Human-readable description for the repository
--- * branch - Active git branch for the Spoon files
--- * url - Base URL for the repository. For now the repository is assumed to be hosted in GitHub, and the URL should be the main base URL of the repository. Repository metadata needs to be stored under `docs/docs.json`, and the Spoon zip files need to be stored under `Spoons/`.
---
--- Default value:
--- ```
--- {
--- default = {
--- url = "https://github.com/Hammerspoon/Spoons",
--- desc = "Main Hammerspoon Spoon repository",
--- branch = "master",
--- }
--- }
--- ```
obj.repos = {
default = {
url = "https://github.com/Hammerspoon/Spoons",
desc = "Main Hammerspoon Spoon repository",
branch = "master",
}
}
--- SpoonInstall.use_syncinstall
--- Variable
--- If `true`, `andUse()` will update repos and install packages synchronously. Defaults to `false`.
---
--- Keep in mind that if you set this to `true`, Hammerspoon will
--- block until all missing Spoons are installed, but the notifications
--- will happen at a more "human readable" rate.
obj.use_syncinstall = false
-- Execute a command and return its output with trailing EOLs trimmed. If the command fails, an error message is logged.
local function _x(cmd, errfmt, ...)
local output, status = hs.execute(cmd)
if status then
local trimstr = string.gsub(output, "\n*$", "")
return trimstr
else
obj.logger.ef(errfmt, ...)
return nil
end
end
-- --------------------------------------------------------------------
-- Spoon repository management
-- Internal callback to process and store the data from docs.json about a repository
-- callback is called with repo as arguments, only if the call is successful
function obj:_storeRepoJSON(repo, callback, status, body, hdrs)
local success=nil
if (status < 100) or (status >= 400) then
self.logger.ef("Error fetching JSON data for repository '%s'. Error code %d: %s", repo, status, body or "<no error message>")
else
local json = hs.json.decode(body)
if json then
self.repos[repo].data = {}
for i,v in ipairs(json) do
v.download_url = self.repos[repo].download_base_url .. v.name .. ".spoon.zip"
self.repos[repo].data[v.name] = v
end
self.logger.df("Updated JSON data for repository '%s'", repo)
success=true
else
self.logger.ef("Invalid JSON received for repository '%s': %s", repo, body)
end
end
if callback then
callback(repo, success)
end
return success
end
-- Internal function to return the URL of the docs.json file based on the URL of a GitHub repo
function obj:_build_repo_json_url(repo)
if self.repos[repo] and self.repos[repo].url then
local branch = self.repos[repo].branch or "master"
self.repos[repo].json_url = string.gsub(self.repos[repo].url, "/$", "") .. "/raw/"..branch.."/docs/docs.json"
self.repos[repo].download_base_url = string.gsub(self.repos[repo].url, "/$", "") .. "/raw/"..branch.."/Spoons/"
return true
else
self.logger.ef("Invalid or unknown repository '%s'", repo)
return nil
end
end
--- SpoonInstall:asyncUpdateRepo(repo, callback)
--- Method
--- Asynchronously fetch the information about the contents of a Spoon repository
---
--- Parameters:
--- * repo - name of the repository to update. Defaults to `"default"`.
--- * callback - if given, a function to be called after the update finishes (also if it fails). The function will receive the following arguments:
--- * repo - name of the repository
--- * success - boolean indicating whether the update succeeded
---
--- Returns:
--- * `true` if the update was correctly initiated (i.e. the repo name is valid), `nil` otherwise
---
--- Notes:
--- * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions.
function obj:asyncUpdateRepo(repo, callback)
if not repo then repo = 'default' end
if self:_build_repo_json_url(repo) then
hs.http.asyncGet(self.repos[repo].json_url, nil, hs.fnutils.partial(self._storeRepoJSON, self, repo, callback))
return true
else
return nil
end
end
--- SpoonInstall:updateRepo(repo)
--- Method
--- Synchronously fetch the information about the contents of a Spoon repository
---
--- Parameters:
--- * repo - name of the repository to update. Defaults to `"default"`.
---
--- Returns:
--- * `true` if the update was successful, `nil` otherwise
---
--- Notes:
--- * This is a synchronous call, which means Hammerspoon will be blocked until it finishes. For use in your configuration files, it's advisable to use `SpoonInstall.asyncUpdateRepo()` instead.
--- * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions.
function obj:updateRepo(repo)
if not repo then repo = 'default' end
if self:_build_repo_json_url(repo) then
local a,b,c = hs.http.get(self.repos[repo].json_url)
return self:_storeRepoJSON(repo, nil, a, b, c)
else
return nil
end
end
--- SpoonInstall:asyncUpdateAllRepos()
--- Method
--- Asynchronously fetch the information about the contents of all Spoon repositories registered in `SpoonInstall.repos`
---
--- Parameters:
--- * None
---
--- Returns:
--- * None
---
--- Notes:
--- * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions.
function obj:asyncUpdateAllRepos()
for k,v in pairs(self.repos) do
self:asyncUpdateRepo(k)
end
end
--- SpoonInstall:updateAllRepos()
--- Method
--- Synchronously fetch the information about the contents of all Spoon repositories registered in `SpoonInstall.repos`
---
--- Parameters:
--- * None
---
--- Returns:
--- * None
---
--- Notes:
--- * This is a synchronous call, which means Hammerspoon will be blocked until it finishes.
--- * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions.
function obj:updateAllRepos()
for k,v in pairs(self.repos) do
self:updateRepo(k)
end
end
--- SpoonInstall:repolist()
--- Method
--- Return a sorted list of registered Spoon repositories
---
--- Parameters:
--- * None
---
--- Returns:
--- * Table containing a list of strings with the repository identifiers
function obj:repolist()
local keys={}
-- Create sorted list of keys
for k,v in pairs(self.repos) do table.insert(keys, k) end
table.sort(keys)
return keys
end
--- SpoonInstall:search(pat)
--- Method
--- Search repositories for a pattern
---
--- Parameters:
--- * pat - Lua pattern that will be matched against the name and description of each spoon in the registered repositories. All text is converted to lowercase before searching it, so you can use all-lowercase in your pattern.
---
--- Returns:
--- * Table containing a list of matching entries. Each entry is a table with the following keys:
--- * name - Spoon name
--- * desc - description of the spoon
--- * repo - identifier in the repository where the match was found
function obj:search(pat)
local res={}
for repo,v in pairs(self.repos) do
if v.data then
for spoon,rec in pairs(v.data) do
if string.find(string.lower(rec.name .. "\n" .. rec.desc), pat) then
table.insert(res, { name = rec.name, desc = rec.desc, repo = repo })
end
end
else
self.logger.ef("Repository data for '%s' not available - call spoon.SpoonInstall:updateRepo('%s'), then try again.", repo, repo)
end
end
return res
end
-- --------------------------------------------------------------------
-- Spoon installation
-- Internal callback function to finalize the installation of a spoon after the zip file has been downloaded.
-- callback, if given, is called with (urlparts, success) as arguments
function obj:_installSpoonFromZipURLgetCallback(urlparts, callback, status, body, headers)
local success=nil
if (status < 100) or (status >= 400) then
self.logger.ef("Error downloading %s. Error code %d: %s", urlparts.absoluteString, status, body or "<none>")
else
-- Write the zip file to disk in a temporary directory
local tmpdir=_x("/usr/bin/mktemp -d", "Error creating temporary directory to download new spoon.")
if tmpdir then
local outfile = string.format("%s/%s", tmpdir, urlparts.lastPathComponent)
local f=assert(io.open(outfile, "w"))
f:write(body)
f:close()
-- Check its contents - only one *.spoon directory should be in there
output = _x(string.format("/usr/bin/unzip -l %s '*.spoon/' | /usr/bin/awk '$NF ~ /\\.spoon\\/$/ { print $NF }' | /usr/bin/wc -l", outfile),
"Error examining downloaded zip file %s, leaving it in place for your examination.", outfile)
if output then
if (tonumber(output) or 0) == 1 then
-- Uncompress the zip file
local outdir = string.format("%s/Spoons", hs.configdir)
if _x(string.format("/usr/bin/unzip -o %s -d %s 2>&1", outfile, outdir),
"Error uncompressing file %s, leaving it in place for your examination.", outfile) then
-- And finally, install it using Hammerspoon itself
self.logger.f("Downloaded and installed %s", urlparts.absoluteString)
_x(string.format("/bin/rm -rf '%s'", tmpdir), "Error removing directory %s", tmpdir)
success=true
end
else
self.logger.ef("The downloaded zip file %s is invalid - it should contain exactly one spoon. Leaving it in place for your examination.", outfile)
end
end
end
end
if callback then
callback(urlparts, success)
end
return success
end
--- SpoonInstall:asyncInstallSpoonFromZipURL(url, callback)
--- Method
--- Asynchronously download a Spoon zip file and install it.
---
--- Parameters:
--- * url - URL of the zip file to install.
--- * callback - if given, a function to call after the installation finishes (also if it fails). The function receives the following arguments:
--- * urlparts - Result of calling `hs.http.urlParts` on the URL of the Spoon zip file
--- * success - boolean indicating whether the installation was successful
---
--- Returns:
--- * `true` if the installation was correctly initiated (i.e. the URL is valid), `false` otherwise
function obj:asyncInstallSpoonFromZipURL(url, callback)
local urlparts = hs.http.urlParts(url)
local dlfile = urlparts.lastPathComponent
if dlfile and dlfile ~= "" and urlparts.pathExtension == "zip" then
hs.http.asyncGet(url, nil, hs.fnutils.partial(self._installSpoonFromZipURLgetCallback, self, urlparts, callback))
return true
else
self.logger.ef("Invalid URL %s, must point to a zip file", url)
return nil
end
end
--- SpoonInstall:installSpoonFromZipURL(url)
--- Method
--- Synchronously download a Spoon zip file and install it.
---
--- Parameters:
--- * url - URL of the zip file to install.
---
--- Returns:
--- * `true` if the installation was successful, `nil` otherwise
function obj:installSpoonFromZipURL(url)
local urlparts = hs.http.urlParts(url)
local dlfile = urlparts.lastPathComponent
if dlfile and dlfile ~= "" and urlparts.pathExtension == "zip" then
a,b,c=hs.http.get(url)
return self:_installSpoonFromZipURLgetCallback(urlparts, nil, a, b, c)
else
self.logger.ef("Invalid URL %s, must point to a zip file", url)
return nil
end
end
-- Internal function to check if a Spoon/Repo combination is valid
function obj:_is_valid_spoon(name, repo)
if self.repos[repo] then
if self.repos[repo].data then
if self.repos[repo].data[name] then
return true
else
self.logger.ef("Spoon '%s' does not exist in repository '%s'. Please check and try again.", name, repo)
end
else
self.logger.ef("Repository data for '%s' not available - call spoon.SpoonInstall:updateRepo('%s'), then try again.", repo, repo)
end
else
self.logger.ef("Invalid or unknown repository '%s'", repo)
end
return nil
end
--- SpoonInstall:asyncInstallSpoonFromRepo(name, repo, callback)
--- Method
--- Asynchronously install a Spoon from a registered repository
---
--- Parameters:
--- * name - Name of the Spoon to install.
--- * repo - Name of the repository to use. Defaults to `"default"`
--- * callback - if given, a function to call after the installation finishes (also if it fails). The function receives the following arguments:
--- * urlparts - Result of calling `hs.http.urlParts` on the URL of the Spoon zip file
--- * success - boolean indicating whether the installation was successful
---
--- Returns:
--- * `true` if the installation was correctly initiated (i.e. the repo and spoon name were correct), `false` otherwise.
function obj:asyncInstallSpoonFromRepo(name, repo, callback)
if not repo then repo = 'default' end
if self:_is_valid_spoon(name, repo) then
self:asyncInstallSpoonFromZipURL(self.repos[repo].data[name].download_url, callback)
end
return nil
end
--- SpoonInstall:installSpoonFromRepo(name, repo)
--- Method
--- Synchronously install a Spoon from a registered repository
---
--- Parameters:
--- * name = Name of the Spoon to install.
--- * repo - Name of the repository to use. Defaults to `"default"`
---
--- Returns:
--- * `true` if the installation was successful, `nil` otherwise.
function obj:installSpoonFromRepo(name, repo, callback)
if not repo then repo = 'default' end
if self:_is_valid_spoon(name, repo) then
return self:installSpoonFromZipURL(self.repos[repo].data[name].download_url)
end
return nil
end
--- SpoonInstall:andUse(name, arg)
--- Method
--- Declaratively install, load and configure a Spoon
---
--- Parameters:
--- * name - the name of the Spoon to install (without the `.spoon` extension). If the Spoon is already installed, it will be loaded using `hs.loadSpoon()`. If it is not installed, it will be installed using `SpoonInstall:asyncInstallSpoonFromRepo()` and then loaded.
--- * arg - if provided, can be used to specify the configuration of the Spoon. The following keys are recognized (all are optional):
--- * repo - repository from where the Spoon should be installed if not present in the system, as defined in `SpoonInstall.repos`. Defaults to `"default"`.
--- * config - a table containing variables to be stored in the Spoon object to configure it. For example, `config = { answer = 42 }` will result in `spoon.<LoadedSpoon>.answer` being set to 42.
--- * hotkeys - a table containing hotkey bindings. If provided, will be passed as-is to the Spoon's `bindHotkeys()` method. The special string `"default"` can be given to use the Spoons `defaultHotkeys` variable, if it exists.
--- * fn - a function which will be called with the freshly-loaded Spoon object as its first argument.
--- * loglevel - if the Spoon has a variable called `logger`, its `setLogLevel()` method will be called with this value.
--- * start - if `true`, call the Spoon's `start()` method after configuring everything else.
--- * disable - if `true`, do nothing. Easier than commenting it out when you want to temporarily disable a spoon.
---
--- Returns:
--- * None
function obj:andUse(name, arg)
if not arg then arg = {} end
if arg.disable then return true end
if hs.spoons.use(name, arg, true) then
return true
else
local repo = arg.repo or "default"
if self.repos[repo] then
if self.repos[repo].data then
local load_and_config = function(_, success)
if success then
hs.notify.show("Spoon installed by SpoonInstall", name .. ".spoon is now available", "")
hs.spoons.use(name, arg)
else
obj.logger.ef("Error installing Spoon '%s' from repo '%s'", name, repo)
end
end
if self.use_syncinstall then
return load_and_config(nil, self:installSpoonFromRepo(name, repo))
else
self:asyncInstallSpoonFromRepo(name, repo, load_and_config)
end
else
local update_repo_and_continue = function(_, success)
if success then
obj:andUse(name, arg)
else
obj.logger.ef("Error updating repository '%s'", repo)
end
end
if self.use_syncinstall then
return update_repo_and_continue(nil, self:updateRepo(repo))
else
self:asyncUpdateRepo(repo, update_repo_and_continue)
end
end
else
obj.logger.ef("Unknown repository '%s' for Spoon", repo, name)
end
end
end
return obj