بنقرة واحدة
hs2type
Guide for creating custom types in Hammerspoon 2 — both shared engine types and module-specific types
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Guide for creating custom types in Hammerspoon 2 — both shared engine types and module-specific types
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
| name | hs2type |
| description | Guide for creating custom types in Hammerspoon 2 — both shared engine types and module-specific types |
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.
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.
Hammerspoon 2/Engine/Types/HSFoo.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 typesWhen the engine type wraps a standard Swift/CoreGraphics value type (CGPoint, CGSize,
CGRect, SwiftUI.Color, etc.), implement JSConvertible on the native type:
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:
extension JSValue {
func toCGPoint() -> CGPoint? {
guard let bridge = toObjectOf(HSPoint.self) as? HSPoint else { return nil }
return CGPoint(from: bridge)
}
}
Every engine type that should be accessible as a global JS constructor or namespace
must be registered in Engine/InjectTypes.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.
@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:
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.
Prefer factory methods over failable inits when construction can fail or requires complex resolution (loading a file, parsing a hex string, etc.):
@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.
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.
Same directory as the module that owns them:
Hammerspoon 2/Modules/hs.xxx/HSFoo.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 mandatoryThe 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 usageAdd @MainActor to the class when it:
JSValue callbacks from OS delegate methodsNon-@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)@MainActor class → plain deinitAlways log in deinit (AKTrace or print). Clean up OS resources (invalidate
timers, stop observers, close file handles) in deinit if the module hasn't already.
identifier propertyAny type the user might hold multiple instances of should have:
@objc let identifier = UUID().uuidString
This satisfies HSTypeAPI's typeName requirement implicitly through the pattern
and gives users a stable handle to correlate objects.
When a module type wraps a specific OS class, put the conversion in an extension
on the OS class in Hammerspoon 2/Extensions/:
// 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.
@objc protocol HSFooAPI: HSTypeAPI, JSExport — protocol inherits both@objc var typeName = "HSFoo" on the class — satisfies HSTypeAPI- Example: on every membersuper.init() called at the end of every init@objc on every property and method in the protocolnew, alloc or copy@_documentation(visibility: private) on the class (engine types are public)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())@Observable, private _value backing store, import Observation@_documentation(visibility: private) on the class — mandatoryModules/hs.xxx/HSFoo.swift alongside its owning module@MainActor if the type schedules timers, calls JS callbacks, or touches UIisolated deinit if @MainActor; plain deinit otherwiseTypeBridgesInstaller (module types are never directly constructable)private var foos: [HSFoo] = []