- What is a Spoon?
- Where do I get Spoons from?
- How do I install a Spoon?
- How do I use a Spoon?
- How do I create a Spoon?
Spoons are intended to be pure-Lua plugins for users to use in their Hammerspoon configs.
As a community, we have created many great configurations for Hammerspoon, but sharing code between them is hard and fragile. Spoons have been created as a way to address these issues. Users should be able to download a Spoon and quickly integrate it into their config without worrying about what it is doing internally.
This is possible because of two things:
- Infrastructure within Hammerspoon for loading Lua code from Spoons
- The authors of Spoons sticking, wherever possible, to a standard API for users to use
The official repository of Spoons is https://www.hammerspoon.org/Spoons (the source for which can be found at https://github.com/Hammerspoon/Spoons), but authors may choose to distribute them separately from their own sites.
Spoons should be distributed as .zip
files. Simply download one, uncompress it (if your browser hasn't done that part automatically) and double click on the Spoon. Hammerspoon will install it for you in ~/.hammerspoon/Spoons/
There are two parts to this, loading the spoon, and integrating it into your configuration.
Hopefully the Spoon came with some documentation, either on its homepage or in ~/.hammerspoon/Spoons/NAME.spoon
. There you should find some documentation of the API offered by the Spoon, and any special requirements it has.
For most Spoons, simply add hs.loadSpoon("NAME")
to your Hammerspoon config (note that NAME
should not include the .spoon
extension). This will make the spoon available in the global Lua namespace as spoon.NAME
.
After loading a Spoon, you are responsible for calling its start()
method if it has one before using it.
Note that hs.loadSpoon()
uses package.path
to find Spoons. Hence you can have it look for Spoons in other paths by adding those paths to package.path
as follows:
-- Look for Spoons in ~/.hammerspoon/MySpoons as well
package.path = package.path .. ";" .. hs.configdir .. "/MySpoons/?.spoon/init.lua"
This can be useful if you have Spoons you are developing for example.
In most cases, the API should take roughly this form:
NAME:init()
- this is called automatically byhs.loadSpoon()
and will do any initial setup work required, but should generally not start taking any actionsNAME:start()
- if any kind of background work is necessary, this method will start itNAME:stop()
- if any kind of background work is running, this method will stop itNAME:bindHotkeys(mapping)
- this method is used to tell the Spoon how to bind hotkeys for its various functions. Depending on the Spoon, these hotkeys may be bound immediately, or when:start()
is called. This method should accept a single argument, a table in the form:
{ someFeature = {{"cmd", "alt"}, "f"},
otherFeature = {{"shift", "ctrl"}, "b"}}
The Spoon should also provide some standard metadata:
NAME.name
- A string containing the name of the SpoonNAME.version
- A string containing the version number of the SpoonNAME.author
- A string containing the name/email of the spoon's authorNAME.license
- A string containing some information about the license that applies to the Spoon, ideally including a URL to the license
and optionally:
NAME.homepage
- A string containing a URL to the Spoon's homepage
Many Spoons will offer additional API points on top of these, and you should consult their documentation to learn more.
Ultimately a Spoon can be as little as a directory whose name ends .spoon
, with an init.lua
inside it.
However, Spoons offer the most value to users of Hammerspoon when they conform to an API convention, allowing users to interact with all of their Spoons in very similar ways.
- Spoon names should use TitleCase
- Spoon methods/variables/constants/etc. should use camelCase
When a user calls hs.loadSpoon()
, Hammerspoon will load and execute init.lua
from the relevant Spoon.
You should generally not perform any work, map any hotkeys, start any timers/watchers/etc. in the main scope of your init.lua
. Instead, it should simply prepare an object with methods to be used later, then return the object.
If the object you return has an :init()
method, Hammerspoon will call it automatically (although users can override this behaviour, so be sure to document your :init()
method).
In the :init()
method, you should do any work that is necessary to prepare resources for later use, although generally you should not be starting any timers/watchers/etc. or mapping any hotkeys here.
You should include at least the following properties on your object:
.name
- The name of your Spoon.version
- The version of your Spoon.author
- Your name and optionally your email address.license
- The software license that applies to your Spoon, ideally with a link to the text of the license (e.g. on https://opensource.org/)
and optionally:
.homepage
- A URL for the home of your Spoon, e.g. its GitHub repo
If your Spoon provides some kind of background activity, e.g. timers, watchers, spotlight searches, etc. you should generally activate them in a :start()
method, and de-activate them in a :stop()
method
If your Spoon provides actions that a user can map to hotkeys, you should expose a :bindHotKeys()
method. The method should accept a single parameter, which is a table.
The keys of the table should be strings that describe the action performed by the hotkeys, and the values of the table should be tables containing modifiers and keynames/keycodes and, optionally, a message to be displayed via hs.alert()
when the hotkey has been triggered.
For example, if the user wants to map two of your actions, show
and hide
, they would pass in:
{
show={{"cmd", "alt"}, "s", message="Show"},
hide={{"cmd", "alt"}, "h"}
}
Your :bindHotkeys()
method now has all of the information it needs to bind hotkeys to its methods.
While you might want to verify the contents of the table, it seems reasonable to be fairly limited in the extent, so long as you have documented the method well.
The function hs.spoons.bindHotkeysToSpec()
can do most of the hard work of the mappings for you. For example, the following would allow binding of actions show
and hide
to showMethod()
and hideMethod()
respectively:
function MySpoon:bindHotKeys(mapping)
local spec = {
show = hs.fnutils.partial(self.showMethod, self),
hide = hs.fnutils.partial(self.hideMethod, self),
}
hs.spoons.bindHotkeysToSpec(spec, mapping)
return self
end
You can present any other methods you want, and while they are all technically accessible to the user, you should only document the ones you actually intend to be public API.
Spoon methods/variables/etc. should be documented using the same docstring format that Hammerspoon uses for its own API. An example of a method for adding a USB device to a Spoon that takes actions when USB devices are connected, might look like this:
--- USBObserver:addDevice(vendorID, productID[, name])
--- Method
--- Adds a device to USBObserver's watch list
---
--- Parameters:
--- * vendorID - A number containing the vendor ID of a USB device
--- * productID - A number containing the product ID of a USB device
--- * name - An optional string containing the name of a USB device
---
--- Returns:
--- * A boolean, true if the device was added, otherwise false
By convention in Hammerspoon, methods tend to return the object they belong to (so methods can be chained, e.g. foo:bar():baz()
), but this isn't always appropriate.
Several tools are able to operate on the docstrings used by Hammerspoon and Spoons. In the simplest case, each Spoon should include a docs.json
file which is little more than the various docstrings collected together.
This file can be generated using the Hammerspoon command line tool (see https://www.hammerspoon.org/docs/hs.ipc.html#cliInstall):
cd /path/too/your/Spoon
hs -c "hs.doc.builder.genJSON(\"$(pwd)\")" | grep -v "^--" > docs.json
Any Spoons that are submitted to the official Spoons repository will have their HTML documentation generated and hosted by GitHub.
If you also want to generate HTML/Markdown versions of your documentation for your own purposes:
- Clone https://github.com/Hammerspoon/hammerspoon
- Install the required Python dependencies (e.g.
pip install --user -r requirements.txt
in the Hammerspoon repo) - Then in your Spoon's directory, run:
/path/to/hammerspoon_repo/scripts/docs/bin/build_docs.py --templates /path/to/hammerspoon_repo/scripts/docs/templates/ --output_dir . --json --html --markdown --standalone .
This will search the current working director for any .lua
files, extract docstrings from them, and write docs.json
to the current directory, along with HTML and Markdown outputs. See build_docs.py --help
for more options.
If your Spoon grows more complex than just an init.lua
, a problem you will quickly run into is how you can load extra .lua
files, or other types of resources (e.g. images).
There is, however, a simple way to discover the true path of your Spoon on the filesystem. Simply use the hs.spoons.scriptPath()
function:
-- Get path to Spoon's init.lua script
obj.spoonPath = hs.spoons.scriptPath()
To access assets bundled with your Spoon, use the hs.spoons.resourcePath()
function:
-- Get path to a resource bundled with the Spoon
obj.imagePath = hs.spoons.resourcePath("images/someImage.png")
You cannot use require()
to load .lua
files in a Spoon, instead you should use:
dofile(hs.spoons.resourcePath("someCode.lua"))
and the someCode.lua
file will be loaded and executed (and if it returns anything, you can capture those values from dofile()
)