From 70ba85e4487b711cff8faae20d5de33c9b4a2117 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Wed, 20 Dec 2023 11:28:57 +0000 Subject: [PATCH 1/8] Prototype moving extension core functionality into a Swift framework --- Hammerspoon Tests/HSmath.m | 4 + Hammerspoon.xcodeproj/project.pbxproj | 688 +++++++++++++++++- .../xcschemes/Hammertime.xcscheme | 66 ++ Hammertime/Hammertime.docc/Hammertime.md | 13 + Hammertime/Hammertime.h | 19 + Hammertime/Math.swift | 69 ++ HammertimeTests/HammertimeTests.swift | 37 + extensions/math/libmath.m | 53 +- extensions/math/test_math.lua | 27 +- 9 files changed, 962 insertions(+), 14 deletions(-) create mode 100644 Hammerspoon.xcodeproj/xcshareddata/xcschemes/Hammertime.xcscheme create mode 100755 Hammertime/Hammertime.docc/Hammertime.md create mode 100644 Hammertime/Hammertime.h create mode 100644 Hammertime/Math.swift create mode 100644 HammertimeTests/HammertimeTests.swift diff --git a/Hammerspoon Tests/HSmath.m b/Hammerspoon Tests/HSmath.m index e2ca58125..dbe10c3dd 100644 --- a/Hammerspoon Tests/HSmath.m +++ b/Hammerspoon Tests/HSmath.m @@ -30,6 +30,10 @@ - (void)testRandomFloat { RUN_LUA_TEST() } +- (void)testRandomFloatFromRange { + RUN_LUA_TEST() +} + - (void)testRandomFromRange { RUN_LUA_TEST() } diff --git a/Hammerspoon.xcodeproj/project.pbxproj b/Hammerspoon.xcodeproj/project.pbxproj index c34c5243a..aec80b25f 100644 --- a/Hammerspoon.xcodeproj/project.pbxproj +++ b/Hammerspoon.xcodeproj/project.pbxproj @@ -55,7 +55,7 @@ 4CD203B21C6D77750089706D /* libsocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CD203A21C6D75950089706D /* libsocket.m */; }; 4CD64AA91C6F15F400EB6E39 /* HSsocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CD64AA81C6F15F400EB6E39 /* HSsocket.m */; }; 4CD64AAB1C6F168700EB6E39 /* test_socket.lua in Resources */ = {isa = PBXBuildFile; fileRef = 4CD64AAA1C6F168700EB6E39 /* test_socket.lua */; }; - 4CD96BD61C73ABD2009FF648 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; + 4CD96BD61C73ABD2009FF648 /* (null) in Frameworks */ = {isa = PBXBuildFile; }; 4CD96BDE1C73AC1C009FF648 /* libsocket_udp.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CD96BDD1C73AC1C009FF648 /* libsocket_udp.m */; }; 4CE6390F1C76C7F500BB7CD2 /* socket.h in Headers */ = {isa = PBXBuildFile; fileRef = 4CE6390E1C76C7F500BB7CD2 /* socket.h */; }; 4CE639101C76C7F500BB7CD2 /* socket.h in Headers */ = {isa = PBXBuildFile; fileRef = 4CE6390E1C76C7F500BB7CD2 /* socket.h */; }; @@ -106,6 +106,14 @@ 4F5560A2279F6A0300B91FD8 /* HSStreamDeckDeviceMk2.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F5560A0279F6A0300B91FD8 /* HSStreamDeckDeviceMk2.m */; }; 4F5B6E6B1E9E86BB003FC580 /* Hammerspoon.sdef in Resources */ = {isa = PBXBuildFile; fileRef = 4F5B6E6A1E9E86BB003FC580 /* Hammerspoon.sdef */; }; 4F5B6E6E1E9E87CE003FC580 /* HSAppleScript.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F5B6E6D1E9E87CE003FC580 /* HSAppleScript.m */; }; + 4F61CD8B2B32F58200407260 /* Hammertime.docc in Sources */ = {isa = PBXBuildFile; fileRef = 4F61CD8A2B32F58200407260 /* Hammertime.docc */; }; + 4F61CD912B32F58300407260 /* Hammertime.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F61CD872B32F58000407260 /* Hammertime.framework */; }; + 4F61CD982B32F58400407260 /* HammertimeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F61CD972B32F58400407260 /* HammertimeTests.swift */; }; + 4F61CD992B32F58400407260 /* Hammertime.h in Headers */ = {isa = PBXBuildFile; fileRef = 4F61CD892B32F58200407260 /* Hammertime.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4F61CD9C2B32F58400407260 /* Hammertime.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F61CD872B32F58000407260 /* Hammertime.framework */; }; + 4F61CD9D2B32F58400407260 /* Hammertime.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4F61CD872B32F58000407260 /* Hammertime.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 4F61CDA92B32F8A500407260 /* Math.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F61CDA82B32F8A500407260 /* Math.swift */; }; + 4F61CDAC2B32FE2500407260 /* Hammertime.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F61CD872B32F58000407260 /* Hammertime.framework */; }; 4F6653E6238C36F100DEE120 /* HSStreamDeckDeviceOriginal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4F6653E4238C36F100DEE120 /* HSStreamDeckDeviceOriginal.h */; }; 4F6653E7238C36F100DEE120 /* HSStreamDeckDeviceOriginal.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F6653E5238C36F100DEE120 /* HSStreamDeckDeviceOriginal.m */; }; 4F6653EA238C6B6600DEE120 /* HSStreamDeckDeviceMini.h in Headers */ = {isa = PBXBuildFile; fileRef = 4F6653E8238C6B6600DEE120 /* HSStreamDeckDeviceMini.h */; }; @@ -771,6 +779,41 @@ remoteGlobalIDString = 4F5134541C2F315C00EFD6D4; remoteInfo = chooser; }; + 4F61CD922B32F58300407260 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9445CA0319083251002568BB /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4F61CD862B32F58000407260; + remoteInfo = Hammertime; + }; + 4F61CD942B32F58400407260 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9445CA0319083251002568BB /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9445CA0A19083251002568BB; + remoteInfo = Hammerspoon; + }; + 4F61CD9A2B32F58400407260 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9445CA0319083251002568BB /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4F61CD862B32F58000407260; + remoteInfo = Hammertime; + }; + 4F61CDAA2B32FE1C00407260 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9445CA0319083251002568BB /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4F61CD862B32F58000407260; + remoteInfo = Hammertime; + }; + 4F61CDAE2B32FE2600407260 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9445CA0319083251002568BB /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4F61CD862B32F58000407260; + remoteInfo = Hammertime; + }; 4F6B3EA91B73D70100B2A4DE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 9445CA0319083251002568BB /* Project object */; @@ -1299,6 +1342,7 @@ dstSubfolderSpec = 10; files = ( 4FB852362735B02400462DD0 /* LuaSkin.framework in Embed Frameworks */, + 4F61CD9D2B32F58400407260 /* Hammertime.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -1728,6 +1772,12 @@ 4F5B6E6A1E9E86BB003FC580 /* Hammerspoon.sdef */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Hammerspoon.sdef; sourceTree = ""; }; 4F5B6E6C1E9E87CE003FC580 /* HSAppleScript.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HSAppleScript.h; sourceTree = ""; }; 4F5B6E6D1E9E87CE003FC580 /* HSAppleScript.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HSAppleScript.m; sourceTree = ""; }; + 4F61CD872B32F58000407260 /* Hammertime.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Hammertime.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4F61CD892B32F58200407260 /* Hammertime.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Hammertime.h; sourceTree = ""; }; + 4F61CD8A2B32F58200407260 /* Hammertime.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = Hammertime.docc; sourceTree = ""; }; + 4F61CD902B32F58200407260 /* HammertimeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HammertimeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4F61CD972B32F58400407260 /* HammertimeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HammertimeTests.swift; sourceTree = ""; }; + 4F61CDA82B32F8A500407260 /* Math.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Math.swift; sourceTree = ""; }; 4F64D8BD1B724C4B00FC4C65 /* alert.lua */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = alert.lua; path = extensions/alert/alert.lua; sourceTree = ""; }; 4F6653E4238C36F100DEE120 /* HSStreamDeckDeviceOriginal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = HSStreamDeckDeviceOriginal.h; path = extensions/streamdeck/HSStreamDeckDeviceOriginal.h; sourceTree = ""; }; 4F6653E5238C36F100DEE120 /* HSStreamDeckDeviceOriginal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = HSStreamDeckDeviceOriginal.m; path = extensions/streamdeck/HSStreamDeckDeviceOriginal.m; sourceTree = ""; }; @@ -2213,7 +2263,7 @@ buildActionMask = 2147483647; files = ( 4FB852502735B2D100462DD0 /* LuaSkin.framework in Frameworks */, - 4CD96BD61C73ABD2009FF648 /* BuildFile in Frameworks */, + 4CD96BD61C73ABD2009FF648 /* (null) in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2320,6 +2370,21 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4F61CD842B32F58000407260 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4F61CD8D2B32F58200407260 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4F61CD912B32F58300407260 /* Hammertime.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 4F6B3EA11B73D6D300B2A4DE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -2518,6 +2583,7 @@ buildActionMask = 2147483647; files = ( 4FB852672735B33A00462DD0 /* LuaSkin.framework in Frameworks */, + 4F61CDAC2B32FE2500407260 /* Hammertime.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2706,6 +2772,7 @@ buildActionMask = 2147483647; files = ( 4FB852382735B15200462DD0 /* LuaSkin.framework in Frameworks */, + 4F61CD9C2B32F58400407260 /* Hammertime.framework in Frameworks */, 394FD223913AE68B92126D37 /* libPods-Hammerspoon.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3225,6 +3292,24 @@ name = chooser; sourceTree = ""; }; + 4F61CD882B32F58200407260 /* Hammertime */ = { + isa = PBXGroup; + children = ( + 4F61CD892B32F58200407260 /* Hammertime.h */, + 4F61CD8A2B32F58200407260 /* Hammertime.docc */, + 4F61CDA82B32F8A500407260 /* Math.swift */, + ); + path = Hammertime; + sourceTree = ""; + }; + 4F61CD962B32F58400407260 /* HammertimeTests */ = { + isa = PBXGroup; + children = ( + 4F61CD972B32F58400407260 /* HammertimeTests.swift */, + ); + path = HammertimeTests; + sourceTree = ""; + }; 4F64D8BC1B724C3A00FC4C65 /* alert */ = { isa = PBXGroup; children = ( @@ -3990,6 +4075,8 @@ 4F766E561C48F789007B9D24 /* scripts */, D02F95251A00221C00E28BB2 /* Hammerspoon Tests */, 4F20B52A1C1F1C5B00F52437 /* Hammerspoon UI Tests */, + 4F61CD882B32F58200407260 /* Hammertime */, + 4F61CD962B32F58400407260 /* HammertimeTests */, 9445CA0D19083251002568BB /* Frameworks */, 9445CA0C19083251002568BB /* Products */, 0024EE4F07CEC6C274F32AB6 /* Pods */, @@ -4095,6 +4182,8 @@ 4FCA05CA27678B100089A5FC /* libmarkdown.dylib */, 22C9154327A108F000E650A2 /* librazer.dylib */, D6C32CCF27DDD47F001AA22E /* libspaces.dylib */, + 4F61CD872B32F58000407260 /* Hammertime.framework */, + 4F61CD902B32F58200407260 /* HammertimeTests.xctest */, ); name = Products; sourceTree = ""; @@ -4570,6 +4659,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4F61CD822B32F58000407260 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 4F61CD992B32F58400407260 /* Hammertime.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 4F6B3EA41B73D6D300B2A4DE /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -5477,6 +5574,43 @@ productReference = 4F51345E1C2F315C00EFD6D4 /* libchooser.dylib */; productType = "com.apple.product-type.library.dynamic"; }; + 4F61CD862B32F58000407260 /* Hammertime */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4F61CDA42B32F58500407260 /* Build configuration list for PBXNativeTarget "Hammertime" */; + buildPhases = ( + 4F61CD822B32F58000407260 /* Headers */, + 4F61CD832B32F58000407260 /* Sources */, + 4F61CD842B32F58000407260 /* Frameworks */, + 4F61CD852B32F58000407260 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Hammertime; + productName = Hammertime; + productReference = 4F61CD872B32F58000407260 /* Hammertime.framework */; + productType = "com.apple.product-type.framework"; + }; + 4F61CD8F2B32F58200407260 /* HammertimeTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4F61CDA52B32F58500407260 /* Build configuration list for PBXNativeTarget "HammertimeTests" */; + buildPhases = ( + 4F61CD8C2B32F58200407260 /* Sources */, + 4F61CD8D2B32F58200407260 /* Frameworks */, + 4F61CD8E2B32F58200407260 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4F61CD932B32F58300407260 /* PBXTargetDependency */, + 4F61CD952B32F58400407260 /* PBXTargetDependency */, + ); + name = HammertimeTests; + productName = HammertimeTests; + productReference = 4F61CD902B32F58200407260 /* HammertimeTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 4F6B3E9E1B73D6D300B2A4DE /* json */ = { isa = PBXNativeTarget; buildConfigurationList = 4F6B3EA51B73D6D300B2A4DE /* Build configuration list for PBXNativeTarget "json" */; @@ -5896,6 +6030,8 @@ buildRules = ( ); dependencies = ( + 4F61CDAB2B32FE1C00407260 /* PBXTargetDependency */, + 4F61CDAF2B32FE2600407260 /* PBXTargetDependency */, ); name = math; productName = alert; @@ -6393,6 +6529,7 @@ 4F70EEDE1B726D730025A7B3 /* PBXTargetDependency */, 4FBE2D181B72699100407788 /* PBXTargetDependency */, 4FBE2D021B72624A00407788 /* PBXTargetDependency */, + 4F61CD9B2B32F58400407260 /* PBXTargetDependency */, ); name = Hammerspoon; productName = Hammerspoon; @@ -6849,6 +6986,8 @@ isa = PBXProject; attributes = { CLASSPREFIX = HS; + DefaultBuildSystemTypeForWorkspace = Latest; + LastSwiftUpdateCheck = 1510; LastUpgradeCheck = 1420; ORGANIZATIONNAME = Hammerspoon; TargetAttributes = { @@ -6856,6 +6995,13 @@ CreatedOnToolsVersion = 7.2; TestTargetID = 9445CA0A19083251002568BB; }; + 4F61CD862B32F58000407260 = { + CreatedOnToolsVersion = 15.1; + }; + 4F61CD8F2B32F58200407260 = { + CreatedOnToolsVersion = 15.1; + TestTargetID = 9445CA0A19083251002568BB; + }; 4FD0AC081B74BACD00A82496 = { CreatedOnToolsVersion = 7.0; }; @@ -6982,6 +7128,8 @@ 4FD0AB8C1B74AE1900A82496 /* wifi */, 4FD0AB971B74AE2800A82496 /* wifiwatcher */, 4FD0ABB11B74AF4E00A82496 /* window */, + 4F61CD862B32F58000407260 /* Hammertime */, + 4F61CD8F2B32F58200407260 /* HammertimeTests */, ); }; /* End PBXProject section */ @@ -6995,6 +7143,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4F61CD852B32F58000407260 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4F61CD8E2B32F58200407260 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 9445CA0919083251002568BB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -7381,6 +7543,23 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4F61CD832B32F58000407260 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4F61CD8B2B32F58200407260 /* Hammertime.docc in Sources */, + 4F61CDA92B32F8A500407260 /* Math.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4F61CD8C2B32F58200407260 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4F61CD982B32F58400407260 /* HammertimeTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 4F6B3E9F1B73D6D300B2A4DE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -8145,6 +8324,31 @@ target = 4F5134541C2F315C00EFD6D4 /* chooser */; targetProxy = 4F5134611C2F31A000EFD6D4 /* PBXContainerItemProxy */; }; + 4F61CD932B32F58300407260 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4F61CD862B32F58000407260 /* Hammertime */; + targetProxy = 4F61CD922B32F58300407260 /* PBXContainerItemProxy */; + }; + 4F61CD952B32F58400407260 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 9445CA0A19083251002568BB /* Hammerspoon */; + targetProxy = 4F61CD942B32F58400407260 /* PBXContainerItemProxy */; + }; + 4F61CD9B2B32F58400407260 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4F61CD862B32F58000407260 /* Hammertime */; + targetProxy = 4F61CD9A2B32F58400407260 /* PBXContainerItemProxy */; + }; + 4F61CDAB2B32FE1C00407260 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4F61CD862B32F58000407260 /* Hammertime */; + targetProxy = 4F61CDAA2B32FE1C00407260 /* PBXContainerItemProxy */; + }; + 4F61CDAF2B32FE2600407260 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4F61CD862B32F58000407260 /* Hammertime */; + targetProxy = 4F61CDAE2B32FE2600407260 /* PBXContainerItemProxy */; + }; 4F6B3EAA1B73D70100B2A4DE /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 4F6B3E9E1B73D6D300B2A4DE /* json */; @@ -8743,6 +8947,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 4F7F3B7B272DE98800E05732 /* Hammerspoon-Profile.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", @@ -9701,6 +9906,463 @@ }; name = Release; }; + 4F61CD9E2B32F58500407260 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Hammerspoon. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.2; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.hammerspoon.Hammertime; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 4F61CD9F2B32F58500407260 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Hammerspoon. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.2; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.hammerspoon.Hammertime; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Profile; + }; + 4F61CDA02B32F58500407260 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Hammerspoon. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.2; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.hammerspoon.Hammertime; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 4F61CDA12B32F58500407260 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = VQCYSNZB89; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.2; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.hammerspoon.HammertimeTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hammerspoon.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Hammerspoon"; + }; + name = Debug; + }; + 4F61CDA22B32F58500407260 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = VQCYSNZB89; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.2; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.hammerspoon.HammertimeTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hammerspoon.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Hammerspoon"; + }; + name = Profile; + }; + 4F61CDA32B32F58500407260 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = VQCYSNZB89; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.2; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.hammerspoon.HammertimeTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hammerspoon.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Hammerspoon"; + }; + name = Release; + }; 4F6B3EA61B73D6D300B2A4DE /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 4F0C1C77272F3EE9002CA157 /* Extensions-Base.xcconfig */; @@ -10513,6 +11175,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 4F7F3B7A272DE83500E05732 /* Hammerspoon-Debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", @@ -10524,6 +11187,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 4F7F3B7C272DE99200E05732 /* Hammerspoon-Release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", @@ -11250,6 +11914,26 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; }; + 4F61CDA42B32F58500407260 /* Build configuration list for PBXNativeTarget "Hammertime" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4F61CD9E2B32F58500407260 /* Debug */, + 4F61CD9F2B32F58500407260 /* Profile */, + 4F61CDA02B32F58500407260 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + 4F61CDA52B32F58500407260 /* Build configuration list for PBXNativeTarget "HammertimeTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4F61CDA12B32F58500407260 /* Debug */, + 4F61CDA22B32F58500407260 /* Profile */, + 4F61CDA32B32F58500407260 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; 4F6B3EA51B73D6D300B2A4DE /* Build configuration list for PBXNativeTarget "json" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Hammerspoon.xcodeproj/xcshareddata/xcschemes/Hammertime.xcscheme b/Hammerspoon.xcodeproj/xcshareddata/xcschemes/Hammertime.xcscheme new file mode 100644 index 000000000..a7782a205 --- /dev/null +++ b/Hammerspoon.xcodeproj/xcshareddata/xcschemes/Hammertime.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Hammertime/Hammertime.docc/Hammertime.md b/Hammertime/Hammertime.docc/Hammertime.md new file mode 100755 index 000000000..c958e32fe --- /dev/null +++ b/Hammertime/Hammertime.docc/Hammertime.md @@ -0,0 +1,13 @@ +# ``Hammertime`` + +Summary + +## Overview + +Text + +## Topics + +### Group + +- ``Symbol`` \ No newline at end of file diff --git a/Hammertime/Hammertime.h b/Hammertime/Hammertime.h new file mode 100644 index 000000000..146aa1e58 --- /dev/null +++ b/Hammertime/Hammertime.h @@ -0,0 +1,19 @@ +// +// Hammertime.h +// Hammertime +// +// Created by Chris Jones on 20/12/2023. +// Copyright © 2023 Hammerspoon. All rights reserved. +// + +#import + +//! Project version number for Hammertime. +FOUNDATION_EXPORT double HammertimeVersionNumber; + +//! Project version string for Hammertime. +FOUNDATION_EXPORT const unsigned char HammertimeVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/Hammertime/Math.swift b/Hammertime/Math.swift new file mode 100644 index 000000000..bd36ac8d8 --- /dev/null +++ b/Hammertime/Math.swift @@ -0,0 +1,69 @@ +// +// Math.swift +// Hammertime +// +// Created by Chris Jones on 20/12/2023. +// Copyright © 2023 Hammerspoon. All rights reserved. +// + +import Foundation + +@objc +public class Math : NSObject { + @objc + public func validateDoubleRange(start: Double, end: Double) -> Bool { + return start <= end + } + + @objc + public func validateIntRange(start: Int, end: Int) -> Bool { + return start <= end + } + + /// Returns a random Double between 0 and 1 (inclusive) + /// - Returns: Double + @objc + public func randomDouble() -> Double { + return self.randomDoubleInRange(start: 0, end: 1) + } + + /// Returns a random Doubld within the supplied range (inclusive) + /// - Parameters: + /// - start: Lower bound of the range + /// - end: Upper bound of the range + /// - Returns: Double + @objc + public func randomDoubleInRange(start: Double, end: Double) -> Double { + if (!self.validateDoubleRange(start: start, end: end)) { + NSException.raise(.rangeException, format: "start must be <= end", arguments: getVaList([""])) + } + return Double.random(in: start...end) + } + + /// Returns a random Float between 0 and 1 (inclusive) + /// - Returns: Float + @objc + public func randomFloat() -> Float { + return self.randomFloatInRange(start: 0, end: 1) + } + + /// Returns a random Float within the supplied range (inclusive) + /// - Parameters: + /// - start: Lower bound of the range + /// - end: Upper bound of the range + /// - Returns: Float + @objc + public func randomFloatInRange(start: Float, end: Float) -> Float { + return Float.random(in: start...end) + } + + /// Returns a random Int within the supplied range (inclusive) + /// - Parameters: + /// - start: Lower bound of the range + /// - end: Upper bound of the range + /// - Returns: Int + @objc + public func randomIntInRange(start: Int, end: Int) -> Int { + return Int.random(in: start...end) + } +} diff --git a/HammertimeTests/HammertimeTests.swift b/HammertimeTests/HammertimeTests.swift new file mode 100644 index 000000000..9a7db980a --- /dev/null +++ b/HammertimeTests/HammertimeTests.swift @@ -0,0 +1,37 @@ +// +// HammertimeTests.swift +// HammertimeTests +// +// Created by Chris Jones on 20/12/2023. +// Copyright © 2023 Hammerspoon. All rights reserved. +// + +import XCTest +@testable import Hammertime + +final class HammertimeTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/extensions/math/libmath.m b/extensions/math/libmath.m index d1cc03aa9..4fd7f5052 100644 --- a/extensions/math/libmath.m +++ b/extensions/math/libmath.m @@ -1,5 +1,6 @@ @import Cocoa ; @import LuaSkin ; +@import Hammertime ; #import @@ -16,10 +17,42 @@ static int math_randomFloat(lua_State* L) { LuaSkin *skin = [LuaSkin sharedWithState:L]; [skin checkArgs:LS_TBREAK]; - uint32_t rand = arc4random(); - double val = ((double)rand / UINT32_MAX); + Math *math = [[Math alloc] init]; + lua_pushnumber(L, [math randomDouble]); - lua_pushnumber(L, val); + return 1; +} + +/// hs.math.randomFloatFromRange(start, end) -> number +/// Function +/// Returns a random floating point number in the supplied range +/// +/// Parameters: +/// * start - Lower bound of the range +/// * end - Upper bound of the range +/// +/// Returns: +/// * A random number +static int math_randomFloatFromRange(lua_State *L) { + LuaSkin *skin = [LuaSkin sharedWithState:L]; + [skin checkArgs:LS_TNUMBER, LS_TNUMBER, LS_TBREAK]; + + Math *math = [[Math alloc] init]; +// double start = lua_tonumber(L, 1); +// double end = lua_tonumber(L, 2); + +// if (![math validateDoubleRangeWithStart:start end:end]) { +// [skin logError:@"hs.math.randomFloatFromRange: start must be <= end"]; +// lua_pushnil(L); +// return 1; +// } + + @try { + lua_pushnumber(L, [math randomDoubleInRangeWithStart:lua_tonumber(L, 1) end:lua_tonumber(L, 2)]); + } @catch (NSException *e){ + [skin logError:e.reason]; + lua_pushnil(L); + } return 1; } @@ -28,8 +61,8 @@ static int math_randomFloat(lua_State* L) { /// Returns a random integer between the start and end parameters /// /// Parameters: -/// * start - A number to start the range, must be greater than or equal to zero -/// * end - A number to end the range, must be greater than zero and greater than `start` +/// * start - Lower bound of the range +/// * end - Upper bound of the range /// /// Returns: /// * A randomly chosen integer between `start` and `end` @@ -37,24 +70,24 @@ static int math_randomFromRange(lua_State* L) { LuaSkin *skin = [LuaSkin sharedWithState:L] ; [skin checkArgs:LS_TNUMBER, LS_TNUMBER, LS_TBREAK] ; + Math *math = [[Math alloc] init]; int start = (int)lua_tointeger(L, 1); int end = (int)lua_tointeger(L, 2); - if (start < 0 || end <= 0 || end <= start) { - [skin logError:[NSString stringWithFormat:@"Please check the docs for hs.math.randomForRange() - your range is not acceptable (%d -> %d)", start, end]]; + if (![math validateIntRangeWithStart:start end:end]) { + [skin logError:@"hs.math.randomFromRange: start must be <= end"]; lua_pushnil(L); return 1; } - int result = arc4random_uniform(end - start + 1) + start; - - lua_pushinteger(L, result); + lua_pushinteger(L, [math randomIntInRangeWithStart:lua_tointeger(L, 1) end:lua_tointeger(L, 2)]); return 1; } // Functions for returned object when module loads static const luaL_Reg mathLib[] = { {"randomFloat", math_randomFloat}, + {"randomFloatFromRange", math_randomFloatFromRange}, {"randomFromRange", math_randomFromRange}, {NULL, NULL} diff --git a/extensions/math/test_math.lua b/extensions/math/test_math.lua index 3da034b84..75725cb39 100644 --- a/extensions/math/test_math.lua +++ b/extensions/math/test_math.lua @@ -9,16 +9,39 @@ function testRandomFloat() return success() end +function testRandomFloatFromRange() + local i = 10 + while i > 0 do + local rand = hs.math.randomFloatFromRange(0, 100) + assertTrue(rand >= 0) + assertTrue(rand <= 100) + i = i - 1 + end + + local rand1 = hs.math.randomFloatFromRange(-1, 100) + assertTrue(rand1 >= -1) + assertTrue(rand1 <= 100) + + local rand2 = hs.math.randomFloatFromRange(1, 1) + assertTrue(rand2 == 1) + + local rand3 = hs.math.randomFloatFromRange(100, 1) + assertIsNil(rand3) + + return success() +end + function testRandomFromRange() local rand1 = hs.math.randomFromRange(0, 100) assertTrue(rand1 >= 0) assertTrue(rand1 <= 100) local rand2 = hs.math.randomFromRange(-1, 100) - assertIsNil(rand2) + assertTrue(rand2 >= -1) + assertTrue(rand2 <= 100) local rand3 = hs.math.randomFromRange(1, 1) - assertIsNil(rand3) + assertTrue(rand3 == 1) local rand4 = hs.math.randomFromRange(1, -1) assertIsNil(rand4) From d34d2c084a74e43d27a15961d3c6b5c8d319bbdb Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Wed, 20 Dec 2023 12:50:31 +0000 Subject: [PATCH 2/8] Add hs.base64 to the Hammertime prototype --- Hammerspoon.xcodeproj/project.pbxproj | 19 ++++++ Hammertime/Base64.swift | 53 +++++++++++++++ extensions/base64/base64.lua | 39 ------------ extensions/base64/libbase64.m | 92 +++++++++++++++------------ extensions/base64/test_base64.lua | 5 ++ 5 files changed, 129 insertions(+), 79 deletions(-) create mode 100644 Hammertime/Base64.swift diff --git a/Hammerspoon.xcodeproj/project.pbxproj b/Hammerspoon.xcodeproj/project.pbxproj index aec80b25f..c5b5c6ac4 100644 --- a/Hammerspoon.xcodeproj/project.pbxproj +++ b/Hammerspoon.xcodeproj/project.pbxproj @@ -114,6 +114,8 @@ 4F61CD9D2B32F58400407260 /* Hammertime.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4F61CD872B32F58000407260 /* Hammertime.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4F61CDA92B32F8A500407260 /* Math.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F61CDA82B32F8A500407260 /* Math.swift */; }; 4F61CDAC2B32FE2500407260 /* Hammertime.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F61CD872B32F58000407260 /* Hammertime.framework */; }; + 4F61CDB22B330C2200407260 /* Base64.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F61CDB12B330C2200407260 /* Base64.swift */; }; + 4F61CDB32B33102400407260 /* Hammertime.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F61CD872B32F58000407260 /* Hammertime.framework */; }; 4F6653E6238C36F100DEE120 /* HSStreamDeckDeviceOriginal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4F6653E4238C36F100DEE120 /* HSStreamDeckDeviceOriginal.h */; }; 4F6653E7238C36F100DEE120 /* HSStreamDeckDeviceOriginal.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F6653E5238C36F100DEE120 /* HSStreamDeckDeviceOriginal.m */; }; 4F6653EA238C6B6600DEE120 /* HSStreamDeckDeviceMini.h in Headers */ = {isa = PBXBuildFile; fileRef = 4F6653E8238C6B6600DEE120 /* HSStreamDeckDeviceMini.h */; }; @@ -814,6 +816,13 @@ remoteGlobalIDString = 4F61CD862B32F58000407260; remoteInfo = Hammertime; }; + 4F61CDB52B33102500407260 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9445CA0319083251002568BB /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4F61CD862B32F58000407260; + remoteInfo = Hammertime; + }; 4F6B3EA91B73D70100B2A4DE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 9445CA0319083251002568BB /* Project object */; @@ -1778,6 +1787,7 @@ 4F61CD902B32F58200407260 /* HammertimeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HammertimeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 4F61CD972B32F58400407260 /* HammertimeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HammertimeTests.swift; sourceTree = ""; }; 4F61CDA82B32F8A500407260 /* Math.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Math.swift; sourceTree = ""; }; + 4F61CDB12B330C2200407260 /* Base64.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Base64.swift; sourceTree = ""; }; 4F64D8BD1B724C4B00FC4C65 /* alert.lua */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = alert.lua; path = extensions/alert/alert.lua; sourceTree = ""; }; 4F6653E4238C36F100DEE120 /* HSStreamDeckDeviceOriginal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = HSStreamDeckDeviceOriginal.h; path = extensions/streamdeck/HSStreamDeckDeviceOriginal.h; sourceTree = ""; }; 4F6653E5238C36F100DEE120 /* HSStreamDeckDeviceOriginal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = HSStreamDeckDeviceOriginal.m; path = extensions/streamdeck/HSStreamDeckDeviceOriginal.m; sourceTree = ""; }; @@ -2494,6 +2504,7 @@ buildActionMask = 2147483647; files = ( 4FB8528B2735B3E900462DD0 /* LuaSkin.framework in Frameworks */, + 4F61CDB32B33102400407260 /* Hammertime.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3298,6 +3309,7 @@ 4F61CD892B32F58200407260 /* Hammertime.h */, 4F61CD8A2B32F58200407260 /* Hammertime.docc */, 4F61CDA82B32F8A500407260 /* Math.swift */, + 4F61CDB12B330C2200407260 /* Base64.swift */, ); path = Hammertime; sourceTree = ""; @@ -5843,6 +5855,7 @@ buildRules = ( ); dependencies = ( + 4F61CDB62B33102500407260 /* PBXTargetDependency */, ); name = base64; productName = alert; @@ -7548,6 +7561,7 @@ buildActionMask = 2147483647; files = ( 4F61CD8B2B32F58200407260 /* Hammertime.docc in Sources */, + 4F61CDB22B330C2200407260 /* Base64.swift in Sources */, 4F61CDA92B32F8A500407260 /* Math.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -8349,6 +8363,11 @@ target = 4F61CD862B32F58000407260 /* Hammertime */; targetProxy = 4F61CDAE2B32FE2600407260 /* PBXContainerItemProxy */; }; + 4F61CDB62B33102500407260 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4F61CD862B32F58000407260 /* Hammertime */; + targetProxy = 4F61CDB52B33102500407260 /* PBXContainerItemProxy */; + }; 4F6B3EAA1B73D70100B2A4DE /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 4F6B3E9E1B73D6D300B2A4DE /* json */; diff --git a/Hammertime/Base64.swift b/Hammertime/Base64.swift new file mode 100644 index 000000000..4b8d29305 --- /dev/null +++ b/Hammertime/Base64.swift @@ -0,0 +1,53 @@ +// +// Base64.swift +// Hammertime +// +// Created by Chris Jones on 20/12/2023. +// Copyright © 2023 Hammerspoon. All rights reserved. +// + +import Foundation + +extension String { + func splitByLength(every length:Int) -> [Substring] { + guard length > 0 && length < count else { return [suffix(from:startIndex)] } + return (0 ... (count - 1) / length).map { dropFirst($0 * length).prefix(length) } + } +} + +@objc +public class Base64 : NSObject { + /// Encode bytes to a Base64 string + /// - Parameter data: Some input bytes as a Data + /// - Returns: String + @objc + public func encode(data: Data) -> String { + return data.base64EncodedString() + } + + /// Encode bytes to a Base64 string, split into lines of specified width + /// - Parameters: + /// - data: Some input bytes as a Data + /// - width: How wide the lines should be + /// - Returns: String + @objc + public func encode(data: Data, width: Int) -> String { + let string = self.encode(data: data) + let lines = string.splitByLength(every: width) + return lines.joined(separator: "\n") + } + + /// Decode a Base64 string to bytes + /// - Parameter input: A Base64 encoded string + /// - Returns: Output bytes as a Data + @objc + public func decode(input: String) -> Data { + if let encoded = input.data(using: .utf8) { + if let data = Data(base64Encoded: encoded, options: .ignoreUnknownCharacters) { + return data + } + } + NSException.raise(.invalidArgumentException, format: "Unable to decode input", arguments: getVaList([""])) + return Data() // Never hit + } +} diff --git a/extensions/base64/base64.lua b/extensions/base64/base64.lua index f9ba60897..9585a99fd 100644 --- a/extensions/base64/base64.lua +++ b/extensions/base64/base64.lua @@ -11,45 +11,6 @@ local module = require("hs.libbase64") -- Public interface ------------------------------------------------------ ---- hs.base64.encode(val[,width]) -> str ---- Function ---- Encodes a given string to base64 ---- ---- Parameters: ---- * val - A string to encode as base64 ---- * width - Optional line width to split the string into (usually 64 or 76) ---- ---- Returns: ---- * A string containing the base64 representation of the input string -module.encode = function(data, width) - local _data = module._encode(data) - if width then - local _hold, i, j = _data, 1, width - _data = "" - repeat - _data = _data.._hold:sub(i,j).."\n" - i = i + width - j = j + width - until i > #_hold - return _data:sub(1,#_data - 1) - else - return _data - end -end - ---- hs.base64.decode(str) -> val ---- Function ---- Decodes a given base64 string ---- ---- Parameters: ---- * str - A base64 encoded string ---- ---- Returns: ---- * A string containing the decoded data -module.decode = function(data) - return module._decode((data:gsub("[\r\n]+",""))) -end - -- Return Module Object -------------------------------------------------- return module diff --git a/extensions/base64/libbase64.m b/extensions/base64/libbase64.m index 6486d18bd..8e6204af4 100644 --- a/extensions/base64/libbase64.m +++ b/extensions/base64/libbase64.m @@ -1,56 +1,68 @@ -#import -#import - -// Source: https://gist.github.com/shpakovski/1902994 - -// NSString *TransformStringWithFunction(NSString *string, SecTransformRef (*function)(CFTypeRef, CFErrorRef *)) { -// NSData *inputData = [string dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO]; -// SecTransformRef transformRef = function(kSecBase64Encoding, NULL); -// SecTransformSetAttribute(transformRef, kSecTransformInputAttributeName, (CFTypeRef)inputData, NULL); -// CFDataRef outputDataRef = SecTransformExecute(transformRef, NULL); -// CFRelease(transformRef); -// return [[[NSString alloc] initWithData:(NSData *)outputDataRef encoding:NSUTF8StringEncoding] autorelease]; -// } - -NSData *TransformDataWithFunction(NSData *inputData, SecTransformRef (*function)(CFTypeRef, CFErrorRef *)) { - SecTransformRef transformRef = function(kSecBase64Encoding, NULL); - SecTransformSetAttribute(transformRef, kSecTransformInputAttributeName, (__bridge_retained CFTypeRef)inputData, NULL); - CFDataRef outputDataRef = SecTransformExecute(transformRef, NULL); - CFRelease(transformRef); - return [[NSData alloc] initWithData:(__bridge_transfer NSData *)outputDataRef]; -} +@import Cocoa; +@import LuaSkin; +@import Hammertime; -// hs.base64.encode(val) -> str -// Function -// Returns the base64 encoding of the string provided. +/// hs.base64.encode(val[,width]) -> str +/// Function +/// Encodes a given string to base64 +/// +/// Parameters: +/// * val - A string to encode as base64 +/// * width - Optional line width to split the string into (usually 64 or 76) +/// +/// Returns: +/// * A string containing the base64 representation of the input string static int base64_encode(lua_State* L) { - [[LuaSkin sharedWithState:L] checkArgs:LS_TNUMBER | LS_TSTRING, LS_TBREAK] ; + LuaSkin *skin = [LuaSkin sharedWithState:L]; + [skin checkArgs:LS_TNUMBER | LS_TSTRING, LS_TNUMBER|LS_TOPTIONAL, LS_TBREAK]; + Base64 *b64 = [[Base64 alloc] init]; + NSUInteger sz ; - const char* data = luaL_tolstring(L, 1, &sz) ; - NSData* decodedStr = [[NSData alloc] initWithBytes:data length:sz] ; + const char *data = luaL_tolstring(L, 1, &sz) ; + NSData *input = [[NSData alloc] initWithBytes:data length:sz] ; + + NSString *output = nil; + if (lua_type(L, 2) == LUA_TNUMBER) { + output = [b64 encodeWithData:input width:lua_tointeger(L, 2)]; + } else { + output = [b64 encodeWithData:input]; + } + [skin pushNSObject:output]; - NSData* encodedStr = TransformDataWithFunction(decodedStr, SecEncodeTransformCreate); - lua_pushlstring(L, [encodedStr bytes], [encodedStr length]) ; return 1; } -// hs.base64.decode(str) -> val -// Function -// Returns a Lua string representing the given base64 string. +/// hs.base64.decode(str) -> val +/// Function +/// Decodes a given base64 string +/// +/// Parameters: +/// * str - A base64 encoded string +/// +/// Returns: +/// * A string containing the decoded data, or nil if it couldn't be decoded static int base64_decode(lua_State* L) { - [[LuaSkin sharedWithState:L] checkArgs:LS_TNUMBER | LS_TSTRING, LS_TBREAK] ; - NSUInteger sz ; - const char* data = luaL_tolstring(L, 1, &sz) ; - NSData* encodedStr = [[NSData alloc] initWithBytes:data length:sz] ; + LuaSkin *skin = [LuaSkin sharedWithState:L]; + [skin checkArgs:LS_TNUMBER | LS_TSTRING, LS_TBREAK]; + Base64 *b64 = [[Base64 alloc] init]; + + const char *data = lua_tostring(L, 1); + NSString *input = [NSString stringWithUTF8String:data]; + + @try { + NSData *output = [b64 decodeWithInput:input]; + [skin pushNSObject:output]; + } @catch (NSException *e) { + [skin logError:@"Unable to decode input"]; + lua_pushnil(L); + } - NSData* decodedStr = TransformDataWithFunction(encodedStr, SecDecodeTransformCreate); - lua_pushlstring(L, [decodedStr bytes], [decodedStr length]) ; return 1; } static const luaL_Reg base64_lib[] = { - {"_encode", base64_encode}, - {"_decode", base64_decode}, + {"encode", base64_encode}, + {"decode", base64_decode}, {NULL, NULL} }; diff --git a/extensions/base64/test_base64.lua b/extensions/base64/test_base64.lua index 03de641d8..b70b4b3b7 100644 --- a/extensions/base64/test_base64.lua +++ b/extensions/base64/test_base64.lua @@ -4,6 +4,11 @@ function testEncode() local original = "encoding test" assertIsEqual("ZW5jb2RpbmcgdGVzdA==", hs.base64.encode(original)) assertIsEqual("ZW5j\nb2Rp\nbmcg\ndGVz\ndA==", hs.base64.encode(original, 4)) + assertIsEqual("MQ==", hs.base64.encode(1)) + + -- Check that we still get back a valid encoded string if our width argument is nonsense + assertIsEqual("MQ==", hs.base64.encode(1, 100)) + assertIsEqual("MQ==", hs.base64.encode(1, -1)) return success() end From f1122ca98c09f18f2213ff1b865ce7dab0c1bf4c Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Thu, 21 Dec 2023 01:24:58 +0000 Subject: [PATCH 3/8] Add hs.camera to the Hammertime prototype --- Hammerspoon.xcodeproj/project.pbxproj | 23 + Hammertime/Camera.swift | 175 ++++++++ Hammertime/Globals.swift | 11 + extensions/camera/libcamera.m | 617 +++++++++----------------- 4 files changed, 417 insertions(+), 409 deletions(-) create mode 100644 Hammertime/Camera.swift create mode 100644 Hammertime/Globals.swift diff --git a/Hammerspoon.xcodeproj/project.pbxproj b/Hammerspoon.xcodeproj/project.pbxproj index c5b5c6ac4..24ff20288 100644 --- a/Hammerspoon.xcodeproj/project.pbxproj +++ b/Hammerspoon.xcodeproj/project.pbxproj @@ -116,6 +116,9 @@ 4F61CDAC2B32FE2500407260 /* Hammertime.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F61CD872B32F58000407260 /* Hammertime.framework */; }; 4F61CDB22B330C2200407260 /* Base64.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F61CDB12B330C2200407260 /* Base64.swift */; }; 4F61CDB32B33102400407260 /* Hammertime.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F61CD872B32F58000407260 /* Hammertime.framework */; }; + 4F61CDB92B3332C100407260 /* Camera.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F61CDB82B3332C100407260 /* Camera.swift */; }; + 4F61CDBD2B334D7900407260 /* Hammertime.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F61CD872B32F58000407260 /* Hammertime.framework */; }; + 4F61CDC32B33B8AE00407260 /* Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F61CDC22B33B8AE00407260 /* Globals.swift */; }; 4F6653E6238C36F100DEE120 /* HSStreamDeckDeviceOriginal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4F6653E4238C36F100DEE120 /* HSStreamDeckDeviceOriginal.h */; }; 4F6653E7238C36F100DEE120 /* HSStreamDeckDeviceOriginal.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F6653E5238C36F100DEE120 /* HSStreamDeckDeviceOriginal.m */; }; 4F6653EA238C6B6600DEE120 /* HSStreamDeckDeviceMini.h in Headers */ = {isa = PBXBuildFile; fileRef = 4F6653E8238C6B6600DEE120 /* HSStreamDeckDeviceMini.h */; }; @@ -823,6 +826,13 @@ remoteGlobalIDString = 4F61CD862B32F58000407260; remoteInfo = Hammertime; }; + 4F61CDBF2B334D7A00407260 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9445CA0319083251002568BB /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4F61CD862B32F58000407260; + remoteInfo = Hammertime; + }; 4F6B3EA91B73D70100B2A4DE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 9445CA0319083251002568BB /* Project object */; @@ -1788,6 +1798,8 @@ 4F61CD972B32F58400407260 /* HammertimeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HammertimeTests.swift; sourceTree = ""; }; 4F61CDA82B32F8A500407260 /* Math.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Math.swift; sourceTree = ""; }; 4F61CDB12B330C2200407260 /* Base64.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Base64.swift; sourceTree = ""; }; + 4F61CDB82B3332C100407260 /* Camera.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Camera.swift; sourceTree = ""; }; + 4F61CDC22B33B8AE00407260 /* Globals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Globals.swift; sourceTree = ""; }; 4F64D8BD1B724C4B00FC4C65 /* alert.lua */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = alert.lua; path = extensions/alert/alert.lua; sourceTree = ""; }; 4F6653E4238C36F100DEE120 /* HSStreamDeckDeviceOriginal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = HSStreamDeckDeviceOriginal.h; path = extensions/streamdeck/HSStreamDeckDeviceOriginal.h; sourceTree = ""; }; 4F6653E5238C36F100DEE120 /* HSStreamDeckDeviceOriginal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = HSStreamDeckDeviceOriginal.m; path = extensions/streamdeck/HSStreamDeckDeviceOriginal.m; sourceTree = ""; }; @@ -2765,6 +2777,7 @@ buildActionMask = 2147483647; files = ( 4FF0611E274AA7A200FB15D6 /* LuaSkin.framework in Frameworks */, + 4F61CDBD2B334D7900407260 /* Hammertime.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3310,6 +3323,8 @@ 4F61CD8A2B32F58200407260 /* Hammertime.docc */, 4F61CDA82B32F8A500407260 /* Math.swift */, 4F61CDB12B330C2200407260 /* Base64.swift */, + 4F61CDB82B3332C100407260 /* Camera.swift */, + 4F61CDC22B33B8AE00407260 /* Globals.swift */, ); path = Hammertime; sourceTree = ""; @@ -6403,6 +6418,7 @@ buildRules = ( ); dependencies = ( + 4F61CDC02B334D7A00407260 /* PBXTargetDependency */, ); name = camera; productName = alert; @@ -7560,8 +7576,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4F61CDB92B3332C100407260 /* Camera.swift in Sources */, 4F61CD8B2B32F58200407260 /* Hammertime.docc in Sources */, 4F61CDB22B330C2200407260 /* Base64.swift in Sources */, + 4F61CDC32B33B8AE00407260 /* Globals.swift in Sources */, 4F61CDA92B32F8A500407260 /* Math.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -8368,6 +8386,11 @@ target = 4F61CD862B32F58000407260 /* Hammertime */; targetProxy = 4F61CDB52B33102500407260 /* PBXContainerItemProxy */; }; + 4F61CDC02B334D7A00407260 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4F61CD862B32F58000407260 /* Hammertime */; + targetProxy = 4F61CDBF2B334D7A00407260 /* PBXContainerItemProxy */; + }; 4F6B3EAA1B73D70100B2A4DE /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 4F6B3E9E1B73D6D300B2A4DE /* json */; diff --git a/Hammertime/Camera.swift b/Hammertime/Camera.swift new file mode 100644 index 000000000..d69bc0c85 --- /dev/null +++ b/Hammertime/Camera.swift @@ -0,0 +1,175 @@ +// +// Camera.swift +// Hammertime +// +// Created by Chris Jones on 20/12/2023. +// Copyright © 2023 Hammerspoon. All rights reserved. +// + +import Foundation +import AVFoundation +import IOKit.audio + +let allCameraTypes:[AVCaptureDevice.DeviceType] = [ + .external, + .builtInWideAngleCamera, + .continuityCamera, + .deskViewCamera +] + +public typealias CameraManagerDiscoveryCallback = @convention(block) (Camera?, String) -> Void + +@objc public class CameraManager : NSObject { + @objc var discoverySession: AVCaptureDevice.DiscoverySession + + // We cache the cameras because we're storing extra information in their class and would lose that if we just used DiscoverySession.devices + var cache:[String: Camera] = [:] + + var cameraObserver: NSKeyValueObservation? = nil + @objc public var observerCallback: CameraManagerDiscoveryCallback? + + @objc public override init() { + discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: allCameraTypes, mediaType: .video, position: .unspecified) + } + + /// Get all cameras known to the OS + /// - Returns: array of AVCaptureDevice + @objc public func getCameras() -> [String:Camera] { + updateCache() + return cache + } + + /// Get a specific camera by its Unique ID + /// - Parameter forID: String containing the camera's ID + /// - Returns: AVCaptureDevice or nil if the ID wasn't found + @objc public func getCamera(forID: String) -> Camera? { + for pair in cache { + if pair.key == forID { + return pair.value + } + } + return nil + } + + /// Update our camera cache + func updateCache() { + for camera in discoverySession.devices { + if (!cache.keys.contains(camera.uniqueID)) { + cache[camera.uniqueID] = Camera(uniqueID: camera.uniqueID) + } + } + } + + /// Empty the camera cache, typically because the host program is restarting and wants a clean slate + @objc public func drainCache() { + cache = [:] + } + + /// True if the camera device watcher is running, otherwise False + @objc public var isWatcherRunning: Bool { + cameraObserver != nil + } + + /// Start watching for camera addition/removal events + @objc public func startWatcher() { + guard let observerCallback = self.observerCallback else { return } + if (cameraObserver != nil) { + return + } + + cameraObserver = observe(\.discoverySession.devices, options: [.new, .old], changeHandler: { object, change in + for change in change.newValue!.difference(from: change.oldValue!) { + switch change { + case let .remove(offset: _, element: device, associatedWith: _): + if let camera = self.getCamera(forID: device.uniqueID) { + observerCallback(camera, "Removed") + self.cache.removeValue(forKey: device.uniqueID) + } + case let .insert(offset: _, element: device, associatedWith: _): + if let camera = Camera(uniqueID: device.uniqueID) { + observerCallback(camera, "Added") + self.cache[device.uniqueID] = camera + } + } + } + }) + } + + /// Stop watching for camera addition/removal events + @objc public func stopWatcher() { + cameraObserver?.invalidate() + } +} + +public typealias CameraPropertyCallback = @convention(block) (Camera, Bool) -> Void + +// FIXME: Documentation +@objc public class Camera : NSObject { + @objc var camera: AVCaptureDevice + + @objc public var observerCallback: CameraPropertyCallback? + @objc public var callbackRef: Int32 = LUA_NOREF // FIXME: This is a smell, Hammertime shouldn't know about Lua + var isInUseObserver: NSKeyValueObservation? = nil + + @objc public init?(uniqueID: String) { + guard let cameraDevice = AVCaptureDevice(uniqueID: uniqueID) else { return nil } + self.camera = cameraDevice + } + + @objc public var uniqueID: String { + camera.uniqueID + } + + @objc public var modelID: String { + camera.modelID + } + + @objc public var name: String { + camera.localizedName + } + + @objc public var manufacturer: String { + camera.manufacturer + } + + @objc public var isInUse: Bool { + camera.isInUseByAnotherApplication + } + + @objc public var transportType: String { + switch (camera.transportType) { + case Int32(kIOAudioDeviceTransportTypeUSB): + return "USB" + case Int32(kIOAudioDeviceTransportTypeBuiltIn): + return "BuiltIn" + case Int32(kIOAudioDeviceTransportTypePCI): + return "PCI" + case Int32(kIOAudioDeviceTransportTypeVirtual): + return "Virtual" + case Int32(kIOAudioDeviceTransportTypeWireless): + return "Wireless" + case Int32(kIOAudioDeviceTransportTypeNetwork): + return "Network" + default: + return "Unknown kIOAudioDeviceTransportType \(camera.transportType). Please file a bug." + } + } + + @objc public var isInUseWatcherRunning: Bool { + isInUseObserver != nil + } + + @objc public func startIsInUseWatcher() { + guard let observerCallback = self.observerCallback else { return } + if (isInUseObserver != nil) { return } + + isInUseObserver = observe(\.camera.isInUseByAnotherApplication, options: [], changeHandler: { object, _ in + observerCallback(self, self.camera.isInUseByAnotherApplication) + }) + } + + @objc public func stopIsInUseWatcher() { + isInUseObserver?.invalidate() + isInUseObserver = nil + } +} diff --git a/Hammertime/Globals.swift b/Hammertime/Globals.swift new file mode 100644 index 000000000..e1c767e35 --- /dev/null +++ b/Hammertime/Globals.swift @@ -0,0 +1,11 @@ +// +// Globals.swift +// Hammertime +// +// Created by Chris Jones on 21/12/2023. +// Copyright © 2023 Hammerspoon. All rights reserved. +// + +import Foundation + +let LUA_NOREF:Int32 = -2 diff --git a/extensions/camera/libcamera.m b/extensions/camera/libcamera.m index 5aedd4035..1d9df22bc 100644 --- a/extensions/camera/libcamera.m +++ b/extensions/camera/libcamera.m @@ -1,7 +1,6 @@ @import Cocoa; -@import AVFoundation; -@import CoreMediaIO; -#import +@import LuaSkin; +@import Hammertime; #pragma mark - Module declarations #define get_objectFromUserdata(objType, L, idx, tag) (objType*)*((void**)luaL_checkudata(L, idx, tag)) @@ -9,321 +8,89 @@ static const char *USERDATA_TAG = "hs.camera"; -#pragma mark - Devices watcher declarations +//#pragma mark - Devices watcher declarations typedef struct _deviceWatcher_t { int callback; - BOOL running; LSGCCanary lsCanary; } deviceWatcher_t; static deviceWatcher_t *deviceWatcher = nil; -static id deviceWatcherAddedObserver = nil; -static id deviceWatcherRemovedObserver = nil; - -#pragma mark - Device property watcher declarations -static const CMIOObjectPropertySelector propertyWatchSelectors[] = { - kAudioDevicePropertyDeviceHasChanged, - kAudioDevicePropertyDeviceIsRunningSomewhere -}; - - -#pragma mark - HSCamera declaration -@interface HSCamera : NSObject -@property(nonatomic) CMIODeviceID deviceId; -@property(nonatomic) NSString *name; -@property(nonatomic) NSString *uid; -@property(nonatomic, readonly, getter=getIsInUse) BOOL isInUse; -@property(nonatomic) int propertyWatcherCallback; -@property(nonatomic) BOOL propertyWatcherRunning; -@property(nonatomic) CMIOObjectPropertyListenerBlock propertyWatcherBlock; -@property(nonatomic) LSGCCanary canary; - -- (id)initWithDeviceID:(CMIODeviceID)deviceId; -- (NSString *)getCameraName; -- (NSString *)getCameraUID; -- (void)wasRemoved; -- (void)stopPropertyWatcher; -- (void)startPropertyWatcher; -@end - - -#pragma mark - HSCameraManager declaration -@interface HSCameraManager : NSObject -@property(nonatomic) NSMutableArray* cameraCache; - -- (NSArray*) getCameras; -- (HSCamera *)cameraForDeviceID:(CMIODeviceID)deviceId; -- (void)deviceRemoved:(CMIODeviceID)deviceId; -- (void)drainCache; -@end - -static HSCameraManager *cameraManager = nil; - - -#pragma mark - HSCamera implementation -@implementation HSCamera -- (id)initWithDeviceID:(CMIODeviceID) deviceId { - self = [super init]; - if (self) { - NSLog(@"HSCamera init: %@ (%d)", self, deviceId); - LuaSkin *skin = [LuaSkin sharedWithState:NULL]; - - self.deviceId = deviceId; - self.propertyWatcherCallback = LUA_NOREF; - self.propertyWatcherRunning = NO; - self.canary = [skin createGCCanary]; - self.uid = [self getCameraUID]; - self.name = [self getCameraName]; - - __weak HSCamera *weakSelf = self; - self.propertyWatcherBlock = ^(UInt32 numberAddresses, const CMIOObjectPropertyAddress *addresses) { - NSMutableArray *events = [[NSMutableArray alloc] init]; - - for (UInt32 i = 0; i < numberAddresses; i++) { - NSString *mSelector = (__bridge_transfer NSString *)UTCreateStringForOSType(addresses[i].mSelector); - NSString *mScope = (__bridge_transfer NSString *)UTCreateStringForOSType(addresses[i].mScope); - NSNumber *mElement = [NSNumber numberWithInt:addresses[i].mElement]; - [events addObject:@{@"mSelector":mSelector, @"mScope":mScope, @"mElement":mElement}]; - } - - dispatch_async(dispatch_get_main_queue(), ^{ - LuaSkin *skin = [LuaSkin sharedWithState:NULL]; - if (![skin checkGCCanary:weakSelf.canary]) { - return; - } - - _lua_stackguard_entry(skin.L); - if (self.propertyWatcherCallback == LUA_NOREF) { - [skin logError:@"hs.camera property watcher fired, but no callback has been set"]; - } else { - for (NSDictionary *event in events) { - [skin pushLuaRef:refTable ref:weakSelf.propertyWatcherCallback]; - [skin pushNSObject:weakSelf]; - [skin pushNSObject:event[@"mSelector"]]; - [skin pushNSObject:event[@"mScope"]]; - [skin pushNSObject:event[@"mElement"]]; - - [skin protectedCallAndError:@"hs.camera:propertyWatcherCallback" nargs:4 nresults:0]; - } - } - _lua_stackguard_exit(skin.L); - }); - }; - } - return self; -} - -- (void)dealloc { - NSLog(@"HSCamera dealloc: %@", self); - [self wasRemoved]; - [[LuaSkin sharedWithState:NULL] destroyGCCanary:&_canary]; -} - -- (void)wasRemoved { - [self stopPropertyWatcher]; -} - -- (void)startPropertyWatcher { - if (self.propertyWatcherRunning == YES) { - return; - } - - CMIOObjectPropertyAddress propertyAddress = { - 0, - kAudioObjectPropertyScopeWildcard, - kAudioObjectPropertyElementWildcard - }; - - const int numSelectors = sizeof(propertyWatchSelectors) / sizeof(propertyWatchSelectors[0]); - - for (int i = 0; i < numSelectors; i++) { - propertyAddress.mSelector = propertyWatchSelectors[i]; - CMIOObjectAddPropertyListenerBlock(self.deviceId, - &propertyAddress, - dispatch_get_main_queue(), - self.propertyWatcherBlock); - } - - self.propertyWatcherRunning = YES; -} - -- (void)stopPropertyWatcher { - if (self.propertyWatcherRunning == NO) { - return; - } - - CMIOObjectPropertyAddress propertyAddress = { - 0, - kAudioObjectPropertyScopeWildcard, - kAudioObjectPropertyElementWildcard - }; - - const int numSelectors = sizeof(propertyWatchSelectors) / sizeof(propertyWatchSelectors[0]); - - for (int i = 0; i < numSelectors; i++) { - propertyAddress.mSelector = propertyWatchSelectors[i]; - CMIOObjectRemovePropertyListenerBlock(self.deviceId, - &propertyAddress, - dispatch_get_main_queue(), - self.propertyWatcherBlock); - } - - self.propertyWatcherRunning = NO; -} - -- (NSString *)getCameraUID { - LuaSkin *skin = [LuaSkin sharedWithState:NULL]; - OSStatus err; - UInt32 dataSize = 0; - UInt32 dataUsed = 0; - - CMIOObjectPropertyAddress prop = {kCMIODevicePropertyDeviceUID, - kCMIOObjectPropertyScopeWildcard, - kCMIOObjectPropertyElementWildcard}; - - err = CMIOObjectGetPropertyDataSize(self.deviceId, &prop, 0, nil, &dataSize); - if (err != kCMIOHardwareNoError) { - [skin logError:[NSString stringWithFormat:@"getUID: Unable to get data size: %d", err]]; - return nil; - } - - CFStringRef uidStringRef = NULL; - err = CMIOObjectGetPropertyData(self.deviceId, &prop, 0, nil, dataSize, &dataUsed, - &uidStringRef); - if (err != kCMIOHardwareNoError) { - [skin logError:[NSString stringWithFormat:@"getUID: Unable to get data: %d", err]]; - return nil; - } - - return (__bridge NSString *)uidStringRef; -} - -- (NSString *)getCameraName { - LuaSkin *skin = [LuaSkin sharedWithState:NULL]; - AVCaptureDevice *avDevice = [AVCaptureDevice deviceWithUniqueID:self.uid]; - if (!avDevice) { - [skin logWarn:[NSString stringWithFormat:@"Unable to get camera name for: %@", self.uid]]; - return nil; - } - return avDevice.localizedName; -} - -- (BOOL)getIsInUse { - LuaSkin *skin = [LuaSkin sharedWithState:NULL]; - OSStatus err; - UInt32 dataSize = 0; - UInt32 dataUsed = 0; - UInt32 isInUse = 0; - - CMIOObjectPropertyAddress prop = {kCMIODevicePropertyDeviceIsRunningSomewhere, - kCMIOObjectPropertyScopeWildcard, - kCMIOObjectPropertyElementWildcard}; - - err = CMIOObjectGetPropertyDataSize(self.deviceId, &prop, 0, nil, &dataSize); - if (err != kCMIOHardwareNoError) { - [skin logError:[NSString stringWithFormat:@"getVideoDeviceIsUsed(): get data size error: %d", err]]; - return NO; - } - - err = CMIOObjectGetPropertyData(self.deviceId, &prop, 0, nil, dataSize, &dataUsed, &isInUse); - if (err != kCMIOHardwareNoError) { - [skin logError:[NSString stringWithFormat:@"getVideoDeviceIsUsed(): get data error: %d", err]]; - return NO; - } - - return isInUse; -} -@end - -#pragma mark - HSCameraManager implementation -@implementation HSCameraManager -- (id)init { - self = [super init]; - if (self) { - self.cameraCache = [[NSMutableArray alloc] init]; - } - return self; -} - -- (void)dealloc { - self.cameraCache = nil; -} - -- (HSCamera *)cameraForDeviceID:(CMIODeviceID)deviceId { - HSCamera *camera = nil; - - // Check if we already have this device cached - for (HSCamera *device in self.cameraCache) { - if (device.deviceId == deviceId) { - // Found it, return the cached object - return device; - } - } - - // We don't have this camera cached, so create a new object and cache it. - camera = [[HSCamera alloc] initWithDeviceID:deviceId]; - [self.cameraCache addObject:camera]; - - return camera; -} - -- (void)deviceRemoved:(CMIODeviceID)deviceId { - for (HSCamera *device in self.cameraCache) { - if (device.deviceId == deviceId) { - [device wasRemoved]; - [self.cameraCache removeObject:device]; - return; - } - } -} - -- (void)drainCache { - self.cameraCache = [[NSMutableArray alloc] init]; -} - -- (NSArray*) getCameras { - LuaSkin *skin = [LuaSkin sharedWithState:NULL]; - NSMutableArray *cameras = [[NSMutableArray alloc] init]; - OSStatus err; - UInt32 dataSize = 0; - CMIOObjectPropertyAddress prop = { - kCMIOHardwarePropertyDevices, - kCMIOObjectPropertyScopeGlobal, - kCMIOObjectPropertyElementMaster - }; - - // Get the number of cameras - UInt32 numCameras = 0; - - err = CMIOObjectGetPropertyDataSize(kCMIOObjectSystemObject, &prop, 0, nil, &dataSize); - if (err != kCMIOHardwareNoError) { - [skin logError:[NSString stringWithFormat:@"Unable to fetch camera device count: %d", err]]; - return @[]; - } - numCameras = (UInt32) dataSize / sizeof(CMIODeviceID); - - // Get the camera devices - UInt32 dataUsed = 0; - CMIODeviceID *cameraList = (CMIODeviceID *) calloc(numCameras, sizeof(CMIODeviceID)); - err = CMIOObjectGetPropertyData(kCMIOObjectSystemObject, &prop, 0, nil, dataSize, &dataUsed, cameraList); - if (err != kCMIOHardwareNoError) { - free(cameraList); - [skin logError:[NSString stringWithFormat:@"Unable to fetch camera devices: %d", err]]; - return @[]; - } - - // Prepare the array - for (UInt32 i = 0; i < numCameras; i++) { - CMIOObjectID cameraID = cameraList[i]; - HSCamera *camera = [cameraManager cameraForDeviceID:cameraID]; - [cameras addObject:camera]; - } - - NSArray *immutableCameras = [cameras copy]; - free(cameraList); - return immutableCameras; -} -@end +static CameraManager *cameraManager = nil; + + +//- (void)startPropertyWatcher { +// if (self.propertyWatcherRunning == YES) { +// return; +// } +// +// CMIOObjectPropertyAddress propertyAddress = { +// 0, +// kAudioObjectPropertyScopeWildcard, +// kAudioObjectPropertyElementWildcard +// }; +// +// const int numSelectors = sizeof(propertyWatchSelectors) / sizeof(propertyWatchSelectors[0]); +// +// for (int i = 0; i < numSelectors; i++) { +// propertyAddress.mSelector = propertyWatchSelectors[i]; +// CMIOObjectAddPropertyListenerBlock(self.deviceId, +// &propertyAddress, +// dispatch_get_main_queue(), +// self.propertyWatcherBlock); +// } +// +// self.propertyWatcherRunning = YES; +//} +// +//- (void)stopPropertyWatcher { +// if (self.propertyWatcherRunning == NO) { +// return; +// } +// +// CMIOObjectPropertyAddress propertyAddress = { +// 0, +// kAudioObjectPropertyScopeWildcard, +// kAudioObjectPropertyElementWildcard +// }; +// +// const int numSelectors = sizeof(propertyWatchSelectors) / sizeof(propertyWatchSelectors[0]); +// +// for (int i = 0; i < numSelectors; i++) { +// propertyAddress.mSelector = propertyWatchSelectors[i]; +// CMIOObjectRemovePropertyListenerBlock(self.deviceId, +// &propertyAddress, +// dispatch_get_main_queue(), +// self.propertyWatcherBlock); +// } +// +// self.propertyWatcherRunning = NO; +//} + +//- (BOOL)getIsInUse { +// LuaSkin *skin = [LuaSkin sharedWithState:NULL]; +// OSStatus err; +// UInt32 dataSize = 0; +// UInt32 dataUsed = 0; +// UInt32 isInUse = 0; +// +// CMIOObjectPropertyAddress prop = {kCMIODevicePropertyDeviceIsRunningSomewhere, +// kCMIOObjectPropertyScopeWildcard, +// kCMIOObjectPropertyElementWildcard}; +// +// err = CMIOObjectGetPropertyDataSize(self.deviceId, &prop, 0, nil, &dataSize); +// if (err != kCMIOHardwareNoError) { +// [skin logError:[NSString stringWithFormat:@"getVideoDeviceIsUsed(): get data size error: %d", err]]; +// return NO; +// } +// +// err = CMIOObjectGetPropertyData(self.deviceId, &prop, 0, nil, dataSize, &dataUsed, &isInUse); +// if (err != kCMIOHardwareNoError) { +// [skin logError:[NSString stringWithFormat:@"getVideoDeviceIsUsed(): get data error: %d", err]]; +// return NO; +// } +// +// return isInUse; +//} #pragma mark - Lua API /// hs.camera.allCameras() -> table @@ -339,17 +106,12 @@ static int allCameras(lua_State *L) { LuaSkin *skin = [LuaSkin sharedWithState:L]; [skin checkArgs:LS_TBREAK]; - [skin pushNSObject:[cameraManager getCameras]]; + [skin pushNSObject:[cameraManager getCameras].allValues]; return 1; } -// NOTE: Private API used here -@interface AVCaptureDevice (PrivateAPI) -- (CMIODeviceID)connectionID; -@end - // This calls the devices watcher callback Lua function when a device is added/removed -void deviceWatcherDoCallback(CMIODeviceID deviceId, NSString *event) { +void deviceWatcherDoCallback(Camera *device, NSString *event) { LuaSkin *skin = [LuaSkin sharedWithState:NULL]; if (!deviceWatcher) { @@ -368,7 +130,7 @@ void deviceWatcherDoCallback(CMIODeviceID deviceId, NSString *event) { } [skin pushLuaRef:refTable ref:deviceWatcher->callback]; - [skin pushNSObject:[cameraManager cameraForDeviceID:deviceId]]; + [skin pushNSObject:device]; [skin pushNSObject:event]; [skin protectedCallAndError:@"hs.camera devices callback" nargs:2 nresults:0]; @@ -393,42 +155,18 @@ static int startWatcher(lua_State *L) { return 0; } - if (deviceWatcher->running == YES) { + if (cameraManager.isWatcherRunning) { return 0; } // For some reason, the device added/removed notifications don't fire unless we ask macOS to enumerate the devices first -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [AVCaptureDevice devices]; -#pragma clang diagnostic pop - - NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; - deviceWatcherAddedObserver = [center addObserverForName:AVCaptureDeviceWasConnectedNotification - object:nil - queue:[NSOperationQueue mainQueue] - usingBlock:^(NSNotification * _Nonnull note) { - AVCaptureDevice *device = [note object]; - if ([device hasMediaType:AVMediaTypeVideo]) { - CMIODeviceID deviceId = device.connectionID; - dispatch_async(dispatch_get_main_queue(), ^{ - deviceWatcherDoCallback(deviceId, @"Added"); - }); - } - }]; - deviceWatcherRemovedObserver = [center addObserverForName:AVCaptureDeviceWasDisconnectedNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { - AVCaptureDevice *device = [note object]; - if ([device hasMediaType:AVMediaTypeVideo]) { - CMIODeviceID deviceId = device.connectionID; - dispatch_async(dispatch_get_main_queue(), ^{ - deviceWatcherDoCallback(deviceId, @"Removed"); - [cameraManager deviceRemoved:deviceId]; - }); - } - }]; + NSDictionary *cameras __unused = [cameraManager getCameras]; + + cameraManager.observerCallback = ^(Camera *camera, NSString *event) { + deviceWatcherDoCallback(camera, event); + }; + [cameraManager startWatcher]; - NSLog(@"startWatcher: got objects: %@, %@", deviceWatcherAddedObserver, deviceWatcherRemovedObserver); - deviceWatcher->running = YES; return 0; } @@ -452,15 +190,7 @@ static int stopWatcher(lua_State *L) { return 0; } - NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; - [center removeObserver:deviceWatcherAddedObserver - name:AVCaptureDeviceWasConnectedNotification - object:nil]; - [center removeObserver:deviceWatcherRemovedObserver - name:AVCaptureDeviceWasDisconnectedNotification - object:nil]; - - deviceWatcher->running = NO; + [cameraManager stopWatcher]; return 0; } @@ -478,7 +208,7 @@ static int isWatcherRunning(lua_State *L) { LuaSkin *skin = [LuaSkin sharedWithState:L]; [skin checkArgs:LS_TBREAK]; - lua_pushboolean(L, deviceWatcher && deviceWatcher->running); + lua_pushboolean(L, deviceWatcher && cameraManager.isWatcherRunning); return 1; } @@ -508,7 +238,6 @@ static int setWatcherCallback(lua_State *L) { if (!deviceWatcher) { deviceWatcher = malloc(sizeof(deviceWatcher_t)); memset(deviceWatcher, 0, sizeof(deviceWatcher_t)); - deviceWatcher->running = NO; deviceWatcher->callback = LUA_NOREF; deviceWatcher->lsCanary = [skin createGCCanary]; } @@ -532,7 +261,7 @@ static int setWatcherCallback(lua_State *L) { /// hs.camera:uid() -> String /// Method -/// Get the UID of the camera +/// Get the unique ID of the camera /// /// Parameters: /// * None @@ -546,26 +275,23 @@ static int camera_uid(lua_State *L) { LuaSkin *skin = [LuaSkin sharedWithState:L]; [skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TBREAK]; - HSCamera *camera = [skin toNSObjectAtIndex:1]; - [skin pushNSObject:camera.uid]; + Camera *camera = [skin toNSObjectAtIndex:1]; + [skin pushNSObject:camera.uniqueID]; return 1; } /// hs.camera:connectionID() -> String -/// Method +/// Deprecated /// Get the raw connection ID of the camera /// /// Parameters: /// * None /// /// Returns: -/// * A number containing the connection ID of the camera +/// * A number that will always be zero static int camera_cID(lua_State *L) { - LuaSkin *skin = [LuaSkin sharedWithState:L]; - [skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TBREAK]; - - HSCamera *camera = [skin toNSObjectAtIndex:1]; - lua_pushinteger(L, camera.deviceId); + // FIXME: Add some kind of log message that a deprecated method is being used + lua_pushinteger(L, 0); return 1; } @@ -582,11 +308,65 @@ static int camera_name(lua_State *L) { LuaSkin *skin = [LuaSkin sharedWithState:L]; [skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TBREAK]; - HSCamera *camera = [skin toNSObjectAtIndex:1]; + Camera *camera = [skin toNSObjectAtIndex:1]; [skin pushNSObject:camera.name]; return 1; } +/// hs.camera:manufacturer() -> String +/// Method +/// Get the manufacturer of the camera +/// +/// Parameters: +/// * None +/// +/// Returns: +/// * A string containing the manufacturer of the camera +static int camera_manufacturer(lua_State *L) { + LuaSkin *skin = [LuaSkin sharedWithState:L]; + [skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TBREAK]; + + Camera *camera = [skin toNSObjectAtIndex:1]; + [skin pushNSObject:camera.manufacturer]; + return 1; +} + +/// hs.camera:model() -> String +/// Method +/// Get the model name of the camera +/// +/// Parameters: +/// * None +/// +/// Returns: +/// * A string containing the model name of the camera +static int camera_model(lua_State *L) { + LuaSkin *skin = [LuaSkin sharedWithState:L]; + [skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TBREAK]; + + Camera *camera = [skin toNSObjectAtIndex:1]; + [skin pushNSObject:camera.modelID]; + return 1; +} + +/// hs.camera:transport() -> String +/// Method +/// Get the transport type (ie how it is connected) of the camera +/// +/// Parameters: +/// * None +/// +/// Returns: +/// * A string containing the transport type of the camera +static int camera_transport(lua_State *L) { + LuaSkin *skin = [LuaSkin sharedWithState:L]; + [skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TBREAK]; + + Camera *camera = [skin toNSObjectAtIndex:1]; + [skin pushNSObject:camera.transportType]; + return 1; +} + /// hs.camera:isInUse() -> Boolean /// Method /// Get the usage status of the camera @@ -596,26 +376,27 @@ static int camera_name(lua_State *L) { /// /// Returns: /// * A boolean, True if the camera is in use, otherwise False -static int camera_isinuse(lua_State *L) { +static int camera_isInUse(lua_State *L) { LuaSkin *skin = [LuaSkin sharedWithState:L]; [skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TBREAK]; - HSCamera *camera = [skin toNSObjectAtIndex:1]; + Camera *camera = [skin toNSObjectAtIndex:1]; lua_pushboolean(L, camera.isInUse); return 1; } /// hs.camera:setPropertyWatcherCallback(fn) -> hs.camera object /// Method -/// Sets or clears a callback for when the properties of an hs.camera object change +/// Sets or clears a callback for when an hs.camera object starts or stops being used by another application /// /// Parameters: -/// * fn - A function to be called when properties of the camera change, or nil to clear a previously set callback. The function should accept the following parameters: +/// * fn - A function to be called when usage the camera change, or nil to clear a previously set callback. The function should accept the following parameters: /// * The hs.camera object that changed -/// * A string describing the property that changed. Possible values are: +/// * (DEPRECATED) A string describing the property that changed. Possible values are: /// * gone - The device's "in use" status changed (ie another app started using the camera, or stopped using it) -/// * A string containing the scope of the event, this will likely always be "glob" -/// * A number containing the element of the event, this will likely always be "0" +/// * (DEPRECATED) A string containing the scope of the event, this will likely always be "glob" +/// * (DEPRECATED) A number containing the element of the event, this will likely always be "0" +/// * A boolean, true if the camera is now in use, otherwise false /// /// Returns: /// * The `hs.camera` object @@ -623,15 +404,16 @@ static int camera_propertyWatcherCallback(lua_State *L) { LuaSkin *skin = [LuaSkin sharedWithState:L]; [skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TFUNCTION|LS_TNIL, LS_TBREAK]; - HSCamera *camera = [skin toNSObjectAtIndex:1]; - camera.propertyWatcherCallback = [skin luaUnref:refTable ref:camera.propertyWatcherCallback]; + Camera *camera = [skin toNSObjectAtIndex:1]; + camera.callbackRef = [skin luaUnref:refTable ref:camera.callbackRef]; switch (lua_type(L, 2)) { case LUA_TFUNCTION: lua_pushvalue(L, 2); - camera.propertyWatcherCallback = [skin luaRef:refTable]; + camera.callbackRef = [skin luaRef:refTable]; break; case LUA_TNIL: + [camera stopIsInUseWatcher]; break; default: break; @@ -654,15 +436,32 @@ static int camera_startPropertyWatcher(lua_State *L) { LuaSkin *skin = [LuaSkin sharedWithState:L]; [skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TBREAK]; - HSCamera *camera = [skin toNSObjectAtIndex:1]; + Camera *camera = [skin toNSObjectAtIndex:1]; - if (camera.propertyWatcherCallback == LUA_NOREF) { + if (camera.callbackRef == LUA_NOREF) { [skin logError:@"You must call hs.camera:setPropertyWatcherCallback() before hs.camera:startPropertyWatcher()"]; lua_pushnil(L); return 1; } - [camera startPropertyWatcher]; + camera.observerCallback = ^(Camera *camera, BOOL isInUse) { + LuaSkin *skin = [LuaSkin sharedWithState:NULL]; + // FIXME: There is no canary checking here, because we don't have anywhere to store the canary + _lua_stackguard_entry(skin.L); + if (camera.callbackRef == LUA_NOREF) { + [skin logError:@"hs.camera property watcher fired, but no Lua callback is currently set"]; + } else { + [skin pushLuaRef:refTable ref:camera.callbackRef]; + [skin pushNSObject:camera]; + lua_pushstring(L, "gone"); + lua_pushstring(L, "glob"); + lua_pushinteger(L, 0); + lua_pushboolean(L, isInUse); + [skin protectedCallAndError:@"hs.camera:propertyWatcherCallback" nargs:5 nresults:0]; + } + _lua_stackguard_exit(skin.L); + }; + [camera startIsInUseWatcher]; lua_pushvalue(L, 1); return 1; @@ -680,9 +479,9 @@ static int camera_startPropertyWatcher(lua_State *L) { static int camera_stopPropertyWatcher(lua_State *L) { LuaSkin *skin = [LuaSkin sharedWithState:L]; [skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TBREAK]; - HSCamera *camera = [skin toNSObjectAtIndex:1]; + Camera *camera = [skin toNSObjectAtIndex:1]; - [camera stopPropertyWatcher]; + [camera stopIsInUseWatcher]; lua_pushvalue(L, 1); return 1; @@ -700,27 +499,27 @@ static int camera_stopPropertyWatcher(lua_State *L) { static int camera_isPropertyWatcherRunning(lua_State *L) { LuaSkin *skin = [LuaSkin sharedWithState:L]; [skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TBREAK]; - HSCamera *camera = [skin toNSObjectAtIndex:1]; + Camera *camera = [skin toNSObjectAtIndex:1]; - lua_pushboolean(L, camera.propertyWatcherRunning); + lua_pushboolean(L, camera.isInUseWatcherRunning); return 1; } #pragma mark - Lua<->NSObject Conversion Functions -static int pushHSCamera(lua_State *L, id obj) { - HSCamera *value = obj; - void** valuePtr = lua_newuserdata(L, sizeof(HSCamera *)); - *valuePtr = (__bridge_retained void *)value; +static int pushCamera(lua_State *L, id obj) { + Camera *value = obj; + void** valuePtr = lua_newuserdata(L, sizeof(Camera *)); + *valuePtr = (__bridge_retained void*)value; luaL_getmetatable(L, USERDATA_TAG); lua_setmetatable(L, -2); return 1; } -static id toHSCameraFromLua(lua_State *L, int idx) { +static id toCameraFromLua(lua_State *L, int idx) { LuaSkin *skin = [LuaSkin sharedWithState:L]; - HSCamera *value; + Camera *value; if (luaL_testudata(L, idx, USERDATA_TAG)) { - value = get_objectFromUserdata(__bridge HSCamera, L, idx, USERDATA_TAG); + value = get_objectFromUserdata(__bridge Camera, L, idx, USERDATA_TAG); } else { [skin logError:[NSString stringWithFormat:@"expected %s object, found %s", USERDATA_TAG, lua_typename(L, lua_type(L, idx))]]; } @@ -730,16 +529,16 @@ static id toHSCameraFromLua(lua_State *L, int idx) { #pragma mark - Core Lua metamethods static int hsCamera_tostring(lua_State *L) { LuaSkin *skin = [LuaSkin sharedWithState:L]; - HSCamera *camera = [skin toNSObjectAtIndex:1]; - [skin pushNSObject:[NSString stringWithFormat:@"%s: (%@:%@)", USERDATA_TAG, camera.uid, camera.name]]; + Camera *camera = [skin toNSObjectAtIndex:1]; + [skin pushNSObject:[NSString stringWithFormat:@"%s: (%@:%@)", USERDATA_TAG, camera.uniqueID, camera.name]]; return 1; } static int hsCamera_eq(lua_State *L) { if (luaL_testudata(L, 1, USERDATA_TAG) && luaL_testudata(L, 2, USERDATA_TAG)) { LuaSkin *skin = [LuaSkin sharedWithState:L] ; - HSCamera *obj1 = [skin luaObjectAtIndex:1 toClass:"HSCamera"] ; - HSCamera *obj2 = [skin luaObjectAtIndex:2 toClass:"HSCamera"] ; + Camera *obj1 = [skin luaObjectAtIndex:1 toClass:"Camera"] ; + Camera *obj2 = [skin luaObjectAtIndex:2 toClass:"Camera"] ; lua_pushboolean(L, [obj1 isEqualTo:obj2]) ; } else { lua_pushboolean(L, NO) ; @@ -747,6 +546,7 @@ static int hsCamera_eq(lua_State *L) { return 1 ; } +// FIXME: Should we stop the camera's inUse watcher here? static int hsCamera_gc(lua_State *L) { lua_pushnil(L); lua_setmetatable(L, 1); @@ -764,9 +564,6 @@ static int module_gc(lua_State *L) { deviceWatcher = nil; } - for (HSCamera *camera in cameraManager.cameraCache) { - [camera wasRemoved]; - } [cameraManager drainCache]; cameraManager = nil; @@ -778,7 +575,10 @@ static int module_gc(lua_State *L) { {"uid", camera_uid}, {"connectionID", camera_cID}, {"name", camera_name}, - {"isInUse", camera_isinuse}, + {"manufacturer", camera_manufacturer}, + {"model", camera_model}, + {"transport", camera_transport}, + {"isInUse", camera_isInUse}, {"setPropertyWatcherCallback", camera_propertyWatcherCallback}, {"startPropertyWatcher", camera_startPropertyWatcher}, {"stopPropertyWatcher", camera_stopPropertyWatcher}, @@ -809,13 +609,12 @@ static int module_gc(lua_State *L) { int luaopen_hs_libcamera(lua_State *L) { LuaSkin *skin = [LuaSkin sharedWithState:L]; - cameraManager = [[HSCameraManager alloc] init]; - + cameraManager = [[CameraManager alloc] init]; refTable = [skin registerLibrary:USERDATA_TAG functions:cameraLib metaFunctions:cameraLibMeta]; [skin registerObject:USERDATA_TAG objectFunctions:cameraDeviceLib]; - [skin registerPushNSHelper:pushHSCamera forClass:"HSCamera"]; - [skin registerLuaObjectHelper:toHSCameraFromLua forClass:"HSCamera" withUserdataMapping:USERDATA_TAG]; + [skin registerPushNSHelper:pushCamera forClass:"Hammertime.Camera"]; + [skin registerLuaObjectHelper:toCameraFromLua forClass:"Hammertime.Camera" withUserdataMapping:USERDATA_TAG]; return 1; } From fcd11cb41719441df0136efb64a2e27328c7a6a6 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Thu, 21 Dec 2023 10:10:08 +0000 Subject: [PATCH 4/8] Some @objc formatting and switch Math over to raising NSExceptions for all definable range methods --- Hammertime/Base64.swift | 12 ++++-------- Hammertime/Globals.swift | 1 + Hammertime/Math.swift | 37 +++++++++++++++++++++---------------- extensions/math/libmath.m | 19 +++++-------------- 4 files changed, 31 insertions(+), 38 deletions(-) diff --git a/Hammertime/Base64.swift b/Hammertime/Base64.swift index 4b8d29305..e26f504bd 100644 --- a/Hammertime/Base64.swift +++ b/Hammertime/Base64.swift @@ -15,13 +15,11 @@ extension String { } } -@objc -public class Base64 : NSObject { +@objc public class Base64 : NSObject { /// Encode bytes to a Base64 string /// - Parameter data: Some input bytes as a Data /// - Returns: String - @objc - public func encode(data: Data) -> String { + @objc public func encode(data: Data) -> String { return data.base64EncodedString() } @@ -30,8 +28,7 @@ public class Base64 : NSObject { /// - data: Some input bytes as a Data /// - width: How wide the lines should be /// - Returns: String - @objc - public func encode(data: Data, width: Int) -> String { + @objc public func encode(data: Data, width: Int) -> String { let string = self.encode(data: data) let lines = string.splitByLength(every: width) return lines.joined(separator: "\n") @@ -40,8 +37,7 @@ public class Base64 : NSObject { /// Decode a Base64 string to bytes /// - Parameter input: A Base64 encoded string /// - Returns: Output bytes as a Data - @objc - public func decode(input: String) -> Data { + @objc public func decode(input: String) -> Data { if let encoded = input.data(using: .utf8) { if let data = Data(base64Encoded: encoded, options: .ignoreUnknownCharacters) { return data diff --git a/Hammertime/Globals.swift b/Hammertime/Globals.swift index e1c767e35..89134a48b 100644 --- a/Hammertime/Globals.swift +++ b/Hammertime/Globals.swift @@ -8,4 +8,5 @@ import Foundation +// FIXME: This shouldn't be in Hammertime. let LUA_NOREF:Int32 = -2 diff --git a/Hammertime/Math.swift b/Hammertime/Math.swift index bd36ac8d8..9c03357a5 100644 --- a/Hammertime/Math.swift +++ b/Hammertime/Math.swift @@ -8,22 +8,22 @@ import Foundation -@objc -public class Math : NSObject { - @objc - public func validateDoubleRange(start: Double, end: Double) -> Bool { +@objc public class Math : NSObject { + func validateDoubleRange(start: Double, end: Double) -> Bool { return start <= end } - @objc - public func validateIntRange(start: Int, end: Int) -> Bool { + func validateFloatRange(start: Float, end: Float) -> Bool { + return start <= end + } + + func validateIntRange(start: Int, end: Int) -> Bool { return start <= end } /// Returns a random Double between 0 and 1 (inclusive) /// - Returns: Double - @objc - public func randomDouble() -> Double { + @objc public func randomDouble() -> Double { return self.randomDoubleInRange(start: 0, end: 1) } @@ -32,8 +32,8 @@ public class Math : NSObject { /// - start: Lower bound of the range /// - end: Upper bound of the range /// - Returns: Double - @objc - public func randomDoubleInRange(start: Double, end: Double) -> Double { + /// - Throws: `NSException` if start > end + @objc public func randomDoubleInRange(start: Double, end: Double) -> Double { if (!self.validateDoubleRange(start: start, end: end)) { NSException.raise(.rangeException, format: "start must be <= end", arguments: getVaList([""])) } @@ -42,8 +42,7 @@ public class Math : NSObject { /// Returns a random Float between 0 and 1 (inclusive) /// - Returns: Float - @objc - public func randomFloat() -> Float { + @objc public func randomFloat() -> Float { return self.randomFloatInRange(start: 0, end: 1) } @@ -52,8 +51,11 @@ public class Math : NSObject { /// - start: Lower bound of the range /// - end: Upper bound of the range /// - Returns: Float - @objc - public func randomFloatInRange(start: Float, end: Float) -> Float { + /// - Throws: `NSException` if start > end + @objc public func randomFloatInRange(start: Float, end: Float) -> Float { + if (!self.validateFloatRange(start: start, end: end)) { + NSException.raise(.rangeException, format: "start must be <= end", arguments: getVaList([""])) + } return Float.random(in: start...end) } @@ -62,8 +64,11 @@ public class Math : NSObject { /// - start: Lower bound of the range /// - end: Upper bound of the range /// - Returns: Int - @objc - public func randomIntInRange(start: Int, end: Int) -> Int { + /// - Throws: `NSException` if start > end + @objc public func randomIntInRange(start: Int, end: Int) -> Int { + if (!self.validateIntRange(start: start, end: end)) { + NSException.raise(.rangeException, format: "start must be <= end", arguments: getVaList([""])) + } return Int.random(in: start...end) } } diff --git a/extensions/math/libmath.m b/extensions/math/libmath.m index 4fd7f5052..dc89d4f18 100644 --- a/extensions/math/libmath.m +++ b/extensions/math/libmath.m @@ -32,20 +32,12 @@ static int math_randomFloat(lua_State* L) { /// * end - Upper bound of the range /// /// Returns: -/// * A random number +/// * A random floating point number static int math_randomFloatFromRange(lua_State *L) { LuaSkin *skin = [LuaSkin sharedWithState:L]; [skin checkArgs:LS_TNUMBER, LS_TNUMBER, LS_TBREAK]; Math *math = [[Math alloc] init]; -// double start = lua_tonumber(L, 1); -// double end = lua_tonumber(L, 2); - -// if (![math validateDoubleRangeWithStart:start end:end]) { -// [skin logError:@"hs.math.randomFloatFromRange: start must be <= end"]; -// lua_pushnil(L); -// return 1; -// } @try { lua_pushnumber(L, [math randomDoubleInRangeWithStart:lua_tonumber(L, 1) end:lua_tonumber(L, 2)]); @@ -74,13 +66,12 @@ static int math_randomFromRange(lua_State* L) { int start = (int)lua_tointeger(L, 1); int end = (int)lua_tointeger(L, 2); - if (![math validateIntRangeWithStart:start end:end]) { - [skin logError:@"hs.math.randomFromRange: start must be <= end"]; + @try { + lua_pushinteger(L, [math randomIntInRangeWithStart:lua_tointeger(L, 1) end:lua_tointeger(L, 2)]); + } @catch (NSException *e){ + [skin logError:e.reason]; lua_pushnil(L); - return 1; } - - lua_pushinteger(L, [math randomIntInRangeWithStart:lua_tointeger(L, 1) end:lua_tointeger(L, 2)]); return 1; } From a20d2d9fb6818e569fcc88791a40d5a6b26a405f Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Thu, 21 Dec 2023 15:43:05 +0000 Subject: [PATCH 5/8] Further updates --- Hammerspoon.xcodeproj/project.pbxproj | 12 ++- Hammertime/Camera.swift | 110 ++++++++++++++++++++------ extensions/camera/libcamera.m | 62 ++++++++++++--- extensions/math/libmath.m | 2 - 4 files changed, 145 insertions(+), 41 deletions(-) diff --git a/Hammerspoon.xcodeproj/project.pbxproj b/Hammerspoon.xcodeproj/project.pbxproj index 24ff20288..d84642a91 100644 --- a/Hammerspoon.xcodeproj/project.pbxproj +++ b/Hammerspoon.xcodeproj/project.pbxproj @@ -9981,12 +9981,13 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -10026,6 +10027,7 @@ ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = org.hammerspoon.Hammertime; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; @@ -10070,12 +10072,13 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -10108,6 +10111,7 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = org.hammerspoon.Hammertime; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; SKIP_INSTALL = YES; SWIFT_COMPILATION_MODE = wholemodule; @@ -10151,12 +10155,13 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -10189,6 +10194,7 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = org.hammerspoon.Hammertime; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; SKIP_INSTALL = YES; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/Hammertime/Camera.swift b/Hammertime/Camera.swift index d69bc0c85..950fc91e3 100644 --- a/Hammertime/Camera.swift +++ b/Hammertime/Camera.swift @@ -8,6 +8,7 @@ import Foundation import AVFoundation +import CoreMediaIO import IOKit.audio let allCameraTypes:[AVCaptureDevice.DeviceType] = [ @@ -101,41 +102,74 @@ public typealias CameraManagerDiscoveryCallback = @convention(block) (Camera?, S } } -public typealias CameraPropertyCallback = @convention(block) (Camera, Bool) -> Void +public typealias CameraPropertyCallback = @convention(block) (Camera) -> Void -// FIXME: Documentation @objc public class Camera : NSObject { + /// Underlying AVFoundation camera object we represent @objc var camera: AVCaptureDevice + /// Optional pointer storage for additional data that needs to be associated with instances of this class + @objc public var userData: UnsafeMutableRawPointer? - @objc public var observerCallback: CameraPropertyCallback? - @objc public var callbackRef: Int32 = LUA_NOREF // FIXME: This is a smell, Hammertime shouldn't know about Lua - var isInUseObserver: NSKeyValueObservation? = nil + private var STATUS_PA = CMIOObjectPropertyAddress( + mSelector: CMIOObjectPropertySelector(kCMIODevicePropertyDeviceIsRunningSomewhere), + mScope: CMIOObjectPropertyScope(kCMIOObjectPropertyScopeWildcard), + mElement: CMIOObjectPropertyElement(kCMIOObjectPropertyElementWildcard) + ) + /// Internal callback block for CoreMediaIO Object Property Listener + private var isInUseWatcherCallbackBlock: CMIOObjectPropertyListenerBlock? + /// True if our `isInUse` watcher is running + @objc public var isInUseWatcherRunning: Bool = false + + // This is ugly and awful, but CoreMediaIO is the only reliable way to tell if a camera is in use + var connectionID: CMIOObjectID { + camera.value(forKey: "_connectionID")! as! CMIOObjectID + } + /// External callback to be called when a watched property changes + @objc public var observerCallback: CameraPropertyCallback? + + /// Initialiser + /// - Parameter uniqueID: The UID of a camera object, as obtained from AVCaptureDevice.uniqueID @objc public init?(uniqueID: String) { guard let cameraDevice = AVCaptureDevice(uniqueID: uniqueID) else { return nil } self.camera = cameraDevice } - + + /// The camera's unique ID @objc public var uniqueID: String { camera.uniqueID } - + + /// The camera's model identifier @objc public var modelID: String { camera.modelID } - + + /// Human readable name of the camera @objc public var name: String { camera.localizedName } - + + /// The camera's manufacturer (often an empty string) @objc public var manufacturer: String { camera.manufacturer } - + + /// True if someone is using the camera, otherwise False @objc public var isInUse: Bool { - camera.isInUseByAnotherApplication + // Code taken from: https://github.com/wouterdebie/onair/blob/master/Sources/onair/Camera.swift + // (although this is pretty much exactly how Hammerspoon's earlier versions did the same thing in Objective C) + var (dataSize, dataUsed) = (UInt32(0), UInt32(0)) + if CMIOObjectGetPropertyDataSize(connectionID, &STATUS_PA, 0, nil, &dataSize) == OSStatus(kCMIOHardwareNoError) { + if let data = malloc(Int(dataSize)) { + CMIOObjectGetPropertyData(connectionID, &STATUS_PA, 0, nil, dataSize, &dataUsed, data) + return data.assumingMemoryBound(to: UInt8.self).pointee > 0 + } + } + return false } - + + /// Underlying interface by which the camera is connected @objc public var transportType: String { switch (camera.transportType) { case Int32(kIOAudioDeviceTransportTypeUSB): @@ -150,26 +184,54 @@ public typealias CameraPropertyCallback = @convention(block) (Camera, Bool) -> V return "Wireless" case Int32(kIOAudioDeviceTransportTypeNetwork): return "Network" + case Int32(kIOAudioDeviceTransportTypeFireWire): + return "Firewire" + case Int32(kIOAudioDeviceTransportTypeOther): + return "Other" + case Int32(kIOAudioDeviceTransportTypeBluetooth): + return "Bluetooth" + case Int32(kIOAudioDeviceTransportTypeDisplayPort): + return "DisplayPort" + case Int32(kIOAudioDeviceTransportTypeHdmi): + return "HDMI" + case Int32(kIOAudioDeviceTransportTypeAVB): + return "AVB" + case Int32(kIOAudioDeviceTransportTypeThunderbolt): + return "Thunderbolt" default: return "Unknown kIOAudioDeviceTransportType \(camera.transportType). Please file a bug." } } - @objc public var isInUseWatcherRunning: Bool { - isInUseObserver != nil - } - + /// Start watching `isInUse` for changes @objc public func startIsInUseWatcher() { - guard let observerCallback = self.observerCallback else { return } - if (isInUseObserver != nil) { return } + guard self.observerCallback != nil else { return } + if (isInUseWatcherRunning) { return } - isInUseObserver = observe(\.camera.isInUseByAnotherApplication, options: [], changeHandler: { object, _ in - observerCallback(self, self.camera.isInUseByAnotherApplication) - }) - } + if (self.isInUseWatcherCallbackBlock == nil) { + self.isInUseWatcherCallbackBlock = { [weak self] (_, _) -> Void in + self?.observerCallback?(self!) + } + } + let result = CMIOObjectAddPropertyListenerBlock(connectionID, &STATUS_PA, DispatchQueue.main, self.isInUseWatcherCallbackBlock!) + if (result == kCMIOHardwareNoError) { + isInUseWatcherRunning = true + } else { + NSLog("Unable to add property listener block: \(result) (\(name))") + } + } + + /// Stop watching `isInUse` @objc public func stopIsInUseWatcher() { - isInUseObserver?.invalidate() - isInUseObserver = nil + guard self.isInUseWatcherCallbackBlock != nil else { return } + if (!isInUseWatcherRunning) { return } + + let result = CMIOObjectRemovePropertyListenerBlock(connectionID, &STATUS_PA, DispatchQueue.main, self.isInUseWatcherCallbackBlock!) + if (result == kCMIOHardwareNoError) { + isInUseWatcherRunning = false + } else { + NSLog("Unable to remove property listener block: \(result) (\(name))") + } } } diff --git a/extensions/camera/libcamera.m b/extensions/camera/libcamera.m index 1d9df22bc..82952a726 100644 --- a/extensions/camera/libcamera.m +++ b/extensions/camera/libcamera.m @@ -7,6 +7,10 @@ static LSRefTable refTable; static const char *USERDATA_TAG = "hs.camera"; +typedef struct _HSuserData_t { + int callbackRef; + LSGCCanary lsCanary; +} HSuserData_t; //#pragma mark - Devices watcher declarations typedef struct _deviceWatcher_t { @@ -396,7 +400,6 @@ static int camera_isInUse(lua_State *L) { /// * gone - The device's "in use" status changed (ie another app started using the camera, or stopped using it) /// * (DEPRECATED) A string containing the scope of the event, this will likely always be "glob" /// * (DEPRECATED) A number containing the element of the event, this will likely always be "0" -/// * A boolean, true if the camera is now in use, otherwise false /// /// Returns: /// * The `hs.camera` object @@ -405,12 +408,14 @@ static int camera_propertyWatcherCallback(lua_State *L) { [skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TFUNCTION|LS_TNIL, LS_TBREAK]; Camera *camera = [skin toNSObjectAtIndex:1]; - camera.callbackRef = [skin luaUnref:refTable ref:camera.callbackRef]; + HSuserData_t *userData = camera.userData; + + userData->callbackRef = [skin luaUnref:refTable ref:userData->callbackRef]; switch (lua_type(L, 2)) { case LUA_TFUNCTION: lua_pushvalue(L, 2); - camera.callbackRef = [skin luaRef:refTable]; + userData->callbackRef = [skin luaRef:refTable]; break; case LUA_TNIL: [camera stopIsInUseWatcher]; @@ -437,27 +442,32 @@ static int camera_startPropertyWatcher(lua_State *L) { [skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TBREAK]; Camera *camera = [skin toNSObjectAtIndex:1]; + HSuserData_t *userData = camera.userData; - if (camera.callbackRef == LUA_NOREF) { + if (userData->callbackRef == LUA_NOREF) { [skin logError:@"You must call hs.camera:setPropertyWatcherCallback() before hs.camera:startPropertyWatcher()"]; lua_pushnil(L); return 1; } - camera.observerCallback = ^(Camera *camera, BOOL isInUse) { + camera.observerCallback = ^(Camera *device) { LuaSkin *skin = [LuaSkin sharedWithState:NULL]; - // FIXME: There is no canary checking here, because we don't have anywhere to store the canary + HSuserData_t *userData = device.userData; + + if (!userData || ![skin checkGCCanary:userData->lsCanary]) { + return; + } + _lua_stackguard_entry(skin.L); - if (camera.callbackRef == LUA_NOREF) { + if (userData->callbackRef == LUA_NOREF) { [skin logError:@"hs.camera property watcher fired, but no Lua callback is currently set"]; } else { - [skin pushLuaRef:refTable ref:camera.callbackRef]; - [skin pushNSObject:camera]; + [skin pushLuaRef:refTable ref:userData->callbackRef]; + [skin pushNSObject:device]; lua_pushstring(L, "gone"); lua_pushstring(L, "glob"); lua_pushinteger(L, 0); - lua_pushboolean(L, isInUse); - [skin protectedCallAndError:@"hs.camera:propertyWatcherCallback" nargs:5 nresults:0]; + [skin protectedCallAndError:@"hs.camera:propertyWatcherCallback" nargs:4 nresults:0]; } _lua_stackguard_exit(skin.L); }; @@ -507,7 +517,17 @@ static int camera_isPropertyWatcherRunning(lua_State *L) { #pragma mark - Lua<->NSObject Conversion Functions static int pushCamera(lua_State *L, id obj) { + LuaSkin *skin = [LuaSkin sharedWithState:L]; Camera *value = obj; + + // Check if Camera needs an HSuserdata_t + if (!value.userData) { + HSuserData_t *userData = malloc(sizeof(HSuserData_t)); + memset(userData, 0, sizeof(HSuserData_t)); + userData->callbackRef = LUA_NOREF; + userData->lsCanary = [skin createGCCanary]; + value.userData = userData; + } void** valuePtr = lua_newuserdata(L, sizeof(Camera *)); *valuePtr = (__bridge_retained void*)value; luaL_getmetatable(L, USERDATA_TAG); @@ -546,8 +566,26 @@ static int hsCamera_eq(lua_State *L) { return 1 ; } -// FIXME: Should we stop the camera's inUse watcher here? static int hsCamera_gc(lua_State *L) { + LuaSkin *skin = [LuaSkin sharedWithState:L]; + [skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TBREAK]; + Camera *camera = get_objectFromUserdata(__bridge_transfer Camera, L, 1, USERDATA_TAG); + + if (camera) { + if (camera.isInUseWatcherRunning) { + [camera stopIsInUseWatcher]; + } + } + + if (camera.userData) { + HSuserData_t *userData = camera.userData; + userData->callbackRef = [skin luaUnref:refTable ref:userData->callbackRef]; + [skin destroyGCCanary:&(userData->lsCanary)]; + free(camera.userData); + camera.userData = nil; + } + camera = nil; + lua_pushnil(L); lua_setmetatable(L, 1); return 0; diff --git a/extensions/math/libmath.m b/extensions/math/libmath.m index dc89d4f18..f00f25127 100644 --- a/extensions/math/libmath.m +++ b/extensions/math/libmath.m @@ -63,8 +63,6 @@ static int math_randomFromRange(lua_State* L) { [skin checkArgs:LS_TNUMBER, LS_TNUMBER, LS_TBREAK] ; Math *math = [[Math alloc] init]; - int start = (int)lua_tointeger(L, 1); - int end = (int)lua_tointeger(L, 2); @try { lua_pushinteger(L, [math randomIntInRangeWithStart:lua_tointeger(L, 1) end:lua_tointeger(L, 2)]); From 3328a675d99faba34db6740e2b83aebef985744f Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Thu, 21 Dec 2023 16:13:50 +0000 Subject: [PATCH 6/8] Tidying up --- extensions/camera/libcamera.m | 90 ++++------------------------------- 1 file changed, 9 insertions(+), 81 deletions(-) diff --git a/extensions/camera/libcamera.m b/extensions/camera/libcamera.m index 82952a726..a698ee768 100644 --- a/extensions/camera/libcamera.m +++ b/extensions/camera/libcamera.m @@ -7,6 +7,8 @@ static LSRefTable refTable; static const char *USERDATA_TAG = "hs.camera"; +//#pragma mark - Swift object userdata declaration +// FIXME: This should move out somewhere more generic, it will be needed for other extensions typedef struct _HSuserData_t { int callbackRef; LSGCCanary lsCanary; @@ -21,81 +23,6 @@ static CameraManager *cameraManager = nil; - -//- (void)startPropertyWatcher { -// if (self.propertyWatcherRunning == YES) { -// return; -// } -// -// CMIOObjectPropertyAddress propertyAddress = { -// 0, -// kAudioObjectPropertyScopeWildcard, -// kAudioObjectPropertyElementWildcard -// }; -// -// const int numSelectors = sizeof(propertyWatchSelectors) / sizeof(propertyWatchSelectors[0]); -// -// for (int i = 0; i < numSelectors; i++) { -// propertyAddress.mSelector = propertyWatchSelectors[i]; -// CMIOObjectAddPropertyListenerBlock(self.deviceId, -// &propertyAddress, -// dispatch_get_main_queue(), -// self.propertyWatcherBlock); -// } -// -// self.propertyWatcherRunning = YES; -//} -// -//- (void)stopPropertyWatcher { -// if (self.propertyWatcherRunning == NO) { -// return; -// } -// -// CMIOObjectPropertyAddress propertyAddress = { -// 0, -// kAudioObjectPropertyScopeWildcard, -// kAudioObjectPropertyElementWildcard -// }; -// -// const int numSelectors = sizeof(propertyWatchSelectors) / sizeof(propertyWatchSelectors[0]); -// -// for (int i = 0; i < numSelectors; i++) { -// propertyAddress.mSelector = propertyWatchSelectors[i]; -// CMIOObjectRemovePropertyListenerBlock(self.deviceId, -// &propertyAddress, -// dispatch_get_main_queue(), -// self.propertyWatcherBlock); -// } -// -// self.propertyWatcherRunning = NO; -//} - -//- (BOOL)getIsInUse { -// LuaSkin *skin = [LuaSkin sharedWithState:NULL]; -// OSStatus err; -// UInt32 dataSize = 0; -// UInt32 dataUsed = 0; -// UInt32 isInUse = 0; -// -// CMIOObjectPropertyAddress prop = {kCMIODevicePropertyDeviceIsRunningSomewhere, -// kCMIOObjectPropertyScopeWildcard, -// kCMIOObjectPropertyElementWildcard}; -// -// err = CMIOObjectGetPropertyDataSize(self.deviceId, &prop, 0, nil, &dataSize); -// if (err != kCMIOHardwareNoError) { -// [skin logError:[NSString stringWithFormat:@"getVideoDeviceIsUsed(): get data size error: %d", err]]; -// return NO; -// } -// -// err = CMIOObjectGetPropertyData(self.deviceId, &prop, 0, nil, dataSize, &dataUsed, &isInUse); -// if (err != kCMIOHardwareNoError) { -// [skin logError:[NSString stringWithFormat:@"getVideoDeviceIsUsed(): get data error: %d", err]]; -// return NO; -// } -// -// return isInUse; -//} - #pragma mark - Lua API /// hs.camera.allCameras() -> table /// Function @@ -547,14 +474,14 @@ static id toCameraFromLua(lua_State *L, int idx) { } #pragma mark - Core Lua metamethods -static int hsCamera_tostring(lua_State *L) { +static int camera_tostring(lua_State *L) { LuaSkin *skin = [LuaSkin sharedWithState:L]; Camera *camera = [skin toNSObjectAtIndex:1]; [skin pushNSObject:[NSString stringWithFormat:@"%s: (%@:%@)", USERDATA_TAG, camera.uniqueID, camera.name]]; return 1; } -static int hsCamera_eq(lua_State *L) { +static int camera_eq(lua_State *L) { if (luaL_testudata(L, 1, USERDATA_TAG) && luaL_testudata(L, 2, USERDATA_TAG)) { LuaSkin *skin = [LuaSkin sharedWithState:L] ; Camera *obj1 = [skin luaObjectAtIndex:1 toClass:"Camera"] ; @@ -566,7 +493,7 @@ static int hsCamera_eq(lua_State *L) { return 1 ; } -static int hsCamera_gc(lua_State *L) { +static int camera_gc(lua_State *L) { LuaSkin *skin = [LuaSkin sharedWithState:L]; [skin checkArgs:LS_TUSERDATA, USERDATA_TAG, LS_TBREAK]; Camera *camera = get_objectFromUserdata(__bridge_transfer Camera, L, 1, USERDATA_TAG); @@ -584,6 +511,7 @@ static int hsCamera_gc(lua_State *L) { free(camera.userData); camera.userData = nil; } + camera = nil; lua_pushnil(L); @@ -622,9 +550,9 @@ static int module_gc(lua_State *L) { {"stopPropertyWatcher", camera_stopPropertyWatcher}, {"isPropertyWatcherRunning", camera_isPropertyWatcherRunning}, - {"__tostring", hsCamera_tostring}, - {"__eq", hsCamera_eq}, - {"__gc", hsCamera_gc}, + {"__tostring", camera_tostring}, + {"__eq", camera_eq}, + {"__gc", camera_gc}, {NULL, NULL} }; From 8204bcd210132d9b43894e49c01e4f51710f2a92 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Mon, 25 Dec 2023 20:33:28 +0100 Subject: [PATCH 7/8] Fix a memory leak in Camera::isInUse() --- Hammertime/Camera.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Hammertime/Camera.swift b/Hammertime/Camera.swift index 950fc91e3..1d931a35b 100644 --- a/Hammertime/Camera.swift +++ b/Hammertime/Camera.swift @@ -163,7 +163,9 @@ public typealias CameraPropertyCallback = @convention(block) (Camera) -> Void if CMIOObjectGetPropertyDataSize(connectionID, &STATUS_PA, 0, nil, &dataSize) == OSStatus(kCMIOHardwareNoError) { if let data = malloc(Int(dataSize)) { CMIOObjectGetPropertyData(connectionID, &STATUS_PA, 0, nil, dataSize, &dataUsed, data) - return data.assumingMemoryBound(to: UInt8.self).pointee > 0 + let output = data.assumingMemoryBound(to: UInt8.self).pointee > 0 + free(data) + return output } } return false From ea66aa9d24fcb53281bef7ed94d3b7d8b87cb51f Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Mon, 1 Apr 2024 23:45:15 +0100 Subject: [PATCH 8/8] Switch from block based CoreMediaIO property watching, to Func based, because it seems to behave properly when removing the watcher --- Hammertime/Camera.swift | 30 ++++++++++----------- extensions/camera/libcamera.m | 50 ++++++++++++++++++++--------------- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/Hammertime/Camera.swift b/Hammertime/Camera.swift index 1d931a35b..94849c4da 100644 --- a/Hammertime/Camera.swift +++ b/Hammertime/Camera.swift @@ -102,8 +102,6 @@ public typealias CameraManagerDiscoveryCallback = @convention(block) (Camera?, S } } -public typealias CameraPropertyCallback = @convention(block) (Camera) -> Void - @objc public class Camera : NSObject { /// Underlying AVFoundation camera object we represent @objc var camera: AVCaptureDevice @@ -115,8 +113,7 @@ public typealias CameraPropertyCallback = @convention(block) (Camera) -> Void mScope: CMIOObjectPropertyScope(kCMIOObjectPropertyScopeWildcard), mElement: CMIOObjectPropertyElement(kCMIOObjectPropertyElementWildcard) ) - /// Internal callback block for CoreMediaIO Object Property Listener - private var isInUseWatcherCallbackBlock: CMIOObjectPropertyListenerBlock? + /// True if our `isInUse` watcher is running @objc public var isInUseWatcherRunning: Bool = false @@ -126,8 +123,11 @@ public typealias CameraPropertyCallback = @convention(block) (Camera) -> Void } /// External callback to be called when a watched property changes - @objc public var observerCallback: CameraPropertyCallback? - + @objc public var isInUseWatcherCallbackProc: CMIOObjectPropertyListenerProc = { _,_,_,_ -> OSStatus in + NSLog("Camera::isInUseWatcherCallbackProc called without initialisation") + return 0 + } + /// Initialiser /// - Parameter uniqueID: The UID of a camera object, as obtained from AVCaptureDevice.uniqueID @objc public init?(uniqueID: String) { @@ -207,16 +207,12 @@ public typealias CameraPropertyCallback = @convention(block) (Camera) -> Void /// Start watching `isInUse` for changes @objc public func startIsInUseWatcher() { - guard self.observerCallback != nil else { return } if (isInUseWatcherRunning) { return } - if (self.isInUseWatcherCallbackBlock == nil) { - self.isInUseWatcherCallbackBlock = { [weak self] (_, _) -> Void in - self?.observerCallback?(self!) - } - } - - let result = CMIOObjectAddPropertyListenerBlock(connectionID, &STATUS_PA, DispatchQueue.main, self.isInUseWatcherCallbackBlock!) + let result = CMIOObjectAddPropertyListener(connectionID, + &STATUS_PA, + self.isInUseWatcherCallbackProc, + Unmanaged.passUnretained(self).toOpaque()) if (result == kCMIOHardwareNoError) { isInUseWatcherRunning = true } else { @@ -226,10 +222,12 @@ public typealias CameraPropertyCallback = @convention(block) (Camera) -> Void /// Stop watching `isInUse` @objc public func stopIsInUseWatcher() { - guard self.isInUseWatcherCallbackBlock != nil else { return } if (!isInUseWatcherRunning) { return } - let result = CMIOObjectRemovePropertyListenerBlock(connectionID, &STATUS_PA, DispatchQueue.main, self.isInUseWatcherCallbackBlock!) + let result = CMIOObjectRemovePropertyListener(connectionID, + &STATUS_PA, + self.isInUseWatcherCallbackProc, + Unmanaged.passUnretained(self).toOpaque()) if (result == kCMIOHardwareNoError) { isInUseWatcherRunning = false } else { diff --git a/extensions/camera/libcamera.m b/extensions/camera/libcamera.m index a698ee768..51ca1c30f 100644 --- a/extensions/camera/libcamera.m +++ b/extensions/camera/libcamera.m @@ -1,6 +1,7 @@ @import Cocoa; @import LuaSkin; @import Hammertime; +@import CoreMediaIO; #pragma mark - Module declarations #define get_objectFromUserdata(objType, L, idx, tag) (objType*)*((void**)luaL_checkudata(L, idx, tag)) @@ -316,6 +317,30 @@ static int camera_isInUse(lua_State *L) { return 1; } +OSStatus propertyWatcherProc(unsigned int connectionID, unsigned int numAddresses, const struct CMIOObjectPropertyAddress *addresses, void *clientData) { + LuaSkin *skin = [LuaSkin shared]; + Camera *camera = (__bridge Camera *)clientData; + HSuserData_t *userData = camera.userData; + + if (!userData || ![skin checkGCCanary:userData->lsCanary]) { + return 0; + } + + _lua_stackguard_entry(skin.L); + if (userData->callbackRef == LUA_NOREF) { + [skin logError:@"hs.camera property watcher fired, but no Lua callback is currently set"]; + } else { + [skin pushLuaRef:refTable ref:userData->callbackRef]; + [skin pushNSObject:camera]; + lua_pushstring(skin.L, "gone"); + lua_pushstring(skin.L, "glob"); + lua_pushinteger(skin.L, 0); + [skin protectedCallAndError:@"hs.camera:propertyWatcherCallback" nargs:4 nresults:0]; + } + _lua_stackguard_exit(skin.L); + return 0; +} + /// hs.camera:setPropertyWatcherCallback(fn) -> hs.camera object /// Method /// Sets or clears a callback for when an hs.camera object starts or stops being used by another application @@ -337,6 +362,9 @@ static int camera_propertyWatcherCallback(lua_State *L) { Camera *camera = [skin toNSObjectAtIndex:1]; HSuserData_t *userData = camera.userData; + // Set our callback handler + camera.isInUseWatcherCallbackProc = propertyWatcherProc; + userData->callbackRef = [skin luaUnref:refTable ref:userData->callbackRef]; switch (lua_type(L, 2)) { @@ -376,28 +404,6 @@ static int camera_startPropertyWatcher(lua_State *L) { lua_pushnil(L); return 1; } - - camera.observerCallback = ^(Camera *device) { - LuaSkin *skin = [LuaSkin sharedWithState:NULL]; - HSuserData_t *userData = device.userData; - - if (!userData || ![skin checkGCCanary:userData->lsCanary]) { - return; - } - - _lua_stackguard_entry(skin.L); - if (userData->callbackRef == LUA_NOREF) { - [skin logError:@"hs.camera property watcher fired, but no Lua callback is currently set"]; - } else { - [skin pushLuaRef:refTable ref:userData->callbackRef]; - [skin pushNSObject:device]; - lua_pushstring(L, "gone"); - lua_pushstring(L, "glob"); - lua_pushinteger(L, 0); - [skin protectedCallAndError:@"hs.camera:propertyWatcherCallback" nargs:4 nresults:0]; - } - _lua_stackguard_exit(skin.L); - }; [camera startIsInUseWatcher]; lua_pushvalue(L, 1);