--- name: hs2type description: Guide for creating custom types in Hammerspoon 2 — both shared engine types and module-specific types --- # Hammerspoon 2 Type System Types are Swift objects exposed to JavaScript via JSExport. There are two distinct categories with different rules. Choose the right one before writing any code. --- ## Category 1 — Engine types (`Engine/Types/`) Engine types are **directly usable from JavaScript** — users can call their static factory methods or constructors at any time, without going through a module. They are global JS objects registered at engine startup. **When to create one:** the type represents a fundamental, module-agnostic value (geometry, colour, font, image, string) that multiple modules will share. **When NOT to create one:** the type wraps a module-specific OS resource (a running application, a display, a timer). Use a module type instead. ### File location `Hammerspoon 2/Engine/Types/HSFoo.swift` ### Structural template ```swift import Foundation import JavaScriptCore // import whatever the wrapped native type needs /// User-facing docstring for the type. The protocol docstring is what the /// docs generator reads — the class is hidden so its docstring is irrelevant. @objc protocol HSFooAPI: HSTypeAPI, JSExport { // Declare every property and method the user can call from JavaScript. // All same @objc bridging rules as module API protocols apply. /// Create a new HSFoo /// - Parameters: /// - x: description @objc init(x: Double) // use static factory methods if init can fail @objc var x: Double { get set } @objc func doSomething() -> String } // No @_documentation here — engine types are intentionally public @objc class HSFoo: NSObject, HSFooAPI { @objc var typeName = "HSFoo" // REQUIRED — satisfies HSTypeAPI // Internal storage (the wrapped native type) var nativeThing: NativeFoo required init(x: Double) { nativeThing = NativeFoo(x: x) super.init() } var x: Double { get { nativeThing.x } set { nativeThing.x = newValue } } @objc func doSomething() -> String { ... } } ``` ### `JSConvertible` — bridging to/from native Swift types When the engine type wraps a standard Swift/CoreGraphics value type (CGPoint, CGSize, CGRect, SwiftUI.Color, etc.), implement `JSConvertible` on the **native** type: ```swift extension CGPoint: JSConvertible { typealias BridgeType = HSPoint init(from bridge: HSPoint) { self.init(x: bridge.x, y: bridge.y) } func toBridge() -> HSPoint { HSPoint(x: Double(x), y: Double(y)) } } ``` This lets any Swift code that receives a `CGPoint` call `.toBridge()` to get a JS-passable `HSPoint`, and vice versa. Also add a `JSValue` extension for ergonomic unboxing in module code: ```swift extension JSValue { func toCGPoint() -> CGPoint? { guard let bridge = toObjectOf(HSPoint.self) as? HSPoint else { return nil } return CGPoint(from: bridge) } } ``` ### Registering with the JS context Every engine type that should be accessible as a global JS constructor or namespace must be registered in `Engine/InjectTypes.swift`: ```swift struct TypeBridgesInstaller: JSContextInstallable { func install(in context: JSContext) throws { let typeBridges: [String: AnyClass] = [ "HSFoo": HSFoo.self, // ... ] typeBridges.forEach { key, value in context.setObject(value, forKeyedSubscript: key as NSString) } } } ``` Only add types here that make sense for users to construct or reference directly. Types that are only ever returned from module methods do NOT belong here. ### Reactive engine types (`@Observable`) If the type's value needs to drive SwiftUI re-renders (e.g. `HSColor`, `HSString`, `HSImage`), use `@Observable` instead of `ObservableObject`. Key constraint: `@Observable` cannot track `@objc` stored properties. Work around this with a private backing store: ```swift import Observation // required in files that don't import SwiftUI @Observable @objc class HSFoo: NSObject, HSFooAPI { @objc var typeName = "HSFoo" // @Observable tracks _value (not @objc). // The computed @objc var forwards to it; SwiftUI sees: value → _value. private var _value: String @objc var value: String { _value } // read-only from JS is fine; set() mutates init(value: String) { self._value = value super.init() } @objc func set(_ newValue: String) { _value = newValue // triggers @Observable tracking } } ``` Non-reactive properties (e.g. `HSColor.color: Color`) that are not `@objc` are tracked normally by `@Observable` with no workaround needed. ### Static factory methods Prefer factory methods over failable inits when construction can fail or requires complex resolution (loading a file, parsing a hex string, etc.): ```swift @objc protocol HSFooAPI: HSTypeAPI, JSExport { @objc static func fromPath(_ path: String) -> HSFoo? @objc static func named(_ name: String) -> HSFoo } ``` Return `nil` for failure rather than throwing; errors are logged with `AKError`. --- ## Category 2 — Module types (`Modules/hs.xxx/HSFoo.swift`) Module types are objects **returned by module methods** — users never construct them directly. They wrap module-specific OS resources (a running application, a display, a hotkey handle, a timer object, etc.). **When to create one:** a module method needs to return something the user can hold a reference to and call further methods on. If the result is a plain dictionary or primitive, don't create a type — just return the value directly. ### File location Same directory as the module that owns them: `Hammerspoon 2/Modules/hs.xxx/HSFoo.swift` ### Structural template ```swift import Foundation import JavaScriptCore // import whatever OS framework the type wraps /// User-facing docstring. The protocol is the documented public surface. /// Mention that users should not instantiate these directly. @objc protocol HSFooAPI: HSTypeAPI, JSExport { /// The unique identifier assigned to this object (UUID string). @objc var identifier: String { get } // Declare all user-callable properties and methods. // Apply the same @objc bridging rules as module API protocols. @objc var someProperty: String { get } @objc func doSomething() } // REQUIRED: hide the implementation class from generated docs @_documentation(visibility: private) // Add @MainActor only if the type accesses actor-isolated state (timers, UI, etc.) @objc class HSFoo: NSObject, HSFooAPI { @objc var typeName = "HSFoo" // REQUIRED — satisfies HSTypeAPI @objc let identifier = UUID().uuidString // if applicable // Internal state — not exposed to JS private let wrapped: NativeFooObject init(wrapped: NativeFooObject) { self.wrapped = wrapped super.init() } // For @MainActor types, use isolated deinit: isolated deinit { print("deinit of HSFoo") } // For non-@MainActor types, use plain deinit: // deinit { print("deinit of HSFoo") } @objc var someProperty: String { wrapped.name } @objc func doSomething() { wrapped.doThing() } } ``` ### `@_documentation(visibility: private)` is mandatory The class is always hidden. The protocol is the public-facing API that the docs generator reads. Never put documentation on the class — put it on the protocol. ### `@MainActor` usage Add `@MainActor` to the class when it: - Schedules or invalidates timers - Calls UIKit/AppKit/SwiftUI APIs - Holds or calls `JSValue` callbacks from OS delegate methods - Is a CLLocationManagerDelegate, AVAudioSession delegate, etc. Non-`@MainActor` examples: `HSApplication` (reads NSRunningApplication properties synchronously), `HSScreen`, `HSAudioDevice`. `@MainActor` examples: `HSTimer`, `HSTask`, `HSHotkey`, `HSLocationWatcher`. ### `isolated deinit` vs `deinit` - `@MainActor` class → `isolated deinit` (lets deinit safely access actor state) - Non-`@MainActor` class → plain `deinit` Always log in deinit (`AKDebug` or `print`). Clean up OS resources (invalidate timers, stop observers, close file handles) in deinit if the module hasn't already. ### `identifier` property Any type the user might hold multiple instances of should have: ```swift @objc let identifier = UUID().uuidString ``` This satisfies `HSTypeAPI`'s `typeName` requirement implicitly through the pattern and gives users a stable handle to correlate objects. ### Factory/conversion extensions on OS types When a module type wraps a specific OS class, put the conversion in an extension on the OS class in `Hammerspoon 2/Extensions/`: ```swift // Extensions/NSRunningApplication.swift extension NSRunningApplication { func asHSApplication() -> HSApplication? { return HSApplication(runningApplication: self) } } ``` This keeps construction logic out of the module and lets it be reused across files. Only create an Extensions file if the extension is genuinely shared; single-use conversions can live in the module file. --- ## Checklist for any new type ### Both categories - [ ] `@objc protocol HSFooAPI: HSTypeAPI, JSExport` — protocol inherits both - [ ] `@objc var typeName = "HSFoo"` on the class — satisfies `HSTypeAPI` - [ ] Protocol has docstrings with `- Example:` on every member - [ ] `super.init()` called at the end of every `init` - [ ] `@objc` on every property and method in the protocol - [ ] There are no methods with names that start with `new`, `alloc` or `copy` ### Engine types only - [ ] No `@_documentation(visibility: private)` on the class (engine types are public) - [ ] Added to `TypeBridgesInstaller` in `InjectTypes.swift` (if user-constructable) - [ ] `JSConvertible` extension on the native type (if wrapping a value type) - [ ] `JSValue` extension for ergonomic unboxing (e.g. `toCGPoint()`) - [ ] If reactive: `@Observable`, private `_value` backing store, `import Observation` ### Module types only - [ ] `@_documentation(visibility: private)` on the class — mandatory - [ ] Lives in `Modules/hs.xxx/HSFoo.swift` alongside its owning module - [ ] `@MainActor` if the type schedules timers, calls JS callbacks, or touches UI - [ ] `isolated deinit` if `@MainActor`; plain `deinit` otherwise - [ ] NOT added to `TypeBridgesInstaller` (module types are never directly constructable) - [ ] If the module tracks instances (for shutdown cleanup), add to `private var foos: [HSFoo] = []`