13 KiB
Contributing to Hammerspoon
- Contributing to Hammerspoon
Hammerspoon is composed of three separate logical areas - a Lua runtime wrapper framework called LuaSkin, the core Hammerspoon app which houses the LuaSkin/Lua runtime and provides the ability to load extensions, and various extension modules that expose system APIs to the user's Lua code.
How is everything built?
The app itself is built using Xcode. You must open Hammerspoon.xcworkspace
rather than Hammerspoon.xcodeproj
. If you open the latter, your build will fail because Xcode will not know about the Cocoapods that Hammerspoon depends on (see our Podfile
for the current list of required pods). On versions of macOS prior to Catalina Python 3 will need to be installed for the build to work.
The extension modules are built before the core Hammerspoon binary as target dependencies. Each extension is defined as an Xcode target in its own right, although there is usually no reason to build these targets manually.
Making frequent local rebuilds more convenient
Self-signing your builds will keep you from having to re-enable permissions for your locally built copy.
Create a self-signed Code Signing certificate named 'Internal Code Signing' or similar as described here.
Then, simply run ./scripts/rebuild.sh
for more streamlined builds.
Contributing to the core app or LuaSkin
This is generally very simple in terms of the workflow, but there's less likely to be any reason to work on the core app:
- Clone our GitHub repository
- Open
Hammerspoon.xcworkspace
in Xcode (Note that you'll generally need the latest available version of Xcode) - Make the changes you want
- Push them up to a fork on GitHub
- Propose a Pull Request on GitHub
- Talk to us in #hammerspoon on Libera if you need any guidance
Contributing to the extensions
This is really where the meat of Hammerspoon is. Extensions can either be pure Lua or a mixture of Lua and Objective-C (although since they are just dynamically loaded libraries, they could ultimately be compiled in almost any language, if there is a sufficiently compelling reason).
Note: all APIs provided by extensions should follow the camelCase naming convention. This does not need to apply to an extension's internal functions, just the ones presented to Lua.
Modifying an existing extension should follow the simple workflow above for the core app.
Writing a new, pure-Lua extension
These extensions generally provide useful helper functionality for users (e.g. abstracting other extensions).
To create such an extension:
- Clone the Hammerspoon git repository
- cd into the
extensions
directory - Make a directory for your extension
- Create a
modulename.lua
to contain your code, giving it the appropriate name. It should behave like any normal Lua library - that is to say, your job is to return a table containing functions/methods/constants/etc - Ensure you document your API in our preferred format (see the code for almost any existing module for reference)
- In Xcode's Project Navigator, select the Hammerspoon root entry, then choose Hammerspoon from the Targets list and find the "Copy Extension Lua Files" build phase. Add your new Lua file
- Build Hammerspoon and test your extension
- Push your changes up to a fork on GitHub
- Propose a Pull Request on GitHub
- Talk to us in #hammerspoon on Libera if you need any guidance
Writing a new mixed Lua/Objective-C extension
These extensions generally expose an OS level API for users to automate (e.g. adjusting screen brightness using IOKit).
To create such an extension:
- Clone the Hammerspoon git repository
- Create the directories/files for your extension:
- cd into the
extensions
directory - Make a directory for your extension
- Create a
modulename.lua
to load your Objective-C code and contain any additional Lua code. You might find it easier to provide much of your API in Lua and just provide undocumented API from Objective C that does the minimum work possible. The choice is ultimately down to you, depending on the nature of the work the extension is doing. - Create a
modulename.m
to contain your Objective-C code. Please use the LuaSkin methods to do as much work as possible, they are well tested and in most extensions can reduce the amount of Lua C API calls to almost zero. Not all of our extensions have been fully converted to LuaSkin yet (a good example ishs.chooser
) - Right click on the
extensions
group in Xcode's Project Browser and add a new sub-group for your extension, then right click on the sub-group and add yourmodulename.lua
andmodulename.m
files (and any supporting.h
/.c
/.m
/etc files) - The files you've added will probably be made members of the Hammerspoon target. You do not want this; Select each file in the Project Browser and using the File Inspector in the Utilities pane on the right of Xcode's window, deselect them from the main Hammerspoon target.
- cd into the
- Configure Xcode to build your extension and include it in the
Hammerspoon.app
bundle:- Click on the
Hammerspoon
workspace at the very top of the Xcode Project Browser (i.e. the bar on the left) - Right click on an existing extension target in the "project and targets list" and choose
Duplicate
, which createsextensionname copy
at the bottom of the list. - Rename the copy and drag it to the right place in the list (alphabetically)
- Click on the target you just created, remove hs.alert's
extension.m
from theCompile Sources
build phase, add in the.m
files from your new module - Check the
Link Binary With Libraries
section for any frameworks you need to add. Typically this will just meanLuaSkin.framework
, plus any additional system frameworks you need to link against. - Click on the
Hammerspoon
target (not the project), and in theTarget Dependencies
build phase, add the module target you just created - Click the menu item Product → Scheme → Manage Schemes, find
extensionname copy
, rename it and move it to the right place in the list of schemes - Add your
modulename.lua
to the "Copy Extension Lua Files" build phase on the Hammerspoon target, and yourmodulename.dylib
to the "Copy Extension Dylibs" build phase
- Click on the
- Build Hammerspoon and test your extension
- Push your changes up to a fork on GitHub
- Propose a Pull Request on GitHub
- Talk to us in #hammerspoon on Libera if you need any guidance
Documenting your extension
Both Lua and Objective-C portions of an extension should contain in-line documentation of all of the functions they expose to users of the extension.
The format for docstrings should follow the standard described below. Note that for Lua files, the lines should begin with ---
and for Objective C files, the lines should begin with ///
.
Constants
--- hs.foo.someConstant
--- Constant
--- This defines the value of a thing
Variables
--- hs.foo.someVariable
--- Variable
--- This lets you influence the behaviour of this extension
Functions
Note that a function is any API function provided by an extension, which doesn't relate to an object created by the extension.
The Parameters
and Returns
sections should always be present. If there is nothing to describe there, simply list * None
. The Notes
section is optional and should only be present if there are useful notes.
--- hs.foo.someFunction(bar[, baz]) -> string or nil
--- Function
--- This is a one-line description of the function
---
--- Parameters:
--- * bar - A value for doing something
--- * baz - Some optional other value. Defaults to 'abc'
---
--- Returns:
--- * A string with some important result, or nil if an error occurred
---
--- Notes:
--- * An important first note
--- * Another important note
Methods
Note that a method is any function provided by an extension which relates to an object created by that extension. They are still technically functions, but the signature is differentiated by the presence of a :
The Parameters
and Returns
sections should always be present. If there is nothing to describe there, simply list * None
. The Notes
section is optional and should only be present if there are useful notes.
--- hs.foo:someMethod() -> bool
--- Method
--- This is a one-line description of the method
---
--- Parameters:
--- * None
---
--- Returns:
--- * Boolean indicating whether the operation succeeded.
Testing
All new extensions in Hammerspoon should be landed with a test suite, and any modifications to existing extensions should add appropriate tests (which may mean creating tests, if the extension in question is not currently being fully tested).
Our test suite is driven by Xcode's XCTest
framework, and the tests can be a mixture of Lua or Lua and Objective C - it would generally only be appropriate to have unit tests in C, and functional tests in Lua.
The best place to start is in the Hammerspoon/Hammerspoon Tests
folder in Xcode. Here are some notes on the expected setup:
- There should be a
.m
for each extension that is being tested, namedHSfoo.m
(wherefoo
is the name of the extension). HSfoo.m
should contain the declaration and implementation of anHSfoo
class which inherits fromHSTestCase
.- The
setUp
method should call[super setUpWithRequire:@"test_foo"];
to loadtest_foo.lua
from the extension's folder (i.e.extensions/foo/
) - The rest of
HSfoo
should be methods namedtestBar
, each of which causes some test action to take place. - There are some helper macros for use inside the test methods:
RUN_LUA_TEST()
will cause a function fromtest_foo.lua
to be run, if its name exactly matches the name of theHSfoo
methodSKIP_IN_GITHUB_ACTIONS()
will cause this test to be skipped when running as part of our GitHub Actions test runs (e.g. because the GitHub VMs lack hardware/network resources required to test)
When Hammerspoon detects it is is being run by XCTest
, it loads a special init.lua
(Hammerspoon/Hammerspoon Tests/init.lua
) which provides a number of helper functions, mainly related to asserting state in test functions. These functions will generate Lua errors if a test failure occurs, which will cause Xcode to report the test has failed, with an appropriate backtrace in the logs. Refer to the file for the full list of assertions, but the most useful are:
assertIsEqual(expected, actual)
- Ensures that the two arguments are of the same type and valueassertTrue(a)
/assertFalse(a)
- Ensure that the argument istrue
/false
respectivelyassertIsString(a)
/assertIsNumber(a)
/assertIsBoolean(a)
/etc - Ensure that the Lua type of a variable is correctassertIsUserdataOfType(type, a)
- Ensures that the argument is a Lua userdata object of a particular type (where the type is a string, as given toLuaSkin
when the extension registered its libraries/objects). This is particularly useful for verifying the return values of constructor functions
When adding both the HSfoo.m
and test_foo.lua
files to Xcode, it is important to ensure that they do not become members of the Hammerspoon
target. They should instead both be members of the Hammerspoon Tests
target (HSfoo.m
in the Compile Sources
Build Phase, test_foo.lua
in the Copy Bundle Resources
Build Phase).
Third party extension distribution
While we want to have Hammerspoon shipping as many useful extensions as possible, there may be reasons for you to ship your extension separately. It would probably be easier to do this in binary form, following the init.lua/internal.so form that Hammerspoon uses, then users can just download your extension into ~/.hammerspoon/<YOUR_EXTENSION_NAME>/
.
If you do choose this route, please list your extension at https://github.com/Hammerspoon/hammerspoon/wiki/Third-Party-Extensions so users can discover it easily.