hammerspoon/extensions/streamdeck/libstreamdeck.m

392 lines
12 KiB
Objective-C

@import Cocoa;
@import LuaSkin;
#import "HSStreamDeckManager.h"
#import "HSStreamDeckDevice.h"
#import "streamdeck.h"
#define get_objectFromUserdata(objType, L, idx, tag) (objType*)*((void**)luaL_checkudata(L, idx, tag))
static HSStreamDeckManager *deckManager;
LSRefTable streamDeckRefTable = LUA_NOREF;
#pragma mark - Lua API
static int streamdeck_gc(lua_State *L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
LSGCCanary tmpLSUUID = deckManager.lsCanary;
[skin destroyGCCanary:&tmpLSUUID];
deckManager.lsCanary = tmpLSUUID;
[deckManager stopHIDManager];
[deckManager doGC];
return 0;
}
/// hs.streamdeck.init(fn)
/// Function
/// Initialises the Stream Deck driver and sets a discovery callback
///
/// Parameters:
/// * fn - A function that will be called when a Streaming Deck is connected or disconnected. It should take the following arguments:
/// * A boolean, true if a device was connected, false if a device was disconnected
/// * An hs.streamdeck object, being the device that was connected/disconnected
///
/// Returns:
/// * None
///
/// Notes:
/// * This function must be called before any other parts of this module are used
static int streamdeck_init(lua_State *L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
[skin checkArgs:LS_TFUNCTION, LS_TBREAK];
deckManager = [[HSStreamDeckManager alloc] init];
deckManager.discoveryCallbackRef = [skin luaRef:streamDeckRefTable atIndex:1];
deckManager.lsCanary = [skin createGCCanary];
[deckManager startHIDManager];
return 0;
}
/// hs.streamdeck.discoveryCallback(fn)
/// Function
/// Sets/clears a callback for reacting to device discovery events
///
/// Parameters:
/// * fn - A function that will be called when a Streaming Deck is connected or disconnected. It should take the following arguments:
/// * A boolean, true if a device was connected, false if a device was disconnected
/// * An hs.streamdeck object, being the device that was connected/disconnected
///
/// Returns:
/// * None
static int streamdeck_discoveryCallback(lua_State *L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
[skin checkArgs:LS_TFUNCTION, LS_TBREAK];
deckManager.discoveryCallbackRef = [skin luaUnref:streamDeckRefTable ref:deckManager.discoveryCallbackRef];
if (lua_type(skin.L, 1) == LUA_TFUNCTION) {
deckManager.discoveryCallbackRef = [skin luaRef:streamDeckRefTable atIndex:1];
}
return 0;
}
/// hs.streamdeck.numDevices()
/// Function
/// Gets the number of Stream Deck devices connected
///
/// Parameters:
/// * None
///
/// Returns:
/// * A number containing the number of Stream Deck devices attached to the system
static int streamdeck_numDevices(lua_State *L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
[skin checkArgs:LS_TBREAK];
lua_pushinteger(skin.L, deckManager.devices.count);
return 1;
}
/// hs.streamdeck.getDevice(num)
/// Function
/// Gets an hs.streamdeck object for the specified device
///
/// Parameters:
/// * num - A number that should be within the bounds of the number of connected devices
///
/// Returns:
/// * An hs.streamdeck object
static int streamdeck_getDevice(lua_State *L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
[skin checkArgs:LS_TNUMBER, LS_TBREAK];
[skin pushNSObject:deckManager.devices[lua_tointeger(skin.L, 1) - 1]];
return 1;
}
/// hs.streamdeck:buttonCallback(fn)
/// Method
/// Sets/clears the button callback function for a deck
///
/// Parameters:
/// * fn - A function to be called when a button is pressed/released on the stream deck. It should receive three arguments:
/// * The hs.streamdeck userdata object
/// * A number containing the button that was pressed/released
/// * A boolean indicating whether the button was pressed (true) or released (false)
///
/// Returns:
/// * The hs.streamdeck device
static int streamdeck_buttonCallback(lua_State *L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
[skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TFUNCTION | LS_TNIL, LS_TBREAK];
HSStreamDeckDevice *device = [skin luaObjectAtIndex:1 toClass:"HSStreamDeckDevice"];
device.buttonCallbackRef = [skin luaUnref:streamDeckRefTable ref:device.buttonCallbackRef];
if (lua_type(skin.L, 2) == LUA_TFUNCTION) {
device.buttonCallbackRef = [skin luaRef:streamDeckRefTable atIndex:2];
}
lua_pushvalue(skin.L, 1);
return 1;
}
/// hs.streamdeck:setBrightness(brightness)
/// Method
/// Sets the brightness of a deck
///
/// Parameters:
/// * brightness - A whole number between 0 and 100 indicating the percentage brightness level to set
///
/// Returns:
/// * The hs.streamdeck device
static int streamdeck_setBrightness(lua_State *L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
[skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TNUMBER, LS_TBREAK];
HSStreamDeckDevice *device = [skin luaObjectAtIndex:1 toClass:"HSStreamDeckDevice"];
[device setBrightness:(int)lua_tointeger(skin.L, 2)];
lua_pushvalue(skin.L, 1);
return 1;
}
/// hs.streamdeck:reset()
/// Method
/// Resets a deck
///
/// Parameters:
/// * None
///
/// Returns:
/// * The hs.streamdeck object
static int streamdeck_reset(lua_State *L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
[skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TBREAK];
HSStreamDeckDevice *device = [skin luaObjectAtIndex:1 toClass:"HSStreamDeckDevice"];
[device reset];
lua_pushvalue(skin.L, 1);
return 1;
}
/// hs.streamdeck:serialNumber()
/// Method
/// Gets the serial number of a deck
///
/// Parameters:
/// * None
///
/// Returns:
/// * A string containing the serial number of the deck
static int streamdeck_serialNumber(lua_State *L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
[skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TBREAK];
HSStreamDeckDevice *device = [skin luaObjectAtIndex:1 toClass:"HSStreamDeckDevice"];
[skin pushNSObject:device.serialNumber];
return 1;
}
/// hs.streamdeck:firmwareVersion()
/// Method
/// Gets the firmware version of a deck
///
/// Parameters:
/// * None
///
/// Returns:
/// * A string containing the firmware version of the deck
static int streamdeck_firmwareVersion(lua_State *L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
[skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TBREAK];
HSStreamDeckDevice *device = [skin luaObjectAtIndex:1 toClass:"HSStreamDeckDevice"];
[skin pushNSObject:[device firmwareVersion]];
return 1;
}
/// hs.streamdeck:buttonLayout()
/// Method
/// Gets the layout of buttons the device has
///
/// Parameters:
/// * None
///
/// Returns:
/// * The number of columns
/// * The number of rows
static int streamdeck_buttonLayout(lua_State *L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
[skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TBREAK];
HSStreamDeckDevice *device = [skin luaObjectAtIndex:1 toClass:"HSStreamDeckDevice"];
lua_pushinteger(skin.L, device.keyColumns);
lua_pushinteger(skin.L, device.keyRows);
return 2;
}
/// hs.streamdeck:setButtonImage(button, image)
/// Method
/// Sets the image of a button on the deck
///
/// Parameters:
/// * button - A number (from 1 to 15) describing which button to set the image for
/// * image - An hs.image object
///
/// Returns:
/// * The hs.streamdeck object
static int streamdeck_setButtonImage(lua_State *L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
[skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TNUMBER, LS_TUSERDATA, "hs.image", LS_TBREAK];
HSStreamDeckDevice *device = [skin luaObjectAtIndex:1 toClass:"HSStreamDeckDevice"];
[device setImage:[skin luaObjectAtIndex:3 toClass:"NSImage"] forButton:(int)lua_tointeger(skin.L, 2)];
lua_pushvalue(skin.L, 1);
return 1;
}
/// hs.streamdeck:setButtonColor(button, color)
/// Method
/// Sets a button on the deck to the specified color
///
/// Parameters:
/// * button - A number (from 1 to 15) describing which button to set the color on
/// * color - An hs.drawing.color object
///
/// Returns:
/// * The hs.streamdeck object
static int streamdeck_setButtonColor(lua_State *L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
[skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TNUMBER, LS_TTABLE, LS_TBREAK];
HSStreamDeckDevice *device = [skin luaObjectAtIndex:1 toClass:"HSStreamDeckDevice"];
[device setColor:[skin luaObjectAtIndex:3 toClass:"NSColor"] forButton:(int)lua_tointeger(skin.L, 2)];
lua_pushvalue(skin.L, 1);
return 1;
}
#pragma mark - Lua<->NSObject Conversion Functions
// These must not throw a lua error to ensure LuaSkin can safely be used from Objective-C
// delegates and blocks.
static int pushHSStreamDeckDevice(lua_State *L, id obj) {
HSStreamDeckDevice *value = obj;
value.selfRefCount++;
void** valuePtr = lua_newuserdata(L, sizeof(HSStreamDeckDevice *));
*valuePtr = (__bridge_retained void *)value;
luaL_getmetatable(L, USERDATA_TAG);
lua_setmetatable(L, -2);
return 1;
}
static id toHSStreamDeckDeviceFromLua(lua_State *L, int idx) {
LuaSkin *skin = [LuaSkin sharedWithState:L] ;
HSStreamDeckDevice *value ;
if (luaL_testudata(L, idx, USERDATA_TAG)) {
value = get_objectFromUserdata(__bridge HSStreamDeckDevice, L, idx, USERDATA_TAG) ;
} else {
[skin logError:[NSString stringWithFormat:@"expected %s object, found %s", USERDATA_TAG,
lua_typename(L, lua_type(L, idx))]] ;
}
return value ;
}
#pragma mark - Hammerspoon/Lua Infrastructure
static int streamdeck_object_tostring(lua_State* L) {
LuaSkin *skin = [LuaSkin sharedWithState:L] ;
HSStreamDeckDevice *obj = [skin luaObjectAtIndex:1 toClass:"HSStreamDeckDevice"] ;
NSString *title = [NSString stringWithFormat:@"%@, serial: %@", obj.deckType, obj.serialNumber];
[skin pushNSObject:[NSString stringWithFormat:@"%s: %@ (%p)", USERDATA_TAG, title, lua_topointer(L, 1)]] ;
return 1 ;
}
static int streamdeck_object_eq(lua_State* L) {
// can't get here if at least one of us isn't a userdata type, and we only care if both types are ours,
// so use luaL_testudata before the macro causes a lua error
if (luaL_testudata(L, 1, USERDATA_TAG) && luaL_testudata(L, 2, USERDATA_TAG)) {
LuaSkin *skin = [LuaSkin sharedWithState:L] ;
HSStreamDeckDevice *obj1 = [skin luaObjectAtIndex:1 toClass:"HSStreamDeckDevice"] ;
HSStreamDeckDevice *obj2 = [skin luaObjectAtIndex:2 toClass:"HSStreamDeckDevice"] ;
lua_pushboolean(L, [obj1 isEqualTo:obj2]) ;
} else {
lua_pushboolean(L, NO) ;
}
return 1 ;
}
static int streamdeck_object_gc(lua_State* L) {
LuaSkin *skin = [LuaSkin sharedWithState:L] ;
HSStreamDeckDevice *theDevice = get_objectFromUserdata(__bridge_transfer HSStreamDeckDevice, L, 1, USERDATA_TAG) ;
if (theDevice) {
theDevice.selfRefCount-- ;
if (theDevice.selfRefCount == 0) {
theDevice.buttonCallbackRef = [skin luaUnref:streamDeckRefTable ref:theDevice.buttonCallbackRef] ;
theDevice = nil ;
}
}
// Remove the Metatable so future use of the variable in Lua won't think its valid
lua_pushnil(L) ;
lua_setmetatable(L, 1) ;
return 0 ;
}
#pragma mark - Lua object function definitions
static const luaL_Reg userdata_metaLib[] = {
{"serialNumber", streamdeck_serialNumber},
{"firmwareVersion", streamdeck_firmwareVersion},
{"buttonLayout", streamdeck_buttonLayout},
{"buttonCallback", streamdeck_buttonCallback},
{"setButtonImage", streamdeck_setButtonImage},
{"setButtonColor", streamdeck_setButtonColor},
{"setBrightness", streamdeck_setBrightness},
{"reset", streamdeck_reset},
{"__tostring", streamdeck_object_tostring},
{"__eq", streamdeck_object_eq},
{"__gc", streamdeck_object_gc},
{NULL, NULL}
};
#pragma mark - Lua Library function definitions
static const luaL_Reg streamdecklib[] = {
{"init", streamdeck_init},
{"discoveryCallback", streamdeck_discoveryCallback},
{"numDevices", streamdeck_numDevices},
{"getDevice", streamdeck_getDevice},
{NULL, NULL}
};
static const luaL_Reg metalib[] = {
{"__gc", streamdeck_gc},
{NULL, NULL}
};
#pragma mark - Lua initialiser
int luaopen_hs_libstreamdeck(lua_State* L) {
LuaSkin *skin = [LuaSkin sharedWithState:L];
streamDeckRefTable = [skin registerLibrary:USERDATA_TAG functions:streamdecklib metaFunctions:metalib];
[skin registerObject:USERDATA_TAG objectFunctions:userdata_metaLib];
[skin registerPushNSHelper:pushHSStreamDeckDevice forClass:"HSStreamDeckDevice"];
[skin registerLuaObjectHelper:toHSStreamDeckDeviceFromLua forClass:"HSStreamDeckDevice" withTableMapping:USERDATA_TAG];
return 1;
}