Initial commit of Hammerspoon config
This commit is contained in:
Vendored
BIN
Binary file not shown.
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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
@@ -0,0 +1,359 @@
|
||||
--- === Seal ===
|
||||
---
|
||||
--- Pluggable launch bar
|
||||
---
|
||||
--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/Seal.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/Seal.spoon.zip)
|
||||
---
|
||||
--- Seal includes a number of plugins, which you can choose to load (see `:loadPlugins()` below):
|
||||
--- * apps : Launch applications by name
|
||||
--- * calc : Simple calculator
|
||||
--- * rot13 : Apply ROT13 substitution cipher
|
||||
--- * safari_bookmarks : Open Safari bookmarks (this is broken since at least High Sierra)
|
||||
--- * screencapture : Lets you take screenshots in various ways
|
||||
--- * urlformats : User defined URL formats to open
|
||||
--- * useractions : User defined custom actions
|
||||
--- * vpn : Connect and disconnect VPNs (currently supports Viscosity and macOS system preferences)A
|
||||
|
||||
|
||||
local obj = {}
|
||||
obj.__index = obj
|
||||
|
||||
-- Metadata
|
||||
obj.name = "Seal"
|
||||
obj.version = "1.0"
|
||||
obj.author = "Chris Jones <cmsj@tenshu.net>"
|
||||
obj.homepage = "https://github.com/Hammerspoon/Spoons"
|
||||
obj.license = "MIT - https://opensource.org/licenses/MIT"
|
||||
|
||||
obj.chooser = nil
|
||||
obj.hotkeyShow = nil
|
||||
obj.hotkeyToggle = nil
|
||||
obj.plugins = {}
|
||||
obj.commands = {}
|
||||
obj.queryChangedTimer = nil
|
||||
|
||||
obj.spoonPath = hs.spoons.scriptPath()
|
||||
|
||||
--- Seal.queryChangedTimerDuration
|
||||
--- Variable
|
||||
--- Time between the last keystroke and the start of the recalculation of the choices to display, in seconds.
|
||||
---
|
||||
--- Notes:
|
||||
--- * Defaults to 0.02s (20ms).
|
||||
obj.queryChangedTimerDuration = 0.02
|
||||
|
||||
--- Seal.plugin_search_paths
|
||||
--- Variable
|
||||
--- List of directories where Seal will look for plugins. Defaults to `~/.hammerspoon/seal_plugins/` and the Seal Spoon directory.
|
||||
obj.plugin_search_paths = { hs.configdir .. "/seal_plugins", obj.spoonPath }
|
||||
|
||||
--- Seal:refreshCommandsForPlugin(plugin_name)
|
||||
--- Method
|
||||
--- Refresh the list of commands provided by the given plugin.
|
||||
---
|
||||
--- Parameters:
|
||||
--- * plugin_name - the name of the plugin. Should be the name as passed to `loadPlugins()` or `loadPluginFromFile`.
|
||||
---
|
||||
--- Returns:
|
||||
--- * The Seal object
|
||||
---
|
||||
--- Notes:
|
||||
--- * Most Seal plugins expose a static list of commands (if any), which are registered at the time the plugin is loaded. This method is used for plugins which expose a dynamic or changing (e.g. depending on configuration) list of commands.
|
||||
function obj:refreshCommandsForPlugin(plugin_name)
|
||||
plugin = self.plugins[plugin_name]
|
||||
if plugin.commands then
|
||||
for cmd,cmdInfo in pairs(plugin:commands()) do
|
||||
if not self.commands[cmd] then
|
||||
print("-- Adding Seal command: "..cmd)
|
||||
self.commands[cmd] = cmdInfo
|
||||
end
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Seal:refreshAllCommands()
|
||||
--- Method
|
||||
--- Refresh the list of commands provided by all the currently loaded plugins.
|
||||
---
|
||||
--- Parameters:
|
||||
--- * None
|
||||
---
|
||||
--- Returns:
|
||||
--- * The Seal object
|
||||
---
|
||||
--- Notes:
|
||||
--- * Most Seal plugins expose a static list of commands (if any), which are registered at the time the plugin is loaded. This method is used for plugins which expose a dynamic or changing (e.g. depending on configuration) list of commands.
|
||||
function obj:refreshAllCommands()
|
||||
for p, _ in pairs(self.plugins) do
|
||||
self:refreshCommandsForPlugin(p)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Seal:loadPluginFromFile(plugin_name, file)
|
||||
--- Method
|
||||
--- Loads a plugin from a given file
|
||||
---
|
||||
--- Parameters:
|
||||
--- * plugin_name - the name of the plugin, without "seal_" at the beginning or ".lua" at the end
|
||||
--- * file - the file where the plugin code is stored.
|
||||
---
|
||||
--- Returns:
|
||||
--- * The Seal object if the plugin was successfully loaded, `nil` otherwise
|
||||
---
|
||||
--- Notes:
|
||||
--- * You should normally use `Seal:loadPlugins()`. This method allows you to load plugins
|
||||
--- from non-standard locations and is mostly a development interface.
|
||||
--- * Some plugins may immediately begin doing background work (e.g. Spotlight searches)
|
||||
function obj:loadPluginFromFile(plugin_name, file)
|
||||
local f,err = loadfile(file)
|
||||
if f~= nil then
|
||||
local plugin = f()
|
||||
plugin.seal = self
|
||||
self.plugins[plugin_name] = plugin
|
||||
self:refreshCommandsForPlugin(plugin_name)
|
||||
return self
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
--- Seal:loadPlugins(plugins)
|
||||
--- Method
|
||||
--- Loads a list of Seal plugins
|
||||
---
|
||||
--- Parameters:
|
||||
--- * plugins - A list containing the names of plugins to load
|
||||
---
|
||||
--- Returns:
|
||||
--- * The Seal object
|
||||
---
|
||||
--- Notes:
|
||||
--- * The plugins live inside the Seal.spoon directory
|
||||
--- * The plugin names in the list, should not have `seal_` at the start, or `.lua` at the end
|
||||
--- * Some plugins may immediately begin doing background work (e.g. Spotlight searches)
|
||||
function obj:loadPlugins(plugins)
|
||||
self.chooser = hs.chooser.new(self.completionCallback)
|
||||
self.chooser:choices(self.choicesCallback)
|
||||
self.chooser:queryChangedCallback(self.queryChangedCallback)
|
||||
|
||||
for k,plugin_name in pairs(plugins) do
|
||||
local loaded=nil
|
||||
print("-- Loading Seal plugin: " .. plugin_name)
|
||||
for _,dir in ipairs(self.plugin_search_paths) do
|
||||
if obj.plugins[plugin_name] == nil then
|
||||
local file = dir .. "/seal_" .. plugin_name .. ".lua"
|
||||
loaded = (self:loadPluginFromFile(plugin_name, file) ~= nil)
|
||||
end
|
||||
end
|
||||
if (not loaded) then
|
||||
hs.showError(string.format("Error: could not find Seal plugin %s in any of the load paths %s", plugin_name, hs.inspect(self.plugin_search_paths)))
|
||||
end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Seal:bindHotkeys(mapping)
|
||||
--- Method
|
||||
--- Binds hotkeys for Seal
|
||||
---
|
||||
--- Parameters:
|
||||
--- * mapping - A table containing hotkey modifier/key details for the following (optional) items:
|
||||
--- * show - This will cause Seal's UI to be shown
|
||||
--- * toggle - This will cause Seal's UI to be shown or hidden depending on its current state
|
||||
---
|
||||
--- Returns:
|
||||
--- * The Seal object
|
||||
function obj:bindHotkeys(mapping)
|
||||
if (self.hotkeyShow) then
|
||||
self.hotkeyShow:delete()
|
||||
end
|
||||
if (self.hotkeyToggle) then
|
||||
self.hotkeyToggle:delete()
|
||||
end
|
||||
|
||||
if mapping["show"] ~= nil then
|
||||
local showMods = mapping["show"][1]
|
||||
local showKey = mapping["show"][2]
|
||||
self.hotkeyShow = hs.hotkey.new(showMods, showKey, function() self:show() end)
|
||||
end
|
||||
if mapping["toggle"] ~= nil then
|
||||
local toggleMods = mapping["toggle"][1]
|
||||
local toggleKey = mapping["toggle"][2]
|
||||
self.hotkeyToggle = hs.hotkey.new(toggleMods, toggleKey, function() self:toggle() end)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Seal:start()
|
||||
--- Method
|
||||
--- Starts Seal
|
||||
---
|
||||
--- Parameters:
|
||||
--- * None
|
||||
---
|
||||
--- Returns:
|
||||
--- * The Seal object
|
||||
function obj:start()
|
||||
print("-- Starting Seal")
|
||||
if self.hotkeyShow then
|
||||
self.hotkeyShow:enable()
|
||||
end
|
||||
if self.hotkeyToggle then
|
||||
self.hotkeyToggle:enable()
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Seal:stop()
|
||||
--- Method
|
||||
--- Stops Seal
|
||||
---
|
||||
--- Parameters:
|
||||
--- * None
|
||||
---
|
||||
--- Returns:
|
||||
--- * The Seal object
|
||||
---
|
||||
--- Notes:
|
||||
--- * Some Seal plugins will continue performing background work even after this call (e.g. Spotlight searches)
|
||||
function obj:stop()
|
||||
print("-- Stopping Seal")
|
||||
self.chooser:hide()
|
||||
if self.hotkeyShow then
|
||||
self.hotkeyShow:disable()
|
||||
end
|
||||
if self.hotkeyToggle then
|
||||
self.hotkeyToggle:disable()
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Seal:show(query)
|
||||
--- Method
|
||||
--- Shows the Seal UI
|
||||
---
|
||||
--- Parameters:
|
||||
--- * query - An optional string to pre-populate the query box with
|
||||
---
|
||||
--- Returns:
|
||||
--- * None
|
||||
---
|
||||
--- Notes:
|
||||
--- * This may be useful if you wish to show Seal in response to something other than its hotkey
|
||||
function obj:show(query)
|
||||
self.chooser:show()
|
||||
if query then self.chooser:query(query) end
|
||||
return self
|
||||
end
|
||||
|
||||
--- Seal:toggle(query)
|
||||
--- Method
|
||||
--- Shows or hides the Seal UI
|
||||
---
|
||||
--- Parameters:
|
||||
--- * query - An optional string to pre-populate the query box with
|
||||
---
|
||||
--- Returns:
|
||||
--- * None
|
||||
function obj:toggle(query)
|
||||
if self.chooser:isVisible() then
|
||||
self.chooser:hide()
|
||||
else
|
||||
self:show(query)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function obj.completionCallback(rowInfo)
|
||||
if rowInfo == nil then
|
||||
return
|
||||
end
|
||||
if rowInfo["type"] == "plugin_cmd" then
|
||||
obj.chooser:query(rowInfo["cmd"])
|
||||
return
|
||||
end
|
||||
for k,plugin in pairs(obj.plugins) do
|
||||
if plugin.__name == rowInfo["plugin"] then
|
||||
plugin.completionCallback(rowInfo)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function obj.choicesCallback()
|
||||
-- TODO: Sort each of these clusters of choices, alphabetically
|
||||
choices = {}
|
||||
query = obj.chooser:query()
|
||||
cmd = nil
|
||||
query_words = {}
|
||||
if tostring(query):find("^%s*$") ~= nil then
|
||||
return choices
|
||||
end
|
||||
for word in string.gmatch(query, "%S+") do
|
||||
if cmd == nil then
|
||||
cmd = word
|
||||
else
|
||||
table.insert(query_words, word)
|
||||
end
|
||||
end
|
||||
query_words = table.concat(query_words, " ")
|
||||
-- First get any direct command matches
|
||||
for _,cmdInfo in pairs(obj.commands) do
|
||||
cmd_fn = cmdInfo["fn"]
|
||||
if cmd:lower() == cmdInfo["cmd"]:lower() then
|
||||
if (query_words or "") == "" then
|
||||
query_words = ".*"
|
||||
end
|
||||
fn_choices = cmd_fn(query_words)
|
||||
if fn_choices ~= nil then
|
||||
for j,choice in pairs(fn_choices) do
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Now get any bare matches
|
||||
for k,plugin in pairs(obj.plugins) do
|
||||
bare = plugin:bare()
|
||||
if bare then
|
||||
for i,choice in pairs(bare(query)) do
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Now add in any matching commands
|
||||
-- TODO: This only makes sense to do if we can select the choice without dismissing the chooser, which requires changes to HSChooser
|
||||
for command,cmdInfo in pairs(obj.commands) do
|
||||
if string.match(command, query) and #query_words == 0 then
|
||||
choice = {}
|
||||
choice["text"] = cmdInfo["name"]
|
||||
choice["subText"] = cmdInfo["description"]
|
||||
choice["type"] = "plugin_cmd"
|
||||
table.insert(choices,choice)
|
||||
end
|
||||
end
|
||||
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.queryChangedCallback(query)
|
||||
if obj.queryChangedTimer then
|
||||
obj.queryChangedTimer:stop()
|
||||
end
|
||||
obj.queryChangedTimer = hs.timer.doAfter(obj.queryChangedTimerDuration,
|
||||
function() obj.chooser:refreshChoicesCallback() end)
|
||||
end
|
||||
|
||||
return obj
|
||||
|
||||
--- === Seal.plugins ===
|
||||
---
|
||||
--- Various APIs for Seal plugins
|
||||
|
||||
-- This isn't really shown, but it's necessary to force Seal.plugins.html to render
|
||||
--- Seal.plugins
|
||||
--- Constant
|
||||
--- This is a table containing all of the loaded plugins for Seal. You should interact with it only via documented API that the plugins expose.
|
||||
@@ -0,0 +1,236 @@
|
||||
--- === Seal.plugins.apps ===
|
||||
---
|
||||
--- A plugin to add launchable apps/scripts, making Seal act as a launch bar
|
||||
local obj = {}
|
||||
obj.__index = obj
|
||||
obj.__name = "seal_apps"
|
||||
obj.appCache = {}
|
||||
|
||||
--- Seal.plugins.apps.appSearchPaths
|
||||
--- Variable
|
||||
--- Table containing the paths to search for launchable items
|
||||
---
|
||||
--- Notes:
|
||||
--- * If you change this, you will need to call `spoon.Seal.plugins.apps:restart()` to force Spotlight to search for new items.
|
||||
obj.appSearchPaths = {
|
||||
"/Applications",
|
||||
"/System/Applications",
|
||||
"~/Applications",
|
||||
"/Developer/Applications",
|
||||
"/Applications/Xcode.app/Contents/Applications",
|
||||
"/System/Library/PreferencePanes",
|
||||
"/Library/PreferencePanes",
|
||||
"~/Library/PreferencePanes",
|
||||
"/System/Library/CoreServices/Applications",
|
||||
"/System/Library/CoreServices/",
|
||||
"/usr/local/Cellar",
|
||||
"/Library/Scripts",
|
||||
"~/Library/Scripts"
|
||||
}
|
||||
|
||||
local modifyNameMap = function(info, add)
|
||||
for _, item in ipairs(info) do
|
||||
icon = nil
|
||||
local displayname = item.kMDItemDisplayName or hs.fs.displayName(item.kMDItemPath)
|
||||
displayname = displayname:gsub("%.app$", "", 1)
|
||||
if string.find(item.kMDItemPath, "%.prefPane$") then
|
||||
displayname = displayname .. " preferences"
|
||||
if add then
|
||||
icon = hs.image.iconForFile(item.kMDItemPath)
|
||||
end
|
||||
end
|
||||
if add then
|
||||
bundleID = item.kMDItemCFBundleIdentifier
|
||||
if (not icon) and (bundleID) then
|
||||
icon = hs.image.imageFromAppBundle(bundleID)
|
||||
end
|
||||
obj.appCache[displayname] = {
|
||||
path = item.kMDItemPath,
|
||||
bundleID = bundleID,
|
||||
icon = icon
|
||||
}
|
||||
else
|
||||
obj.appCache[displayname] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local updateNameMap = function(obj, msg, info)
|
||||
if info then
|
||||
-- all three can occur in either message, so check them all!
|
||||
if info.kMDQueryUpdateAddedItems then modifyNameMap(info.kMDQueryUpdateAddedItems, true) end
|
||||
if info.kMDQueryUpdateChangedItems then modifyNameMap(info.kMDQueryUpdateChangedItems, true) end
|
||||
if info.kMDQueryUpdateRemovedItems then modifyNameMap(info.kMDQueryUpdateRemovedItems, false) end
|
||||
else
|
||||
-- shouldn't happen for didUpdate or inProgress
|
||||
print("~~~ userInfo from SpotLight was empty for " .. msg)
|
||||
end
|
||||
end
|
||||
|
||||
--- Seal.plugins.apps:start()
|
||||
--- Method
|
||||
--- Starts the Spotlight app searcher
|
||||
---
|
||||
--- Parameters:
|
||||
--- * None
|
||||
---
|
||||
--- Returns:
|
||||
--- * None
|
||||
---
|
||||
--- Notes:
|
||||
--- * This is called automatically when the plugin is loaded
|
||||
function obj:start()
|
||||
obj.spotlight = hs.spotlight.new():queryString([[ (kMDItemContentType = "com.apple.application-bundle") || (kMDItemContentType = "com.apple.systempreference.prefpane") || (kMDItemContentType = "com.apple.applescript.text") || (kMDItemContentType = "com.apple.applescript.script") ]])
|
||||
:callbackMessages("didUpdate", "inProgress")
|
||||
:setCallback(updateNameMap)
|
||||
:searchScopes(obj.appSearchPaths)
|
||||
:start()
|
||||
end
|
||||
|
||||
--- Seal.plugins.apps:stop()
|
||||
--- Method
|
||||
--- Stops the Spotlight app searcher
|
||||
---
|
||||
--- Parameters:
|
||||
--- * None
|
||||
---
|
||||
--- Returns:
|
||||
--- * None
|
||||
function obj:stop()
|
||||
obj.spotlight:stop()
|
||||
obj.spotlight = nil
|
||||
obj.appCache = {}
|
||||
end
|
||||
|
||||
--- Seal.plugins.apps:restart()
|
||||
--- Method
|
||||
--- Restarts the Spotlight app searcher
|
||||
---
|
||||
--- Parameters:
|
||||
--- * None
|
||||
---
|
||||
--- Returns:
|
||||
--- * None
|
||||
function obj:restart()
|
||||
self:stop()
|
||||
self:start()
|
||||
end
|
||||
|
||||
hs.application.enableSpotlightForNameSearches(true)
|
||||
obj:start()
|
||||
|
||||
function obj:commands()
|
||||
return {kill = {
|
||||
cmd = "kill",
|
||||
fn = obj.choicesKillCommand,
|
||||
plugin = obj.__name,
|
||||
name = "Kill",
|
||||
description = "Kill an application"
|
||||
},
|
||||
reveal = {
|
||||
cmd = "reveal",
|
||||
fn = obj.choicesRevealCommand,
|
||||
plugin = obj.__name,
|
||||
name = "Reveal",
|
||||
description = "Reveal an application in the Finder"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function obj:bare()
|
||||
return self.choicesApps
|
||||
end
|
||||
|
||||
function obj.choicesApps(query)
|
||||
local choices = {}
|
||||
if query == nil or query == "" then
|
||||
return choices
|
||||
end
|
||||
for name,app in pairs(obj.appCache) do
|
||||
if string.match(name:lower(), query:lower()) then
|
||||
local choice = {}
|
||||
local instances = {}
|
||||
if app["bundleID"] then
|
||||
instances = hs.application.applicationsForBundleID(app["bundleID"])
|
||||
end
|
||||
if #instances > 0 then
|
||||
choice["text"] = name .. " (Running)"
|
||||
else
|
||||
choice["text"] = name
|
||||
end
|
||||
choice["subText"] = app["path"]
|
||||
if app["icon"] then
|
||||
choice["image"] = app["icon"]
|
||||
end
|
||||
choice["path"] = app["path"]
|
||||
choice["uuid"] = obj.__name.."__"..(app["bundleID"] or name)
|
||||
choice["plugin"] = obj.__name
|
||||
choice["type"] = "launchOrFocus"
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
end
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.choicesKillCommand(query)
|
||||
local choices = {}
|
||||
if query == nil then
|
||||
return choices
|
||||
end
|
||||
local apps = hs.application.runningApplications()
|
||||
for k, app in pairs(apps) do
|
||||
local name = app:name()
|
||||
if string.match(name:lower(), query:lower()) and app:mainWindow() then
|
||||
local choice = {}
|
||||
choice["text"] = "Kill "..name
|
||||
choice["subText"] = app:path().." PID: "..app:pid()
|
||||
choice["pid"] = app:pid()
|
||||
choice["plugin"] = obj.__name
|
||||
choice["type"] = "kill"
|
||||
choice["image"] = hs.image.imageFromAppBundle(app:bundleID())
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
end
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.choicesRevealCommand(query)
|
||||
local choices = {}
|
||||
if query == nil then
|
||||
return choices
|
||||
end
|
||||
local apps = obj.choicesApps(query)
|
||||
for k, app in pairs(apps) do
|
||||
local name = app.text
|
||||
if string.match(name:lower(), query:lower()) then
|
||||
local choice = {}
|
||||
choice["text"] = "Reveal "..name
|
||||
choice["path"] = app.path
|
||||
choice["subText"] = app.path
|
||||
choice["plugin"] = obj.__name
|
||||
choice["type"] = "reveal"
|
||||
if app.image then
|
||||
choice["image"] = app.image
|
||||
end
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
end
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.completionCallback(rowInfo)
|
||||
if rowInfo["type"] == "launchOrFocus" then
|
||||
if string.find(rowInfo["path"], "%.applescript$") or string.find(rowInfo["path"], "%.scpt$") then
|
||||
hs.task.new("/usr/bin/osascript", nil, { rowInfo["path"] }):start()
|
||||
else
|
||||
hs.task.new("/usr/bin/open", nil, { rowInfo["path"] }):start()
|
||||
end
|
||||
elseif rowInfo["type"] == "kill" then
|
||||
hs.application.get(rowInfo["pid"]):kill()
|
||||
elseif rowInfo["type"] == "reveal" then
|
||||
hs.osascript.applescript(string.format([[tell application "Finder" to reveal (POSIX file "%s")]], rowInfo["path"]))
|
||||
hs.application.launchOrFocus("Finder")
|
||||
end
|
||||
end
|
||||
|
||||
return obj
|
||||
@@ -0,0 +1,47 @@
|
||||
local obj = {}
|
||||
obj.__index = obj
|
||||
obj.__name = "seal_calc"
|
||||
obj.icon = hs.image.imageFromAppBundle("com.apple.Calculator")
|
||||
|
||||
function obj:commands()
|
||||
return {}
|
||||
end
|
||||
|
||||
function obj:bare()
|
||||
return self.bareCalc
|
||||
end
|
||||
|
||||
function obj.bareCalc(query)
|
||||
local choices = {}
|
||||
if query == nil or query == "" then
|
||||
return choices
|
||||
end
|
||||
|
||||
-- Filter out commas and dollar signs
|
||||
query, _ = query:gsub("[%,%$]", "")
|
||||
|
||||
-- We need to determine if the query only contains mathematical calculations
|
||||
-- To do this we'll see if it matches the inverse of that set of characters
|
||||
if string.match(query, "[^%d^%.^%+^%-^/^%*^%^^ ^%(^%)]") == nil then
|
||||
local choice = {}
|
||||
local compile_result, fn = load("return " .. query)
|
||||
if type(compile_result) == "function" then
|
||||
local result = compile_result()
|
||||
choice["text"] = result
|
||||
choice["subText"] = "Copy result to clipboard"
|
||||
choice["image"] = obj.icon
|
||||
choice["plugin"] = obj.__name
|
||||
choice["type"] = "copyToClipboard"
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
end
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.completionCallback(rowInfo)
|
||||
if rowInfo["type"] == "copyToClipboard" then
|
||||
hs.pasteboard.setContents(rowInfo["text"])
|
||||
end
|
||||
end
|
||||
|
||||
return obj
|
||||
@@ -0,0 +1,168 @@
|
||||
--- === Seal.plugins.filesearch ===
|
||||
---
|
||||
--- A plugin to add file search capabilities, making Seal act as a spotlight file search
|
||||
local obj = {}
|
||||
obj.__index = obj
|
||||
obj.__name = "seal_filesearch"
|
||||
|
||||
--- Seal.plugins.filesearch.fileSearchPaths
|
||||
--- Variable
|
||||
--- Table containing the paths to search for files
|
||||
---
|
||||
--- Notes:
|
||||
--- * You will need to authorize hammerspoon to access the folders in this list in order for this to work.
|
||||
obj.fileSearchPaths = {"~/", "~/Downloads", "~/Documents", "~/Movies", "~/Desktop", "~/Music", "~/Pictures"}
|
||||
|
||||
--- Seal.plugins.filesearch.maxResults
|
||||
--- Variable
|
||||
--- Maximum number of results to display
|
||||
obj.maxQueryResults = 40
|
||||
|
||||
--- Seal.plugins.filesearch.displayResultsTimeout
|
||||
--- Variable
|
||||
--- Maximum time to wait before displaying the results
|
||||
--- Defaults to 0.2s (200ms).
|
||||
---
|
||||
--- Notes:
|
||||
--- * higher value might give you more results but will give a less snappy experience
|
||||
obj.displayResultsTimeout = 0.2
|
||||
|
||||
-- Variables
|
||||
obj.currentQuery = nil
|
||||
obj.currentQueryResults = {}
|
||||
obj.currentQueryResultsDisplayed = false
|
||||
obj.showQueryResultsTimer = nil
|
||||
|
||||
obj.spotlight = hs.spotlight.new()
|
||||
|
||||
-- hammerspoon passes .* as empty query
|
||||
EMPTY_QUERY = ".*"
|
||||
|
||||
-- Private functions
|
||||
|
||||
local stopCurrentSearch = function()
|
||||
if obj.spotlight:isRunning() then
|
||||
obj.spotlight:stop()
|
||||
end
|
||||
if obj.showQueryResultsTimer ~= nil and obj.showQueryResultsTimer:running() then
|
||||
obj.showQueryResultsTimer:stop()
|
||||
end
|
||||
end
|
||||
|
||||
local displayQueryResults = function()
|
||||
stopCurrentSearch()
|
||||
if not obj.currentQueryResultsDisplayed then
|
||||
obj.currentQueryResultsDisplayed = true
|
||||
-- we force seal to refresh the choices so we can serve the real query results
|
||||
obj.seal.chooser:refreshChoicesCallback()
|
||||
end
|
||||
end
|
||||
|
||||
local buildSpotlightQuery = function(query)
|
||||
local queryWords = hs.fnutils.split(query, "%s+")
|
||||
local searchFilters = hs.fnutils.map(queryWords, function(word)
|
||||
return [[kMDItemFSName like[c] "*]] .. word .. [[*"]]
|
||||
end)
|
||||
local spotligthQuery = table.concat(searchFilters, [[ && ]])
|
||||
return spotligthQuery
|
||||
end
|
||||
|
||||
local convertSpotlightResultToQueryResult = function(item)
|
||||
local icon = hs.image.iconForFile(item.kMDItemPath)
|
||||
local bundleID = item.kMDItemCFBundleIdentifier
|
||||
if (not icon) and (bundleID) then
|
||||
icon = hs.image.imageFromAppBundle(bundleID)
|
||||
end
|
||||
return {
|
||||
text = item.kMDItemDisplayName,
|
||||
subText = item.kMDItemPath,
|
||||
path = item.kMDItemPath,
|
||||
uuid = obj.__name .. "__" .. (bundleID or item.kMDItemDisplayName),
|
||||
plugin = obj.__name,
|
||||
type = "open",
|
||||
image = icon
|
||||
}
|
||||
end
|
||||
|
||||
local updateQueryResults = function(items)
|
||||
for _, item in ipairs(items) do
|
||||
if #obj.currentQueryResults >= obj.maxQueryResults then
|
||||
break
|
||||
end
|
||||
table.insert(obj.currentQueryResults, convertSpotlightResultToQueryResult(item))
|
||||
end
|
||||
end
|
||||
|
||||
local handleSpotlightCallback = function(_, msg, info)
|
||||
if msg == "inProgress" and info.kMDQueryUpdateAddedItems ~= nil then
|
||||
updateQueryResults(info.kMDQueryUpdateAddedItems)
|
||||
end
|
||||
|
||||
if msg == "didFinish" or #obj.currentQueryResults >= obj.maxQueryResults then
|
||||
displayQueryResults()
|
||||
end
|
||||
end
|
||||
|
||||
-- Public methods
|
||||
|
||||
function obj:commands()
|
||||
return {
|
||||
filesearch = {
|
||||
cmd = "'",
|
||||
fn = obj.fileSearch,
|
||||
name = "Search file",
|
||||
description = "Search file",
|
||||
plugin = obj.__name
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function obj:bare()
|
||||
return nil
|
||||
end
|
||||
|
||||
function obj.completionCallback(rowInfo)
|
||||
if rowInfo["type"] == "open" then
|
||||
if string.find(rowInfo["path"], "%.applescript$") or string.find(rowInfo["path"], "%.scpt$") then
|
||||
hs.task.new("/usr/bin/osascript", nil, {rowInfo["path"]}):start()
|
||||
else
|
||||
hs.task.new("/usr/bin/open", nil, {rowInfo["path"]}):start()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function obj.fileSearch(query)
|
||||
stopCurrentSearch()
|
||||
|
||||
if query == EMPTY_QUERY then
|
||||
obj.currentQuery = ""
|
||||
obj.currentQueryResults = {}
|
||||
return {}
|
||||
end
|
||||
|
||||
if query ~= obj.currentQuery then
|
||||
-- Seal want the results synchronously, but spotlight will return then asynchronously
|
||||
-- to workaround that, we launch the spotlight search in the background and
|
||||
-- return the previous results (so that Seal doesn't change the current results list)
|
||||
-- We force a refresh later once we have the results
|
||||
local previousResults = obj.currentQueryResults
|
||||
obj.currentQuery = query
|
||||
obj.currentQueryResults = {}
|
||||
obj.currentQueryResultsDisplayed = false
|
||||
|
||||
obj.spotlight:queryString(buildSpotlightQuery(query)):start()
|
||||
obj.showQueryResultsTimer = hs.timer.doAfter(obj.displayResultsTimeout, displayQueryResults)
|
||||
|
||||
return previousResults
|
||||
else
|
||||
-- If we are here, it's mean the force refreshed has been triggered after receving spotlight results
|
||||
-- we just return the results we accumulated from spotlight
|
||||
return obj.currentQueryResults
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
obj.spotlight:searchScopes(obj.fileSearchPaths):callbackMessages("inProgress", "didFinish"):setCallback(
|
||||
handleSpotlightCallback)
|
||||
|
||||
return obj
|
||||
@@ -0,0 +1,182 @@
|
||||
--- ==== Seal.plugins.pasteboard ====
|
||||
---
|
||||
--- Visual, searchable pasteboard (ie clipboard) history
|
||||
local obj = {}
|
||||
obj.__index = obj
|
||||
obj.__name = "seal_pasteboard"
|
||||
obj.timer = nil
|
||||
obj.lastItem = nil
|
||||
obj.itemBuffer = {}
|
||||
obj.choices = {}
|
||||
|
||||
--- Seal.plugins.pasteboard.historySize
|
||||
--- Variable
|
||||
---
|
||||
--- The number of history items to keep. Defaults to 50
|
||||
obj.historySize = 50
|
||||
|
||||
--- Seal.plugins.pasteboard.saveHistory
|
||||
--- Variable
|
||||
---
|
||||
--- A boolean, true if Seal should automatically load/save clipboard history. Defaults to true
|
||||
obj.saveHistory = true
|
||||
|
||||
--- Seal.plugins.pasteboard.skipUTIs
|
||||
--- Variable
|
||||
---
|
||||
--- An array of UTIs to skip when saving to the history. Defaults to:
|
||||
--- ```
|
||||
--- {
|
||||
--- "de.petermaurer.TransientPasteboardType",
|
||||
--- "com.typeit4me.clipping",
|
||||
--- "Pasteboard generator type",
|
||||
--- "com.agilebits.onepassword",
|
||||
--- "org.nspasteboard.TransientType",
|
||||
--- "org.nspasteboard.ConcealedType",
|
||||
--- "org.nspasteboard.AutoGeneratedType"
|
||||
--- }
|
||||
--- ```
|
||||
obj.skipUTIs = {
|
||||
"de.petermaurer.TransientPasteboardType",
|
||||
"com.typeit4me.clipping",
|
||||
"Pasteboard generator type",
|
||||
"com.agilebits.onepassword",
|
||||
"org.nspasteboard.TransientType",
|
||||
"org.nspasteboard.ConcealedType",
|
||||
"org.nspasteboard.AutoGeneratedType"
|
||||
}
|
||||
|
||||
function obj:commands()
|
||||
return {
|
||||
pb = {
|
||||
cmd = "pb",
|
||||
fn = obj.choicesPasteboardCommand,
|
||||
name = "Pasteboard",
|
||||
description = "Pasteboard history",
|
||||
plugin = obj.__name
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function obj:bare()
|
||||
return nil
|
||||
end
|
||||
|
||||
function obj.choicesPasteboardCommand(query)
|
||||
-- Return the choices that match the query
|
||||
return hs.fnutils.filter(obj.choices, function(choice)
|
||||
return string.find(string.lower(choice["text"]), string.lower(query))
|
||||
end)
|
||||
end
|
||||
|
||||
function obj.pasteboardToChoice(item)
|
||||
local choice = {}
|
||||
|
||||
choice["uuid"] = item["uuid"]
|
||||
choice["name"] = item["text"]
|
||||
choice["text"] = item["text"]
|
||||
choice["kind"] = kind
|
||||
choice["plugin"] = obj.__name
|
||||
choice["type"] = "copy"
|
||||
choice["subText"] = ""
|
||||
|
||||
if item["uti"] then
|
||||
choice["subText"] = item["uti"]
|
||||
if hs.application.defaultAppForUTI then
|
||||
local bundleID = hs.application.defaultAppForUTI(item["uti"])
|
||||
print("Default app for " .. item["uti"] .. " :: " .. (bundleID or "(null)"))
|
||||
if bundleID then
|
||||
choice["image"] = hs.image.imageFromAppBundle(bundleID)
|
||||
end
|
||||
end
|
||||
end
|
||||
if item["dateTime"] then
|
||||
choice["subText"] = choice["subText"] .. " :: " .. item["dateTime"]
|
||||
end
|
||||
|
||||
return choice
|
||||
end
|
||||
|
||||
function obj.completionCallback(rowInfo)
|
||||
if rowInfo["type"] == "copy" then
|
||||
hs.pasteboard.setContents(rowInfo["name"])
|
||||
end
|
||||
end
|
||||
|
||||
function obj.checkPasteboard()
|
||||
local pasteboard = hs.pasteboard.getContents()
|
||||
local shouldSave = false
|
||||
|
||||
if pasteboard == nil then
|
||||
return
|
||||
end
|
||||
|
||||
if (#obj.itemBuffer == 0) or (pasteboard ~= obj.itemBuffer[#obj.itemBuffer]["text"]) then
|
||||
local currentTypes = hs.pasteboard.allContentTypes()[1]
|
||||
if currentTypes == nil then
|
||||
print("ERROR: NO PASTEBOARD CURRENT TYPES. Please file a bug so we can understand this:")
|
||||
print(hs.inspect(pasteboard))
|
||||
return
|
||||
end
|
||||
for _, aType in pairs(currentTypes) do
|
||||
for _, uti in pairs(obj.skipUTIs) do
|
||||
if uti == aType then
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
local item = {}
|
||||
item["uuid"] = hs.host.uuid()
|
||||
item["text"] = pasteboard
|
||||
item["uti"] = currentTypes[1]
|
||||
item["dateTime"] = os.date()
|
||||
|
||||
table.insert(obj.itemBuffer, item)
|
||||
table.insert(obj.choices, obj.pasteboardToChoice(item))
|
||||
|
||||
shouldSave = true
|
||||
end
|
||||
if #obj.itemBuffer > obj.historySize then
|
||||
table.remove(obj.itemBuffer, 1)
|
||||
table.remove(obj.choices, 1)
|
||||
|
||||
shouldSave = true
|
||||
end
|
||||
|
||||
if shouldSave then
|
||||
obj.save()
|
||||
end
|
||||
end
|
||||
|
||||
function obj.save()
|
||||
local json = hs.json.encode(obj.itemBuffer)
|
||||
local file = io.open(os.getenv("HOME") .. "/.hammerspoon/pasteboard_history.json", "w")
|
||||
if file then
|
||||
file:write(json)
|
||||
file:close()
|
||||
end
|
||||
end
|
||||
|
||||
function obj.load()
|
||||
local file = io.open(os.getenv("HOME") .. "/.hammerspoon/pasteboard_history.json", "r")
|
||||
if file then
|
||||
local json = hs.json.decode(file:read())
|
||||
if json then
|
||||
obj.itemBuffer = json
|
||||
|
||||
-- Convert all the items to the choice buffer
|
||||
for _, v in ipairs(obj.itemBuffer) do
|
||||
table.insert(obj.choices, obj.pasteboardToChoice(v))
|
||||
end
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
end
|
||||
|
||||
obj.load()
|
||||
if obj.timer == nil then
|
||||
obj.timer = hs.timer.doEvery(1, function() obj.checkPasteboard(obj) end)
|
||||
obj.timer:start()
|
||||
end
|
||||
|
||||
return obj
|
||||
@@ -0,0 +1,48 @@
|
||||
local obj = {}
|
||||
obj.__index = obj
|
||||
obj.__name = "seal_rot13"
|
||||
|
||||
function obj:commands()
|
||||
return {
|
||||
rot13 = {
|
||||
cmd = "rot13",
|
||||
fn = obj.rot13,
|
||||
name = "ROT13",
|
||||
description = "Apply ROT13 substitution cipher"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function obj:bare()
|
||||
return nil
|
||||
end
|
||||
|
||||
function obj.rot13(query)
|
||||
-- ROT13 implementation taken from https://rosettacode.org/wiki/Rot-13#Lua
|
||||
local a = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
local b = "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm"
|
||||
local rot13Text =
|
||||
query:gsub(
|
||||
"%a",
|
||||
function(c)
|
||||
return b:sub(a:find(c))
|
||||
end
|
||||
)
|
||||
|
||||
return {
|
||||
{
|
||||
text = rot13Text,
|
||||
subText = "Copy result to clipboard",
|
||||
plugin = obj.__name,
|
||||
type = "copyToClipboard"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function obj.completionCallback(rowInfo)
|
||||
if rowInfo["type"] == "copyToClipboard" then
|
||||
hs.pasteboard.setContents(rowInfo["text"])
|
||||
end
|
||||
end
|
||||
|
||||
return obj
|
||||
@@ -0,0 +1,90 @@
|
||||
--- === Seal.plugins.safari_bookmarks ===
|
||||
--- Access Safari bookmarks from Seal
|
||||
---
|
||||
--- Note: Apple has changed the way Safari stores bookmarks and this plugin no longer works on recent macOS releases.
|
||||
local obj = {}
|
||||
obj.__index = obj
|
||||
obj.__name = "seal_safari_bookmarks"
|
||||
obj.bookmarkCache = {}
|
||||
obj.icon = hs.image.iconForFileType("com.apple.safari.bookmark")
|
||||
|
||||
--- Seal.plugins.safari_bookmarks.always_open_with_safari
|
||||
--- Variable
|
||||
--- If `true` (default), bookmarks are always opened with Safari, otherwise they are opened with the default application using the `/usr/bin/open` command.
|
||||
obj.always_open_with_safari = true
|
||||
|
||||
local modifyNameMap = function(info, add)
|
||||
local name
|
||||
for _, item in ipairs(info) do
|
||||
name = item.kMDItemDisplayName
|
||||
if name ~= nil then
|
||||
if add then
|
||||
obj.bookmarkCache[name] = {
|
||||
url = item.kMDItemURL,
|
||||
}
|
||||
else
|
||||
obj.bookmarkCache[name] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local updateNameMap = function(obj, msg, info)
|
||||
if info then
|
||||
-- all three can occur in either message, so check them all!
|
||||
if info.kMDQueryUpdateAddedItems then modifyNameMap(info.kMDQueryUpdateAddedItems, true) end
|
||||
if info.kMDQueryUpdateChangedItems then modifyNameMap(info.kMDQueryUpdateChangedItems, true) end
|
||||
if info.kMDQueryUpdateRemovedItems then modifyNameMap(info.kMDQueryUpdateRemovedItems, false) end
|
||||
else
|
||||
-- shouldn't happen for didUpdate or inProgress
|
||||
print("~~~ userInfo from SpotLight was empty for " .. msg)
|
||||
end
|
||||
end
|
||||
|
||||
obj.spotlight = hs.spotlight.new():queryString([[ kMDItemContentType = "com.apple.safari.bookmark" ]])
|
||||
:callbackMessages("didUpdate", "inProgress")
|
||||
:setCallback(updateNameMap)
|
||||
:start()
|
||||
|
||||
function obj:commands()
|
||||
return {}
|
||||
end
|
||||
|
||||
function obj:bare()
|
||||
return self.choicesBookmarks
|
||||
end
|
||||
|
||||
function obj.choicesBookmarks(query)
|
||||
local choices = {}
|
||||
if query == nil or query == "" then
|
||||
return choices
|
||||
end
|
||||
for name,bookmark in pairs(obj.bookmarkCache) do
|
||||
url = bookmark["url"]
|
||||
if url and (string.match(name:lower(), query:lower()) or string.match(url:lower(), query:lower())) then
|
||||
local choice = {}
|
||||
local instances = {}
|
||||
choice["text"] = name
|
||||
choice["subText"] = url
|
||||
choice["url"] = url
|
||||
choice["image"] = obj.icon
|
||||
choice["uuid"] = obj.__name.."__"..name.."__"..url
|
||||
choice["plugin"] = obj.__name
|
||||
choice["type"] = "openURL"
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
end
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.completionCallback(rowInfo)
|
||||
if rowInfo["type"] == "openURL" then
|
||||
if obj.always_open_with_safari then
|
||||
hs.urlevent.openURLWithBundle(rowInfo["url"], "com.apple.Safari")
|
||||
else
|
||||
hs.execute(string.format("/usr/bin/open '%s'", rowInfo["url"]))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return obj
|
||||
@@ -0,0 +1,98 @@
|
||||
--- === Seal.plugins.screencapture ===
|
||||
---
|
||||
--- A plugin to capture the screen in various ways
|
||||
local obj = {}
|
||||
obj.__index = obj
|
||||
obj.__name = "seal_screencapture"
|
||||
|
||||
--- Seal.plugins.screencapture.showPostUI
|
||||
--- Variable
|
||||
--- Whether or not to show the screen capture UI in macOS 10.14 or later
|
||||
obj.showPostUI = true
|
||||
|
||||
local static_choices = {
|
||||
{
|
||||
text = "Capture menu",
|
||||
subText = "Show macOS screen capture menu",
|
||||
plugin = obj.__name,
|
||||
type = "screenUI"
|
||||
},
|
||||
{
|
||||
text = "Capture Screen",
|
||||
subText = "Capture the current screen",
|
||||
plugin = obj.__name,
|
||||
type = "screen"
|
||||
},
|
||||
{
|
||||
text = "Capture Screen to Clipboard",
|
||||
subText = "Capture the current screen to the clipboard",
|
||||
plugin = obj.__name,
|
||||
type = "screen_clipboard"
|
||||
},
|
||||
{
|
||||
text = "Capture Interactive",
|
||||
subText = "Draw a rectangle to capture",
|
||||
plugin = obj.__name,
|
||||
type = "interactive"
|
||||
},
|
||||
{
|
||||
text = "Capture Interactive to Clipboard",
|
||||
subText = "Draw a rectangle to capture to the clipboard",
|
||||
plugin = obj.__name,
|
||||
type = "interactive_clipboard"
|
||||
}
|
||||
}
|
||||
|
||||
function obj:commands()
|
||||
return {sc = {
|
||||
cmd = "sc",
|
||||
fn = obj.choicesScreenCaptureCommand,
|
||||
name = "Screencapture",
|
||||
description = "Capture the screen",
|
||||
plugin = obj.__name
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function obj:bare()
|
||||
return nil
|
||||
end
|
||||
|
||||
function obj.choicesScreenCaptureCommand(query)
|
||||
local choices = {}
|
||||
|
||||
for k,choice in pairs(static_choices) do
|
||||
if string.match(choice["text"]:lower(), query:lower()) then
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
end
|
||||
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.completionCallback(rowInfo)
|
||||
local filename = hs.fs.pathToAbsolute("~").."/Desktop/Screen Capture at "..os.date("!%Y-%m-%d-%T")..".png"
|
||||
local args = ""
|
||||
local scType = rowInfo["type"]
|
||||
|
||||
if scType == "screen" then
|
||||
-- Nothing required here
|
||||
elseif scType == "screen_clipboard" then
|
||||
args = "-c"
|
||||
elseif scType == "interactive" then
|
||||
args = "-i"
|
||||
elseif scType == "screenUI" then
|
||||
args = "-iU"
|
||||
elseif scType == "interactive_clipboard" then
|
||||
args = "-ci"
|
||||
end
|
||||
|
||||
if obj.showPostUI then
|
||||
args = args .. "u"
|
||||
end
|
||||
|
||||
print(hs.inspect(args))
|
||||
hs.task.new("/usr/sbin/screencapture", nil, {args, filename}):start()
|
||||
end
|
||||
|
||||
return obj
|
||||
@@ -0,0 +1,125 @@
|
||||
--- === Seal.plugins.urlformats ===
|
||||
---
|
||||
--- A plugin to quickly open URLs containing a search/query term
|
||||
--- This plugin is invoked with the `uf` keyword and requires some configuration, see `:providersTable()`
|
||||
---
|
||||
--- The way this works is by defining a set of providers, each of which contains a URL with a `%s` somewhere insert it.
|
||||
--- When the user types `uf` in Seal, followed by some more characters, those characters will be inserted into the string at the point where the `%s` is.
|
||||
---
|
||||
--- By way of an example, you could define a provider with a url like `http://bugs.mycorp.com/showBug?id=%s`, and just need to type `uf 123456` in Seal to get a quick shortcut to open the full URL.
|
||||
local obj = {}
|
||||
obj.__index = obj
|
||||
obj.__name = "seal_urlformats"
|
||||
|
||||
obj.providers = {}
|
||||
|
||||
-- Example format for providers table
|
||||
-- {
|
||||
-- rhbz = {
|
||||
-- name = "Red Hat Bugzilla",
|
||||
-- url = "https://bugzilla.redhat.com/show_bug.cgi?id=%s",
|
||||
-- },
|
||||
-- lp = {
|
||||
-- name = "Launchpad Bug",
|
||||
-- url = "https://launchpad.net/bugs/%s",
|
||||
-- },
|
||||
-- }
|
||||
|
||||
function obj:commands()
|
||||
return {uf = {
|
||||
cmd = "uf",
|
||||
fn = obj.choicesURLPart,
|
||||
name = "URL Formats",
|
||||
description = "Open a full URL with a search term",
|
||||
plugin = obj.__name
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function obj:bare()
|
||||
return obj.choicesBareURL
|
||||
end
|
||||
|
||||
function obj.choicesBareURL(query)
|
||||
local choices = {}
|
||||
if string.find(query, "://") ~= nil then
|
||||
local scheme = string.sub(query, 1, string.find(query, "://") - 1)
|
||||
local handlers = hs.urlevent.getAllHandlersForScheme(scheme)
|
||||
for _,bundleID in pairs(handlers) do
|
||||
local choice = {}
|
||||
local bundleInfo = hs.application.infoForBundleID(bundleID)
|
||||
if bundleInfo and bundleInfo["CFBundleName"] then
|
||||
choice["text"] = "Open URI with "..bundleInfo["CFBundleName"]
|
||||
choice["handler"] = bundleID
|
||||
choice["scheme"] = scheme
|
||||
choice["type"] = "launch"
|
||||
choice["url"] = query
|
||||
choice["plugin"] = obj.__name
|
||||
choice["image"] = hs.image.imageFromAppBundle(bundleID)
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
end
|
||||
end
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.choicesURLPart(query)
|
||||
--print("choicesURLPart for: "..query)
|
||||
local choices = {}
|
||||
for name,data in pairs(obj.providers) do
|
||||
local data_url = data["url"]:gsub("([^%%])%%([^s])", "%1%%%%%2")
|
||||
local full_url = string.format(data_url, query)
|
||||
local url_scheme = string.sub(full_url, 1, string.find(full_url, "://") - 1)
|
||||
local choice = {}
|
||||
choice["text"] = data["name"]
|
||||
choice["subText"] = full_url
|
||||
choice["plugin"] = obj.__name
|
||||
choice["type"] = "launch"
|
||||
choice["url"] = full_url
|
||||
choice["scheme"] = url_scheme
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.completionCallback(rowInfo)
|
||||
if rowInfo["type"] == "launch" then
|
||||
local handler = nil
|
||||
if rowInfo["handler"] == nil then
|
||||
handler = hs.urlevent.getDefaultHandler(rowInfo["scheme"])
|
||||
else
|
||||
handler = rowInfo["handler"]
|
||||
end
|
||||
hs.urlevent.openURLWithBundle(rowInfo["url"], handler)
|
||||
end
|
||||
end
|
||||
|
||||
--- Seal.plugins.urlformats:providersTable(aTable)
|
||||
--- Method
|
||||
--- Gets or sets the current providers table
|
||||
---
|
||||
--- Parameters:
|
||||
--- * aTable - An optional table of providers, which must contain the following keys:
|
||||
--- * name - A string naming the provider, which will be shown in the Seal results
|
||||
--- * url - A string containing the URL to insert the user's query into. This should contain one and only one `%s`
|
||||
---
|
||||
--- Returns:
|
||||
--- * Either a table of current providers, if no parameter was passed, or nothing if a parmameter was passed.
|
||||
---
|
||||
--- Notes:
|
||||
--- * An example table might look like:
|
||||
--- ```lua
|
||||
--- {
|
||||
--- rhbz = { name = "Red Hat Bugzilla", url = "https://bugzilla.redhat.com/show_bug.cgi?id=%s", },
|
||||
--- lp = { name = "Launchpad Bug", url = "https://launchpad.net/bugs/%s", },
|
||||
--- }
|
||||
--- ```
|
||||
function obj:providersTable(aTable)
|
||||
if aTable then
|
||||
self.providers = aTable
|
||||
else
|
||||
return self.providers
|
||||
end
|
||||
end
|
||||
|
||||
return obj
|
||||
@@ -0,0 +1,286 @@
|
||||
--- ==== Seal.plugins.useractions ====
|
||||
---
|
||||
--- Allow accessing user-defined bookmarks and arbitrary actions from Seal.
|
||||
---
|
||||
|
||||
local obj = {}
|
||||
obj.__index = obj
|
||||
obj.__basename = "useractions"
|
||||
obj.__name = "seal_" .. obj.__basename
|
||||
obj.default_icon = hs.image.imageFromName(hs.image.systemImageNames.ActionTemplate)
|
||||
|
||||
--- Seal.plugins.useractions.actions
|
||||
--- Variable
|
||||
---
|
||||
--- Notes:
|
||||
--- * A table containing the definitions of static user-defined actions. Each entry is indexed by the name of the entry as it will be shown in the chooser. Its value is a table which can have the following keys (one of `fn` or `url` is required. If both are provided, `url` is ignored):
|
||||
--- * fn - A function which will be called when the entry is selected. The function receives no arguments.
|
||||
--- * url - A URL which will be opened when the entry is selected. Can also be non-HTTP URLs, such as `mailto:` or other app-specific URLs.
|
||||
--- * description - (optional) A string or `hs.styledtext` object that will be shown underneath the main text of the choice.
|
||||
--- * icon - (optional) An `hs.image` object that will be shown next to the entry in the chooser. If not provided, `Seal.plugins.useractions.default_icon` is used. For `url` bookmarks, it can be set to `"favicon"` to fetch and use the website's favicon.
|
||||
--- * keyword - (optional) A command by which this action will be invoked, effectively turning it into a Seal command. Any arguments passed to the command will be handled as follows:
|
||||
--- * For `fn` actions, passed as an argument to the function
|
||||
--- * For `url` actions, substituted into the URL, taking the place of any occurrences of `${query}`.
|
||||
--- * hotkey - (optional) A hotkey specification in the form `{ modifiers, key }` by which this action can be invoked.
|
||||
--- * Example configuration:
|
||||
--- ```
|
||||
--- spoon.Seal:loadPlugins({"useractions"})
|
||||
--- spoon.Seal.plugins.useractions.actions =
|
||||
--- {
|
||||
--- ["Hammerspoon docs webpage"] = {
|
||||
--- url = "http://hammerspoon.org/docs/",
|
||||
--- icon = hs.image.imageFromName(hs.image.systemImageNames.ApplicationIcon),
|
||||
--- description = "Open Hammerspoon documentation",
|
||||
--- hotkey = { hyper, "h" },
|
||||
--- },
|
||||
--- ["Leave corpnet"] = {
|
||||
--- fn = function()
|
||||
--- spoon.WiFiTransitions:processTransition('foo', 'corpnet01')
|
||||
--- end,
|
||||
--- },
|
||||
--- ["Arrive in corpnet"] = {
|
||||
--- fn = function()
|
||||
--- spoon.WiFiTransitions:processTransition('corpnet01', 'foo')
|
||||
--- end,
|
||||
--- },
|
||||
--- ["Translate using Leo"] = {
|
||||
--- url = "http://dict.leo.org/ende/index_de.html#/search=${query}",
|
||||
--- icon = 'favicon',
|
||||
--- keyword = "leo",
|
||||
--- },
|
||||
--- ["Tell me something"] = {
|
||||
--- keyword = "tellme",
|
||||
--- fn = function(str) hs.alert.show(str) end,
|
||||
--- }
|
||||
--- ```
|
||||
obj.actions = {}
|
||||
|
||||
--- Seal.plugins.useractions.get_favicon
|
||||
--- Variable
|
||||
---
|
||||
--- If `true`, attempt to obtain the favicon for URLs added through the `add` command, and use it in the chooser. Defaults to `true`
|
||||
obj.get_favicon = true
|
||||
|
||||
-- Internal functions for storing/retrieving bookmarks in the settings database.
|
||||
local getSetting = function(label, default) return hs.settings.get(obj.__name.."."..label) or default end
|
||||
local setSetting = function(label, value) hs.settings.set(obj.__name.."."..label, value); return value end
|
||||
|
||||
-- Internal variable where the dynamically-added bookmarks are kept
|
||||
obj.stored_actions = getSetting('stored_actions', {})
|
||||
|
||||
-- Internal variable where the merged list of bookmarks/actions is kept
|
||||
obj.all_actions = nil
|
||||
|
||||
function update_all_actions()
|
||||
if (obj.all_actions == nil) then
|
||||
obj.all_actions = {}
|
||||
for k,v in pairs(obj.actions) do obj.all_actions[k] = hs.fnutils.copy(v) end
|
||||
for k,v in pairs(obj.stored_actions) do
|
||||
obj.all_actions[k] = hs.fnutils.copy(v)
|
||||
if v.encoded_icon then
|
||||
obj.all_actions[k].icon = hs.image.imageFromURL(v.encoded_icon)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function obj:commands()
|
||||
local cmds={
|
||||
add = {
|
||||
cmd = "add",
|
||||
fn = obj.choicesAddURLCommand,
|
||||
name = "Add URL",
|
||||
description = "Add URL to bookmarks",
|
||||
plugin = obj.__name
|
||||
},
|
||||
del = {
|
||||
cmd = "del",
|
||||
fn = obj.choicesDelURLCommand,
|
||||
name = "Delete URL",
|
||||
description = "Delete URL from bookmarks",
|
||||
plugin = obj.__name
|
||||
}
|
||||
}
|
||||
local hotkeys_def = {}
|
||||
local hotkeys_map = {}
|
||||
local any_hotkeys = false
|
||||
for k,v in pairs(self.actions or {}) do
|
||||
if v.keyword and (not cmds[v.keyword]) then
|
||||
if v.url ~= nil and v.icon == 'favicon' then
|
||||
v.icon = obj.favIcon(v.url)
|
||||
end
|
||||
cmds[v.keyword] = {
|
||||
cmd = v.keyword,
|
||||
fn = hs.fnutils.partial(obj.choicesActionKeyword, k, v),
|
||||
name = k,
|
||||
icon = v.icon,
|
||||
plugin = obj.__name
|
||||
}
|
||||
end
|
||||
if v.hotkey then
|
||||
local choice = obj.buildChoice(k,v)
|
||||
hotkeys_def[k] = hs.fnutils.partial(obj.completionCallback, choice)
|
||||
hotkeys_map[k] = v.hotkey
|
||||
any_hotkeys = true
|
||||
end
|
||||
end
|
||||
if any_hotkeys then
|
||||
hs.spoons.bindHotkeysToSpec(hotkeys_def, hotkeys_map)
|
||||
end
|
||||
return cmds
|
||||
end
|
||||
|
||||
function obj:bare()
|
||||
return self.bareActions
|
||||
end
|
||||
|
||||
function obj.buildChoice(action, v)
|
||||
local icon, kind
|
||||
local choice=nil
|
||||
if type(v) == 'table' then
|
||||
if v.fn then
|
||||
kind = 'runFunction'
|
||||
elseif v.url then
|
||||
kind = 'openURL'
|
||||
if v.icon == 'favicon' then
|
||||
v.icon = obj.favIcon(v.url)
|
||||
end
|
||||
end
|
||||
icon = v.icon or obj.default_icon
|
||||
choice = {}
|
||||
choice.text = action
|
||||
choice.type = kind
|
||||
choice.plugin = obj.__name
|
||||
choice.image = icon
|
||||
if v.description then
|
||||
choice.subText = v.description
|
||||
end
|
||||
end
|
||||
return choice
|
||||
end
|
||||
|
||||
function obj.bareActions(query)
|
||||
local choices = {}
|
||||
if query == nil or query == "" then
|
||||
return choices
|
||||
end
|
||||
|
||||
update_all_actions()
|
||||
obj.seal:refreshCommandsForPlugin(obj.__basename)
|
||||
|
||||
for action,v in pairs(obj.all_actions) do
|
||||
if string.match(action:lower(), query:lower()) then
|
||||
local choice = obj.buildChoice(action, v)
|
||||
if choice then
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.favIcon(url)
|
||||
local query=string.format("http://www.google.com/s2/favicons?sz=64&domain_url=%s", hs.http.encodeForQuery(url))
|
||||
return hs.image.imageFromURL(query)
|
||||
end
|
||||
|
||||
function obj.choicesAddURLCommand(query)
|
||||
local choices = {}
|
||||
if query == ".*" then
|
||||
query = "<url> <name>"
|
||||
end
|
||||
local url,name = string.match(query, "([^%s]+)%s+(.*)")
|
||||
local subtext = ""
|
||||
if url then
|
||||
subtext = string.format("New bookmark '%s' pointing to %s", name,url)
|
||||
end
|
||||
local choice = {
|
||||
text = "add " .. query,
|
||||
subText = subtext,
|
||||
url = url,
|
||||
name = name,
|
||||
plugin = obj.__name,
|
||||
type = 'addURL',
|
||||
}
|
||||
table.insert(choices, choice)
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.choicesDelURLCommand(query)
|
||||
local choices = {}
|
||||
for k,v in pairs(obj.stored_actions) do
|
||||
if string.match(k:lower(), query:lower()) or string.match(v.url:lower(), query:lower()) then
|
||||
local choice = {
|
||||
text = string.format("delete '%s'", k),
|
||||
subText = v.url,
|
||||
delKey = k,
|
||||
plugin = obj.__name,
|
||||
type = 'delURL',
|
||||
}
|
||||
if v.encoded_icon then
|
||||
choice.image = hs.image.imageFromURL(v.encoded_icon)
|
||||
end
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
end
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.choicesActionKeyword(action, def, query)
|
||||
local choices = {}
|
||||
if query == ".*" then
|
||||
query = ""
|
||||
end
|
||||
local choice = {
|
||||
text = def.keyword .. " " .. query,
|
||||
subText = def.description or action,
|
||||
actionname = action,
|
||||
arg = query,
|
||||
plugin = obj.__name,
|
||||
image = def.icon,
|
||||
type = 'invokeKeyword',
|
||||
}
|
||||
table.insert(choices, choice)
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.openURL(url)
|
||||
hs.execute(string.format("/usr/bin/open '%s'", url))
|
||||
end
|
||||
|
||||
function obj.completionCallback(row)
|
||||
update_all_actions()
|
||||
if row.type == 'runFunction' then
|
||||
local fn = obj.all_actions[row.text].fn
|
||||
fn()
|
||||
elseif row.type == 'openURL' then
|
||||
local url = obj.all_actions[row.text].url
|
||||
obj.openURL(url)
|
||||
elseif row.type == 'addURL' then
|
||||
obj.stored_actions[row.name] = { url = row.url }
|
||||
obj.all_actions = nil
|
||||
if obj.get_favicon then
|
||||
local ico=obj.favIcon(row.url)
|
||||
if ico then
|
||||
obj.stored_actions[row.name]['encoded_icon'] = ico:encodeAsURLString()
|
||||
end
|
||||
end
|
||||
setSetting('stored_actions', obj.stored_actions)
|
||||
elseif row.type == 'delURL' then
|
||||
obj.stored_actions[row.delKey] = nil
|
||||
obj.all_actions = nil
|
||||
setSetting('stored_actions', obj.stored_actions)
|
||||
elseif row.type == 'invokeKeyword' then
|
||||
if obj.actions[row.actionname].fn then
|
||||
obj.actions[row.actionname].fn(row.arg)
|
||||
elseif obj.actions[row.actionname].url then
|
||||
row.arg = hs.http.encodeForQuery(row.arg)
|
||||
local query = row.arg:gsub("%%", "%%%%")
|
||||
local url = string.gsub(obj.actions[row.actionname].url, '${query}', query)
|
||||
obj.openURL(url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return obj
|
||||
@@ -0,0 +1,266 @@
|
||||
local obj = {}
|
||||
obj.__index = obj
|
||||
obj.__name = "seal_vpn"
|
||||
|
||||
function obj:commands()
|
||||
return {vpn = {
|
||||
cmd = "vpn",
|
||||
fn = obj.choicesVPNCommand,
|
||||
name = "VPN",
|
||||
description = "Manage VPN connections",
|
||||
plugin = obj.__name
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function obj:bare()
|
||||
return nil
|
||||
end
|
||||
|
||||
function obj.getVPNConnections()
|
||||
connections = {}
|
||||
code, output, descriptor = hs.osascript.applescript([[
|
||||
set output to ""
|
||||
|
||||
on join_list(the_list, delimiter)
|
||||
set the_string to ""
|
||||
set old_delims to AppleScript's text item delimiters
|
||||
repeat with the_item in the_list
|
||||
if the_string is equal to "" then
|
||||
set the_string to the_string & the_item
|
||||
else
|
||||
set the_string to the_string & delimiter & the_item
|
||||
end if
|
||||
end repeat
|
||||
set AppleScript's text item delimiters to old_delims
|
||||
return the_string
|
||||
end join_list
|
||||
|
||||
set vpn_connections to {}
|
||||
|
||||
tell application "Viscosity"
|
||||
repeat with the_connection in connections
|
||||
set the end of vpn_connections to (name of the_connection) & tab & (state of the_connection) & tab & "Viscosity"
|
||||
end repeat
|
||||
end tell
|
||||
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
repeat with vpn in (every service whose (kind is greater than 10 and kind is less than 17))
|
||||
set state to "Disconnected"
|
||||
if connected of current configuration of vpn is equal to true then
|
||||
set state to "Connected"
|
||||
end if
|
||||
set the end of vpn_connections to (name of vpn) & tab & state & tab & "macOS"
|
||||
end repeat
|
||||
end tell
|
||||
end tell
|
||||
|
||||
return my join_list(vpn_connections, linefeed)
|
||||
]])
|
||||
|
||||
if code == false or output == nil or output == "" then
|
||||
return connections
|
||||
end
|
||||
|
||||
for line in output:gmatch("[^\r\n]+") do
|
||||
parts = {}
|
||||
for part in line:gmatch("%S+") do
|
||||
table.insert(parts, part)
|
||||
end
|
||||
kind = parts[#parts]
|
||||
table.remove(parts, #parts)
|
||||
state = parts[#parts]
|
||||
table.remove(parts, #parts)
|
||||
name = table.concat(parts, " ")
|
||||
table.insert(connections, {name=name, state=state, kind=kind})
|
||||
end
|
||||
return connections
|
||||
end
|
||||
|
||||
function obj.disconnectVPN(name, kind)
|
||||
if kind == "Viscosity" then
|
||||
obj.disconnectViscosity(name)
|
||||
elseif kind == "macOS" then
|
||||
obj.disconnectMacOS(name)
|
||||
end
|
||||
end
|
||||
|
||||
function obj.disconnectViscosity(name)
|
||||
code, output, descriptor = hs.osascript.applescript(string.format([[
|
||||
-- Return true if VPN is active
|
||||
on vpn_is_active(vpn_name)
|
||||
tell application "Viscosity"
|
||||
repeat with the_connection in connections
|
||||
if (name of the_connection) is equal to vpn_name then
|
||||
if (state of the_connection) is equal to "Connected" then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end if
|
||||
end if
|
||||
end repeat
|
||||
end tell
|
||||
return false
|
||||
end vpn_is_active
|
||||
|
||||
on run argv
|
||||
set vpn_name to "%s"
|
||||
if my vpn_is_active(vpn_name) then
|
||||
tell application "Viscosity"
|
||||
disconnect vpn_name
|
||||
end tell
|
||||
end if
|
||||
end run
|
||||
]], name))
|
||||
end
|
||||
|
||||
function obj.disconnectMacOS(name)
|
||||
code, output, descriptor = hs.osascript.applescript(string.format([[
|
||||
-- Return true if VPN is active
|
||||
on vpn_is_active(vpn_name)
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
return connected of current configuration of service vpn_name
|
||||
end tell
|
||||
end tell
|
||||
end vpn_is_active
|
||||
|
||||
on connect_vpn(vpn_name)
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
connect service vpn_name
|
||||
end tell
|
||||
end tell
|
||||
end connect_vpn
|
||||
|
||||
on run argv
|
||||
set vpn_name to "%s"
|
||||
if my vpn_is_active(vpn_name) then
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
disconnect service vpn_name
|
||||
end tell
|
||||
end tell
|
||||
end if
|
||||
end run
|
||||
]], name))
|
||||
end
|
||||
|
||||
function obj.connectVPN(name, kind)
|
||||
if kind == "Viscosity" then
|
||||
obj.connectViscosity(name)
|
||||
elseif kind == "macOS" then
|
||||
obj.connectMacOS(name)
|
||||
end
|
||||
end
|
||||
|
||||
function obj.connectViscosity(name)
|
||||
code, output, descriptor = hs.osascript.applescript(string.format([[
|
||||
-- Return true if VPN is active
|
||||
on vpn_is_active(vpn_name)
|
||||
tell application "Viscosity"
|
||||
repeat with the_connection in connections
|
||||
if (name of the_connection) is equal to vpn_name then
|
||||
if (state of the_connection) is equal to "Connected" then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end if
|
||||
end if
|
||||
end repeat
|
||||
end tell
|
||||
return false
|
||||
end vpn_is_active
|
||||
|
||||
-- Connect to specified VPN
|
||||
on connect_vpn(vpn_name)
|
||||
tell application "Viscosity"
|
||||
connect vpn_name
|
||||
end tell
|
||||
end connect_vpn
|
||||
|
||||
on run argv
|
||||
set vpn_name to "%s"
|
||||
if my vpn_is_active(vpn_name) then
|
||||
log "VPN " & quote & vpn_name & quote & " is already active."
|
||||
return
|
||||
end if
|
||||
connect_vpn(vpn_name)
|
||||
end run
|
||||
]], name))
|
||||
end
|
||||
|
||||
function obj.connectMacOS(name)
|
||||
code, output, descriptor = hs.osascript.applescript(string.format([[
|
||||
-- Return true if VPN is active
|
||||
on vpn_is_active(vpn_name)
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
return connected of current configuration of service vpn_name
|
||||
end tell
|
||||
end tell
|
||||
end vpn_is_active
|
||||
|
||||
-- Connect to specified VPN
|
||||
on connect_vpn(vpn_name)
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
connect service vpn_name
|
||||
end tell
|
||||
end tell
|
||||
end connect_vpn
|
||||
|
||||
on run argv
|
||||
set vpn_name to "%s"
|
||||
if my vpn_is_active(vpn_name) then
|
||||
log "VPN " & quote & vpn_name & quote & " is already active."
|
||||
return
|
||||
end if
|
||||
connect_vpn(vpn_name)
|
||||
end run
|
||||
]], name))
|
||||
end
|
||||
|
||||
|
||||
function obj.choicesVPNCommand(query)
|
||||
local choices = {}
|
||||
local connections = obj.getVPNConnections()
|
||||
local img_connected = hs.image.imageFromPath(obj.seal.spoonPath.."/viscosity_locked.png")
|
||||
local img_disconnected = hs.image.imageFromPath(obj.seal.spoonPath.."/viscosity_unlocked.png")
|
||||
|
||||
for k,v in pairs(connections) do
|
||||
name = v["name"]
|
||||
if string.match(name:lower(), query:lower()) then
|
||||
state = v["state"]
|
||||
kind = v["kind"]
|
||||
local choice = {}
|
||||
choice["text"] = name
|
||||
choice["subText"] = state .. " (" .. kind .. ")"
|
||||
if state == "Connected" then
|
||||
choice["image"] = img_connected
|
||||
else
|
||||
choice["image"] = img_disconnected
|
||||
end
|
||||
choice["name"] = name
|
||||
choice["state"] = state
|
||||
choice["kind"] = kind
|
||||
choice["plugin"] = obj.__name
|
||||
choice["type"] = "toggle"
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
end
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.completionCallback(rowInfo)
|
||||
if rowInfo["type"] == "toggle" then
|
||||
if rowInfo["state"] == "Connected" then
|
||||
obj.disconnectVPN(rowInfo["name"], rowInfo["kind"])
|
||||
else
|
||||
obj.connectVPN(rowInfo["name"], rowInfo["kind"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return obj
|
||||
@@ -0,0 +1,266 @@
|
||||
local obj = {}
|
||||
obj.__index = obj
|
||||
obj.__name = "seal_vpn"
|
||||
|
||||
function obj:commands()
|
||||
return {vpn = {
|
||||
cmd = "vpn",
|
||||
fn = obj.choicesVPNCommand,
|
||||
name = "VPN",
|
||||
description = "Manage VPN connections",
|
||||
plugin = obj.__name
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function obj:bare()
|
||||
return nil
|
||||
end
|
||||
|
||||
function obj.getVPNConnections()
|
||||
connections = {}
|
||||
code, output, descriptor = hs.osascript.applescript([[
|
||||
set output to ""
|
||||
|
||||
on join_list(the_list, delimiter)
|
||||
set the_string to ""
|
||||
set old_delims to AppleScript's text item delimiters
|
||||
repeat with the_item in the_list
|
||||
if the_string is equal to "" then
|
||||
set the_string to the_string & the_item
|
||||
else
|
||||
set the_string to the_string & delimiter & the_item
|
||||
end if
|
||||
end repeat
|
||||
set AppleScript's text item delimiters to old_delims
|
||||
return the_string
|
||||
end join_list
|
||||
|
||||
set vpn_connections to {}
|
||||
|
||||
tell application "Viscosity"
|
||||
repeat with the_connection in connections
|
||||
set the end of vpn_connections to (name of the_connection) & tab & (state of the_connection) & tab & "Viscosity"
|
||||
end repeat
|
||||
end tell
|
||||
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
repeat with vpn in (every service whose (kind is greater than 10 and kind is less than 17))
|
||||
set state to "Disconnected"
|
||||
if connected of current configuration of vpn is equal to true then
|
||||
set state to "Connected"
|
||||
end if
|
||||
set the end of vpn_connections to (name of vpn) & tab & state & tab & "macOS"
|
||||
end repeat
|
||||
end tell
|
||||
end tell
|
||||
|
||||
return my join_list(vpn_connections, linefeed)
|
||||
]])
|
||||
|
||||
if code == false or output == nil or output == "" then
|
||||
return connections
|
||||
end
|
||||
|
||||
for line in output:gmatch("[^\r\n]+") do
|
||||
parts = {}
|
||||
for part in line:gmatch("%S+") do
|
||||
table.insert(parts, part)
|
||||
end
|
||||
kind = parts[#parts]
|
||||
table.remove(parts, #parts)
|
||||
state = parts[#parts]
|
||||
table.remove(parts, #parts)
|
||||
name = table.concat(parts, " ")
|
||||
table.insert(connections, {name=name, state=state, kind=kind})
|
||||
end
|
||||
return connections
|
||||
end
|
||||
|
||||
function obj.disconnectVPN(name, kind)
|
||||
if kind == "Viscosity" then
|
||||
obj.disconnectViscosity(name)
|
||||
elseif kind == "macOS" then
|
||||
obj.disconnectMacOS(name)
|
||||
end
|
||||
end
|
||||
|
||||
function obj.disconnectViscosity(name)
|
||||
code, output, descriptor = hs.osascript.applescript(string.format([[
|
||||
-- Return true if VPN is active
|
||||
on vpn_is_active(vpn_name)
|
||||
tell application "Viscosity"
|
||||
repeat with the_connection in connections
|
||||
if (name of the_connection) is equal to vpn_name then
|
||||
if (state of the_connection) is equal to "Connected" then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end if
|
||||
end if
|
||||
end repeat
|
||||
end tell
|
||||
return false
|
||||
end vpn_is_active
|
||||
|
||||
on run argv
|
||||
set vpn_name to "%s"
|
||||
if my vpn_is_active(vpn_name) then
|
||||
tell application "Viscosity"
|
||||
disconnect vpn_name
|
||||
end tell
|
||||
end if
|
||||
end run
|
||||
]], name))
|
||||
end
|
||||
|
||||
function obj.disconnectMacOS(name)
|
||||
code, output, descriptor = hs.osascript.applescript(string.format([[
|
||||
-- Return true if VPN is active
|
||||
on vpn_is_active(vpn_name)
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
return connected of current configuration of service vpn_name
|
||||
end tell
|
||||
end tell
|
||||
end vpn_is_active
|
||||
|
||||
on connect_vpn(vpn_name)
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
connect service vpn_name
|
||||
end tell
|
||||
end tell
|
||||
end connect_vpn
|
||||
|
||||
on run argv
|
||||
set vpn_name to "%s"
|
||||
if my vpn_is_active(vpn_name) then
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
disconnect service vpn_name
|
||||
end tell
|
||||
end tell
|
||||
end if
|
||||
end run
|
||||
]], name))
|
||||
end
|
||||
|
||||
function obj.connectVPN(name, kind)
|
||||
if kind == "Viscosity" then
|
||||
obj.connectViscosity(name)
|
||||
elseif kind == "macOS" then
|
||||
obj.connectMacOS(name)
|
||||
end
|
||||
end
|
||||
|
||||
function obj.connectViscosity(name)
|
||||
code, output, descriptor = hs.osascript.applescript(string.format([[
|
||||
-- Return true if VPN is active
|
||||
on vpn_is_active(vpn_name)
|
||||
tell application "Viscosity"
|
||||
repeat with the_connection in connections
|
||||
if (name of the_connection) is equal to vpn_name then
|
||||
if (state of the_connection) is equal to "Connected" then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end if
|
||||
end if
|
||||
end repeat
|
||||
end tell
|
||||
return false
|
||||
end vpn_is_active
|
||||
|
||||
-- Connect to specified VPN
|
||||
on connect_vpn(vpn_name)
|
||||
tell application "Viscosity"
|
||||
connect vpn_name
|
||||
end tell
|
||||
end connect_vpn
|
||||
|
||||
on run argv
|
||||
set vpn_name to "%s"
|
||||
if my vpn_is_active(vpn_name) then
|
||||
log "VPN " & quote & vpn_name & quote & " is already active."
|
||||
return
|
||||
end if
|
||||
connect_vpn(vpn_name)
|
||||
end run
|
||||
]], name))
|
||||
end
|
||||
|
||||
function obj.connectMacOS(name)
|
||||
code, output, descriptor = hs.osascript.applescript(string.format([[
|
||||
-- Return true if VPN is active
|
||||
on vpn_is_active(vpn_name)
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
return connected of current configuration of service vpn_name
|
||||
end tell
|
||||
end tell
|
||||
end vpn_is_active
|
||||
|
||||
-- Connect to specified VPN
|
||||
on connect_vpn(vpn_name)
|
||||
tell application "System Events"
|
||||
tell current location of network preferences
|
||||
connect service vpn_name
|
||||
end tell
|
||||
end tell
|
||||
end connect_vpn
|
||||
|
||||
on run argv
|
||||
set vpn_name to "%s"
|
||||
if my vpn_is_active(vpn_name) then
|
||||
log "VPN " & quote & vpn_name & quote & " is already active."
|
||||
return
|
||||
end if
|
||||
connect_vpn(vpn_name)
|
||||
end run
|
||||
]], name))
|
||||
end
|
||||
|
||||
|
||||
function obj.choicesVPNCommand(query)
|
||||
local choices = {}
|
||||
local connections = obj.getVPNConnections()
|
||||
local img_connected = hs.image.imageFromPath(obj.seal.spoonPath.."/viscosity_locked.png")
|
||||
local img_disconnected = hs.image.imageFromPath(obj.seal.spoonPath.."/viscosity_unlocked.png")
|
||||
|
||||
for k,v in pairs(connections) do
|
||||
name = v["name"]
|
||||
if string.match(name:lower(), query:lower()) then
|
||||
state = v["state"]
|
||||
kind = v["kind"]
|
||||
local choice = {}
|
||||
choice["text"] = name
|
||||
choice["subText"] = state .. " (" .. kind .. ")"
|
||||
if state == "Connected" then
|
||||
choice["image"] = img_connected
|
||||
else
|
||||
choice["image"] = img_disconnected
|
||||
end
|
||||
choice["name"] = name
|
||||
choice["state"] = state
|
||||
choice["kind"] = kind
|
||||
choice["plugin"] = obj.__name
|
||||
choice["type"] = "toggle"
|
||||
table.insert(choices, choice)
|
||||
end
|
||||
end
|
||||
return choices
|
||||
end
|
||||
|
||||
function obj.completionCallback(rowInfo)
|
||||
if rowInfo["type"] == "toggle" then
|
||||
if rowInfo["state"] == "Connected" then
|
||||
obj.disconnectVPN(rowInfo["name"], rowInfo["kind"])
|
||||
else
|
||||
obj.connectVPN(rowInfo["name"], rowInfo["kind"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return obj
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user