diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2d53e62 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012-2020 Members of the Raising the Floor Consortium. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may +be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/MorphicMacOS.xcodeproj/project.pbxproj b/MorphicMacOS.xcodeproj/project.pbxproj new file mode 100644 index 0000000..8bb8e89 --- /dev/null +++ b/MorphicMacOS.xcodeproj/project.pbxproj @@ -0,0 +1,532 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 9D2EE02623E88F3500F2F1D1 /* NAPIValueCompatible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D2EE02523E88F3500F2F1D1 /* NAPIValueCompatible.swift */; }; + 9D2EE02A23E8969700F2F1D1 /* NAPIValueType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D2EE02923E8969700F2F1D1 /* NAPIValueType.swift */; }; + 9D2EE03423E8FC9F00F2F1D1 /* NAPIFunctionHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D2EE03323E8FC9F00F2F1D1 /* NAPIFunctionHelpers.swift */; }; + 9D2EE03623E908AB00F2F1D1 /* NAPIJavaScriptError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D2EE03523E908AB00F2F1D1 /* NAPIJavaScriptError.swift */; }; + 9D41E89F240C755D004A8E98 /* NAPIObjectCompatible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D41E89E240C755D004A8E98 /* NAPIObjectCompatible.swift */; }; + 9D540F41241468AA002CBBB7 /* NAPIBridgingCoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D540F40241468AA002CBBB7 /* NAPIBridgingCoding.swift */; }; + 9D540F43241468B1002CBBB7 /* NAPISwiftBridgeJavaScriptThrowableError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D540F42241468B1002CBBB7 /* NAPISwiftBridgeJavaScriptThrowableError.swift */; }; + 9D683E5724147A9C00411E0D /* NAPIJavaScriptFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D683E5624147A9C00411E0D /* NAPIJavaScriptFunction.swift */; }; + 9DAB111F2411E25C00EDA979 /* MorphicInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DAB111E2411E25C00EDA979 /* MorphicInput.swift */; }; + 9DAB11212411E26600EDA979 /* NAPIInputFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DAB11202411E26600EDA979 /* NAPIInputFunctions.swift */; }; + 9DAB11292411E7A200EDA979 /* MorphicWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DAB11282411E7A200EDA979 /* MorphicWindow.swift */; }; + 9DAB112B2411E7AA00EDA979 /* NAPIWindowFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DAB112A2411E7AA00EDA979 /* NAPIWindowFunctions.swift */; }; + 9DC1604723FB7BCD003A3DB0 /* NAPIAudioFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DC1604623FB7BCD003A3DB0 /* NAPIAudioFunctions.swift */; }; + 9DC1605123FB7CB3003A3DB0 /* MorphicAudio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DC1605023FB7CB3003A3DB0 /* MorphicAudio.swift */; }; + 9DCC462624045CFB00224358 /* NAPIDisplayFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DCC462524045CFB00224358 /* NAPIDisplayFunctions.swift */; }; + 9DCC462A24045D3F00224358 /* MorphicDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DCC462924045D3F00224358 /* MorphicDisplay.swift */; }; + 9DCE644623BA970200243A18 /* MorphicMacOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 9DCE644523BA970200243A18 /* MorphicMacOS.h */; }; + 9DCE644823BA970200243A18 /* MorphicMacOS.m in Sources */ = {isa = PBXBuildFile; fileRef = 9DCE644723BA970200243A18 /* MorphicMacOS.m */; }; + 9DCE645023BA982D00243A18 /* Main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DCE644F23BA982D00243A18 /* Main.swift */; }; + 9DCE646223BAA8A300243A18 /* main.c in Sources */ = {isa = PBXBuildFile; fileRef = 9DCE646123BAA8A300243A18 /* main.c */; }; + 9DCE646623BD863900243A18 /* NAPIProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DCE646523BD863900243A18 /* NAPIProperty.swift */; }; + 9DCE646823BD8DBE00243A18 /* NAPIValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DCE646723BD8DBE00243A18 /* NAPIValue.swift */; }; + 9DE3F77E23FCD18400634AA2 /* MorphicProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE3F77D23FCD18400634AA2 /* MorphicProcess.swift */; }; + 9DE3F78223FCD59600634AA2 /* MorphicLaunchDaemonsAndAgents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE3F78123FCD59600634AA2 /* MorphicLaunchDaemonsAndAgents.swift */; }; + 9DE3F78623FCD92700634AA2 /* NAPIProcessFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE3F78523FCD92700634AA2 /* NAPIProcessFunctions.swift */; }; + 9DE3F78C23FCF4F000634AA2 /* MorphicLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE3F78B23FCF4F000634AA2 /* MorphicLanguage.swift */; }; + 9DE3F79023FCFA5A00634AA2 /* NAPILanguageFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE3F78F23FCFA5A00634AA2 /* NAPILanguageFunctions.swift */; }; + 9DFCE56123F628B100300EC4 /* MorphicDisk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DFCE56023F628B100300EC4 /* MorphicDisk.swift */; }; + CE4DEBD623F65E8000519CD9 /* NAPIDiskFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4DEBD523F65E8000519CD9 /* NAPIDiskFunctions.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 9D2EE02523E88F3500F2F1D1 /* NAPIValueCompatible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAPIValueCompatible.swift; sourceTree = ""; }; + 9D2EE02923E8969700F2F1D1 /* NAPIValueType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAPIValueType.swift; sourceTree = ""; }; + 9D2EE03323E8FC9F00F2F1D1 /* NAPIFunctionHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAPIFunctionHelpers.swift; sourceTree = ""; }; + 9D2EE03523E908AB00F2F1D1 /* NAPIJavaScriptError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAPIJavaScriptError.swift; sourceTree = ""; }; + 9D41E89E240C755D004A8E98 /* NAPIObjectCompatible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAPIObjectCompatible.swift; sourceTree = ""; }; + 9D540F40241468AA002CBBB7 /* NAPIBridgingCoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAPIBridgingCoding.swift; sourceTree = ""; }; + 9D540F42241468B1002CBBB7 /* NAPISwiftBridgeJavaScriptThrowableError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAPISwiftBridgeJavaScriptThrowableError.swift; sourceTree = ""; }; + 9D683E5624147A9C00411E0D /* NAPIJavaScriptFunction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAPIJavaScriptFunction.swift; sourceTree = ""; }; + 9DAB111E2411E25C00EDA979 /* MorphicInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MorphicInput.swift; sourceTree = ""; }; + 9DAB11202411E26600EDA979 /* NAPIInputFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAPIInputFunctions.swift; sourceTree = ""; }; + 9DAB11282411E7A200EDA979 /* MorphicWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MorphicWindow.swift; sourceTree = ""; }; + 9DAB112A2411E7AA00EDA979 /* NAPIWindowFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAPIWindowFunctions.swift; sourceTree = ""; }; + 9DC1604623FB7BCD003A3DB0 /* NAPIAudioFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAPIAudioFunctions.swift; sourceTree = ""; }; + 9DC1605023FB7CB3003A3DB0 /* MorphicAudio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MorphicAudio.swift; sourceTree = ""; }; + 9DCC462524045CFB00224358 /* NAPIDisplayFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAPIDisplayFunctions.swift; sourceTree = ""; }; + 9DCC462924045D3F00224358 /* MorphicDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MorphicDisplay.swift; sourceTree = ""; }; + 9DCE644223BA970200243A18 /* libMorphicMacOS.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libMorphicMacOS.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; + 9DCE644523BA970200243A18 /* MorphicMacOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MorphicMacOS.h; sourceTree = ""; }; + 9DCE644723BA970200243A18 /* MorphicMacOS.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MorphicMacOS.m; sourceTree = ""; }; + 9DCE644E23BA982C00243A18 /* MorphicMacOS-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MorphicMacOS-Bridging-Header.h"; sourceTree = ""; }; + 9DCE644F23BA982D00243A18 /* Main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Main.swift; sourceTree = ""; }; + 9DCE645A23BA9DB700243A18 /* node_api.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = node_api.h; sourceTree = ""; }; + 9DCE645B23BA9DBF00243A18 /* node_api_types.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = node_api_types.h; sourceTree = ""; }; + 9DCE645E23BAA1CC00243A18 /* js_native_api.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = js_native_api.h; sourceTree = ""; }; + 9DCE645F23BAA20200243A18 /* js_native_api_types.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = js_native_api_types.h; sourceTree = ""; }; + 9DCE646123BAA8A300243A18 /* main.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = main.c; sourceTree = ""; }; + 9DCE646523BD863900243A18 /* NAPIProperty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAPIProperty.swift; sourceTree = ""; }; + 9DCE646723BD8DBE00243A18 /* NAPIValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAPIValue.swift; sourceTree = ""; }; + 9DE3F77D23FCD18400634AA2 /* MorphicProcess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MorphicProcess.swift; sourceTree = ""; }; + 9DE3F78123FCD59600634AA2 /* MorphicLaunchDaemonsAndAgents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MorphicLaunchDaemonsAndAgents.swift; sourceTree = ""; }; + 9DE3F78523FCD92700634AA2 /* NAPIProcessFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAPIProcessFunctions.swift; sourceTree = ""; }; + 9DE3F78B23FCF4F000634AA2 /* MorphicLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MorphicLanguage.swift; sourceTree = ""; }; + 9DE3F78F23FCFA5A00634AA2 /* NAPILanguageFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAPILanguageFunctions.swift; sourceTree = ""; }; + 9DFCE56023F628B100300EC4 /* MorphicDisk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MorphicDisk.swift; sourceTree = ""; }; + CE4DEBD523F65E8000519CD9 /* NAPIDiskFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NAPIDiskFunctions.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 9DCE644023BA970200243A18 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9D540F3F24146892002CBBB7 /* NAPIBridging */ = { + isa = PBXGroup; + children = ( + 9D540F40241468AA002CBBB7 /* NAPIBridgingCoding.swift */, + 9D540F42241468B1002CBBB7 /* NAPISwiftBridgeJavaScriptThrowableError.swift */, + ); + path = NAPIBridging; + sourceTree = ""; + }; + 9DAB111D2411E25100EDA979 /* Input */ = { + isa = PBXGroup; + children = ( + 9DAB111E2411E25C00EDA979 /* MorphicInput.swift */, + ); + path = Input; + sourceTree = ""; + }; + 9DAB11272411E79000EDA979 /* Window */ = { + isa = PBXGroup; + children = ( + 9DAB11282411E7A200EDA979 /* MorphicWindow.swift */, + ); + path = Window; + sourceTree = ""; + }; + 9DC1604823FB7BE6003A3DB0 /* Audio */ = { + isa = PBXGroup; + children = ( + 9DC1605023FB7CB3003A3DB0 /* MorphicAudio.swift */, + ); + path = Audio; + sourceTree = ""; + }; + 9DCC462124045CB200224358 /* Display */ = { + isa = PBXGroup; + children = ( + 9DCC462924045D3F00224358 /* MorphicDisplay.swift */, + ); + path = Display; + sourceTree = ""; + }; + 9DCE643923BA970200243A18 = { + isa = PBXGroup; + children = ( + 9DCE644423BA970200243A18 /* MorphicMacOS */, + 9DCE644323BA970200243A18 /* Products */, + ); + sourceTree = ""; + }; + 9DCE644323BA970200243A18 /* Products */ = { + isa = PBXGroup; + children = ( + 9DCE644223BA970200243A18 /* libMorphicMacOS.dylib */, + ); + name = Products; + sourceTree = ""; + }; + 9DCE644423BA970200243A18 /* MorphicMacOS */ = { + isa = PBXGroup; + children = ( + 9DFCE55923F6275800300EC4 /* BridgeFunctions */, + 9DFCE55C23F627AB00300EC4 /* NativeClasses */, + 9DCE645423BA99E500243A18 /* N-API */, + 9DCE644523BA970200243A18 /* MorphicMacOS.h */, + 9DCE644723BA970200243A18 /* MorphicMacOS.m */, + 9DCE646123BAA8A300243A18 /* main.c */, + 9DCE644F23BA982D00243A18 /* Main.swift */, + 9DCE644E23BA982C00243A18 /* MorphicMacOS-Bridging-Header.h */, + ); + path = MorphicMacOS; + sourceTree = ""; + }; + 9DCE645423BA99E500243A18 /* N-API */ = { + isa = PBXGroup; + children = ( + 9D540F3F24146892002CBBB7 /* NAPIBridging */, + 9DCE645523BA99EE00243A18 /* include */, + 9D2EE03323E8FC9F00F2F1D1 /* NAPIFunctionHelpers.swift */, + 9D2EE03523E908AB00F2F1D1 /* NAPIJavaScriptError.swift */, + 9D683E5624147A9C00411E0D /* NAPIJavaScriptFunction.swift */, + 9D41E89E240C755D004A8E98 /* NAPIObjectCompatible.swift */, + 9DCE646523BD863900243A18 /* NAPIProperty.swift */, + 9DCE646723BD8DBE00243A18 /* NAPIValue.swift */, + 9D2EE02923E8969700F2F1D1 /* NAPIValueType.swift */, + 9D2EE02523E88F3500F2F1D1 /* NAPIValueCompatible.swift */, + ); + path = "N-API"; + sourceTree = ""; + }; + 9DCE645523BA99EE00243A18 /* include */ = { + isa = PBXGroup; + children = ( + 9DCE645E23BAA1CC00243A18 /* js_native_api.h */, + 9DCE645F23BAA20200243A18 /* js_native_api_types.h */, + 9DCE645A23BA9DB700243A18 /* node_api.h */, + 9DCE645B23BA9DBF00243A18 /* node_api_types.h */, + ); + path = include; + sourceTree = ""; + }; + 9DE3F77923FCD14500634AA2 /* Process */ = { + isa = PBXGroup; + children = ( + 9DE3F77D23FCD18400634AA2 /* MorphicProcess.swift */, + 9DE3F78123FCD59600634AA2 /* MorphicLaunchDaemonsAndAgents.swift */, + ); + path = Process; + sourceTree = ""; + }; + 9DE3F78A23FCF4E700634AA2 /* Language */ = { + isa = PBXGroup; + children = ( + 9DE3F78B23FCF4F000634AA2 /* MorphicLanguage.swift */, + ); + path = Language; + sourceTree = ""; + }; + 9DFCE55923F6275800300EC4 /* BridgeFunctions */ = { + isa = PBXGroup; + children = ( + 9DC1604623FB7BCD003A3DB0 /* NAPIAudioFunctions.swift */, + CE4DEBD523F65E8000519CD9 /* NAPIDiskFunctions.swift */, + 9DCC462524045CFB00224358 /* NAPIDisplayFunctions.swift */, + 9DAB11202411E26600EDA979 /* NAPIInputFunctions.swift */, + 9DE3F78F23FCFA5A00634AA2 /* NAPILanguageFunctions.swift */, + 9DE3F78523FCD92700634AA2 /* NAPIProcessFunctions.swift */, + 9DAB112A2411E7AA00EDA979 /* NAPIWindowFunctions.swift */, + ); + path = BridgeFunctions; + sourceTree = ""; + }; + 9DFCE55C23F627AB00300EC4 /* NativeClasses */ = { + isa = PBXGroup; + children = ( + 9DC1604823FB7BE6003A3DB0 /* Audio */, + 9DFCE55D23F6282C00300EC4 /* Disk */, + 9DCC462124045CB200224358 /* Display */, + 9DAB111D2411E25100EDA979 /* Input */, + 9DE3F78A23FCF4E700634AA2 /* Language */, + 9DE3F77923FCD14500634AA2 /* Process */, + 9DAB11272411E79000EDA979 /* Window */, + ); + path = NativeClasses; + sourceTree = ""; + }; + 9DFCE55D23F6282C00300EC4 /* Disk */ = { + isa = PBXGroup; + children = ( + 9DFCE56023F628B100300EC4 /* MorphicDisk.swift */, + ); + path = Disk; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 9DCE643E23BA970200243A18 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 9DCE644623BA970200243A18 /* MorphicMacOS.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 9DCE644123BA970200243A18 /* MorphicMacOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 9DCE644B23BA970200243A18 /* Build configuration list for PBXNativeTarget "MorphicMacOS" */; + buildPhases = ( + 9DCE643E23BA970200243A18 /* Headers */, + 9DCE643F23BA970200243A18 /* Sources */, + 9DCE644023BA970200243A18 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MorphicMacOS; + productName = MorphicMacOS; + productReference = 9DCE644223BA970200243A18 /* libMorphicMacOS.dylib */; + productType = "com.apple.product-type.library.dynamic"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 9DCE643A23BA970200243A18 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1130; + ORGANIZATIONNAME = "Raising the Floor -- US Inc."; + TargetAttributes = { + 9DCE644123BA970200243A18 = { + CreatedOnToolsVersion = 11.3; + LastSwiftMigration = 1130; + }; + }; + }; + buildConfigurationList = 9DCE643D23BA970200243A18 /* Build configuration list for PBXProject "MorphicMacOS" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 9DCE643923BA970200243A18; + productRefGroup = 9DCE644323BA970200243A18 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 9DCE644123BA970200243A18 /* MorphicMacOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 9DCE643F23BA970200243A18 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9D41E89F240C755D004A8E98 /* NAPIObjectCompatible.swift in Sources */, + 9D540F41241468AA002CBBB7 /* NAPIBridgingCoding.swift in Sources */, + 9DE3F77E23FCD18400634AA2 /* MorphicProcess.swift in Sources */, + 9DE3F79023FCFA5A00634AA2 /* NAPILanguageFunctions.swift in Sources */, + 9D683E5724147A9C00411E0D /* NAPIJavaScriptFunction.swift in Sources */, + 9DCE646623BD863900243A18 /* NAPIProperty.swift in Sources */, + 9DAB112B2411E7AA00EDA979 /* NAPIWindowFunctions.swift in Sources */, + 9D2EE03423E8FC9F00F2F1D1 /* NAPIFunctionHelpers.swift in Sources */, + 9DCE645023BA982D00243A18 /* Main.swift in Sources */, + 9D2EE03623E908AB00F2F1D1 /* NAPIJavaScriptError.swift in Sources */, + 9DAB111F2411E25C00EDA979 /* MorphicInput.swift in Sources */, + 9DE3F78623FCD92700634AA2 /* NAPIProcessFunctions.swift in Sources */, + 9DCC462A24045D3F00224358 /* MorphicDisplay.swift in Sources */, + 9DE3F78223FCD59600634AA2 /* MorphicLaunchDaemonsAndAgents.swift in Sources */, + 9DAB11212411E26600EDA979 /* NAPIInputFunctions.swift in Sources */, + CE4DEBD623F65E8000519CD9 /* NAPIDiskFunctions.swift in Sources */, + 9D540F43241468B1002CBBB7 /* NAPISwiftBridgeJavaScriptThrowableError.swift in Sources */, + 9DCE644823BA970200243A18 /* MorphicMacOS.m in Sources */, + 9DC1605123FB7CB3003A3DB0 /* MorphicAudio.swift in Sources */, + 9DC1604723FB7BCD003A3DB0 /* NAPIAudioFunctions.swift in Sources */, + 9DAB11292411E7A200EDA979 /* MorphicWindow.swift in Sources */, + 9DE3F78C23FCF4F000634AA2 /* MorphicLanguage.swift in Sources */, + 9DCC462624045CFB00224358 /* NAPIDisplayFunctions.swift in Sources */, + 9DCE646823BD8DBE00243A18 /* NAPIValue.swift in Sources */, + 9D2EE02623E88F3500F2F1D1 /* NAPIValueCompatible.swift in Sources */, + 9DFCE56123F628B100300EC4 /* MorphicDisk.swift in Sources */, + 9D2EE02A23E8969700F2F1D1 /* NAPIValueType.swift in Sources */, + 9DCE646223BAA8A300243A18 /* main.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 9DCE644923BA970200243A18 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + 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_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; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = Debug; + }; + 9DCE644A23BA970200243A18 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + 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_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; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + }; + name = Release; + }; + 9DCE644C23BA970200243A18 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + EXECUTABLE_PREFIX = lib; + INSTALL_PATH = "@rpath"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.11; + OTHER_LDFLAGS = ( + "-undefined", + dynamic_lookup, + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "MorphicMacOS/MorphicMacOS-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; + }; + name = Debug; + }; + 9DCE644D23BA970200243A18 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + EXECUTABLE_PREFIX = lib; + INSTALL_PATH = "@rpath"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.11; + OTHER_LDFLAGS = ( + "-undefined", + dynamic_lookup, + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "MorphicMacOS/MorphicMacOS-Bridging-Header.h"; + SWIFT_VERSION = 4.2; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 9DCE643D23BA970200243A18 /* Build configuration list for PBXProject "MorphicMacOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9DCE644923BA970200243A18 /* Debug */, + 9DCE644A23BA970200243A18 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 9DCE644B23BA970200243A18 /* Build configuration list for PBXNativeTarget "MorphicMacOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9DCE644C23BA970200243A18 /* Debug */, + 9DCE644D23BA970200243A18 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 9DCE643A23BA970200243A18 /* Project object */; +} diff --git a/MorphicMacOS/BridgeFunctions/NAPIAudioFunctions.swift b/MorphicMacOS/BridgeFunctions/NAPIAudioFunctions.swift new file mode 100644 index 0000000..49bd67d --- /dev/null +++ b/MorphicMacOS/BridgeFunctions/NAPIAudioFunctions.swift @@ -0,0 +1,93 @@ +// +// NAPIAudioFunctions.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +class NAPIAudioFunctions { + // MARK: - Swift NAPI bridge setup + + static func getFunctionsAsPropertyDescriptors(cNapiEnv: napi_env!) -> [napi_property_descriptor] { + var result: [napi_property_descriptor] = [] + + // getAudioVolume + result.append(NAPIProperty.createMethodProperty(cNapiEnv: cNapiEnv, name: "getAudioVolume", method: getAudioVolume).cNapiPropertyDescriptor) + + // setAudioVolume + result.append(NAPIProperty.createMethodProperty(cNapiEnv: cNapiEnv, name: "setAudioVolume", method: setAudioVolume).cNapiPropertyDescriptor) + + // getAudioMuteState + result.append(NAPIProperty.createMethodProperty(cNapiEnv: cNapiEnv, name: "getAudioMuteState", method: getAudioMuteState).cNapiPropertyDescriptor) + + // setAudioMuteState + result.append(NAPIProperty.createMethodProperty(cNapiEnv: cNapiEnv, name: "setAudioMuteState", method: setAudioMuteState).cNapiPropertyDescriptor) + + return result + } + + // MARK: - Swift NAPI bridge functions + + public static func getAudioVolume() throws -> Double { + guard let defaultAudioOutputDeviceId = MorphicAudio.getDefaultAudioDeviceId() else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not get default audio device id") + } + + // get the current volume + guard let volume = MorphicAudio.getVolume(for: defaultAudioOutputDeviceId) else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not get volume of default audio device") + } + + return Double(volume) + } + + public static func getAudioMuteState() throws -> Bool { + guard let defaultAudioOutputDeviceId = MorphicAudio.getDefaultAudioDeviceId() else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not get default audio device id") + } + + // also get the mute state + guard let muteState = MorphicAudio.getMuteState(for: defaultAudioOutputDeviceId) else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not get mute state of default audio device") + } + + return muteState + } + + public static func setAudioVolume(value: Double) throws { + guard let defaultAudioOutputDeviceId = MorphicAudio.getDefaultAudioDeviceId() else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not get default audio device id") + } + + do { + try MorphicAudio.setVolume(for: defaultAudioOutputDeviceId, volume: Float(value)) + } catch MorphicAudio.MorphicAudioError.propertyUnavailable { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not find 'volume' property") + } catch MorphicAudio.MorphicAudioError.cannotSetProperty { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not set 'volume' property") + } catch MorphicAudio.MorphicAudioError.coreAudioError(let error) { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "CoreAudio error: OSStatus(\(error))") + } + } + + public static func setAudioMuteState(muteState: Bool) throws { + guard let defaultAudioOutputDeviceId = MorphicAudio.getDefaultAudioDeviceId() else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not get default audio device id") + } + + do { + try MorphicAudio.setMuteState(for: defaultAudioOutputDeviceId, muteState: muteState) + } catch MorphicAudio.MorphicAudioError.propertyUnavailable { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not find 'mute state' property") + } catch MorphicAudio.MorphicAudioError.cannotSetProperty { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not set 'mute state' property") + } catch MorphicAudio.MorphicAudioError.coreAudioError(let error) { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "CoreAudio error: OSStatus(\(error))") + } + } +} diff --git a/MorphicMacOS/BridgeFunctions/NAPIDiskFunctions.swift b/MorphicMacOS/BridgeFunctions/NAPIDiskFunctions.swift new file mode 100644 index 0000000..9de1e26 --- /dev/null +++ b/MorphicMacOS/BridgeFunctions/NAPIDiskFunctions.swift @@ -0,0 +1,84 @@ +// +// NAPIDiskFunctions.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +class NAPIDiskFunctions { + // MARK: - Swift NAPI bridge setup + + static func getFunctionsAsPropertyDescriptors(cNapiEnv: napi_env!) -> [napi_property_descriptor] { + var result: [napi_property_descriptor] = [] + + // getAllUsbDriveMountPaths + result.append(NAPIProperty.createMethodProperty(cNapiEnv: cNapiEnv, name: "getAllUsbDriveMountPaths", method: getAllUsbDriveMountPaths).cNapiPropertyDescriptor) + + // openDirectories + result.append(NAPIProperty.createMethodProperty(cNapiEnv: cNapiEnv, name: "openDirectories", method: openDirectories).cNapiPropertyDescriptor) + + // safelyEjectUsbDrives + result.append(NAPIProperty.createMethodProperty(cNapiEnv: cNapiEnv, name: "safelyEjectUsbDrives", method: safelyEjectUsbDrives).cNapiPropertyDescriptor) + + return result + } + + // MARK: - Swift NAPI bridge functions + + public static func getAllUsbDriveMountPaths() throws -> [String] { + guard let result = MorphicDisk.getAllUsbDriveMountPaths() else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not retrieve a list of all USB drive mount paths") + } + + return result + } + + public static func openDirectories(_ paths: [String]) { + // open directory paths using Finder + for path in paths { + MorphicDisk.openDirectory(path: path) + } + } + + public static func safelyEjectUsbDrives(_ usbDriveMountingPaths: [String], _ callback: NAPIJavaScriptFunction?) throws { + let numberOfDisks = usbDriveMountingPaths.count + var numberOfDiskEjectsAttempted = 0 + var failedMountPaths: [String] = [] + + // unmount and eject disk using disk arbitration + for mountPath in usbDriveMountingPaths { + do { + try MorphicDisk.ejectDisk(mountPath: mountPath) { + ejectedDiskPath, success in + // + numberOfDiskEjectsAttempted += 1 + // + if success == true { + // we have ejected the disk at mount path: 'ejectedDiskPath' + } else { + // we failed to eject the disk at mount path: 'ejectedDiskPath' + failedMountPaths.append(mountPath) + } + + if numberOfDiskEjectsAttempted == numberOfDisks { + // if a callback was provided, call it with success/failure (and an array of all mounting paths which failed) + if failedMountPaths.count == 0 { + callback?.call(args: [true, Array?(nil)]) + } else { + callback?.call(args: [false, failedMountPaths]) + } + } + } + } catch MorphicDisk.EjectDiskError.volumeNotFound { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Failed to eject the disk at mount path: \(mountPath); volume was not found") + } catch MorphicDisk.EjectDiskError.otherError { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Failed to eject the disk at mount path: \(mountPath); misc. error encountered") + } + } + } +} diff --git a/MorphicMacOS/BridgeFunctions/NAPIDisplayFunctions.swift b/MorphicMacOS/BridgeFunctions/NAPIDisplayFunctions.swift new file mode 100644 index 0000000..cde2161 --- /dev/null +++ b/MorphicMacOS/BridgeFunctions/NAPIDisplayFunctions.swift @@ -0,0 +1,165 @@ +// +// NAPIDiplayFunctions.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +class NAPIDisplayFunctions { + // MARK: - Swift NAPI bridge setup + + static func getFunctionsAsPropertyDescriptors(cNapiEnv: napi_env!) -> [napi_property_descriptor] { + var result: [napi_property_descriptor] = [] + + // getDisplayModes + result.append(NAPIProperty.createMethodProperty(cNapiEnv: cNapiEnv, name: "getAllDisplayModes", method: getAllDisplayModes).cNapiPropertyDescriptor) + + // getCurrentDisplayMode + result.append(NAPIProperty.createMethodProperty(cNapiEnv: cNapiEnv, name: "getCurrentDisplayMode", method: getCurrentDisplayMode).cNapiPropertyDescriptor) + + // setCurrentDisplayMode + result.append(NAPIProperty.createMethodProperty(cNapiEnv: cNapiEnv, name: "setCurrentDisplayMode", method: setCurrentDisplayMode).cNapiPropertyDescriptor) + + return result + } + + // MARK: - Swift NAPI bridge functions + + public struct NAPIDisplayMode: NAPIObjectCompatible { + let ioDisplayModeId: Double // Int32 + let widthInPixels: Double // Int (Int32/Int64) + let heightInPixels: Double // Int (Int32/Int64) + let widthInVirtualPixels: Double // Int (Int32/Int64) + let heightInVirtualPixels: Double // Int (Int32/Int64) + let refreshRateInHertz: Double? + let isUsableForDesktopGui: Bool // NOTE: we can use this flag, in theory, to limit the resolutions we provide to user + + static var NAPIPropertyCodingKeysAndTypes: [(propertyKey: CodingKey, type: NAPIValueType)] = + [ + (propertyKey: CodingKeys.ioDisplayModeId, type: .number), + (propertyKey: CodingKeys.widthInPixels, type: .number), + (propertyKey: CodingKeys.heightInPixels, type: .number), + (propertyKey: CodingKeys.widthInVirtualPixels, type: .number), + (propertyKey: CodingKeys.heightInVirtualPixels, type: .number), + // TODO: we should probably change "type" to "wrappedType" (in the .nullable definition) and then maybe make it optional to write + (propertyKey: CodingKeys.refreshRateInHertz, type: .nullable(type: .number)), + (propertyKey: CodingKeys.isUsableForDesktopGui, type: .boolean) + ] + + init(displayMode: MorphicDisplay.DisplayMode) { + self.ioDisplayModeId = Double(exactly: displayMode.ioDisplayModeId)! + // + guard let widthInPixelsAsDouble = Double(exactly: displayMode.widthInPixels) else { + fatalError("widthInPixels cannot be represented as a 64-bit floating point value") + } + self.widthInPixels = widthInPixelsAsDouble + // + guard let heightInPixelsAsDouble = Double(exactly: displayMode.heightInPixels) else { + fatalError("heightInPixels cannot be represented as a 64-bit floating point value") + } + self.heightInPixels = heightInPixelsAsDouble + // + guard let widthInVirtualPixelsAsDouble = Double(exactly: displayMode.widthInVirtualPixels) else { + fatalError("widthInVirtualPixels cannot be represented as a 64-bit floating point value") + } + self.widthInVirtualPixels = widthInVirtualPixelsAsDouble + // + guard let heightInVirtualPixelsAsDouble = Double(exactly: displayMode.heightInVirtualPixels) else { + fatalError("heightInVirtualPixels cannot be represented as a 64-bit floating point value") + } + self.heightInVirtualPixels = heightInVirtualPixelsAsDouble + // + self.refreshRateInHertz = displayMode.refreshRateInHertz + // + self.isUsableForDesktopGui = displayMode.isUsableForDesktopGui + } + } + + public static func getAllDisplayModes() throws -> [NAPIDisplayMode] { + guard let mainDisplayId = MorphicDisplay.getMainDisplayId() else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not get main display id") + } + + guard let allDisplayModes = MorphicDisplay.getAllDisplayModes(for: mainDisplayId) else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not get list of display modes for main display") + } + + var napiDisplayModes: [NAPIDisplayMode] = [] + napiDisplayModes.reserveCapacity(allDisplayModes.count) + for displayMode in allDisplayModes { + let napiDisplayMode = NAPIDisplayMode(displayMode: displayMode) + napiDisplayModes.append(napiDisplayMode) + } + + return napiDisplayModes + } + + public static func getCurrentDisplayMode() throws -> NAPIDisplayMode { + guard let mainDisplayId = MorphicDisplay.getMainDisplayId() else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not get main display id") + } + + guard let currentDisplayMode = MorphicDisplay.getCurrentDisplayMode(for: mainDisplayId) else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not get current display mode for main display") + } + + let currentNapiDisplayMode = NAPIDisplayMode(displayMode: currentDisplayMode) + return currentNapiDisplayMode + } + + public static func setCurrentDisplayMode(_ newNapiDisplayMode: NAPIDisplayMode) throws { + guard let mainDisplayId = MorphicDisplay.getMainDisplayId() else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not get main display id") + } + + guard let newDisplayMode = MorphicDisplay.DisplayMode(napiDisplayMode: newNapiDisplayMode) else { + throw NAPISwiftBridgeJavaScriptThrowableError.rangeError(message: "Argument 'newNapiDisplayMode' contains values which are out of range") + } + + do { + try MorphicDisplay.setCurrentDisplayMode(for: mainDisplayId, to: newDisplayMode) + } catch MorphicDisplay.SetCurrentDisplayModeError.invalidDisplayMode { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Argument 'newNapiDisplayMode' is not a valid display mode") + } catch MorphicDisplay.SetCurrentDisplayModeError.otherError { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not set current display mode due to misc. error") + } + } +} +// +extension MorphicDisplay.DisplayMode { + init?(napiDisplayMode: NAPIDisplayFunctions.NAPIDisplayMode) { + guard let ioDisplayModeId = Int32(exactly: napiDisplayMode.ioDisplayModeId) else { + return nil + } + self.ioDisplayModeId = ioDisplayModeId + // + guard let widthInPixelsAsInt = Int(exactly: napiDisplayMode.widthInPixels) else { + return nil + } + self.widthInPixels = widthInPixelsAsInt + // + guard let heightInPixelsAsInt = Int(exactly: napiDisplayMode.heightInPixels) else { + return nil + } + self.heightInPixels = heightInPixelsAsInt + // + guard let widthInVirtualPixelsAsInt = Int(exactly: napiDisplayMode.widthInVirtualPixels) else { + return nil + } + self.widthInVirtualPixels = widthInVirtualPixelsAsInt + // + guard let heightInVirtualPixelsAsInt = Int(exactly: napiDisplayMode.heightInVirtualPixels) else { + return nil + } + self.heightInVirtualPixels = heightInVirtualPixelsAsInt + // + self.refreshRateInHertz = napiDisplayMode.refreshRateInHertz + // + self.isUsableForDesktopGui = napiDisplayMode.isUsableForDesktopGui + } +} diff --git a/MorphicMacOS/BridgeFunctions/NAPIInputFunctions.swift b/MorphicMacOS/BridgeFunctions/NAPIInputFunctions.swift new file mode 100644 index 0000000..3b479a3 --- /dev/null +++ b/MorphicMacOS/BridgeFunctions/NAPIInputFunctions.swift @@ -0,0 +1,101 @@ +// +// NAPIInputFunctions.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +import Foundation + +class NAPIInputFunctions { + // MARK: - Swift NAPI bridge setup + + static func getFunctionsAsPropertyDescriptors(cNapiEnv: napi_env!) -> [napi_property_descriptor] { + var result: [napi_property_descriptor] = [] + + // sendKey + result.append(NAPIProperty.createMethodProperty(cNapiEnv: cNapiEnv, name: "sendKey", method: sendKey).cNapiPropertyDescriptor) + + return result + } + + // MARK: - Swift NAPI bridge functions + + public struct NAPIKeyOptions: NAPIObjectCompatible { + let withControlKey: Bool + let withAlternateKey: Bool + let withCommandKey: Bool + + static var NAPIPropertyCodingKeysAndTypes: [(propertyKey: CodingKey, type: NAPIValueType)] = + [ + (propertyKey: CodingKeys.withControlKey, type: .boolean), + (propertyKey: CodingKeys.withAlternateKey, type: .boolean), + (propertyKey: CodingKeys.withCommandKey, type: .boolean) + ] + } + + public static func sendKey(_ keyCode: Double, _ keyOptions: NAPIKeyOptions, _ processId: Double) throws { + guard let keyCodeAsInt16 = Int16(exactly: keyCode) else { + throw NAPISwiftBridgeJavaScriptThrowableError.rangeError(message: "Argument 'keyCode' is out of range") + } + + var keyOptionsRawValue: UInt32 = 0 + // + // hold down control key (if applicable) + if keyOptions.withControlKey == true { + keyOptionsRawValue |= MorphicInput.KeyOptions.withControlKey.rawValue + } + // + // hold down alternate/option key (if applicable) + if keyOptions.withAlternateKey == true { + keyOptionsRawValue |= MorphicInput.KeyOptions.withAlternateKey.rawValue + } + // + // hold down command key (if applicable) + if keyOptions.withCommandKey == true { + keyOptionsRawValue |= MorphicInput.KeyOptions.withCommandKey.rawValue + } + // + let keyOptionsAsKeyOptions = MorphicInput.KeyOptions(napiKeyOptions: keyOptions) + + guard let processIdAsInt = Int(exactly: processId) else { + throw NAPISwiftBridgeJavaScriptThrowableError.rangeError(message: "Argument 'processId' is out of range") + } + + // NOTE: key codes may be different on non-EN_US keyboards; we may want to add a mapping capability to choose the proper "local" virtual keycodes based on "universal" metacodes + let keyCodeACGKeyCode = CGKeyCode(keyCodeAsInt16) + + let sendKeyResult = MorphicInput.sendKey(keyCode: keyCodeACGKeyCode, keyOptions: keyOptionsAsKeyOptions, toProcessId: processIdAsInt) + if sendKeyResult == false { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not send key code") + } + } +} + +extension MorphicInput.KeyOptions { + init(napiKeyOptions: NAPIInputFunctions.NAPIKeyOptions) { + var keyOptionsRawValue: UInt32 = 0 + // + // hold down control key (if applicable) + if napiKeyOptions.withControlKey == true { + keyOptionsRawValue |= MorphicInput.KeyOptions.withControlKey.rawValue + } + // + // hold down alternate/option key (if applicable) + if napiKeyOptions.withAlternateKey == true { + keyOptionsRawValue |= MorphicInput.KeyOptions.withAlternateKey.rawValue + } + // + // hold down command key (if applicable) + if napiKeyOptions.withCommandKey == true { + keyOptionsRawValue |= MorphicInput.KeyOptions.withCommandKey.rawValue + } + // + self = MorphicInput.KeyOptions(rawValue: keyOptionsRawValue) + } +} diff --git a/MorphicMacOS/BridgeFunctions/NAPILanguageFunctions.swift b/MorphicMacOS/BridgeFunctions/NAPILanguageFunctions.swift new file mode 100644 index 0000000..f811ac9 --- /dev/null +++ b/MorphicMacOS/BridgeFunctions/NAPILanguageFunctions.swift @@ -0,0 +1,98 @@ +// +// NAPILanguageFunctions.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +class NAPILanguageFunctions { + // MARK: - Swift NAPI bridge setup + + static func getFunctionsAsPropertyDescriptors(cNapiEnv: napi_env!) -> [napi_property_descriptor] { + var result: [napi_property_descriptor] = [] + + // getInstalledAppleLanguages + result.append(NAPIProperty.createMethodProperty(cNapiEnv: cNapiEnv, name: "getInstalledAppleLanguages", method: getInstalledAppleLanguages).cNapiPropertyDescriptor) + + // getPrimaryInstalledAppleLanguage + result.append(NAPIProperty.createMethodProperty(cNapiEnv: cNapiEnv, name: "getPrimaryInstalledAppleLanguage", method: getPrimaryInstalledAppleLanguage).cNapiPropertyDescriptor) + + // setPrimaryInstalledAppleLanguage + result.append(NAPIProperty.createMethodProperty(cNapiEnv: cNapiEnv, name: "setPrimaryInstalledAppleLanguage", method: setPrimaryInstalledAppleLanguage).cNapiPropertyDescriptor) + + // getLanguageName + result.append(NAPIProperty.createMethodProperty(cNapiEnv: cNapiEnv, name: "getLanguageName", method: getLanguageName).cNapiPropertyDescriptor) + + // getCountryName + result.append(NAPIProperty.createMethodProperty(cNapiEnv: cNapiEnv, name: "getCountryName", method: getCountryName).cNapiPropertyDescriptor) + + return result + } + + // MARK: - Swift NAPI bridge functions + + public static func getInstalledAppleLanguages() throws -> [String] { + guard let installedAppleLanguages = MorphicLanguage.getPreferredLanguages() else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not retrieve list of Apple preferred languages") + } + + let sortedLanguages = installedAppleLanguages.sorted(by: { $0 < $1 }) + + return sortedLanguages + } + + public static func getPrimaryInstalledAppleLanguage() throws -> String { + // NOTE: we capture the list (sorted by preference...so that the first item in the list is the current (primary) language) + guard let installedAppleLanguages = MorphicLanguage.getPreferredLanguages() else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not retrieve list of Apple preferred languages") + } + + return installedAppleLanguages.first! + } + + // this function returns true if setting the language was successful + public static func setPrimaryInstalledAppleLanguage(_ primaryLanguage: String) throws { + guard MorphicLanguage.setPrimaryAppleLanguageInGlobalDomain(primaryLanguage) == true else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not set primary preferred language") + } + } + + public static func getLanguageName(_ language: String, _ translatedToLanguage: String) throws -> String { + // get the language code for the provided language + guard let languageLocale = MorphicLanguage.createLocale(from: language) else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not create locale for: \(language)") + } + guard let languageIso639LanguageCode = MorphicLanguage.getIso639LanguageCode(for: languageLocale) else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not get language code for: \(language)") + } + + // create a locale to match the desired target language + guard let targetTranslationLanguageLocale = MorphicLanguage.createLocale(from: translatedToLanguage) else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not create locale for: \(translatedToLanguage)") + } + + return MorphicLanguage.getLanguageName(for: languageIso639LanguageCode, translateTo: targetTranslationLanguageLocale) + } + + public static func getCountryName(_ language: String, _ translatedToLanguage: String) throws -> String { + // get the language code for the provided language + guard let languageLocale = MorphicLanguage.createLocale(from: language) else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not create locale for: \(language)") + } + guard let languageIso3166CountryCode = MorphicLanguage.getIso3166CountryCode(for: languageLocale) else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not get country code for: \(language)") + } + + // create a locale to match the desired target language + guard let targetTranslationLanguageLocale = MorphicLanguage.createLocale(from: translatedToLanguage) else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not create locale for: \(translatedToLanguage)") + } + + return MorphicLanguage.getCountryName(for: languageIso3166CountryCode, translateTo: targetTranslationLanguageLocale) + } +} diff --git a/MorphicMacOS/BridgeFunctions/NAPIProcessFunctions.swift b/MorphicMacOS/BridgeFunctions/NAPIProcessFunctions.swift new file mode 100644 index 0000000..4ce26a1 --- /dev/null +++ b/MorphicMacOS/BridgeFunctions/NAPIProcessFunctions.swift @@ -0,0 +1,45 @@ +// +// NAPIProcessFunctions.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +class NAPIProcessFunctions { + // MARK: - Swift NAPI bridge setup + + static func getFunctionsAsPropertyDescriptors(cNapiEnv: napi_env!) -> [napi_property_descriptor] { + var result: [napi_property_descriptor] = [] + + // getAllLaunchDaemonsAndAgentsAsServiceNames + result.append(NAPIProperty.createMethodProperty(cNapiEnv: cNapiEnv, name: "getAllLaunchDaemonsAndAgentsAsServiceNames", method: getAllLaunchDaemonsAndAgentsAsServiceNames).cNapiPropertyDescriptor) + + // restartServicesViaLaunchctl + result.append(NAPIProperty.createMethodProperty(cNapiEnv: cNapiEnv, name: "restartServicesViaLaunchctl", method: restartServicesViaLaunchctl).cNapiPropertyDescriptor) + + return result + } + + // MARK: - Swift NAPI bridge functions + + public static func getAllLaunchDaemonsAndAgentsAsServiceNames() -> [String] { + let allLaunchDaemonsAndAgents = MorphicLaunchDaemonsAndAgents.allCases + + var serviceNames: [String] = [] + + for daemonOrAgent in allLaunchDaemonsAndAgents { + serviceNames.append(daemonOrAgent.serviceName) + } + + return serviceNames + } + + public static func restartServicesViaLaunchctl(serviceNames: [String]) { + MorphicProcess.restartViaLaunchctl(serviceNames: serviceNames) + } +} diff --git a/MorphicMacOS/BridgeFunctions/NAPIWindowFunctions.swift b/MorphicMacOS/BridgeFunctions/NAPIWindowFunctions.swift new file mode 100644 index 0000000..1e0d441 --- /dev/null +++ b/MorphicMacOS/BridgeFunctions/NAPIWindowFunctions.swift @@ -0,0 +1,50 @@ +// +// NAPIWindowFunctions.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +class NAPIWindowFunctions { + // MARK: - Swift NAPI bridge setup + + static func getFunctionsAsPropertyDescriptors(cNapiEnv: napi_env!) -> [napi_property_descriptor] { + var result: [napi_property_descriptor] = [] + + // getWindowOwnerNameAndProcessIdOfTopmostWindow + result.append(NAPIProperty.createMethodProperty(cNapiEnv: cNapiEnv, name: "getWindowOwnerNameAndProcessIdOfTopmostWindow", method: getWindowOwnerNameAndProcessIdOfTopmostWindow).cNapiPropertyDescriptor) + + return result + } + + // MARK: - Swift NAPI bridge functions + + public struct NAPIWindowOwnerNameAndProcessId: NAPIObjectCompatible { + let windowOwnerName: String + let processId: Double // Int (Int32/Int64) + + public static var NAPIPropertyCodingKeysAndTypes: [(propertyKey: CodingKey, type: NAPIValueType)] = + [ + (propertyKey: CodingKeys.windowOwnerName, type: .string), + (propertyKey: CodingKeys.processId, type: .number) + ] + } + + public static func getWindowOwnerNameAndProcessIdOfTopmostWindow() throws -> NAPIWindowOwnerNameAndProcessId { + guard let (windowOwnerName, processId) = MorphicWindow.getWindowOwnerNameAndProcessIdOfTopmostWindow() else { + throw NAPISwiftBridgeJavaScriptThrowableError.error(message: "Could not get window owner name and process id for topmost window") + } + + guard let processIdAsDouble = Double(exactly: processId) else { + throw NAPISwiftBridgeJavaScriptThrowableError.rangeError(message: "Argument 'processId' cannot be represented as a 64-bit floating point value") + } + + let result = NAPIWindowOwnerNameAndProcessId(windowOwnerName: windowOwnerName, processId: processIdAsDouble) + return result + } +} diff --git a/MorphicMacOS/Main.swift b/MorphicMacOS/Main.swift new file mode 100644 index 0000000..d6f72af --- /dev/null +++ b/MorphicMacOS/Main.swift @@ -0,0 +1,55 @@ +// +// Main.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +// NOTE: Node.JS's N-API documentation is located at: https://nodejs.org/api/n-api.html + +@_cdecl("Init") +public func Init(env: napi_env!, exports: napi_value!) -> napi_value? { + guard env != nil else { + return nil + } + guard exports != nil else { + return nil + } + + var status: napi_status? = nil + + var napiPropertyDescriptors: [napi_property_descriptor] = [] + + // NAPIAudioFunctions (MorphicAudio) + napiPropertyDescriptors.append(contentsOf: NAPIAudioFunctions.getFunctionsAsPropertyDescriptors(cNapiEnv: env)) + + // NAPIDiskFunctions (MorphicDisk) + napiPropertyDescriptors.append(contentsOf: NAPIDiskFunctions.getFunctionsAsPropertyDescriptors(cNapiEnv: env)) + + // NAPIDisplayFunctions (MorphicDisplay) + napiPropertyDescriptors.append(contentsOf: NAPIDisplayFunctions.getFunctionsAsPropertyDescriptors(cNapiEnv: env)) + + // NAPIInputFunctions (MorphicInput) + napiPropertyDescriptors.append(contentsOf: NAPIInputFunctions.getFunctionsAsPropertyDescriptors(cNapiEnv: env)) + + // NAPILanguageFunctions (MorphicLanguage) + napiPropertyDescriptors.append(contentsOf: NAPILanguageFunctions.getFunctionsAsPropertyDescriptors(cNapiEnv: env)) + + // NAPIProcessFunctions (MorphicProcess) + napiPropertyDescriptors.append(contentsOf: NAPIProcessFunctions.getFunctionsAsPropertyDescriptors(cNapiEnv: env)) + + // NAPIWindowFunctions (MorphicWindow) + napiPropertyDescriptors.append(contentsOf: NAPIWindowFunctions.getFunctionsAsPropertyDescriptors(cNapiEnv: env)) + + status = napi_define_properties(env, exports, napiPropertyDescriptors.count, &napiPropertyDescriptors) + guard status == napi_ok else { + return nil + } + + return exports +} diff --git a/MorphicMacOS/MorphicMacOS-Bridging-Header.h b/MorphicMacOS/MorphicMacOS-Bridging-Header.h new file mode 100644 index 0000000..ab4f4e2 --- /dev/null +++ b/MorphicMacOS/MorphicMacOS-Bridging-Header.h @@ -0,0 +1,16 @@ +// +// MorphicMacOS-Bridging-Header.h +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +// dynamic node.js N-API headers (imported for use from Swift) +// NOTE: we specify NAPI_VERSION 4 (for Node.js 10.16 or newer); for Node.js 12.11 or newer, we could use NAPI_VERSION 5 instead +#define NAPI_VERSION 4 +#import "./N-API/include/node_api.h" diff --git a/MorphicMacOS/MorphicMacOS.h b/MorphicMacOS/MorphicMacOS.h new file mode 100644 index 0000000..21153c4 --- /dev/null +++ b/MorphicMacOS/MorphicMacOS.h @@ -0,0 +1,17 @@ +// +// MorphicMacOS.h +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +#import + +@interface MorphicMacOS : NSObject + +@end diff --git a/MorphicMacOS/MorphicMacOS.m b/MorphicMacOS/MorphicMacOS.m new file mode 100644 index 0000000..31de9fe --- /dev/null +++ b/MorphicMacOS/MorphicMacOS.m @@ -0,0 +1,17 @@ +// +// MorphicMacOS.m +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +#import "MorphicMacOS.h" + +@implementation MorphicMacOS + +@end diff --git a/MorphicMacOS/N-API/NAPIBridging/NAPIBridgingCoding.swift b/MorphicMacOS/N-API/NAPIBridging/NAPIBridgingCoding.swift new file mode 100644 index 0000000..4beafb8 --- /dev/null +++ b/MorphicMacOS/N-API/NAPIBridging/NAPIBridgingCoding.swift @@ -0,0 +1,175 @@ +// +// NAPIBridingCoding.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +// MARK: Object bridging protocols/functions + +internal struct NAPIBridgingKeyedDecodingContainerProtocol: KeyedDecodingContainerProtocol { + var codingPath: [CodingKey] = [] + + var allKeys: [Key] = [] + + let propertyNamesAndValues: [String: Any] + + init(propertyNamesAndValues: [String: Any]) { + self.propertyNamesAndValues = propertyNamesAndValues + } + + func contains(_ key: Key) -> Bool { + return self.propertyNamesAndValues.keys.contains(key.stringValue) + } + + func decodeNil(forKey key: Key) throws -> Bool { + let keyAsString = key.stringValue + + // capture the value (if the property exists) + if self.propertyNamesAndValues.keys.contains(keyAsString) == false { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: codingPath, debugDescription: "Property \(keyAsString) cannot be initialized because it was not provided.")) + } + let value = self.propertyNamesAndValues[keyAsString] + + if case Optional.none = value { + // value is nil + return true + } else { + // value is not nil + return false + } + } + + func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { + return try innerDecode(nativeType: type, napiValueType: .boolean, forKey: key) as! Bool + } + + func decode(_ type: String.Type, forKey key: Key) throws -> String { + return try innerDecode(nativeType: type, napiValueType: .string, forKey: key) as! String + } + + func decode(_ type: Double.Type, forKey key: Key) throws -> Double { + return try innerDecode(nativeType: type, napiValueType: .number, forKey: key) as! Double + } + + func decode(_ type: Float.Type, forKey key: Key) throws -> Float { + fatalError("NOT SUPPORTED") + } + + func decode(_ type: Int.Type, forKey key: Key) throws -> Int { + fatalError("NOT SUPPORTED") + } + + func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { + fatalError("NOT SUPPORTED") + } + + func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { + fatalError("NOT SUPPORTED") + } + + func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { + fatalError("NOT SUPPORTED") + } + + func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { + fatalError("NOT SUPPORTED") + } + + func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { + fatalError("NOT SUPPORTED") + } + + func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { + fatalError("NOT SUPPORTED") + } + + func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { + fatalError("NOT SUPPORTED") + } + + func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { + fatalError("NOT SUPPORTED") + } + + func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { + fatalError("NOT SUPPORTED") + } + + func decode(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable { + guard let typeAsNapiValueType = (type as? NAPIValueCompatible.Type)?.napiValueType else { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "Property \(key.stringValue) cannot be initialized with a value of type \(T.Type.self)")) + } + + return try innerDecode(nativeType: type, napiValueType: typeAsNapiValueType, forKey: key) as! T + } + + // NOTE: this function returns the requested type (or throws an error if the property is missing or the type is mismatched + private func innerDecode(nativeType: Any.Type, napiValueType: NAPIValueType, forKey key: Key) throws -> Any? { + let keyAsString = key.stringValue + + // capture the value (if the property exists) + if self.propertyNamesAndValues.keys.contains(keyAsString) == false { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.codingPath, debugDescription: "Property \(keyAsString) cannot be initialized because it is missing.")) + } + let value = self.propertyNamesAndValues[keyAsString] + + let nativeTypeAsNapiValueType: NAPIValueType + // verify that the type is a NAPIValueCompatible type + guard let nativeTypeAsNapiValueCompatibleType = nativeType as? NAPIValueCompatible.Type else { + throw DecodingError.typeMismatch(nativeType, DecodingError.Context(codingPath: self.codingPath, debugDescription: "Property \(keyAsString) cannot be initialized with a value of type \(nativeType.self)")) + } + nativeTypeAsNapiValueType = nativeTypeAsNapiValueCompatibleType.napiValueType + + if nativeTypeAsNapiValueType.isCompatible(withRhs: napiValueType, disregardRhsOptionals: true) { + return value + } else { + throw DecodingError.typeMismatch(nativeType, DecodingError.Context(codingPath: self.codingPath, debugDescription: "Property \(keyAsString) cannot be initialized with a value of type \(nativeType.self)")) + } + } + + func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey { + fatalError("NOT IMPLEMENTED") + } + + func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { + fatalError("NOT IMPLEMENTED") + } + + func superDecoder() throws -> Decoder { + fatalError("NOT IMPLEMENTED") + } + + func superDecoder(forKey key: Key) throws -> Decoder { + fatalError("NOT IMPLEMENTED") + } + } + + internal struct NAPIBridgingDecoder: Decoder { + var codingPath: [CodingKey] = [] + + var userInfo: [CodingUserInfoKey : Any] = [:] + + let propertyNamesAndValues: [String: Any] + + init(propertyNamesAndValues: [String: Any]) { + self.propertyNamesAndValues = propertyNamesAndValues + } + + func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key : CodingKey { + return KeyedDecodingContainer(NAPIBridgingKeyedDecodingContainerProtocol(propertyNamesAndValues: propertyNamesAndValues)) + } + + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + fatalError("NOT IMPLEMENTED") + } + + func singleValueContainer() throws -> SingleValueDecodingContainer { + fatalError("NOT IMPLEMENTED") + } + } diff --git a/MorphicMacOS/N-API/NAPIBridging/NAPISwiftBridgeJavaScriptThrowableError.swift b/MorphicMacOS/N-API/NAPIBridging/NAPISwiftBridgeJavaScriptThrowableError.swift new file mode 100644 index 0000000..7fcc880 --- /dev/null +++ b/MorphicMacOS/N-API/NAPIBridging/NAPISwiftBridgeJavaScriptThrowableError.swift @@ -0,0 +1,22 @@ +// +// NAPISwiftBridgeJavaScriptThrowableError.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +public enum NAPISwiftBridgeJavaScriptThrowableError: Error { + case value(_ value: NAPIValueCompatible) + // + // NOTE: we have intentionally broken these base error types out into separate entities for ease of use; if the developer would like to raise a NAPIJavaScriptError, simply pass it to the value(...) case + case error(message: String, code: String? = nil) + case typeError(message: String, code: String? = nil) + case rangeError(message: String, code: String? = nil) + // + case fatalError(message: String, location: String? = nil) +} diff --git a/MorphicMacOS/N-API/NAPIFunctionHelpers.swift b/MorphicMacOS/N-API/NAPIFunctionHelpers.swift new file mode 100644 index 0000000..626b03a --- /dev/null +++ b/MorphicMacOS/N-API/NAPIFunctionHelpers.swift @@ -0,0 +1,261 @@ +// +// NAPIFunctionHelpers.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +import Foundation + +// MARK: - Bridge function types + +// NOTE: NAPISwiftBridgeFunction functions emulate a "Void" return by returning "nil" +// NOTE: NAPISwiftBridgeFunctions are only allowed to throw NAPISwiftBridgeJavaScriptThrowableError (which map to JavaScript Errors) +typealias NAPISwiftBridgeFunction = (_ cNapiEnv: napi_env, _ args: [Any?]) throws -> Any? + +// NAPIFunctionData is an internal structure associated with NAPI function callbacks +class NAPIFunctionData { + let swiftBridgeFunction: NAPISwiftBridgeFunction + let argumentTypes: [NAPIValueType] + let returnType: NAPIValueType? + + init(swiftBridgeFunction: @escaping NAPISwiftBridgeFunction, argumentTypes: [NAPIValueType], returnType: NAPIValueType?) { + self.swiftBridgeFunction = swiftBridgeFunction + self.argumentTypes = argumentTypes + self.returnType = returnType + } +} + +let maximumArgumentsInNativeFunctions = 3 + +// MARK: - Trampoline for native function callbacks + +// NOTE: according to N-API documentation, a default handle scope exists when our native function is called from JavaScript, +// and that scope is tied to the lifespan of the native method call (at which point napi_values are marked for GC) +// NOTE: native functions emulate a "Void" return by returning nil (which will return "undefined" via JavaScript +internal func napiFunctionTrampoline(_ cNapiEnv: napi_env!, _ cNapiCallbackInfo: napi_callback_info!) -> napi_value? { + // NOTE: info should never be nil; this is just a precaution for debug-time sanity + assert(cNapiCallbackInfo != nil, "Argument 'cNapiCallbackInfo' may not be nil.") + + var status: napi_status + + let pointerToPointerToNapiFunctionData = UnsafeMutablePointer.allocate(capacity: 1) + // + // capture up to the maximum number of arguments allowed in our native functions + var numberOfArguments: Int = maximumArgumentsInNativeFunctions + // + // NOTE: althtough the arguments are returned as optional napi_values, they are indeed non-nil (unless an error occured) + let pointerToArgumentsAsCNapiValues = UnsafeMutablePointer.allocate(capacity: numberOfArguments) + // + var pointerToThisArgumentAsCNapiValue: napi_value? = nil + + /* retrieve the callback function info (e.g. indirect pointer to NAPIFunctionData, array of passed-in arguments a,nd the implicit 'this' argument) */ + + // NOTE: numberOfArguments is an in/out parameter: we pass in the maximum number of arguments we support and it returns the actual number of arguments populated at pointerToArguments + // NOTE: if the maximum argument count we pass in is not high enough, we will not get all the passed-in arguments; the "maximumNativeFunctionArgumentCount" must therefore be synced with the maximum allowable number of function arguments + status = napi_get_cb_info(cNapiEnv, cNapiCallbackInfo, &numberOfArguments, pointerToArgumentsAsCNapiValues, &pointerToThisArgumentAsCNapiValue, pointerToPointerToNapiFunctionData) + guard status == napi_ok else { + return nil + } + + /* capture the NAPIFunctionData we created when we set up this callback (so we know which function to call, the required parameter types and the return type) */ + + guard let pointerToNapiFunctionData = pointerToPointerToNapiFunctionData.pointee else { + // NOTE: we should never _not_ get a pointer to our NAPIFunctionData; this would indicate a shutdown/corruption/programming issue + // TODO: throw an error + return nil + } + let napiFunctionData = Unmanaged.fromOpaque(pointerToNapiFunctionData).takeUnretainedValue() + + /* type-check the passed-in arguments and convert them to their respective native-code types */ + + let argumentsAsCNapiValues = Array(UnsafeBufferPointer(start: pointerToArgumentsAsCNapiValues, count: numberOfArguments)) + var arguments: [Any?] = [] + + guard argumentsAsCNapiValues.count >= napiFunctionData.argumentTypes.count else { + // TODO: consider throwing an error (although it might be better to error out so that the programmer can fix the issue immediately) + fatalError("Not enough arguments were provided. Received: \(argumentsAsCNapiValues.count); Expected: \(napiFunctionData.argumentTypes.count)") + } + + for index in 0.. Any? { + let napiValueTypeOfArgument = NAPIValueType.getNAPIValueType(cNapiEnv: cNapiEnv, cNapiValue: cNapiValue) + + // convert the napi_value to a NAPIValue type (and then use the NAPIValue to convert the napi_value to its corresponding native type respresentation) + do { + switch napiValueTypeOfArgument { + case .boolean: + let argumentAsBool = try NAPIValue(cNapiEnv: cNapiEnv, cNapiValue: cNapiValue).asBool()! + return argumentAsBool + case .number: + let argumentAsDouble = try NAPIValue(cNapiEnv: cNapiEnv, cNapiValue: cNapiValue).asDouble()! + return argumentAsDouble + case .string: + let argumentAsString = try NAPIValue(cNapiEnv: cNapiEnv, cNapiValue: cNapiValue).asString()! + return argumentAsString + case .nullable(let wrappedType): + precondition(wrappedType == nil, "Arguments of type .nullable(...) should only be mapped to null itself.") + return nil + case .object(_, _): + if targetNapiValueType == nil { + fatalError("Cannot create object without target type") + } + + if case let .object(_ , swiftType) = targetNapiValueType! { + guard let swiftType = swiftType else { + fatalError("NAPI value has no associated Swift type") + } + let argumentAsObject = try NAPIValue(cNapiEnv: cNapiEnv, cNapiValue: cNapiValue, napiValueType: napiValueTypeOfArgument).asObject(ofType: swiftType) + return argumentAsObject + } else { + // unreachable code: swiftType must always be (auto-)populated in napiFunctionData + fatalError() + } + case .array(let elementNapiValueType): + if let elementNapiValueType = elementNapiValueType { + let argumentAsArray = try NAPIValue(cNapiEnv: cNapiEnv, cNapiValue: cNapiValue, napiValueType: napiValueTypeOfArgument).asArray(elementNapiValueType: elementNapiValueType)! + return argumentAsArray + } else { + return [] + } + case .function: + let argumentAsJavaScriptFunction = try NAPIValue(cNapiEnv: cNapiEnv, cNapiValue: cNapiValue).asJavaScriptFunction()! + return argumentAsJavaScriptFunction + case .error: + let argumentAsJavaScriptError = try NAPIValue(cNapiEnv: cNapiEnv, cNapiValue: cNapiValue).asJavaScriptError()! + return argumentAsJavaScriptError + case .undefined: + // this is an unsupported type (and should be unreachable code) + fatalError() + case .unsupported: + // this is an unsupported type (and should be unreachable code) + fatalError() + } + } catch let error { + throw error + } +} diff --git a/MorphicMacOS/N-API/NAPIJavaScriptError.swift b/MorphicMacOS/N-API/NAPIJavaScriptError.swift new file mode 100644 index 0000000..8091284 --- /dev/null +++ b/MorphicMacOS/N-API/NAPIJavaScriptError.swift @@ -0,0 +1,34 @@ +// +// NAPIJavaScriptError.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +public struct NAPIJavaScriptError: NAPIValueCompatible { + public enum NameOption: String { + case Error + case RangeError + case TypeError + } + + public let name: NameOption + public let message: String + public let code: String? + + public init(name: NameOption, message: String, code: String? = nil) { + self.name = name + self.message = message + self.code = code + } +} +extension NAPIJavaScriptError { + public static var napiValueType: NAPIValueType { + return .error + } +} diff --git a/MorphicMacOS/N-API/NAPIJavaScriptFunction.swift b/MorphicMacOS/N-API/NAPIJavaScriptFunction.swift new file mode 100644 index 0000000..c7f36df --- /dev/null +++ b/MorphicMacOS/N-API/NAPIJavaScriptFunction.swift @@ -0,0 +1,53 @@ +// +// NAPIJavaScriptFunction.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +public struct NAPIJavaScriptFunction: NAPIValueCompatible { + public let cNapiEnv: napi_env + public let cNapiValue: napi_value + + public func call(args: [NAPIValueCompatible]) { + var argsAsCNapiValues: [napi_value?] = [] + for arg in args { + let argAsNapiValue = NAPIValue.create(cNapiEnv: self.cNapiEnv, nativeValue: arg, napiValueType: type(of: arg).napiValueType) + argsAsCNapiValues.append(argAsNapiValue.cNapiValue) + } + + var status: napi_status? = nil + + // capture the JavaScript global object (to pass as "this" to the function) + var globalAsCNapiValue: napi_value? + status = napi_get_global(self.cNapiEnv, &globalAsCNapiValue) + guard status == napi_ok else { + // todo: throw an error! + fatalError("Could not get the JavaScript 'global' object") + } + + // NOTE: this function intentionally ignores any returned values from the JavaScript function; we do this because in testing (even in same-thread environments) that return values which were strings would cause Node (v12.8.1) to crash when we tried to retrieve their contents + var resultAsCNapiValue: napi_value? + + // TODO: use thread-safe callbacks instead + // TODO NOTE: in our initial tests, we were unable to retrieve a String from a callback's return value; re-test this scenario with thread-safe callbacks. If we do allow result values, we must make the caller specify the return type (via a generic-tied argument perhaps) and we must consider how to deal with wrong, .undefined and .unsupported results) + status = napi_call_function(self.cNapiEnv, globalAsCNapiValue, self.cNapiValue, argsAsCNapiValues.count, argsAsCNapiValues, &resultAsCNapiValue) + guard status == napi_ok else { + // TODO: if the status is "napi_pending_exception", the callback threw an error; should we convert it to a NAPISwiftBridgeJavaScriptThrowableError and throw it to the caller? + // TODO: under all circumstances, we should throw an error (perhaps including a copy of the JavaScript error) instead of using fatalError(...) + fatalError("Could not call the JavaScript callback") + } + + // function succeeded; no errors to report + } +} +extension NAPIJavaScriptFunction { + public static var napiValueType: NAPIValueType { + return .function + } +} diff --git a/MorphicMacOS/N-API/NAPIObjectCompatible.swift b/MorphicMacOS/N-API/NAPIObjectCompatible.swift new file mode 100644 index 0000000..fd2bb03 --- /dev/null +++ b/MorphicMacOS/N-API/NAPIObjectCompatible.swift @@ -0,0 +1,40 @@ +// +// NAPIObjectCompatible.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +// NOTE: ideally we would want to limit NAPIObjectCompatible to only support structs (in the future, if Swift provides such a programmatic constraint) +public protocol NAPIObjectCompatible: Decodable, NAPIValueCompatible { + static var NAPIPropertyCodingKeysAndTypes: [(propertyKey: CodingKey, type: NAPIValueType)] { get } +} +extension NAPIObjectCompatible { + static var NAPIPropertyNamesAndTypes: [String: NAPIValueType] + { + get { + var result: [String: NAPIValueType] = [:] + for (propertyName, type) in self.NAPIPropertyCodingKeysAndTypes { + result[propertyName.stringValue] = type + } + + return result + } + } +} +extension NAPIObjectCompatible { + public static var napiValueType: NAPIValueType { + var propertyNamesAndTypes: [String : NAPIValueType] = [:] + + for (propertyKey, type) in self.NAPIPropertyCodingKeysAndTypes { + propertyNamesAndTypes[propertyKey.stringValue] = type + } + + return .object(propertyNamesAndTypes: propertyNamesAndTypes, swiftType: self) + } +} diff --git a/MorphicMacOS/N-API/NAPIProperty.swift b/MorphicMacOS/N-API/NAPIProperty.swift new file mode 100644 index 0000000..db19a63 --- /dev/null +++ b/MorphicMacOS/N-API/NAPIProperty.swift @@ -0,0 +1,205 @@ +// +// NAPIProperty.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +import Foundation + +public class NAPIProperty { + public let cNapiPropertyDescriptor: napi_property_descriptor + + private init(cNapiPropertyDescriptor: napi_property_descriptor) { + self.cNapiPropertyDescriptor = cNapiPropertyDescriptor + } + + // + + /* createMethodProperty: 0 to 3 parameter varieties (without return type) */ + // NOTE: we MUST support up to 'maximumArgumentsInNativeFunctions' parameters (from NAPIFunctionHelpers) + + public static func createMethodProperty(cNapiEnv: napi_env, name: String, method: @escaping () throws -> Void) -> NAPIProperty { + let swiftBridgeFunction = createSwiftBridgeFunction(method: method) + let napiArgumentTypes: [NAPIValueType] = [] + + return createMethodProperty(cNapiEnv: cNapiEnv, name: name, swiftBridgeFunction: swiftBridgeFunction, napiArgumentTypes: napiArgumentTypes) + } + // + public static func createMethodProperty(cNapiEnv: napi_env, name: String, method: @escaping (_ arg0: T0) throws -> Void) -> NAPIProperty { + let swiftBridgeFunction = createSwiftBridgeFunction(method: method) + let napiArgumentTypes: [NAPIValueType] = [T0.napiValueType] + + return createMethodProperty(cNapiEnv: cNapiEnv, name: name, swiftBridgeFunction: swiftBridgeFunction, napiArgumentTypes: napiArgumentTypes) + } + // + public static func createMethodProperty(cNapiEnv: napi_env, name: String, method: @escaping (_ arg0: T0, _ arg1: T1) throws -> Void) -> NAPIProperty { + let swiftBridgeFunction = createSwiftBridgeFunction(method: method) + let napiArgumentTypes: [NAPIValueType] = [T0.napiValueType, T1.napiValueType] + + return createMethodProperty(cNapiEnv: cNapiEnv, name: name, swiftBridgeFunction: swiftBridgeFunction, napiArgumentTypes: napiArgumentTypes) + } + // + public static func createMethodProperty(cNapiEnv: napi_env, name: String, method: @escaping (_ arg0: T0, _ arg1: T1, _ arg2: T2) throws -> Void) -> NAPIProperty { + let swiftBridgeFunction = createSwiftBridgeFunction(method: method) + let napiArgumentTypes: [NAPIValueType] = [T0.napiValueType, T1.napiValueType, T2.napiValueType] + + return createMethodProperty(cNapiEnv: cNapiEnv, name: name, swiftBridgeFunction: swiftBridgeFunction, napiArgumentTypes: napiArgumentTypes) + } + + /* createMethodProperty: 0 to 3 parameter varieties (with return type) */ + // NOTE: we MUST support up to 'maximumArgumentsInNativeFunctions' parameters (from NAPIFunctionHelpers) + + public static func createMethodProperty(cNapiEnv: napi_env, name: String, method: @escaping () throws -> TReturn) -> NAPIProperty { + let swiftBridgeFunction = createSwiftBridgeFunction(method: method) + let napiArgumentTypes: [NAPIValueType] = [] + + return createMethodProperty(cNapiEnv: cNapiEnv, name: name, swiftBridgeFunction: swiftBridgeFunction, napiArgumentTypes: napiArgumentTypes, napiReturnType: TReturn.napiValueType) + } + // + public static func createMethodProperty(cNapiEnv: napi_env, name: String, method: @escaping (_ arg0: T0) throws -> TReturn) -> NAPIProperty { + let swiftBridgeFunction = createSwiftBridgeFunction(method: method) + let napiArgumentTypes: [NAPIValueType] = [T0.napiValueType] + + return createMethodProperty(cNapiEnv: cNapiEnv, name: name, swiftBridgeFunction: swiftBridgeFunction, napiArgumentTypes: napiArgumentTypes, napiReturnType: TReturn.napiValueType) + } + // + public static func createMethodProperty(cNapiEnv: napi_env, name: String, method: @escaping (_ arg0: T0, _ arg1: T1) throws -> TReturn) -> NAPIProperty { + let swiftBridgeFunction = createSwiftBridgeFunction(method: method) + let napiArgumentTypes: [NAPIValueType] = [T0.napiValueType, T1.napiValueType] + + return createMethodProperty(cNapiEnv: cNapiEnv, name: name, swiftBridgeFunction: swiftBridgeFunction, napiArgumentTypes: napiArgumentTypes, napiReturnType: TReturn.napiValueType) + } + // + public static func createMethodProperty(cNapiEnv: napi_env, name: String, method: @escaping (_ arg0: T0, _ arg1: T1, _ arg2: T2) throws -> TReturn) -> NAPIProperty { + let swiftBridgeFunction = createSwiftBridgeFunction(method: method) + let napiArgumentTypes: [NAPIValueType] = [T0.napiValueType, T1.napiValueType, T2.napiValueType] + + return createMethodProperty(cNapiEnv: cNapiEnv, name: name, swiftBridgeFunction: swiftBridgeFunction, napiArgumentTypes: napiArgumentTypes, napiReturnType: TReturn.napiValueType) + } + + // NOTE: this function is the master "createMethodProperty" function called by all the other "createMethodProperty" functions + fileprivate static func createMethodProperty(cNapiEnv: napi_env, name: String, swiftBridgeFunction: @escaping NAPISwiftBridgeFunction, napiArgumentTypes: [NAPIValueType], napiReturnType: NAPIValueType? = nil) -> NAPIProperty { + let nameAsNapiValue = NAPIValue.create(cNapiEnv: cNapiEnv, nativeValue: name) + + let napiFunctionData = NAPIFunctionData(swiftBridgeFunction: swiftBridgeFunction, argumentTypes: napiArgumentTypes, returnType: napiReturnType) + let pointerToNapiFunctionData = Unmanaged.passRetained(napiFunctionData).toOpaque() + + // TODO: consider setting the attributes (instead of just using the default settings) + let napiPropertyDescriptor = napi_property_descriptor(utf8name: nil, name: nameAsNapiValue.cNapiValue, method: napiFunctionTrampoline, getter: nil, setter: nil, value: nil, attributes: napi_default, data: pointerToNapiFunctionData) + + let result = NAPIProperty(cNapiPropertyDescriptor: napiPropertyDescriptor) + return result + } + + // + + /* createSwiftBridgeFunction: 0 to 3 parameter varieties (without return type) */ + // NOTE: we MUST support up to 'maximumArgumentsInNativeFunctions' parameters (from NAPIFunctionHelpers) + + private static func createSwiftBridgeFunction(method: @escaping () throws -> Void) -> NAPISwiftBridgeFunction { + let swiftBridgeFunction: NAPISwiftBridgeFunction = { (env, args) throws in + try method() + // as we have no actual return value: return nil (to satisfy the Swift bridge function signature) + return nil + } + // + return swiftBridgeFunction + } + // + private static func createSwiftBridgeFunction(method: @escaping (_ arg0: T0) throws -> Void) -> NAPISwiftBridgeFunction { + let swiftBridgeFunction: NAPISwiftBridgeFunction = { (env, args) throws in + let arg0 = args[0] as! T0 + // + try method(arg0) + // as we have no actual return value: return nil (to satisfy the Swift bridge function signature) + return nil + } + // + return swiftBridgeFunction + } + // + private static func createSwiftBridgeFunction(method: @escaping (_ arg0: T0, _ arg1: T1) throws -> Void) -> NAPISwiftBridgeFunction { + let swiftBridgeFunction: NAPISwiftBridgeFunction = { (env, args) throws in + let arg0 = args[0] as! T0 + let arg1 = args[1] as! T1 + // + try method(arg0, arg1) + // as we have no actual return value: return nil (to satisfy the Swift bridge function signature) + return nil + } + // + return swiftBridgeFunction + } + // + private static func createSwiftBridgeFunction(method: @escaping (_ arg0: T0, _ arg1: T1, _ arg2: T2) throws -> Void) -> NAPISwiftBridgeFunction { + let swiftBridgeFunction: NAPISwiftBridgeFunction = { (env, args) throws in + let arg0 = args[0] as! T0 + let arg1 = args[1] as! T1 + let arg2 = args[2] as! T2 + // + try method(arg0, arg1, arg2) + // as we have no actual return value: return nil (to satisfy the Swift bridge function signature) + return nil + } + // + return swiftBridgeFunction + } + + /* createSwiftBridgeFunction: 0 to 3 parameter varieties (with return type) */ + // NOTE: we MUST support up to 'maximumArgumentsInNativeFunctions' parameters (from NAPIFunctionHelpers) + + private static func createSwiftBridgeFunction(method: @escaping () throws -> TReturn) -> NAPISwiftBridgeFunction { + let swiftBridgeFunction: NAPISwiftBridgeFunction = { (env, args) throws in + let result = try method() + // + return result + } + // + return swiftBridgeFunction + } + // + private static func createSwiftBridgeFunction(method: @escaping (_ arg0: T0) throws -> TReturn) -> NAPISwiftBridgeFunction { + let swiftBridgeFunction: NAPISwiftBridgeFunction = { (env, args) throws in + let arg0 = args[0] as! T0 + // + let result = try method(arg0) + // + return result + } + // + return swiftBridgeFunction + } + // + private static func createSwiftBridgeFunction(method: @escaping (_ arg0: T0, _ arg1: T1) throws -> TReturn) -> NAPISwiftBridgeFunction { + let swiftBridgeFunction: NAPISwiftBridgeFunction = { (env, args) throws in + let arg0 = args[0] as! T0 + let arg1 = args[1] as! T1 + // + let result = try method(arg0, arg1) + // + return result + } + // + return swiftBridgeFunction + } + // + private static func createSwiftBridgeFunction(method: @escaping (_ arg0: T0, _ arg1: T1, _ arg2: T2) throws -> TReturn) -> NAPISwiftBridgeFunction { + let swiftBridgeFunction: NAPISwiftBridgeFunction = { (env, args) throws in + let arg0 = args[0] as! T0 + let arg1 = args[1] as! T1 + let arg2 = args[2] as! T2 + // + let result = try method(arg0, arg1, arg2) + // + return result + } + // + return swiftBridgeFunction + } +} diff --git a/MorphicMacOS/N-API/NAPIValue.swift b/MorphicMacOS/N-API/NAPIValue.swift new file mode 100644 index 0000000..614dc2a --- /dev/null +++ b/MorphicMacOS/N-API/NAPIValue.swift @@ -0,0 +1,1023 @@ +// +// NAPIValue.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +public class NAPIValue { + private enum NAPIValueError: Error { + case typeMismatch + case otherNapiError + } + + private let cNapiEnv: napi_env + public let cNapiValue: napi_value + public let napiValueType: NAPIValueType + + public convenience init(cNapiEnv: napi_env, cNapiValue: napi_value) { + let napiValueType = NAPIValueType.getNAPIValueType(cNapiEnv: cNapiEnv, cNapiValue: cNapiValue) + + self.init(cNapiEnv: cNapiEnv, cNapiValue: cNapiValue, napiValueType: napiValueType) + } + + public init(cNapiEnv: napi_env, cNapiValue: napi_value, napiValueType: NAPIValueType) { + self.cNapiEnv = cNapiEnv + self.cNapiValue = cNapiValue + self.napiValueType = napiValueType + } + + public static func create(cNapiEnv: napi_env, nativeValue: T) -> NAPIValue where T: NAPIValueCompatible { + return create(cNapiEnv: cNapiEnv, nativeValue: nativeValue, napiValueType: T.napiValueType) + } + + // NOTE: if a napiValueType of .nullable(...) is provided the function will either return a ".nullable(nil)" or a NAPIValue of the wrapped type + public static func create(cNapiEnv: napi_env, nativeValue: Any, napiValueType: NAPIValueType) -> NAPIValue { + // convert the array to the NAPIValueCompatible protocol + guard let _ = nativeValue as? NAPIValueCompatible else { + fatalError("Argument 'nativeValue' must be be compatible with the NAPIValueCompatible protocol.") + } + + if case .nullable(_) = napiValueType { + // if napiValueType is nullable, the nativeValue may be nil: proceed + } else { + // if napiValueType is not optional, make sure that nativeValue is not nil + if case Optional.none = nativeValue { + fatalError("Argument 'nativeValue' cannot be nil if its type is not optional.") + } + } + + switch napiValueType { + case .boolean: + return createBoolean(cNapiEnv: cNapiEnv, nativeValue: nativeValue as! Bool) + case .number: + return createNumber(cNapiEnv: cNapiEnv, nativeValue: nativeValue as! Double) + case .string: + return createString(cNapiEnv: cNapiEnv, nativeValue: nativeValue as! String) + case .nullable(let napiValueTypeOfWrapped): + if napiValueTypeOfWrapped != nil { + if case Optional.none = nativeValue { + // if we have a wrapped type but the value is nil, return a JavaScript null + return createNull(cNapiEnv: cNapiEnv) + } else { + // if the value is non-nil, return the appropriate JavaScript type + return create(cNapiEnv: cNapiEnv, nativeValue: nativeValue, napiValueType: napiValueTypeOfWrapped!) + } + } else { + // if we don't have a type then return null + return createNull(cNapiEnv: cNapiEnv) + } + case .object(_, let swiftType): + // NOTE: all NAPIValues which we create from Swift types must have an associated swiftType + guard let swiftType = swiftType else { + fatalError("Argument 'nativeValueType' is an object but has no associated Swift type") + } + guard swiftType.self == type(of: nativeValue).self else { + fatalError("Argument 'nativeValueType' specified an object with a different Swift type than argument 'nativeValue'") + } + guard let nativeValueAsNapiObjectCompatible = nativeValue as? NAPIObjectCompatible else { + fatalError("Argument 'nativeValue' does not conform to the NAPIObjectCompatible protocol") + } + return createObject(cNapiEnv: cNapiEnv, nativeValue: nativeValueAsNapiObjectCompatible, ofType: swiftType) + case .array(let napiValueTypeOfElements): + if let napiValueTypeOfElements = napiValueTypeOfElements { + return createArray(cNapiEnv: cNapiEnv, nativeArray: nativeValue, napiValueTypeOfElements: napiValueTypeOfElements) + } else { + // array is empty; return an empty array + fatalError("WE NEED TO ADD SUPPORT TO CREATE AN EMPTY ARRAY") + } + case .function: + return createFunction(cNapiEnv: cNapiEnv, nativeValue: nativeValue as! NAPIJavaScriptFunction) + case .error: + return createError(cNapiEnv: cNapiEnv, nativeValue: nativeValue as! NAPIJavaScriptError) + case .undefined: + // TODO: throw a JavaScript error instead + fatalError() + case .unsupported: + // TODO: throw a JavaScript error instead + fatalError() + } + } + + private static func createBoolean(cNapiEnv: napi_env, nativeValue: Bool) -> NAPIValue { + var resultAsCNapiValue: napi_value! = nil + + // NOTE: napi_get_boolean gets the JavaScript boolean singleton (true or false) + let status = napi_get_boolean(cNapiEnv, nativeValue, &resultAsCNapiValue) + guard status == napi_ok else { + // TODO: check for JavaScript errors instead and throw them instead + fatalError() + } + + return NAPIValue(cNapiEnv: cNapiEnv, cNapiValue: resultAsCNapiValue) + } + + private static func createNumber(cNapiEnv: napi_env, nativeValue: Double) -> NAPIValue { + var resultAsCNapiValue: napi_value! = nil + + let status = napi_create_double(cNapiEnv, nativeValue, &resultAsCNapiValue) + guard status == napi_ok else { + // TODO: check for JavaScript errors instead and throw them instead + fatalError() + } + + return NAPIValue(cNapiEnv: cNapiEnv, cNapiValue: resultAsCNapiValue) + } + + private static func createString(cNapiEnv: napi_env, nativeValue: String) -> NAPIValue { + var resultAsCNapiValue: napi_value! = nil + + let status = napi_create_string_utf8(cNapiEnv, nativeValue, nativeValue.utf8.count, &resultAsCNapiValue) + guard status == napi_ok else { + // TODO: check for JavaScript errors instead and throw them instead + fatalError() + } + + return NAPIValue(cNapiEnv: cNapiEnv, cNapiValue: resultAsCNapiValue) + } + + private static func createNull(cNapiEnv: napi_env) -> NAPIValue { + var resultAsCNapiValue: napi_value! = nil + + // NOTE: napi_get_boolean gets the JavaScript null singleton + let status = napi_get_null(cNapiEnv, &resultAsCNapiValue) + guard status == napi_ok else { + // TODO: check for JavaScript errors instead and throw them instead + fatalError() + } + + return NAPIValue(cNapiEnv: cNapiEnv, cNapiValue: resultAsCNapiValue) + } + + private static func createObject(cNapiEnv: napi_env, nativeValue: NAPIObjectCompatible, ofType targetType: NAPIObjectCompatible.Type) -> NAPIValue { + var resultAsCNapiValue: napi_value! = nil + + var status = napi_create_object(cNapiEnv, &resultAsCNapiValue) + guard status == napi_ok else { + // TODO: check for JavaScript errors instead and throw them instead + fatalError() + } + + // create a NAPI value by reflecting on the properties of the provided object + let mirror = Mirror(reflecting: nativeValue) + for child in mirror.children { + guard let propertyName = child.label else { + fatalError("Mirror reflection failed: could not retrieve property name") + } + let propertyValue = child.value + + let propertyNameAsNapiValue = NAPIValue.create(cNapiEnv: cNapiEnv, nativeValue: propertyName, napiValueType: .string) + + let propertyAsCNapiValue: napi_value + if case Optional.none = propertyValue { + propertyAsCNapiValue = createNull(cNapiEnv: cNapiEnv).cNapiValue + } else { + guard let propertyValueAsNapiValueCompatible = propertyValue as? NAPIValueCompatible else { + fatalError("Property \(propertyName) is not a NAPIValueCompatible type") + } + let napiValueTypeOfPropertyValue = type(of: propertyValueAsNapiValueCompatible).napiValueType + propertyAsCNapiValue = NAPIValue.create(cNapiEnv: cNapiEnv, nativeValue: propertyValueAsNapiValueCompatible, napiValueType: napiValueTypeOfPropertyValue).cNapiValue + } + + status = napi_set_property(cNapiEnv, resultAsCNapiValue, propertyNameAsNapiValue.cNapiValue, propertyAsCNapiValue) + guard status == napi_ok else { + // TODO: check for JavaScript errors instead and throw them instead + fatalError() + } + } + + return NAPIValue(cNapiEnv: cNapiEnv, cNapiValue: resultAsCNapiValue) + } + + private static func createArray(cNapiEnv: napi_env, nativeArray: Any, napiValueTypeOfElements: NAPIValueType) -> NAPIValue { + // convert the array to the NAPIValueCompatible protocol + guard let napiCompatibleValueArray = nativeArray as? Array else { + fatalError("Argument 'nativeArray' must be an array of elementscompatible with the NAPIValueCompatible protocol.") + } + + var subelementsAsNapiValues: [NAPIValue] = [] + for index in 0.. NAPIValue { + precondition(napiValues.count < UInt32.max, "Argument 'napiValues may not have an element count greater than UInt32.max") + + var status: napi_status + // + // create the array + var arrayAsCNapiValue: napi_value! = nil + status = napi_create_array_with_length(cNapiEnv, napiValues.count, &arrayAsCNapiValue) + guard status == napi_ok, arrayAsCNapiValue != nil else { + // TODO: check for JavaScript errors instead and throw them instead + fatalError() + } + // + // populate the napi array + for index in 0.. NAPIValue { + return NAPIValue(cNapiEnv: cNapiEnv, cNapiValue: nativeValue.cNapiValue) + } + + private static func createError(cNapiEnv: napi_env, nativeValue: NAPIJavaScriptError) -> NAPIValue { + var status: napi_status + + // create a napi_value representing the error's message + let messageAsCNapiValue = NAPIValue.createString(cNapiEnv: cNapiEnv, nativeValue: nativeValue.message).cNapiValue + + // create a napi_value representing the error's code (optional) + let codeAsCNapiValue: napi_value? + if let code = nativeValue.code { + codeAsCNapiValue = NAPIValue.createString(cNapiEnv: cNapiEnv, nativeValue: code).cNapiValue + } else { + codeAsCNapiValue = nil + } + + // create an error napi_value + var errorAsCNapiValue: napi_value! = nil + switch nativeValue.name { + case .Error: + status = napi_create_error(cNapiEnv, codeAsCNapiValue, messageAsCNapiValue, &errorAsCNapiValue) + guard status == napi_ok, errorAsCNapiValue != nil else { + // TODO: check for JavaScript errors instead and throw them instead + fatalError() + } + case .TypeError: + status = napi_create_type_error(cNapiEnv, codeAsCNapiValue, messageAsCNapiValue, &errorAsCNapiValue) + guard status == napi_ok, errorAsCNapiValue != nil else { + // TODO: check for JavaScript errors instead and throw them instead + fatalError() + } + case .RangeError: + status = napi_create_range_error(cNapiEnv, codeAsCNapiValue, messageAsCNapiValue, &errorAsCNapiValue) + guard status == napi_ok, errorAsCNapiValue != nil else { + // TODO: check for JavaScript errors instead and throw them instead + fatalError() + } + } + // + return NAPIValue(cNapiEnv: cNapiEnv, cNapiValue: errorAsCNapiValue) + } + + public func asNAPIValueCompatible() throws -> NAPIValueCompatible? { + do { + switch self.napiValueType { + case .boolean: + let value = try self.asBool() + return value + case .number: + let value = try self.asDouble() + return value + case .string: + let value = try self.asString() + return value + case .nullable(let wrappedType): + precondition(wrappedType == nil, "NAPIValues of type .nullable(...) should only be mapped to null itself.") + return nil + case .object: + fatalError("This function should not be called for object NAPIValueTypes; call .asNAPIValueCompatibleObject(...) instead") + case .array(_): + fatalError("This function should not be called for array NAPIValueTypes; call .asArrayOfNAPIValueCompatible(...) instead") + case .function: + let value = try self.asJavaScriptFunction() + return value + case .error: + let value = try self.asJavaScriptError() + return value + case .undefined: + return nil + case .unsupported: + return nil + } + } catch NAPIValueError.otherNapiError { + // TODO: handle this error...or convert it into a NAPISwiftBridgeJavaScriptThrowableError and throw it instead + fatalError() + } catch { + // any other errors indicate a programming bug + fatalError() + } + } + + public func asNAPIValueCompatibleObject(ofType targetType: NAPIObjectCompatible.Type) throws -> NAPIValueCompatible? { + do { + if case .object(_) = self.napiValueType { + let value = try self.asObject(ofType: targetType) + return value + } else { + // not an object + fatalError("This funciton should not be called for non-object NAPIValueTypes") + } + } catch NAPIValueError.otherNapiError { + // TODO: handle this error...or convert it into a NAPISwiftBridgeJavaScriptThrowableError and throw it instead + fatalError() + } catch { + // any other errors indicate a programming bug + fatalError() + } + } + + public func asArrayOfNAPIValueCompatible(elementNapiValueType: NAPIValueType) throws -> [Any]? { + do { + // TODO: use this same "case" and "let" combo (case before ".", let in the parens) EVERYWHERE in our code...for consistency + if case .array(_) = self.napiValueType { + if let valueAsNAPIValueCompatible = try self.asArray(elementNapiValueType: elementNapiValueType) { + return valueAsNAPIValueCompatible + } else { + return nil + } + } else { + // not an array + fatalError("This funciton should not be called for non-array NAPIValueTypes") + } + } catch NAPIValueError.otherNapiError { + // TODO: handle this error...or convert it into a NAPISwiftBridgeJavaScriptThrowableError and throw it instead + fatalError() + } catch { + // any other errors indicate a programming bug + fatalError() + } + } + + public func asBool() throws -> Bool? { + do { + switch self.napiValueType { + case .boolean: + let valueAsBool = try self.convertCNapiValueToBool() + return valueAsBool + case .number: + return nil + case .string: + return nil + case .nullable(let wrappedType): + precondition(wrappedType == nil, "NAPIValues of type .nullable(...) should only be mapped to null itself.") + return nil + case .object: + return nil + case .array(_): + return nil + case .function: + return nil + case .error: + return nil + case .undefined: + return nil + case .unsupported: + return nil + } + } catch NAPIValueError.otherNapiError { + // TODO: handle this error...or convert it into a NAPISwiftBridgeJavaScriptThrowableError and throw it instead + fatalError() + } catch { + // any other errors indicate a programming bug + fatalError() + } + } + + public func asDouble() throws -> Double? { + do { + switch self.napiValueType { + case .boolean: + let valueAsBool = try self.convertCNapiValueToBool() + return valueAsBool ? 1.0 : 0.0 + case .number: + let valueAsDouble = try self.convertCNapiValueToDouble() + return valueAsDouble + case .string: + let valueAsString = try self.convertCNapiValueToString() + return Double(valueAsString) + case .nullable(let wrappedType): + precondition(wrappedType == nil, "NAPIValues of type .nullable(...) should only be mapped to null itself.") + return nil + case .object: + return nil + case .array(_): + return nil + case .function: + return nil + case .error: + return nil + case .undefined: + return nil + case .unsupported: + return nil + } + } catch NAPIValueError.otherNapiError { + // TODO: handle this error...or convert it into a NAPISwiftBridgeJavaScriptThrowableError and throw it instead + fatalError() + } catch { + // any other errors indicate a programming bug + fatalError() + } + } + + public func asString() throws -> String? { + do { + switch self.napiValueType { + case .boolean: + let valueAsBool = try self.convertCNapiValueToBool() + return String(valueAsBool) + case .number: + let valueAsDouble = try self.convertCNapiValueToDouble() + return String(valueAsDouble) + case .string: + let valueAsString = try self.convertCNapiValueToString() + return valueAsString + case .nullable(let wrappedType): + precondition(wrappedType == nil, "NAPIValues of type .nullable(...) should only be mapped to null itself.") + return nil + case .object: + return nil + case .array(_): + return nil + case .function: + return nil + case .error: + return nil + case .undefined: + return nil + case .unsupported: + return nil + } + } catch NAPIValueError.otherNapiError { + // TODO: handle this error...or convert it into a NAPISwiftBridgeJavaScriptThrowableError and throw it instead + fatalError() + } catch { + // any other errors indicate a programming bug + fatalError() + } + } + + public func asArray(elementNapiValueType: NAPIValueType) throws -> [Any]? { + do { + switch self.napiValueType { + case .boolean: + return nil + case .number: + return nil + case .string: + return nil + case .nullable(let wrappedType): + precondition(wrappedType == nil, "NAPIValues of type .nullable(...) should only be mapped to null itself.") + return nil + case .object: + return nil + case .array(let elementNAPIValueType): + if let elementNAPIValueType = elementNAPIValueType { + let valueAsArray = try self.convertCNapiValueToArray(elementNapiValueType: elementNAPIValueType) + return valueAsArray + } else { + return [] + } + case .function: + return nil + case .error: + return nil + case .undefined: + return nil + case .unsupported: + return nil + } + } catch NAPIValueError.otherNapiError { + // TODO: handle this error...or convert it into a NAPISwiftBridgeJavaScriptThrowableError and throw it instead + fatalError() + } catch { + // any other errors indicate a programming bug + fatalError() + } + } + + public func asArrayOfNapiValues() throws -> [NAPIValue]? { + do { + switch self.napiValueType { + case .boolean: + return nil + case .number: + return nil + case .string: + return nil + case .nullable(let wrappedType): + precondition(wrappedType == nil, "NAPIValues of type .nullable(...) should only be mapped to null itself.") + return nil + case .object: + return nil + case .array(_): + let valueAsArrayOrNapiValues = try self.convertCNapiValueToArrayOfNapiValues() + return valueAsArrayOrNapiValues + case .function: + return nil + case .error: + return nil + case .undefined: + return nil + case .unsupported: + return nil + } + } catch NAPIValueError.otherNapiError { + // TODO: handle this error...or convert it into a NAPISwiftBridgeJavaScriptThrowableError and throw it instead + fatalError() + } catch { + // any other errors indicate a programming bug + fatalError() + } + } + + public func asObject(ofType targetType: NAPIObjectCompatible.Type) throws -> NAPIValueCompatible? { + do { + switch self.napiValueType { + case .boolean: + return nil + case .number: + return nil + case .string: + return nil + case .nullable(let wrappedType): + precondition(wrappedType == nil, "NAPIValues of type .nullable(...) should only be mapped to null itself.") + return nil + case .object: + let valueAsObject = try self.convertCNapiValueToObject(ofType: targetType) + return valueAsObject + case .array(_): + return nil + case .function: + return nil + case .error: + return nil + case .undefined: + return nil + case .unsupported: + return nil + } + } catch NAPIValueError.otherNapiError { + // TODO: handle this error...or convert it into a NAPISwiftBridgeJavaScriptThrowableError and throw it instead + fatalError() + } catch { + // any other errors indicate a programming bug + fatalError() + } + } + + public func asJavaScriptFunction() throws -> NAPIValueCompatible? { + do { + switch self.napiValueType { + case .boolean: + return nil + case .number: + return nil + case .string: + return nil + case .nullable(let wrappedType): + precondition(wrappedType == nil, "NAPIValues of type .nullable(...) should only be mapped to null itself.") + return nil + case .object: + return nil + case .array(_): + return nil + case .function: + let valueAsFunction = try self.convertCNapiValueToFunction() + return valueAsFunction + case .error: + return nil + case .undefined: + return nil + case .unsupported: + return nil + } + } catch NAPIValueError.otherNapiError { + // TODO: handle this error...or convert it into a NAPISwiftBridgeJavaScriptThrowableError and throw it instead + fatalError() + } catch { + // any other errors indicate a programming bug + fatalError() + } + } + + public func asJavaScriptError() throws -> NAPIValueCompatible? { + do { + switch self.napiValueType { + case .boolean: + return nil + case .number: + return nil + case .string: + return nil + case .nullable(let wrappedType): + precondition(wrappedType == nil, "NAPIValues of type .nullable(...) should only be mapped to null itself.") + return nil + case .object: + return nil + case .array(_): + return nil + case .function: + return nil + case .error: + let valueAsError = try self.convertCNapiValueToError() + return valueAsError + case .undefined: + return nil + case .unsupported: + return nil + } + } catch NAPIValueError.otherNapiError { + // TODO: handle this error...or convert it into a NAPISwiftBridgeJavaScriptThrowableError and throw it instead + fatalError() + } catch { + // any other errors indicate a programming bug + fatalError() + } + } + + // MARK: Conversion functions + + private func convertCNapiValueToBool() throws -> Bool { + guard self.napiValueType == .boolean else { + throw NAPIValueError.typeMismatch + } + + var status: napi_status + var valueAsBoolean: Bool = false + // + status = napi_get_value_bool(self.cNapiEnv, self.cNapiValue, &valueAsBoolean) + guard status == napi_ok else { + if status == napi_boolean_expected { + // type mismatch + // TODO: we should still check for a JavaScript exception + throw NAPIValueError.typeMismatch + } else { + // TODO: we should check for a JavaScript exception + throw NAPIValueError.otherNapiError + } + } + + return valueAsBoolean + } + + private func convertCNapiValueToDouble() throws -> Double { + guard self.napiValueType == .number else { + throw NAPIValueError.typeMismatch + } + + var status: napi_status + var valueAsDouble: Double = 0 + // + status = napi_get_value_double(self.cNapiEnv, self.cNapiValue, &valueAsDouble) + guard status == napi_ok else { + if status == napi_number_expected { + // type mismatch + // TODO: we should still check for a JavaScript exception + throw NAPIValueError.typeMismatch + } else { + // TODO: we should check for a JavaScript exception + throw NAPIValueError.otherNapiError + } + } + + return valueAsDouble + } + + private func convertCNapiValueToString() throws -> String { + guard self.napiValueType == .string else { + throw NAPIValueError.typeMismatch + } + + var status: napi_status + + var bufferSize = 0 + + // first, get the size of the string; we do this by passing in a nil buffer (and then we get the size from its 'result' parameter) + var requiredBufferSize: Int = 0 + status = napi_get_value_string_utf8(self.cNapiEnv, self.cNapiValue, nil, 0, &requiredBufferSize) + guard status == napi_ok else { + if status == napi_string_expected { + // type mismatch + // TODO: we should still check for a JavaScript exception + throw NAPIValueError.typeMismatch + } else { + // TODO: we should check for a JavaScript exception + throw NAPIValueError.otherNapiError + } + } + bufferSize = requiredBufferSize + 1 // +1 for the null terminator + + // then get the full string; pass in a buffer equal to its size; we do not worry about null terminators since N-API auto-truncates for us + let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize) + var populatedBufferSize: Int = 0 + status = napi_get_value_string_utf8(self.cNapiEnv, self.cNapiValue, buffer, bufferSize, &populatedBufferSize) + guard status == napi_ok else { + // TODO: we should check for a JavaScript exception + throw NAPIValueError.otherNapiError + } + + return String(cString: buffer) + } + + private func convertCNapiValueToObject(ofType targetType: NAPIObjectCompatible.Type) throws -> NAPIValueCompatible { + // verify that our napiValueType is an object type + guard case .object(let napiPropertyNamesAndTypes, let swiftType) = self.napiValueType else { + throw NAPIValueError.typeMismatch + } + // verify that our napiValueType's swiftType (if one is provided) matches the provided targetType + if let swiftType = swiftType { + guard swiftType.self == type(of: targetType).self else { + throw NAPIValueError.typeMismatch + } + } + + let swiftPropertyNamesAndTypes = targetType.NAPIPropertyNamesAndTypes + + // verify that our NAPIValue's properties and the target swift type's properties are compatible + if napiPropertyNamesAndTypes.count != swiftPropertyNamesAndTypes.count { + fatalError("NAPI argument count \(napiPropertyNamesAndTypes.count) does not match native argument count \(napiPropertyNamesAndTypes.count)") + } + for napiPropertyNameAndType in napiPropertyNamesAndTypes { + let napiPropertyName = napiPropertyNameAndType.key + let napiPropertyNapiValueType = napiPropertyNameAndType.value + + if swiftPropertyNamesAndTypes.keys.contains(napiPropertyName) == false { + fatalError("NAPI property \(napiPropertyName) does not exist in Swift object.") + } + let swiftPropertyNapiValueType = swiftPropertyNamesAndTypes[napiPropertyName]! + + if napiPropertyNapiValueType.isCompatible(withRhs: swiftPropertyNapiValueType, disregardRhsOptionals: true) == false { + fatalError("Type mismatch: NAPI property type is incompatible with Swift property type for property \(napiPropertyNameAndType.key).") + } + } + + // build up a set of property names (with their associated values) as we deecode the underlying napi_value + var propertyNamesAndValues: [String: Any] = [:] + + var status: napi_status + + for propertyNameAndType in napiPropertyNamesAndTypes { + // get the property name + let propertyName = propertyNameAndType.key + // NOTE: we do not retrieve the propertyNameAndType's value (i.e. the property's NAPIValueType) because the decoder validates that the property types match instead; this is necessary because of type comformance limitations in Swift. We could choose to verify against non-array types, but using the decoder provides a single path for type match validation +// // retrieve the property's NAPIValueType (to verify for compatibility against the actual property's NAPIValueType) +// let propertyNapiValueType = propertyNameAndType.value + + let propertyNameAsNapiValue = NAPIValue.create(cNapiEnv: cNapiEnv, nativeValue: propertyName) + + // get the property's associated value (initially as a napi_value but then converted into a Swift type) + var propertyValueAsCNapiValue: napi_value! = nil + status = napi_get_property(cNapiEnv, self.cNapiValue, propertyNameAsNapiValue.cNapiValue, &propertyValueAsCNapiValue) + guard status == napi_ok else { + // TODO: check for JavaScript errors instead and throw them instead + fatalError() + } + // NOTE: we use "Any" as our type here because .asArrayOfNAPIValueCompatible(...) must return a result of type Any because Swift cannot return an array of NAPIValueCompatible-conformant objects via cast to NAPIValueCompatible + var propertyValueAsOptionalNapiValueCompatible: Any + do { + switch swiftPropertyNamesAndTypes[propertyName] { + case .object(_, let propertySwiftType): + // object type + if let propertySwiftType = propertySwiftType { + propertyValueAsOptionalNapiValueCompatible = try NAPIValue(cNapiEnv: cNapiEnv, cNapiValue: propertyValueAsCNapiValue).asNAPIValueCompatibleObject(ofType: propertySwiftType) as Any + } else { + fatalError("Swift type must be specified for Swift property \(propertyName); found: nil") + } + case .array(let elementNapiValueType): + // array type + if let elementNapiValueType = elementNapiValueType { + propertyValueAsOptionalNapiValueCompatible = try NAPIValue(cNapiEnv: cNapiEnv, cNapiValue: propertyValueAsCNapiValue).asArrayOfNAPIValueCompatible(elementNapiValueType: elementNapiValueType) as Any + } else { + // if elementNapiValueType is nil, then the array is empty + propertyValueAsOptionalNapiValueCompatible = [] + } + default: + propertyValueAsOptionalNapiValueCompatible = try NAPIValue(cNapiEnv: cNapiEnv, cNapiValue: propertyValueAsCNapiValue).asNAPIValueCompatible() as Any + } + } catch (let error) { + throw error + } + // TODO: does this really test for nil? Or do we need to do "== nil" check, etc? + if case Optional.none = propertyValueAsOptionalNapiValueCompatible { + // value is nil + propertyNamesAndValues[propertyName] = nil + } else { + propertyNamesAndValues[propertyName] = propertyValueAsOptionalNapiValueCompatible + } + } + + // create an instance of the target type by using our NAPIBridgingDecoder combined with Decodable's auto-compiler-generated "init" function + // NOTE: in the future, if Swift allows us to dynamically generate our own class properties in compiled code on the fly (as it does for Encodable), we can remove the need for Swift struct creators to manually describe their struct's properties' types + let decoder = NAPIBridgingDecoder(propertyNamesAndValues: propertyNamesAndValues) + do { + let result = try targetType.init(from: decoder) + return result + } catch let error { + // TODO: catch the actual Decodable error and pass it along (or at least log/display it properly) + fatalError("Type mismatch or other error \(error)") + } + } + + private func convertCNapiValueToArray(elementNapiValueType: NAPIValueType) throws -> [Any] { + let selfAsArrayOfNapiValues: [NAPIValue] + do { + selfAsArrayOfNapiValues = try self.convertCNapiValueToArrayOfNapiValues() + } catch (let error) { + throw error + } + + var selfAsArray: [Any] = [] + selfAsArray.reserveCapacity(selfAsArrayOfNapiValues.count) + // + for napiValue in selfAsArrayOfNapiValues { + let element: Any? + switch elementNapiValueType { + case .object(_, let propertySwiftType): + // object type + if let propertySwiftType = propertySwiftType { + element = try napiValue.asNAPIValueCompatibleObject(ofType: propertySwiftType) + } else { + fatalError("Swift type must be specified for array elements' native objects; found: nil") + } + case .array(let elementNapiValueType): + // array type + if let elementNapiValueType = elementNapiValueType { + element = try napiValue.asArrayOfNAPIValueCompatible(elementNapiValueType: elementNapiValueType) + } else { + // if elementNapiValueType is nil, then the array is empty + element = [] + } + default: + element = try napiValue.asNAPIValueCompatible() + } + + if let element = element { + selfAsArray.append(element) + } else { + // if we could not convert the value, throw an error + throw NAPIValueError.otherNapiError + } + } + + return selfAsArray + } + + private func convertCNapiValueToArrayOfNapiValues() throws -> Array { + guard case .array(_) = self.napiValueType else { + throw NAPIValueError.typeMismatch + } + + var status: napi_status + + var valueAsArrayOfNapiValues: Array = [] + // + // capture the array length + var arrayLength: UInt32 = 0 + status = napi_get_array_length(self.cNapiEnv, self.cNapiValue, &arrayLength) + guard status == napi_ok else { + if status == napi_array_expected { + // type mismatch + // TODO: we should still check for a JavaScript exception + throw NAPIValueError.typeMismatch + } else { + // TODO: we should check for a JavaScript exception + throw NAPIValueError.otherNapiError + } + } + if arrayLength > Int.max { + fatalError("Array cannot exceed Int.max in length") + } + let arrayLengthAsInt = Int(arrayLength) + // + valueAsArrayOfNapiValues.reserveCapacity(arrayLengthAsInt) + // capture each array element + for indexAsUInt32 in 0.. NAPIJavaScriptFunction { + guard case .function = self.napiValueType else { + throw NAPIValueError.typeMismatch + } + + return NAPIJavaScriptFunction(cNapiEnv: self.cNapiEnv, cNapiValue: self.cNapiValue) + } + + private func convertCNapiValueToError() throws -> NAPIJavaScriptError { + guard case .error = self.napiValueType else { + throw NAPIValueError.typeMismatch + } + + var status: napi_status + + // capture the error name + let namePropertyKeyAsCNapiValue = NAPIValue.create(cNapiEnv: self.cNapiEnv, nativeValue: "name").cNapiValue + // + var namePropertyValueAsOptionalCNapiValue: napi_value? + status = napi_get_property(self.cNapiEnv, self.cNapiValue, namePropertyKeyAsCNapiValue, &namePropertyValueAsOptionalCNapiValue) + guard status == napi_ok else { + fatalError("Could not get value of property 'name' of argument 'errorAsCNapiValue'") + } + guard let namePropertyValueAsOptional = try? NAPIValue(cNapiEnv: self.cNapiEnv, cNapiValue: namePropertyValueAsOptionalCNapiValue!).asString() else { + fatalError("Could not get value of property 'name' of argument 'errorAsCNapiValue'") + } + guard let namePropertyValue = namePropertyValueAsOptional else { + fatalError("Could not get value of property 'name' of argument 'errorAsCNapiValue'") + } + + switch namePropertyValue { + case "Error", + "TypeError", + "RangeError": + // + guard let namePropertyValueAsNameOption = NAPIJavaScriptError.NameOption(rawValue: namePropertyValue) else { + fatalError("Invalid code path: all namePropertyValue options should have satisfied this test") + } + + // capture the error message + let messagePropertyKeyAsCNapiValue = NAPIValue.create(cNapiEnv: self.cNapiEnv, nativeValue: "message").cNapiValue + // + var errorHasMessageProperty: Bool = false + status = napi_has_property(self.cNapiEnv, self.cNapiValue, messagePropertyKeyAsCNapiValue, &errorHasMessageProperty) + guard status == napi_ok else { + fatalError("Could not determine if argument 'errorAsCNapiValue' has property 'message'") + } + guard errorHasMessageProperty == true else { + fatalError("Argument 'errorAsCNapiValue' is missing required property 'message'") + } + // + var messagePropertyValueAsOptionalCNapiValue: napi_value? + status = napi_get_property(self.cNapiEnv, self.cNapiValue, messagePropertyKeyAsCNapiValue, &messagePropertyValueAsOptionalCNapiValue) + guard status == napi_ok else { + fatalError("Could not get value of property 'message' of argument 'errorAsCNapiValue'") + } + guard let messagePropertyValueAsOptional = try? NAPIValue(cNapiEnv: self.cNapiEnv, cNapiValue: messagePropertyValueAsOptionalCNapiValue!).asString() else { + fatalError("Could not get value of property 'message' of argument 'errorAsCNapiValue'") + } + guard let messagePropertyValue = messagePropertyValueAsOptional else { + fatalError("Could not get value of property 'message' of argument 'errorAsCNapiValue'") + } + + // capture the (optional) error code (optionally used by N-API) + let codePropertyKeyAsCNapiValue = NAPIValue.create(cNapiEnv: self.cNapiEnv, nativeValue: "code").cNapiValue + // + var errorHasCodeProperty: Bool = false + status = napi_has_property(self.cNapiEnv, self.cNapiValue, codePropertyKeyAsCNapiValue, &errorHasCodeProperty) + guard status == napi_ok else { + fatalError("Could not determine if argument 'errorAsCNapiValue' has property 'code'") + } + // + var codePropertyValueAsOptional: String? = nil + if errorHasCodeProperty == true { + var codePropertyValueAsOptionalCNapiValue: napi_value? + status = napi_get_property(self.cNapiEnv, self.cNapiValue, codePropertyKeyAsCNapiValue, &codePropertyValueAsOptionalCNapiValue) + guard status == napi_ok else { + fatalError("Could not get value of property 'code' of argument 'errorAsCNapiValue'") + } + guard let codePropertyValueAsNonOptional = try? NAPIValue(cNapiEnv: self.cNapiEnv, cNapiValue: codePropertyValueAsOptionalCNapiValue!).asString() else { + fatalError("Could not get value of property 'code' of argument 'errorAsCNapiValue'") + } + codePropertyValueAsOptional = codePropertyValueAsNonOptional + } + + return NAPIJavaScriptError(name: namePropertyValueAsNameOption, message: messagePropertyValue, code: codePropertyValueAsOptional) + default: + // TODO: throw "rangeError" indicating that we cannot handle this type of error + fatalError("Unknown error type") + } + } +} diff --git a/MorphicMacOS/N-API/NAPIValueCompatible.swift b/MorphicMacOS/N-API/NAPIValueCompatible.swift new file mode 100644 index 0000000..16d67fa --- /dev/null +++ b/MorphicMacOS/N-API/NAPIValueCompatible.swift @@ -0,0 +1,46 @@ +// +// NAPIValueCompatible.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +// NOTE: NAPIValueCompatible designates that a Swift type can be converted directly to and from a napi_value +public protocol NAPIValueCompatible { + static var napiValueType: NAPIValueType { get } +} + +extension Bool: NAPIValueCompatible { + public static var napiValueType: NAPIValueType { + return .boolean + } +} +// +extension Double: NAPIValueCompatible { + public static var napiValueType: NAPIValueType { + return .number + } +} +// +extension String: NAPIValueCompatible { + public static var napiValueType: NAPIValueType { + return .string + } +} + +extension Optional: NAPIValueCompatible where Wrapped: NAPIValueCompatible { + public static var napiValueType: NAPIValueType { + return .nullable(type: Wrapped.napiValueType) + } +} + +extension Array: NAPIValueCompatible where Element: NAPIValueCompatible { + public static var napiValueType: NAPIValueType { + return .array(type: Element.napiValueType) + } +} diff --git a/MorphicMacOS/N-API/NAPIValueType.swift b/MorphicMacOS/N-API/NAPIValueType.swift new file mode 100644 index 0000000..6665d48 --- /dev/null +++ b/MorphicMacOS/N-API/NAPIValueType.swift @@ -0,0 +1,341 @@ +// +// NAPIValueType.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +public indirect enum NAPIValueType { + case boolean + case number + case string + // + // NOTE: a nullable of type 'nil' denotes a JavaScript "null" with no attached type information + case nullable(type: NAPIValueType?) + // + // NOTE: for .object, swiftType will be set to null for incoming napi_values (since the type will be resolved when matching against the function's signature); the swiftType must/will be specified in the Swift struct signatures however (see NAPIObjectCompatible.napiValuetype). + case object(propertyNamesAndTypes: [String : NAPIValueType], swiftType: NAPIObjectCompatible.Type?) + // + // NOTE: an array of type 'nil' denotes an empty array + case array(type: NAPIValueType?) + // + case function + // + case error + // + case undefined + // + // NOTE: 'unsupported' denotes a type which we do not support + case unsupported + + // NOTE: setting disregardOptionals to true will equate optionally-wrapped types and non-wrapped types as a match (as long as the optional's wrapped type is the same as the non-wrapped type); this will be done recursively, ignoring any sub-optionals + public func isCompatible(withRhs rhs: NAPIValueType, disregardRhsOptionals: Bool = false) -> Bool { + switch self { + case .boolean: + if case .boolean = rhs { + return true + } + case .number: + if case .number = rhs { + return true + } + case .string: + if case .string = rhs { + return true + } + case .nullable(let selfType): + if case let .nullable(rhsType) = rhs { + if selfType == nil || rhsType == nil { + // if one nullable contains no type (i.e. 'nil' type), its type always matches any other nullable (since a nullable of type nil is a JavaScript "null" and can therefore satisfy any nullable type) + return true + } else { + return selfType!.isCompatible(withRhs: rhsType!, disregardRhsOptionals: disregardRhsOptionals) + } + } + case .object(let selfPropertyNamesAndTypes, let selfSwiftType): + var objectPropertyNamesAndTypesMatch = true + + if case let .object(rhsPropertyNamesAndTypes, rhsSwiftType) = rhs { + // make sure that the object types match (or that one is nil) + if (selfSwiftType == nil && rhsSwiftType != nil) || + (selfSwiftType != nil && rhsSwiftType == nil) { + // if one swiftType is nil, then we consider them a match; this is allowable because incoming napi_values which are objects do not actually have a SwiftType (yet the parameters to which they are passed _do_) + } else { + // if both swiftTypes are non-nil, they _must_ be the same type (i.e. we do not do type coersion for objects) + if selfSwiftType.self != rhsSwiftType.self { + objectPropertyNamesAndTypesMatch = false + break + } + } + + // check that the objects contain the same number of properties (and then compare the property names/types) + if selfPropertyNamesAndTypes.count == rhsPropertyNamesAndTypes.count { + // objects contain the same number of properties + + // check that all properties in self are compatible with the same-named properties in rhs (and that the same-named properties exist in rhs) + for selfPropertyNameAndType in selfPropertyNamesAndTypes { + let propertyName = selfPropertyNameAndType.key + if rhsPropertyNamesAndTypes.keys.contains(propertyName) { + if selfPropertyNamesAndTypes[propertyName]!.isCompatible(withRhs: rhsPropertyNamesAndTypes[propertyName]!, disregardRhsOptionals: disregardRhsOptionals) == false { + objectPropertyNamesAndTypesMatch = false + break + } + } else { + // property does not exist on rhs + objectPropertyNamesAndTypesMatch = false + break + } + } + } else { + // if the numbere of properties don't match, the object types are not a match + objectPropertyNamesAndTypesMatch = false + break + } + + // if the object swiftType matched (or one was nil)--and if all the property names/types matched--then the types are compatible + if objectPropertyNamesAndTypesMatch == true { + return true + } + } + case .array(let selfType): + if case let .array(rhsType) = rhs { + if selfType == nil || rhsType == nil { + // if one array contains no elements (i.e. 'nil' type), its type matches any other array (since JavaScript arrays with no elements "contain no type information"; likewise, two empty ('nil' type) arrays always match + return true + } else { + return selfType!.isCompatible(withRhs: rhsType!, disregardRhsOptionals: disregardRhsOptionals) + } + } + case .function: + if case .function = rhs { + return true + } + case .error: + if case .error = rhs { + return true + } + case .undefined: + if case .undefined = rhs { + return true + } + case .unsupported: + if case .unsupported = rhs { + return true + } + } + + // if no direct matches were found...but the user allows us to disregard optionals on the right-hand side...and if the right-side is an optional...then try that comparison now too + if disregardRhsOptionals == true { + if case let .nullable(rhsWrapped) = rhs { + if let rhsWrapped = rhsWrapped { + return self.isCompatible(withRhs: rhsWrapped, disregardRhsOptionals: disregardRhsOptionals) + } + } + } + + // otherwise, if no matches were found, return false + return false + } + // + public static func ==(lhs: NAPIValueType, rhs: NAPIValueType) -> Bool { + return lhs.isCompatible(withRhs: rhs, disregardRhsOptionals: false) + } + // + public static func !=(lhs: NAPIValueType, rhs: NAPIValueType) -> Bool { + return !(lhs == rhs) + } +} + +extension NAPIValueType { + public static func getNAPIValueType(cNapiEnv: napi_env, cNapiValue: napi_value) -> NAPIValueType { + var cNapiValuetype: napi_valuetype = napi_undefined + + var status: napi_status + + status = napi_typeof(cNapiEnv, cNapiValue, &cNapiValuetype) + guard status == napi_ok else { + fatalError("Could not get type of napi value") + } + + switch cNapiValuetype { + case napi_boolean: + return .boolean + case napi_number: + return .number + case napi_string: + return .string + case napi_null: + return .nullable(type: nil) + case napi_object: + // determine if this object is an array + var cNapiValueIsArray: Bool = false + status = napi_is_array(cNapiEnv, cNapiValue, &cNapiValueIsArray) + guard status == napi_ok else { + fatalError("Could not get type of napi value") + } + // + if cNapiValueIsArray == true { + return getNapiValueTypeOfArray(cNapiEnv: cNapiEnv, arrayAsCNapiValue: cNapiValue) + } + + // determine if this object is an error + var cNapiValueIsError: Bool = false + status = napi_is_error(cNapiEnv, cNapiValue, &cNapiValueIsError) + guard status == napi_ok else { + fatalError("Could not get type of napi value") + } + // + if cNapiValueIsError == true { + return .error + } + + // otherwise, napiValue is an object (an un-specialized object, not a specific object type which we already handle); create its type by enumerating the names/types of its properties + return getNapiValueTypeOfObject(cNapiEnv: cNapiEnv, objectAsCNapiValue: cNapiValue) + case napi_function: + return .function + case napi_undefined: + return .undefined + default: + // other types are unsupported + return .unsupported + } + } + + // NOTE: this function returns nil if it does not know the napi_valuetype of the element + private static func getNapiValueTypeOfArray(cNapiEnv: napi_env, arrayAsCNapiValue: napi_value) -> NAPIValueType { + var status: napi_status + + // make sure that the napi_value is an array + var isArray: Bool = false + status = napi_is_array(cNapiEnv, arrayAsCNapiValue, &isArray) + guard status == napi_ok else { + fatalError("Could not determine if argument 'arrayAsCNapiValue' represents an array") + } + + precondition(isArray == true, "Argument 'arrayAsCNapiValue' must represent an array") + + // make sure that the array contains elements + var lengthOfArray : UInt32 = 0 + status = napi_get_array_length(cNapiEnv, arrayAsCNapiValue, &lengthOfArray) + guard status == napi_ok else { + fatalError("Could not get element count of array") + } + + if lengthOfArray == 0 { + // if our array is empty, its type is effectively "undefined" + return .undefined + } + precondition(lengthOfArray <= Int.max, "Arrays may not have a length greater than Int.max") + + // get type of first element + let napiValueTypeOfFirstElement = getNapiValueTypeOfArrayElement(cNapiEnv: cNapiEnv, arrayAsCNapiValue: arrayAsCNapiValue, index: 0) + if napiValueTypeOfFirstElement == .unsupported { + // if the first element is an unsupported type, the array itself is an unsupported type + // NOTE: we do not return ".array(unsupported)" since we do not know if the array's elements are of the same unsupported type + return .unsupported + } + // capture the type of the first element as the type of "all" elements (which is true so far, since we have only explored one element) + var napiValueTypeOfAllElements = napiValueTypeOfFirstElement + + // get types of all subsequent elements (to ensure that they are all the same type...as we do not support mixed types in arrays) + for index in 1.. NAPIValueType { + var status: napi_status + + var cNapiValue: napi_value! = nil + status = napi_get_element(cNapiEnv, arrayAsCNapiValue, index, &cNapiValue) + guard status == napi_ok, cNapiValue != nil else { + fatalError("Could not get type of napi array element at specified index") + } + + return getNAPIValueType(cNapiEnv: cNapiEnv, cNapiValue: cNapiValue) + } + + private static func getNapiValueTypeOfObject(cNapiEnv: napi_env, objectAsCNapiValue: napi_value) -> NAPIValueType { + var status: napi_status + + // make sure that the napi_value is an object + var cNapiValuetype: napi_valuetype = napi_undefined + status = napi_typeof(cNapiEnv, objectAsCNapiValue, &cNapiValuetype) + guard status == napi_ok else { + fatalError("Could not determine if argument 'objectAsCNapiValue' represents an object") + } + + precondition(cNapiValuetype == napi_object, "Argument 'objectAsCNapiValue' must represent an object") + + // enumerate the properties of the object + var propertyNamesAsCNapiValue: napi_value! + status = napi_get_property_names(cNapiEnv, objectAsCNapiValue, &propertyNamesAsCNapiValue) + guard status == napi_ok else { + // TODO: check for JavaScript errors instead and throw them instead + fatalError("Could not get object's properties' names") + } + // + // convert the property names napi_value to an array of NAPIValues + let propertyNamesAsNapiValue = NAPIValue(cNapiEnv: cNapiEnv, cNapiValue: propertyNamesAsCNapiValue, napiValueType: NAPIValueType.array(type: .string)) + let propertyNamesAsArrayOfNapiValues: [NAPIValue] + do { + guard let propertyNamesAsArrayOfNapiValuesAsNonOptional = try propertyNamesAsNapiValue.asArrayOfNapiValues() else { + fatalError("Failed to enumerate array of object's property names") + } + propertyNamesAsArrayOfNapiValues = propertyNamesAsArrayOfNapiValuesAsNonOptional + } catch { + fatalError("Failed to enumerate array of object's property names") + } + // + // capture the properties' names and NAPIValueTypes and store them in a dictionary + var propertyNamesAndNapiValueTypes: [String : NAPIValueType] = [:] + for propertyNameAsNapiValue in propertyNamesAsArrayOfNapiValues { + // capture the property name + let propertyName: String + do { + guard let propertyNameAsNonOptional = try propertyNameAsNapiValue.asString() else { + fatalError("Could not convert object property name into String") + } + propertyName = propertyNameAsNonOptional + } catch { + fatalError("Could not convert object property name into String") + } + + // capture the property value's type + var propertyValueAsCNapiValue: napi_value! = nil + status = napi_get_property(cNapiEnv, objectAsCNapiValue, propertyNameAsNapiValue.cNapiValue, &propertyValueAsCNapiValue) + guard status == napi_ok else { + fatalError("Could not determine if argument 'objectAsCNapiValue' represents an object") + } + let propertyNapiValueType = NAPIValue(cNapiEnv: cNapiEnv, cNapiValue: propertyValueAsCNapiValue).napiValueType + + // add the property name and value type to our array + propertyNamesAndNapiValueTypes[propertyName] = propertyNapiValueType + } + + // NOTE: because object napi_values are typeless, we set the swiftType to nil; when this object is matched against an actual Swift function definition or Swift struct, we will verify that the types match (by comparing the number, names and types of properties) + return .object(propertyNamesAndTypes: propertyNamesAndNapiValueTypes, swiftType: nil) + } +} diff --git a/MorphicMacOS/N-API/include/js_native_api.h b/MorphicMacOS/N-API/include/js_native_api.h new file mode 100644 index 0000000..072b195 --- /dev/null +++ b/MorphicMacOS/N-API/include/js_native_api.h @@ -0,0 +1,196 @@ +// +// js_native_api.h +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +// These headers were copied or derived from the Node.js repositories at GitHub: +// +// Source: https://github.com/nodejs/node/blob/master/src/js_native_api.h +// License: https://github.com/nodejs/node/blob/master/LICENSE + +#ifndef js_native_api_h +#define js_native_api_h + +// This file needs to be compatible with C compilers. +#include // NOLINT(modernize-deprecated-headers) +#include // NOLINT(modernize-deprecated-headers) +#include "js_native_api_types.h" + +// Use INT_MAX, this should only be consumed by the pre-processor anyway. +#ifndef NAPI_VERSION +// The baseline version for N-API. +// The NAPI_VERSION controls which version will be used by default when +// compilling a native addon. If the addon developer specifically wants to use +// functions available in a new version of N-API that is not yet ported in all +// LTS versions, they can set NAPI_VERSION knowing that they have specifically +// depended on that version. +#define NAPI_VERSION 5 +#endif + +#ifndef NAPI_EXTERN + #define NAPI_EXTERN __attribute__((visibility("default"))) +#endif + +#define NAPI_AUTO_LENGTH SIZE_MAX + +#ifdef __cplusplus +#define EXTERN_C_START extern "C" { +#define EXTERN_C_END } +#else +#define EXTERN_C_START +#define EXTERN_C_END +#endif + +EXTERN_C_START + +// Getters for defined singletons +NAPI_EXTERN napi_status napi_get_null(napi_env env, napi_value* result); +NAPI_EXTERN napi_status napi_get_global(napi_env env, napi_value* result); +NAPI_EXTERN napi_status napi_get_boolean(napi_env env, + bool value, + napi_value* result); + +// Methods to create Primitive types/Objects +NAPI_EXTERN napi_status napi_create_object(napi_env env, napi_value* result); +NAPI_EXTERN napi_status napi_create_array_with_length(napi_env env, + size_t length, + napi_value* result); +NAPI_EXTERN napi_status napi_create_double(napi_env env, + double value, + napi_value* result); +NAPI_EXTERN napi_status napi_create_string_utf8(napi_env env, + const char* str, + size_t length, + napi_value* result); +NAPI_EXTERN napi_status napi_create_function(napi_env env, + const char* utf8name, + size_t length, + napi_callback cb, + void* data, + napi_value* result); +NAPI_EXTERN napi_status napi_create_error(napi_env env, + napi_value code, + napi_value msg, + napi_value* result); +NAPI_EXTERN napi_status napi_create_type_error(napi_env env, + napi_value code, + napi_value msg, + napi_value* result); +NAPI_EXTERN napi_status napi_create_range_error(napi_env env, + napi_value code, + napi_value msg, + napi_value* result); + +// Methods to get the native napi_value from Primitive type +NAPI_EXTERN napi_status napi_typeof(napi_env env, + napi_value value, + napi_valuetype* result); +NAPI_EXTERN napi_status napi_get_value_double(napi_env env, + napi_value value, + double* result); +NAPI_EXTERN napi_status napi_get_value_bool(napi_env env, + napi_value value, + bool* result); + +// Copies UTF-8 encoded bytes from a string into a buffer. +NAPI_EXTERN napi_status napi_get_value_string_utf8(napi_env env, + napi_value value, + char* buf, + size_t bufsize, + size_t* result); + +// Methods to work with Objects +NAPI_EXTERN napi_status napi_get_property_names(napi_env env, + napi_value object, + napi_value* result); +NAPI_EXTERN napi_status napi_set_property(napi_env env, + napi_value object, + napi_value key, + napi_value value); +NAPI_EXTERN napi_status napi_has_property(napi_env env, + napi_value object, + napi_value key, + bool* result); +NAPI_EXTERN napi_status napi_get_property(napi_env env, + napi_value object, + napi_value key, + napi_value* result); +NAPI_EXTERN napi_status napi_set_element(napi_env env, + napi_value object, + uint32_t index, + napi_value value); +NAPI_EXTERN napi_status napi_get_element(napi_env env, + napi_value object, + uint32_t index, + napi_value* result); +NAPI_EXTERN napi_status +napi_define_properties(napi_env env, + napi_value object, + size_t property_count, + const napi_property_descriptor* properties); + +// Methods to work with Arrays +NAPI_EXTERN napi_status napi_is_array(napi_env env, + napi_value value, + bool* result); +NAPI_EXTERN napi_status napi_get_array_length(napi_env env, + napi_value value, + uint32_t* result); + +// Methods to work with Functions +NAPI_EXTERN napi_status napi_call_function(napi_env env, + napi_value recv, + napi_value func, + size_t argc, + const napi_value* argv, + napi_value* result); + +// Methods to work with napi_callbacks + +// Gets all callback info in a single call. (Ugly, but faster.) +NAPI_EXTERN napi_status napi_get_cb_info( + napi_env env, // [in] NAPI environment handle + napi_callback_info cbinfo, // [in] Opaque callback-info handle + size_t* argc, // [in-out] Specifies the size of the provided argv array + // and receives the actual count of args. + napi_value* argv, // [out] Array of values + napi_value* this_arg, // [out] Receives the JS 'this' arg for the call + void** data); // [out] Receives the data pointer for the callback. + +// Methods to support error handling +NAPI_EXTERN napi_status napi_throw(napi_env env, napi_value error); +NAPI_EXTERN napi_status napi_throw_error(napi_env env, + const char* code, + const char* msg); +NAPI_EXTERN napi_status napi_throw_type_error(napi_env env, + const char* code, + const char* msg); +NAPI_EXTERN napi_status napi_throw_range_error(napi_env env, + const char* code, + const char* msg); +NAPI_EXTERN napi_status napi_is_error(napi_env env, + napi_value value, + bool* result); + +#if NAPI_VERSION >= 5 + +// Add finalizer for pointer +NAPI_EXTERN napi_status napi_add_finalizer(napi_env env, + napi_value js_object, + void* native_object, + napi_finalize finalize_cb, + void* finalize_hint, + napi_ref* result); + +#endif // NAPI_VERSION >= 5 + +EXTERN_C_END + +#endif /* js_native_api_h */ diff --git a/MorphicMacOS/N-API/include/js_native_api_types.h b/MorphicMacOS/N-API/include/js_native_api_types.h new file mode 100644 index 0000000..37f083d --- /dev/null +++ b/MorphicMacOS/N-API/include/js_native_api_types.h @@ -0,0 +1,109 @@ +// +// js_native_api_types.h +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +// These headers were copied or derived from the Node.js repositories at GitHub: +// +// Source: https://github.com/nodejs/node/blob/master/src/js_native_api_types.h +// License: https://github.com/nodejs/node/blob/master/LICENSE + +#ifndef js_native_api_types_h +#define js_native_api_types_h + +#include // NOLINT(modernize-deprecated-headers) +// JSVM API types are all opaque pointers for ABI stability +// typedef undefined structs instead of void* for compile time type safety +typedef struct napi_env__* napi_env; +typedef struct napi_value__* napi_value; +typedef struct napi_ref__* napi_ref; +typedef struct napi_callback_info__* napi_callback_info; + +typedef enum { + napi_default = 0, + napi_writable = 1 << 0, + napi_enumerable = 1 << 1, + napi_configurable = 1 << 2, + + // Used with napi_define_class to distinguish static properties + // from instance properties. Ignored by napi_define_properties. + napi_static = 1 << 10, +} napi_property_attributes; + +typedef enum { + // ES6 types (corresponds to typeof) + napi_undefined, + napi_null, + napi_boolean, + napi_number, + napi_string, + napi_symbol, + napi_object, + napi_function, + napi_external, + napi_bigint, +} napi_valuetype; + +typedef enum { + napi_ok, + napi_invalid_arg, + napi_object_expected, + napi_string_expected, + napi_name_expected, + napi_function_expected, + napi_number_expected, + napi_boolean_expected, + napi_array_expected, + napi_generic_failure, + napi_pending_exception, + napi_cancelled, + napi_escape_called_twice, + napi_handle_scope_mismatch, + napi_callback_scope_mismatch, + napi_queue_full, + napi_closing, + napi_bigint_expected, + napi_date_expected, + napi_arraybuffer_expected, + napi_detachable_arraybuffer_expected, +} napi_status; +// Note: when adding a new enum value to `napi_status`, please also update +// `const int last_status` in `napi_get_last_error_info()' definition, +// in file js_native_api_v8.cc. Please also update the definition of +// `napi_status` in doc/api/n-api.md to reflect the newly added value(s). + +typedef napi_value (*napi_callback)(napi_env env, + napi_callback_info info); +typedef void (*napi_finalize)(napi_env env, + void* finalize_data, + void* finalize_hint); + + typedef struct { + // One of utf8name or name should be NULL. + const char* utf8name; + napi_value name; + + napi_callback method; + napi_callback getter; + napi_callback setter; + napi_value value; + + napi_property_attributes attributes; + void* data; + } napi_property_descriptor; + + + + + + + + +#endif /* js_native_api_types_h */ diff --git a/MorphicMacOS/N-API/include/node_api.h b/MorphicMacOS/N-API/include/node_api.h new file mode 100644 index 0000000..1cf0b29 --- /dev/null +++ b/MorphicMacOS/N-API/include/node_api.h @@ -0,0 +1,84 @@ +// +// node_api.h +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +// These headers were copied or derived from the Node.js repositories at GitHub: +// +// Source: https://github.com/nodejs/node/blob/master/src/node_api.h +// License: https://github.com/nodejs/node/blob/master/LICENSE +// +// Source: https://github.com/nodejs/node-addon-api/blob/master/src/node_api.h +// License: MIT (https://github.com/nodejs/node-addon-api/blob/master/LICENSE.md) + +#ifndef node_api_h +#define node_api_h + +#include "js_native_api.h" +#include "node_api_types.h" + +#define NAPI_MODULE_EXPORT __attribute__((visibility("default"))) + +#ifdef __GNUC__ +#define NAPI_NO_RETURN __attribute__((noreturn)) +#else +#define NAPI_NO_RETURN /* nothing */ +#endif + +typedef napi_value (*napi_addon_register_func)(napi_env env, + napi_value exports); + +typedef struct { + int nm_version; + unsigned int nm_flags; + const char* nm_filename; + napi_addon_register_func nm_register_func; + const char* nm_modname; + void* nm_priv; + void* reserved[4]; +} napi_module; + +#define NAPI_MODULE_VERSION 1 + +#define NAPI_C_CTOR(fn) \ + static void fn(void) __attribute__((constructor)); \ + static void fn(void) + +#define NAPI_MODULE_X(modname, regfunc, priv, flags) \ + EXTERN_C_START \ + static napi_module _module = \ + { \ + NAPI_MODULE_VERSION, \ + flags, \ + __FILE__, \ + regfunc, \ + #modname, \ + priv, \ + {0}, \ + }; \ + NAPI_C_CTOR(_register_ ## modname) { \ + napi_module_register(&_module); \ + } \ + EXTERN_C_END + +#define NAPI_MODULE(modname, regfunc) \ + NAPI_MODULE_X(modname, regfunc, NULL, 0) // NOLINT (readability/null_usage) + + +EXTERN_C_START + +NAPI_EXTERN NAPI_NO_RETURN void napi_fatal_error(const char* location, + size_t location_len, + const char* message, + size_t message_len); + + + +#endif /* node_api_h */ diff --git a/MorphicMacOS/N-API/include/node_api_types.h b/MorphicMacOS/N-API/include/node_api_types.h new file mode 100644 index 0000000..7753e66 --- /dev/null +++ b/MorphicMacOS/N-API/include/node_api_types.h @@ -0,0 +1,24 @@ +// +// node_api_types.h +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +// These headers were copied or derived from the Node.js repositories at GitHub: +// +// Source: https://github.com/nodejs/node/blob/master/src/node_api_types.h +// License: https://github.com/nodejs/node/blob/master/LICENSE +// +// Source: https://github.com/nodejs/node-addon-api/blob/master/src/node_api_types.h +// License: MIT (https://github.com/nodejs/node-addon-api/blob/master/LICENSE.md) + +#ifndef node_api_types_h +#define node_api_types_h + +#endif /* node_api_types_h */ diff --git a/MorphicMacOS/NativeClasses/Audio/MorphicAudio.swift b/MorphicMacOS/NativeClasses/Audio/MorphicAudio.swift new file mode 100644 index 0000000..a1d5b44 --- /dev/null +++ b/MorphicMacOS/NativeClasses/Audio/MorphicAudio.swift @@ -0,0 +1,166 @@ +// +// MorphicAudio.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +import Foundation +import AudioToolbox +//import CoreAudio + +// NOTE: the MorphicAudio class contains the functionality used by Obj-C and Swift applications + +public class MorphicAudio { + // MARK: Custom errors + public enum MorphicAudioError: Error { + case propertyUnavailable + case cannotSetProperty + case coreAudioError(error: OSStatus) + } + + // MARK: - Audio device enumeration + + // NOTE: this function returns nil if it encounters an error; we do this to distinguish an error condition ( nil ) from an empty set ( [] ). + // Apple docs: https://developer.apple.com/library/archive/technotes/tn2223/_index.html + public static func getDefaultAudioDeviceId() -> UInt32? { + var outputDeviceId: AudioDeviceID = 0 + var sizeOfAudioDeviceID = UInt32(MemoryLayout.size) + + // option 1: kAudioHardwarePropertyDefaultOutputDevice + var outputDevicePropertyAddress = AudioObjectPropertyAddress(mSelector: kAudioHardwarePropertyDefaultOutputDevice, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMaster) +// // option 2: kAudioHardwarePropertyDefaultSystemOutputDevice +// var outputDevicePropertyAddress = AudioObjectPropertyAddress(mSelector: kAudioHardwarePropertyDefaultSystemOutputDevice, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMaster) + + let getPropertyError = AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &outputDevicePropertyAddress, 0, nil, &sizeOfAudioDeviceID, &outputDeviceId) + if getPropertyError != noErr { + // if we cannot retrieve the default output device's id, return nil + return nil + } + + return outputDeviceId as UInt32 + } + + // MARK: - Get/set volume (and mute state) + + public static func getVolume(for audioDeviceId: UInt32) -> Float? { + var volume: Float = 0 + var sizeOfFloat = UInt32(MemoryLayout.size) + + var volumePropertyAddress = AudioObjectPropertyAddress(mSelector: kAudioHardwareServiceDeviceProperty_VirtualMasterVolume, mScope: kAudioDevicePropertyScopeOutput, mElement: kAudioObjectPropertyElementMaster) + + // verify that the output device has a volume property to get + if AudioObjectHasProperty(AudioObjectID(audioDeviceId), &volumePropertyAddress) == false { + // if there is no volume property to get, return nil + return nil + } + + let getPropertyError = AudioObjectGetPropertyData(AudioObjectID(audioDeviceId), &volumePropertyAddress, 0, nil, &sizeOfFloat, &volume) + if getPropertyError != noErr { + // if we cannot retrieve the volume, return nil + return nil + } + + // sanity-check: force the volume into the range 0.0 through 1.0 + if volume < 0.0 { + volume = 0.0 + } else if volume > 1.0 { + volume = 1.0 + } + + return volume + } + + public static func setVolume(for audioDeviceId: UInt32, volume: Float) throws { + var newVolume = volume + let sizeOfFloat = UInt32(MemoryLayout.size) + + var volumePropertyAddress = AudioObjectPropertyAddress(mSelector: kAudioHardwareServiceDeviceProperty_VirtualMasterVolume, mScope: kAudioDevicePropertyScopeOutput, mElement: kAudioObjectPropertyElementMaster) + + // verify that the output device has a volume property + if AudioObjectHasProperty(AudioObjectID(audioDeviceId), &volumePropertyAddress) == false { + // if there is no volume property, throw an error + throw MorphicAudioError.propertyUnavailable + } + + // verify that we can set the volume property + var canSetVolume: DarwinBoolean = true + let checkSettableError = AudioObjectIsPropertySettable(AudioObjectID(audioDeviceId), &volumePropertyAddress, &canSetVolume) + if checkSettableError != noErr { + // if we cannot determine if volume is settable, throw an error + throw MorphicAudioError.coreAudioError(error: checkSettableError) + } + + if canSetVolume == false { + // if we cannot set the volume, throw an error + throw MorphicAudioError.cannotSetProperty + } + + let setPropertyError = AudioObjectSetPropertyData(audioDeviceId, &volumePropertyAddress, 0, nil, sizeOfFloat, &newVolume) + // + if setPropertyError != noErr { + // if we cannot set the volume, throw an error + throw MorphicAudioError.coreAudioError(error: setPropertyError) + } + } + + public static func getMuteState(for audioDeviceId: UInt32) -> Bool? { + var muteState: UInt32 = 0 + var sizeOfUInt32 = UInt32(MemoryLayout.size) + + var mutePropertyAddress = AudioObjectPropertyAddress(mSelector: kAudioDevicePropertyMute, mScope: kAudioDevicePropertyScopeOutput, mElement: kAudioObjectPropertyElementMaster) + + // verify that the output device has a mute state to get + if AudioObjectHasProperty(AudioObjectID(audioDeviceId), &mutePropertyAddress) == false { + // if there is no mute state to get, return nil + return nil + } + + let getPropertyError = AudioObjectGetPropertyData(AudioObjectID(audioDeviceId), &mutePropertyAddress, 0, nil, &sizeOfUInt32, &muteState) + if getPropertyError != noErr { + // if we cannot retrieve the mute state, return nil + return nil + } + + return (muteState != 0) ? true : false + } + + // NOTE: to mute, set value to true; to unmute, set value to false. + public static func setMuteState(for audioDeviceId: UInt32, muteState: Bool) throws { + var newValue = muteState ? UInt32(1) : UInt32(0) + let sizeOfUInt32 = UInt32(MemoryLayout.size) + + var mutePropertyAddress = AudioObjectPropertyAddress(mSelector: kAudioDevicePropertyMute, mScope: kAudioDevicePropertyScopeOutput, mElement: kAudioObjectPropertyElementMaster) + + // verify that the output device has a mute property + if AudioObjectHasProperty(AudioObjectID(audioDeviceId), &mutePropertyAddress) == false { + // if there is no mute state property, throw an error + throw MorphicAudioError.propertyUnavailable + } + + // verify that we can set the mute property + var canMute: DarwinBoolean = true + let checkSettableError = AudioObjectIsPropertySettable(AudioObjectID(audioDeviceId), &mutePropertyAddress, &canMute) + if checkSettableError != noErr { + // if we cannot determine if mute is settable, throw an error + throw MorphicAudioError.coreAudioError(error: checkSettableError) + } + + if canMute == false { + // if we cannot mute/unmute the audio output device, throw an error + throw MorphicAudioError.cannotSetProperty + } + + let setPropertyError = AudioObjectSetPropertyData(audioDeviceId, &mutePropertyAddress, 0, nil, sizeOfUInt32, &newValue) + // + if setPropertyError != noErr { + // if we cannot set the mute state, throw an error + throw MorphicAudioError.coreAudioError(error: setPropertyError) + } + } +} diff --git a/MorphicMacOS/NativeClasses/Disk/MorphicDisk.swift b/MorphicMacOS/NativeClasses/Disk/MorphicDisk.swift new file mode 100644 index 0000000..959911f --- /dev/null +++ b/MorphicMacOS/NativeClasses/Disk/MorphicDisk.swift @@ -0,0 +1,620 @@ +// +// MorphicDisk.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +import Foundation +//import IOKit +import IOKit.usb +//import IOKit.usb.IOUSBLib + +// NOTE: the MorphicDisk class contains the functionality used by Obj-C and Swift applications + +public class MorphicDisk { + // MARK - Open Directory functionality + + public static func openDirectory(path: String) { + // open directory path in Finder + + // NOTE: some of the below methods of opening up the drives may prompt the user for permission under Apple's latest (year 2019) OS security requirements; we may want to look at ways to suppress this prompt in applications consuming this library (including after such apps are updated in a way that changes their signature). One possible option is installing a background helper app which never changes (or runs at a higher permission level that doesn't prompt for permission). + + // NOTE: NSWorkspace may not be available from node-ffi and similar, so we want to explore alternative methods too. + + // NOTE: some of the below methods of opening up the drives may not give us the finesse and control we want regarding how the Finder opens, if it's in three-pane vs. single-pane mode, etc. More research and investigation is in order. + +// // METHOD 1: +// // NOTE: we open each folder separately so that we get multiple separate file view +// let pathsAsUrls: [URL] = [URL(fileURLWithPath: path, isDirectory: true)] +// NSWorkspace.shared.activateFileViewerSelecting(pathsAsUrls) + +// // METHOD 2: +// NSWorkspace.shared.selectFile(path, inFileViewerRootedAtPath: "") +// // ALTERNATIVE METHOD 2: +// NSWorkspace.shared.selectFile(path, inFileViewerRootedAtPath: path) +// +// // METHOD 3: +// // NOTE: this method opens folders with one file in a solo pane (but opens folders with multiple files in the second pane next to the drive name) +// NSWorkspace.shared.openFile(path, withApplication: "Finder") + + // METHOD 4 (AppleScript): + let script: NSString = NSString(format: "tell application \"Finder\"\nactivate\nopen folder (\"%@\" as POSIX file)\nend tell\n", path) + + guard let openScript = NSAppleScript(source: script as String) else { + NSLog("Could not create AppleScript to open folder at path: \(path)") + // NOTE: in the future, we could provide this information to JavaScript via a callback or promise + return + } + openScript.executeAndReturnError(nil) + } + + // MARK: - Eject Disk functionality + + public enum EjectDiskError: Error { + case otherError + case volumeNotFound + } + // + public typealias EjectDiskCallback = (_ mountPath: String, _ success: Bool) -> Void + // + private class EjectDiskInternalCallbackData { + let diskArbitrationSession: DASession + let mountPath: String + let callback: EjectDiskCallback + + init(diskArbitrationSession: DASession, mountPath: String, callback: @escaping EjectDiskCallback) { + self.diskArbitrationSession = diskArbitrationSession + self.mountPath = mountPath + self.callback = callback + } + } + // + // NOTE: this function safely unmounts and ejects the disk at the specified mount path + public static func ejectDisk(mountPath: String, callback: @escaping EjectDiskCallback) throws { + // STEP 1: convert mount path to BSD name + guard let bsdName = convertMountPathToBsdName(mountPath) else { + throw EjectDiskError.volumeNotFound + } + // + guard var bsdNameAsCString = bsdName.cString(using: String.Encoding.utf8) else { + throw EjectDiskError.otherError + } + + // create a Disk Arbitration session so that we can unmount and eject the disk + guard let diskArbitrationSession = DASessionCreate(kCFAllocatorDefault) else { + // if we cannot create a disk arbitration session, fail + throw EjectDiskError.otherError + } + // NOTE: because our session is retained for callbacks (because we need it to do callbacks on the run loop), we do not release it here because the callbacks will do that instead. +// // NOTE: Swift automatically manages Core Foundation references, so this is included here just for porting purposes. +// var diskArbitrationSessionRequiresCleanup = false +// defer { +// if diskArbitrationSessionRequiresCleanup == true { +// CFRelease(diskArbitrationSession) +// } +// } + + guard let disk = DADiskCreateFromBSDName(kCFAllocatorDefault, diskArbitrationSession, &bsdNameAsCString) else { + // if we cannot get a reference to the disk, fail + throw EjectDiskError.volumeNotFound + } + // NOTE: Swift automatically manages Core Foundation references, so this is included here just for porting purposes. +// defer { +// CFRelease(disk) +// } + + guard let diskDescription = DADiskCopyDescription(disk) else { + // if we cannot get a copy of the disk's description, fail + throw EjectDiskError.otherError + } + // NOTE: Swift automatically manages Core Foundation references, so this is included here just for porting purposes. +// defer { +// CFRelease(diskDescription) +// } + + // determine if the currently-resolved partition is a leaf partition or the whole disk + let diskDescriptionMediaWholeKey = kDADiskDescriptionMediaWholeKey as CFString + guard let pointerToMediaWholeKeyValue = CFDictionaryGetValue(diskDescription, unsafeBitCast(diskDescriptionMediaWholeKey, to: UnsafeRawPointer.self)) else { + // if we cannot get the volume path key for this disk, fail + throw EjectDiskError.otherError + } + // convert the mediaWholeKey's UnsafeRawPointer pointer to a NSNumber instance + let mediaWholeAsNSNumber = unsafeBitCast(pointerToMediaWholeKeyValue, to: NSNumber.self) + let diskIsWholeDisk: Bool = (mediaWholeAsNSNumber != 0) + + // NOTE: diskToEject is the specified disk by default; if that disk is a leaf partition we will need to move up to the whole-disk partition instead in a moment (since we need to unmount all partitions on the disk and eject the whole disk) + var diskToEject = disk + + // if the partition is a leaf partition and not the whole-disk partition, get a reference to the whole disk instead + var wholeDisk: DADisk? = nil + if diskIsWholeDisk == false { + // get a reference to the whole-disk partition for this BSD Name (in case we're a leaf partition, we need to eject the whole disk, not just the leaf partition) + wholeDisk = DADiskCopyWholeDisk(disk) + if let wholeDisk = wholeDisk { + // NOTE: we will need to clean up the "whole disk" later as well; see the CFRelease immediately following this block + diskToEject = wholeDisk + } else { + throw EjectDiskError.volumeNotFound + } + } + // NOTE: Swift automatically manages Core Foundation references, so this is included here just for porting purposes. +// defer { +// if wholeDisk != nil { +// CFRelease(wholeDisk) +// } +// } + + // capture all the data we need to use during our callbacks + let ejectDiskInternalCallbackData = EjectDiskInternalCallbackData(diskArbitrationSession: diskArbitrationSession, mountPath: mountPath, callback: callback) + + // manually retain a reference to our callback data; this MUST be manually released by our callback + let pointerToEjectDiskInternalCallbackData = Unmanaged.passRetained(ejectDiskInternalCallbackData).toOpaque() + // + // attach our run loop to our Disk Arbitration session so we can capture the unmount/eject comletion callbacks + // NOTE: this must be unattached in our unmount/eject callbacks (when the callbacks have either succeeded or failed) + DASessionScheduleWithRunLoop(diskArbitrationSession, CFRunLoopGetMain(), CFRunLoopMode.defaultMode.rawValue) + + // start to unmount all volumes tied to our (whole) disk object; once unmounted our callback will "eject" the drive too + // NOTE: this unmount operation intentionally does _not_ request force-unmounting--so the operation can be blocked by apps which have open files, etc; if this happens we'll report that failure in our callback. + DADiskUnmount(diskToEject, DADiskUnmountOptions(kDADiskUnmountOptionWhole), MorphicDisk.unmountDiskCallback, pointerToEjectDiskInternalCallbackData) + + // NOTE: at this point, DADiskUnmount will run asynchronously and our callbacks will do the rest of the work + } + + private static let unmountDiskCallback: @convention(c) (DADisk, DADissenter?, UnsafeMutableRawPointer?) -> Void = + { disk, dissenter, context in + guard let context = context else { + fatalError("unmountDiskCallback must always be called with an Unmanaged EjectDiskInternalCallbackData class instance as its context.") + } + + // convert our context into a EjectDiskInternalCallbackData instance, releasing the retained reference at the same time + let ejectDiskInternalCallbackData = Unmanaged.fromOpaque(context).takeRetainedValue() + + if dissenter != nil { + // first: since our operation is completee, we should unschedule the run loop for our disk arbitration session + DASessionUnscheduleFromRunLoop(ejectDiskInternalCallbackData.diskArbitrationSession, CFRunLoopGetMain(), CFRunLoopMode.defaultMode.rawValue) + // and we should release the Disk Arbitration session + // NOTE: Swift automatically manages Core Foundation references, so this is included here just for porting purposes. +// CFRelease(diskArbitrationSession) + + // we could not unmount the volume because we were blocked; report this to our caller + + // return the failure to our caller + ejectDiskInternalCallbackData.callback(ejectDiskInternalCallbackData.mountPath, false /* success = false */) + } else { + // first: since our operation is complete, we should unschedule the run loop for our disk arbitration session + DASessionUnscheduleFromRunLoop(ejectDiskInternalCallbackData.diskArbitrationSession, CFRunLoopGetMain(), CFRunLoopMode.defaultMode.rawValue) + + // manually retain a reference to our callback data; this MUST be manually released by our callback + let pointerToEjectDiskInternalCallbackData = Unmanaged.passRetained(ejectDiskInternalCallbackData).toOpaque() + // + DADiskEject(disk, DADiskEjectOptions(kDADiskEjectOptionDefault), MorphicDisk.ejectDiskCallback, pointerToEjectDiskInternalCallbackData) + } + } + + private static let ejectDiskCallback: @convention(c) (DADisk, DADissenter?, UnsafeMutableRawPointer?) -> Void = + { disk, dissenter, context in + guard let context = context else { + fatalError("ejectDiskCallback must always be called with an Unmanaged EjectDiskInternalCallbackData class instance as its context.") + } + + // convert our context into a EjectDiskInternalCallbackData instance, releasing the retained reference at the same time + let ejectDiskInternalCallbackData = Unmanaged.fromOpaque(context).takeRetainedValue() + + // first: since our operation is complete, we should unschedule the run loop for our disk arbitration session + DASessionUnscheduleFromRunLoop(ejectDiskInternalCallbackData.diskArbitrationSession, CFRunLoopGetMain(), CFRunLoopMode.defaultMode.rawValue) + // and we should release the Disk Arbitration session + // NOTE: Swift automatically manages Core Foundation references, so this is included here just for porting purposes. +// CFRelease(ejectDiskInternalCallbackData.diskArbitrationSession) + + if dissenter != nil { + // we could not eject the volume because we were blocked; report this to our caller + + // return the failure to our caller + ejectDiskInternalCallbackData.callback(ejectDiskInternalCallbackData.mountPath, false /* success = false */) + } else { + // return the success to our caller + ejectDiskInternalCallbackData.callback(ejectDiskInternalCallbackData.mountPath, true /* success = true */) + } + } + + // MARK: - Enumerate USB drive functionality + + // NOTE: this function returns nil if it encounters an error; we do this to distinguish an error condition ( nil ) from an empty set ( [] ). + public static func getAllUsbDriveMountPaths() -> [String]? { + var result: [String] = [] + + /* NOTE: to get our list of all USB Drive mount paths, we + * - create a matching dictionary so we can filter attached devices/device interfaces (via the I/O registry) to only those who are USB Mass Storage devices. + * - create an iterator which will iterate through USB interfaces (not devices) which match the matching dictionary + * - iterate through each matching device, obtaining the BSD Name (e.g. 'disk2') of each USB Mass Storage device + * - look for children of each device (in case an interface has a child such as one BSD-named 'disk2s1') + * - use Disk Arbitration to obtain the mount path for each drive (passing in the BSD Name to get disk info) + */ + + // STEP 1: create a matching dictionary which filters on USB interfaces of mass storage devices + guard let matchingDictionary = MorphicDisk.createMatchingDictionaryForUsbDriveInterfaces() else { + return nil + } + + // STEP 2: iterate through each currently-attached USB device's interfaces which match our matching dictionary's filter criteria + var interfaceIterator: io_iterator_t = 0 // NOTE: must be initialized to a value so that Swift can pass-by-reference + // + // STEP 2.1: obtain an iterator (to walk through the list of attached USB Mass Storage interface kernel objects in the I/O Registry) + // NOTE: the IOServiceGetMatchingServices function automatically releases the matching dictionary (so we do not need to CFRelease in non-Swift languages) + if IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDictionary, &interfaceIterator) != KERN_SUCCESS { + // if we could not get an interface iterator, return nil + return nil + } + // + // iterate through each device interface in the I/O registry which matched, using our interface iterator + // NOTE: we iterate inside a 'do' block so that we can scope memory management to this singular block using 'defer' + do { + // release our iterator when we exit this block + defer { + IOObjectRelease(interfaceIterator) + } + + // itereate through all of our enumerated usb drives, capturing their mount paths + result.append(contentsOf: MorphicDisk.enumerateUsbDriveMountPaths(interfaceIterator: interfaceIterator)) + } + + return result + } + + private static func enumerateUsbDriveMountPaths(interfaceIterator: io_service_t) -> [String] { + var result: [String] = [] + + // NOTE: on macOS, it is CRITICAL to completely iterate through the iterator; to this end, we include a defer block here to complete the iteration sequence in any situation where we had to abort early + // NOTE: technically this may be over-engineering, but it's a good best-practice to use (especially when we're handling notifications, raising callbacks or doing anything else where we need cleanup assurance) + var currentKernelObject: io_object_t? = nil + defer { + if currentKernelObject != nil { + // complete all iterations of our device iterator + // NOTE: IOIteratorNext will return a null pointer (i.e. result of zero) when it passes beyond its list + while case let kernelObject = IOIteratorNext(interfaceIterator), kernelObject != 0 { + // clean up each kernel object we just iterated to, as we iterate + IOObjectRelease(kernelObject) + } + } + } + + // STEP 1: iterate through the matched device interfaces + // NOTE: IOIteratorNext will return a null pointer (i.e. result of zero) when it passes beyond its list + while case let kernelObject = IOIteratorNext(interfaceIterator), kernelObject != 0 { + defer { + // clean up each kernel object we just iterated to (after this block is complete) + IOObjectRelease(kernelObject) + } + + // capture a copy of the current kernelObject in case we are aborted (so that the appropriate 'defer' block can clean up for us + currentKernelObject = kernelObject + + var bsdNamesToConvertToMountPaths: [String] = [] + + // capture the BSD name for this kernel object + let ioBsdNameKey = kIOBSDNameKey as CFString + guard let bsdNameOfWholeDiskAsCFTypeRef = IORegistryEntrySearchCFProperty(kernelObject, kIOServicePlane, ioBsdNameKey, kCFAllocatorDefault, IOOptionBits(kIORegistryIterateRecursively)) else { + // if the device does not have a BSD name, skip to the next interface + continue + } + guard let bsdNameOfWholeDisk = MorphicDisk.convertCFTypeRefToString(bsdNameOfWholeDiskAsCFTypeRef) else { + // if we could not convert the BSD Name to a string, skip to the next interface + continue + } + + // get the BSD Names of any leaf partitions of the disk (using the whole-disk's BSD Name as input) + guard let bsdNamesOfLeafPartitions = MorphicDisk.getBsdNamesOfLeafPartitions(bsdNameOfWholeDisk) else { + // if we could not get a list of the child BSD names, skip to the next interface + continue + } + + if bsdNamesOfLeafPartitions.count > 0 { + // if the device had children, add those children's names to our list of BSD Names + bsdNamesToConvertToMountPaths = bsdNamesOfLeafPartitions + } else { + // otherwise if the device had no leaf partitions, use the whole disk's name itself as the sole partition in our list of BSD Names + bsdNamesToConvertToMountPaths = [bsdNameOfWholeDisk] + } + +// // FOR DEBUG PURPOSES: show the BSD names for the drives +// for bsdName in bsdNamesToConvertToMountPaths { +// print("Drive BSD Name: \(bsdName)") +// } + + // now convert the BSD names to mount paths + for bsdNameToConvertToMountPath in bsdNamesToConvertToMountPaths { + guard let mountPath = MorphicDisk.convertBsdNameToMountPath(bsdNameToConvertToMountPath) else { + // if we cannot convert the BSD Name to a mount path, skip to the next BSD Name + continue + } + + // add the mount path to the result array + result.append(mountPath) + } + } + + // set our currentKernelObject to nil since we have completed our iterations (so that the defer block doens't try to clean up) + currentKernelObject = nil + + return result + } + + // MARK: - BSD Name to/from Mount Path conversion + + public static func convertBsdNameToMountPath(_ bsdName: String) -> String? { + // create a Disk Arbitration session so that we can retrieve mounting paths using BSD names + guard let diskArbitrationSession = DASessionCreate(kCFAllocatorDefault) else { + // if we cannot create a disk arbitration session, fail + return nil + } + // NOTE: Swift automatically manages Core Foundation references, so this is included here just for porting purposes. +// defer { +// CFRelease(diskArbitrationSession) +// } + + guard var bsdNameAsCString = bsdName.cString(using: String.Encoding.utf8) else { + return nil + } + + guard let disk = DADiskCreateFromBSDName(kCFAllocatorDefault, diskArbitrationSession, &bsdNameAsCString) else { + // if we cannot get a reference to the disk, fail + return nil + } + // NOTE: Swift automatically manages Core Foundation references, so this is included here just for porting purposes. +// defer { +// CFRelease(disk) +// } + + guard let diskDescription = DADiskCopyDescription(disk) else { + // if we cannot get a copy of the disk's description, fail + return nil + } + // NOTE: Swift automatically manages Core Foundation references, so this is included here just for porting purposes. +// defer { +// CFRelease(diskDescription) +// } + + let diskDescriptionVolumePathKey = kDADiskDescriptionVolumePathKey as CFString + guard let pointerToVolumePathKeyValue = CFDictionaryGetValue(diskDescription, unsafeBitCast(diskDescriptionVolumePathKey, to: UnsafeRawPointer.self)) else { + // if we cannot get the volume path key for this disk, fail + return nil + } + // convert the volumePathKey's UnsafeRawPointer pointer to a CFURL instance + let volumePathDictionary = unsafeBitCast(pointerToVolumePathKeyValue, to: CFURL.self) + // convert CFURL to byte array (i.e. CString in byte array form) + let maxBufferLength = 1024 + var buffer = [UInt8](repeating: 0, count: maxBufferLength) + if CFURLGetFileSystemRepresentation(volumePathDictionary, false, &buffer, maxBufferLength as CFIndex) == false { + // if we could not convert the url to a byte buffer, fail + return nil + } + // convert CString buffer to mounting path string + let mountPath = String(cString: buffer) + + return mountPath + } + + public static func convertMountPathToBsdName(_ mountPath: String) -> String? { + // create a Disk Arbitration session so that we can retrieve BSD names using mounting paths + guard let diskArbitrationSession = DASessionCreate(kCFAllocatorDefault) else { + // if we cannot create a disk arbitration session, fail + return nil + } + // NOTE: Swift automatically manages Core Foundation references, so this is included here just for porting purposes. +// defer { +// CFRelease(diskArbitrationSession) +// } + + // convert mount path String to CFURL + guard let bufferAsCCharArray: [CChar] = mountPath.cString(using: String.Encoding.utf8) else { + // if we could not convert the string to a byte buffer, fail + return nil + } + let bufferLength = bufferAsCCharArray.count + // + // convert cchar array to UInt8 array + var bufferAsUInt8Array: [UInt8] = [] + for cchar in bufferAsCCharArray { + bufferAsUInt8Array.append(UInt8(bitPattern: cchar)) + } + // + // converet mount path to CFURL + guard let mountPathAsCFURL = CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault, &bufferAsUInt8Array, bufferLength, true) else { + // if we could not convert the cchar buffer to a url, fail + return nil + } + // NOTE: Swift automatically manages Core Foundation references, so this is included here just for porting purposes. +// defer { +// CFRelease(mountPathAsCFURL) +// } + + guard let diskDescription = DADiskCreateFromVolumePath(kCFAllocatorDefault, diskArbitrationSession, mountPathAsCFURL) else { + // if we cannot get a copy of the disk's description, fail + return nil + } + // NOTE: Swift automatically manages Core Foundation references, so this is included here just for porting purposes. +// defer { +// CFRelease(diskDescription) +// } + + guard let bsdNameAsCCharArray = DADiskGetBSDName(diskDescription) else { + // if we cannot resolve the disk's BSD name, return nil + return nil + } + + // convert CString buffer to mounting path string + let bsdName = String(cString: bsdNameAsCCharArray) + + return bsdName + } + + // MARK: - Helper functions for USB drive enumeration and detection + + // NOTE: the CFMutableDictionary returned by this function MUST be released by the consumer (if not using a platform like Swift where this is done automatically), either directly (CFRelease) or indirectly (IOServiceGetMatchingServices) + private static func createMatchingDictionaryForUsbDriveInterfaces() -> CFMutableDictionary? { + // NOTE: normally in Swift we would simply cast the result from IOServiceMatching to an NSMatchingDictionary? -- at which point we could simply populate the matching dictionary using key-value syntax (i.e. dictionary[key] = value). However this test mule is designed to illustrate the necessary API calls on macOS with an eye towards porting the implementation to another non-Foundation-based language, so we illustrate use of the APIs in the most language-portable manner possible (including dealing with CFMutableDictionaries via CF* function calls. + // + // example of the "traditional" simplified Swift pattern of using toll-free bridged CFDictionaries follows: + // guard var matchingDictionary = IOServiceMatching(kIOUSBInterfaceClassName) as NSMutableDictionary? else { + // return nil + // } + // // ... + // // ... + // // ... + // matchingDictionary[kUSBInterfaceClass] = kUSBMassStorageInterfaceClass + // matchingDictionary[kUSBInterfaceSubClass] = kUSBMassStorageSCSISubClass + + // STEP 1: create a matching dictionary which filters on USB interfaces (not on USB devices); we do not want to match on non-USB storage devices' interfaces + guard let matchingDictionary = IOServiceMatching(kIOUSBInterfaceClassName) else { + return nil + } + // NOTE: our call to IOServiceGetMatchingServices (farther below) will release the matching dictionary for us; but if we do not successfully get that far then the dictionary traditionally needs to be released by CFRelease + // NOTE: Swift automatically manages Core Foundation references, so CFRelease usage is described here just for porting purposes. + var matchingDictionaryRequiresRelease = true + defer { + if matchingDictionaryRequiresRelease == true { +// CFRelease(matchingDictionary) + } + } + + // STEP 2: filter the matching dictionary on USB Mass Storage device interfaces only; note that we must use pointers to numbers here when creating CFNumbers. + // + // add the device class (USB Mass Storage Interface) to the matching dictionary + var deviceClass: Int32 = Int32(kUSBMassStorageInterfaceClass) + let referenceToDeviceClass = CFNumberCreate(kCFAllocatorDefault, CFNumberType.sInt32Type, &deviceClass) + if referenceToDeviceClass == nil { + return nil + } + let usbInterfaceClass = kUSBInterfaceClass as CFString + CFDictionaryAddValue(matchingDictionary, unsafeBitCast(usbInterfaceClass, to: UnsafeRawPointer.self), unsafeBitCast(referenceToDeviceClass, to: UnsafeRawPointer.self)) + // NOTE: Swift manages Core Foundation memory for us; in other languages, be sure to CFRelease + // CFRelease(referenceToDeviceClass) + // + // add the device subclass (USB Mass Storage SCSI) to the matching dictionary + var deviceSubClass: Int32 = Int32(kUSBMassStorageSCSISubClass) + let referenceToDeviceSubClass = CFNumberCreate(kCFAllocatorDefault, CFNumberType.sInt32Type, &deviceSubClass) + if referenceToDeviceSubClass == nil { + return nil + } + let usbInterfaceSubClass = kUSBInterfaceSubClass as CFString + CFDictionaryAddValue(matchingDictionary, unsafeBitCast(usbInterfaceSubClass, to: UnsafeRawPointer.self), unsafeBitCast(referenceToDeviceSubClass, to: UnsafeRawPointer.self)) + // NOTE: Swift manages Core Foundation memory for us; in other languages, be sure to CFRelease + // CFRelease(referenceToDeviceSubClass) + + // NOTE: we need to return the successfully-built matching dictionary to the caller now, so make sure we mark the matching dictionary as "does not need to be released" at this point + matchingDictionaryRequiresRelease = false + + return matchingDictionary + } + + // MARK: - BSD Name search functions + + // NOTE: this function returns nil if it encounters an error; we do this to distinguish an error condition ( nil ) from an empty set ( [] ). + private static func getBsdNamesOfLeafPartitions(_ bsdNameOfWholeDisk: String) -> [String]? { + var result: [String] = [] + + // STEP 1: create a matching dictionary for the passed-in bsdName + guard var bsdNameOfWholeDiskAsCString = bsdNameOfWholeDisk.cString(using: String.Encoding.utf8) else { + return nil + } + // + // NOTE: the second parameter in the following function (the number 0) is passed in for 'options' since no options are defined for this function under macOS + guard let matchingDictionary = IOBSDNameMatching(kIOMasterPortDefault, 0, &bsdNameOfWholeDiskAsCString) else { + return nil + } + + // STEP 3: iterate through each service which matches our matching dictionary's filter criteria + var serviceIterator: io_iterator_t = 0 // NOTE: must be initialized to a value so that Swift can pass-by-reference + // + // STEP 3.1: obtain an iterator (to walk through the list of service kernel objects in the I/O Registry) + // NOTE: the IOServiceGetMatchingServices function automatically releases the matching dictionary, so we do not need to clean it up manually (unless we create code which produces an exit path between where it was created and here) + if IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDictionary, &serviceIterator) != KERN_SUCCESS { + // if we could not get a service iterator, return nil + return nil + } + // + // iterate through each service in the I/O registry which matched, using our service iterator + // NOTE: we iterate in a 'do' block so that we can scope memory management to this singular block using 'defer' + do { + // release our iterator when we exit this block + defer { + IOObjectRelease(serviceIterator) + } + + // STEP 3.2: iterate through the matched services + // NOTE: IOIteratorNext will return a null pointer (i.e. result of zero) when it passes beyond its list + while case let kernelObject = IOIteratorNext(serviceIterator), kernelObject != 0 { + defer { + // clean up each kernel object we just iterated to (after this block is complete) + IOObjectRelease(kernelObject) + } + + // get the children of this service + var childServiceIterator: io_iterator_t = 0 // NOTE: must be initialized to a value so that Swift can pass-by-reference + if IORegistryEntryGetChildIterator(kernelObject, kIOServicePlane, &childServiceIterator) != KERN_SUCCESS { + // if we could not get children for this service, continue to the next service + continue + } + // + do { + // release our iterator when we exit this block + defer { + IOObjectRelease(childServiceIterator) + } + + while case let childKernelObject = IOIteratorNext(childServiceIterator), childKernelObject != 0 { + defer { + // clean up each child kernel object we just iterated to (after this block is complete) + IOObjectRelease(childKernelObject) + } + + // capture the BSD name for this child + let ioBSDNameKey = kIOBSDNameKey as CFString + guard let bsdNameOfLeafPartitionAsCFType = IORegistryEntrySearchCFProperty(childKernelObject, kIOServicePlane, ioBSDNameKey, kCFAllocatorDefault, IOOptionBits(kIORegistryIterateRecursively)) else { + // if the device does not have a BSD name, skip to the next device + continue + } + guard let bsdNameOfLeafPartition = MorphicDisk.convertCFTypeRefToString(bsdNameOfLeafPartitionAsCFType) else { + // if we cannot convert the BSD Name to a string, skip to the next device + continue + } + + // save the child's BSD name in our result + result.append(bsdNameOfLeafPartition) + } + } + } + } + + return result + } + + // MARK: - Utility functions + + private static func convertCFTypeRefToString(_ value: CFTypeRef) -> String? { + guard let valueAsString = value as? String else { + return nil + } + + return valueAsString + } + + private static func convertCFTypeRefToCString(_ value: CFTypeRef) -> [CChar]? { + guard let valueAsString = convertCFTypeRefToString(value) else { + return nil + } + guard let valueAsCString = valueAsString.cString(using: String.Encoding.utf8) else { + return nil + } + + return valueAsCString + } + +} diff --git a/MorphicMacOS/NativeClasses/Display/MorphicDisplay.swift b/MorphicMacOS/NativeClasses/Display/MorphicDisplay.swift new file mode 100644 index 0000000..c1ad327 --- /dev/null +++ b/MorphicMacOS/NativeClasses/Display/MorphicDisplay.swift @@ -0,0 +1,323 @@ +// +// MorphicDisplay.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +import Foundation + +// NOTE: the MorphicDisplay class contains the functionality used by Obj-C and Swift applications + +public class MorphicDisplay { + + // MARK: - DisplayMode struct + + public struct DisplayMode: Equatable { + public let ioDisplayModeId: Int32 + public let widthInPixels: Int + public let heightInPixels: Int + public let widthInVirtualPixels: Int + public let heightInVirtualPixels: Int + // NOTE: refreshRateInHertz encodes as Double(0) if nil + public let refreshRateInHertz: Double? + // NOTE: isUsableForDesktopGui encodes as UInt8(1) if true and UInt8(0) if false + public let isUsableForDesktopGui: Bool // NOTE: we can use this flag, in theory, to limit the resolutions we provide to user + + private init( + ioDisplayModeId: Int32, + widthInPixels: Int, + heightInPixels: Int, + widthInVirtualPixels: Int, + heightInVirtualPixels: Int, + refreshRateInHertz: Double?, + isUsableForDesktopGui: Bool + ) { + self.ioDisplayModeId = ioDisplayModeId + self.widthInPixels = widthInPixels + self.heightInPixels = heightInPixels + self.widthInVirtualPixels = widthInVirtualPixels + self.heightInVirtualPixels = heightInVirtualPixels + self.refreshRateInHertz = refreshRateInHertz + self.isUsableForDesktopGui = isUsableForDesktopGui + } + + // this function converts the data from a macOS CGDisplayMode class instance to a DisplayMode structure which we can use conveniently and which we can pass via exported cdecl functions + public static func create(from cgDisplayMode: CGDisplayMode) -> DisplayMode? { + // NOTE: if desired, we could use functions like CGDisplayModeCopyPixelEncoding here to retrieve bits-per-pixel etc. + // CGDisplayModeCopyPixelEncoding(...) <-- NOTE: we would also need a defer {...} block with an associated release call + + // convert the data we have gathered into our own struct + let result = DisplayMode( + ioDisplayModeId: cgDisplayMode.ioDisplayModeID, + widthInPixels: cgDisplayMode.pixelWidth, + heightInPixels: cgDisplayMode.pixelHeight, + widthInVirtualPixels: cgDisplayMode.width, + heightInVirtualPixels: cgDisplayMode.height, + refreshRateInHertz: cgDisplayMode.refreshRate != 0 ? cgDisplayMode.refreshRate : nil, + isUsableForDesktopGui: cgDisplayMode.isUsableForDesktopGUI()) + + return result + } + + public static func ==(lhs: MorphicDisplay.DisplayMode, rhs: MorphicDisplay.DisplayMode) -> Bool { + // NOTE: for purposes of equality, we do not check 'isUsableForDesktopGui' + + if lhs.ioDisplayModeId != rhs.ioDisplayModeId || + lhs.widthInPixels != rhs.widthInPixels || + lhs.heightInPixels != rhs.heightInPixels || + lhs.widthInVirtualPixels != rhs.widthInVirtualPixels || + lhs.heightInVirtualPixels != rhs.heightInVirtualPixels || + lhs.refreshRateInHertz != rhs.refreshRateInHertz { + // + return false + } + + // otherwise, the arguments are equal + return true + } + + // NOTE: we automatically bridge MorphicDisplay.DisplayMode and CGDisplayMode for comparison purposes + public static func ==(displayMode: MorphicDisplay.DisplayMode, cgDisplayMode: CGDisplayMode) -> Bool { + // NOTE: for purposes of equality, we do not check 'isUsableForDesktopGui' + + if displayMode.ioDisplayModeId != cgDisplayMode.ioDisplayModeID || + displayMode.widthInPixels != cgDisplayMode.pixelWidth || + displayMode.heightInPixels != cgDisplayMode.pixelHeight || + displayMode.widthInVirtualPixels != cgDisplayMode.width || + displayMode.heightInVirtualPixels != cgDisplayMode.height { + // + return false + } + + if (displayMode.refreshRateInHertz == nil && cgDisplayMode.refreshRate != 0) || + (displayMode.refreshRateInHertz != nil && displayMode.refreshRateInHertz! != cgDisplayMode.refreshRate) { + // + return false + } + + // otherwise, the arguments are equal + return true + } + } + + // MARK: - Display enumeration functions + + public static func getActiveDisplayIds() -> [UInt32]? { + var result: [UInt32] = [] + + // NOTE: here, we establish a "maximum number of displays" that we support for Core Graphics. For our primary applications, this number is fairly irrelevant (since the first display we get information about should be the main display--which is usually what we'd be looking for). But we may find other applications in the future where we want to reset the resolutions of multiple displays--in which case we can increment this constant. + let maximumNumberOfDisplaysToSupport: UInt32 = 32 // NOTE: we use 32 active displays as a reasonable "upper bound" because we have to allocate an array to hold "up to max # supported" entries; otherwise we would just use UInt32.max + // + let activeDisplayIds = UnsafeMutablePointer.allocate(capacity: Int(maximumNumberOfDisplaysToSupport)) + defer { + activeDisplayIds.deallocate() + } + // + var numberOfActiveDisplays: UInt32 = 0 + // + if CGGetActiveDisplayList(maximumNumberOfDisplaysToSupport, activeDisplayIds, &numberOfActiveDisplays) != CGError.success { + // if we could not get the list of active diplays, return nil + return nil + } + + // return active display IDs + for index in 0.. UInt32? { + // NOTE: this implementation fetches the first active display as the main display ID (as described in Apple's documentation); we may also want to consider using CGMainDisplayID() instead. + + // get the number of active displays + guard let activeDisplayIds = MorphicDisplay.getActiveDisplayIds() else { + fatalError("could not retrieve list of active display ids.") + } + + if activeDisplayIds.count < 1 { + NSLog("No displays; this feature is not applicable on headless platforms.") + return nil + } + + let mainDisplayId = activeDisplayIds[0] + + return mainDisplayId + } + + // MARK: Display mode functions + + // NOTE: this function returns nil if it encounters an error; we do this to distinguish an error condition ( nil ) from an empty set ( [] ). + public static func getAllDisplayModes(for displayId: UInt32) -> [MorphicDisplay.DisplayMode]? { + var result: [MorphicDisplay.DisplayMode] = [] + + // get a list of modes for the specified display + guard let allDisplayModesAsCGDisplayModeArray = MorphicDisplay.getAllDisplayModesAsCGDisplayModeArray(for: displayId) else { + // if we cannot get a list of all display modes for this display, return nil + return nil + } + + for displayModeAsCGDisplayMode in allDisplayModesAsCGDisplayModeArray { + guard let displayMode = DisplayMode.create(from: displayModeAsCGDisplayMode) else { + // if we cannot process the mode-specific data, return nil + return nil + } + + result.append(displayMode) + } + + return result + } + + // NOTE: this fuction makes a copy of all the original OS-allocated CGDisplayMode values so that the result array is memory-safe to consume in Swift + private static func getAllDisplayModesAsCGDisplayModeArray(for displayId: UInt32) -> [CGDisplayMode]? { + var result: [CGDisplayMode] = [] + + // get a list of modes for the specified display + + // NOTE: according to the CGDisplayCopyAllDisplayModes documentation, there are no options for CGDisplayCopyAllDisplayModes; however that documentation is incorrect as it supports (and we need to use) the kCGDisplayShowDuplicateLowResolutionModes option so that we get a list of all modes, both Retina and non-Retina. + var dummyKeyCallBacks = kCFTypeDictionaryKeyCallBacks + var dummyValueCallBacks = kCFTypeDictionaryValueCallBacks + // + let options: CFMutableDictionary = CFDictionaryCreateMutable(kCFAllocatorDefault, 1, &dummyKeyCallBacks, &dummyValueCallBacks) + // NOTE: Swift manages Core Foundation memory for us; in other languages, be sure to CFRelease +// defer { +// CFRelease(options) +// } + // + let displayShowDuplicateLowResolutionModesOption = kCGDisplayShowDuplicateLowResolutionModes + CFDictionaryAddValue(options, unsafeBitCast(displayShowDuplicateLowResolutionModesOption, to: UnsafeRawPointer.self), unsafeBitCast(kCFBooleanTrue, to: UnsafeRawPointer.self)) + + guard let displayModes = CGDisplayCopyAllDisplayModes(displayId, options) else { + // if we could not get a list of display modes, return nil + return nil + } + // NOTE: Swift automatically manages Core Foundation references, so this is included here just for porting purposes. +// defer { +// CFRelease(listOfModes) +// } + let numberOfDisplayModes = CFArrayGetCount(displayModes) + + for index in 0...allocate(capacity: 1) + defer { + pointerToCopyOfDisplayModeAsCGDisplayMode.deallocate() + } + pointerToCopyOfDisplayModeAsCGDisplayMode.initialize(to: displayModeAsCGDisplayMode) + + result.append(pointerToCopyOfDisplayModeAsCGDisplayMode.pointee) + } + + return result + } + + public static func getCurrentDisplayMode(for displayId: UInt32) -> MorphicDisplay.DisplayMode? { + guard let displayModeAsCGDisplayMode = CGDisplayCopyDisplayMode(displayId) else { + // if we could not get our current display's mode details, return nil + return nil + } + // NOTE: Swift automatically manages Core Foundation references, so this is included here just for porting purposes. + // NOTE: Swift also automatically maps the result to an object of type CGDisplayMode? so we don't even _have_ a pointer +// defer { +// CGDisplayModeRelease(pointerToDisplayModeAsCGDisplayMode) +// } + // + guard let displayMode = DisplayMode.create(from: displayModeAsCGDisplayMode) else { + // if we cannot process the mode-specific data, return nil + return nil + } + + return displayMode + } + + public enum SetCurrentDisplayModeError: Error { + case invalidDisplayMode +// case unusableForDesktopGui + case otherError + } + public static func setCurrentDisplayMode(for displayId: UInt32, to newDisplayMode: MorphicDisplay.DisplayMode) throws { +// // OPTIONAL: if we want, we can throw an error if the selected display mode is already known not to be suitable for a desktop GUI +// if newDisplayMode.isUsableForDesktopGui == false { +// throw SetCurrentDisplayModeError.unusableForDesktopGui +// } + + // obtain a CGDisplayMode object which matches the "newDisplayMode" struct passed in by our caller + // + // get a list of modes for the specified display + guard let allDisplayModesAsCGDisplayModeArray = MorphicDisplay.getAllDisplayModesAsCGDisplayModeArray(for: displayId) else { + // if we cannot get a list of all display modes for this display, throw an error + throw SetCurrentDisplayModeError.otherError + } + // + var newDisplayModeAsCGDisplayMode: CGDisplayMode? = nil + for displayModeAsCGDisplayMode in allDisplayModesAsCGDisplayModeArray { + // do an equality check (for now...based on the io display mode ID) + if newDisplayMode == displayModeAsCGDisplayMode { + newDisplayModeAsCGDisplayMode = displayModeAsCGDisplayMode + break + } + } + // + if newDisplayModeAsCGDisplayMode == nil { + // if we could not find the requested display mode in our available options, throw an error + // NOTE: if we ever see this error thrown even though we chose a valid display mode option, change our equality check code (above) to match on the individual known-immutable properties like width and height instead (or have our function called with those specific "search" parameters) + throw SetCurrentDisplayModeError.invalidDisplayMode + } + + // now set up our display configuration transaction--and then execute it + + // ask Core Graphics to initialize a new configuration to use to change the display mode (using a pointer reference) + // NOTE: the pointer is a pointer to a nullable object + let pointerToConfig = UnsafeMutablePointer.allocate(capacity: 1) + defer { + pointerToConfig.deallocate() + } + // + if CGBeginDisplayConfiguration(pointerToConfig) != CGError.success { + // if we could not initialize a configuration object, throw na error + throw SetCurrentDisplayModeError.otherError + } + // NOTE: Core Graphics requires that we "cancel" any config transaction which we don't complete + var configRequiresCoreGraphicsCancel = false + defer { + if configRequiresCoreGraphicsCancel == true { + // cancel (and release) the Core Graphics configuration + CGCancelDisplayConfiguration(pointerToConfig.pointee) + } + } + + // configure the display with our configuration mode + // NOTE: we pass "nil" to the options parameter; as of 30-Aug-2019 there are no options available for this API + if CGConfigureDisplayWithDisplayMode(pointerToConfig.pointee, displayId, newDisplayModeAsCGDisplayMode, nil) != CGError.success { + // if we could not configure the new display mode settings data, throw an error + throw SetCurrentDisplayModeError.otherError + } + + // specify our configuration option (i.e. "permanent display change, not just while our app is running or while we are logged into the current session") + let configureOption: CGConfigureOption = CGConfigureOption.permanently + + // finally, complete the display mode change transaction using CGCompleteDisplayConfiguration + if CGCompleteDisplayConfiguration(pointerToConfig.pointee, configureOption) == CGError.success { + // NOTE: as we have called "complete", there is no need to cancel the transaction (so update this flag here so our DEFER block does not try to cleanup a completed config transaction) + configRequiresCoreGraphicsCancel = false + // NOTE: the documentation is NOT clear as to if we need to "cancel" if CGCompleteDisplayConfiguration returns an error...but we do so out of an abundance of caution. If this is in error, we should remove this "set to false" code to just before the CGCompleteDisplayConfiguration call + } else { + // if we could not set the display mode, throw an error + throw SetCurrentDisplayModeError.otherError + } + } +} diff --git a/MorphicMacOS/NativeClasses/Input/MorphicInput.swift b/MorphicMacOS/NativeClasses/Input/MorphicInput.swift new file mode 100644 index 0000000..84cf7c1 --- /dev/null +++ b/MorphicMacOS/NativeClasses/Input/MorphicInput.swift @@ -0,0 +1,91 @@ +// +// MorphicInput.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +import Foundation + +// NOTE: the MorphicInput class contains the functionality used by Obj-C and Swift applications + +public class MorphicInput { + + // MARK: - KeyOptions struct + + public struct KeyOptions: OptionSet { + public typealias RawValue = UInt32 + public let rawValue: RawValue + + public init(rawValue: RawValue) { + self.rawValue = rawValue + } + + public static let withControlKey = KeyOptions(rawValue: 1 << 00) + public static let withAlternateKey = KeyOptions(rawValue: 1 << 01) + public static let withCommandKey = KeyOptions(rawValue: 1 << 02) + } + + // MARK: - Keyboard virtualization functions + + // NOTE: we have no way to know if the key press was successful: sendKey is "fire and forget" + public static func sendKey(keyCode: CGKeyCode, keyOptions: KeyOptions, toProcessId processId: Int) -> Bool { + // NOTE: this implementation of sendKey sends the key to a process via CGEvents and using its processId; we might also consider using AppleScript in the future to send keystrokes to apps (potentially by name) + + // NOTE: a CGEventSource of "nil" seems to work just as well, but we're following established practices here; realistically it will probably work fine either way + guard let eventSource = CGEventSource(stateID: .hidSystemState) else { + NSLog("Could not set event source to .hidSystemState") + return false + } + + guard let keyDownEvent: CGEvent = CGEvent(keyboardEventSource: eventSource, virtualKey: keyCode, keyDown: true), + let keyUpEvent: CGEvent = CGEvent(keyboardEventSource: eventSource, virtualKey: keyCode, keyDown: false) else { + // + NSLog("Could not create keyUp/keyDown events") + return false + } + // NOTE: Swift manages Core Foundation memory for us; in other languages, be sure to CFRelease +// defer { +// CFRelease(keyUpEvent) +// CFRelease(keyDownEvent) +// } + + var keyDownEventFlagsRawValue = keyDownEvent.flags.rawValue + var keyUpEventFlagsRawValue = keyUpEvent.flags.rawValue + // + // hold down control key (if applicable) + if keyOptions.contains(.withControlKey) { + keyDownEventFlagsRawValue |= CGEventFlags.maskControl.rawValue + keyUpEventFlagsRawValue |= CGEventFlags.maskControl.rawValue + } + // + // hold down alternate/option key (if applicable) + if keyOptions.contains(.withAlternateKey) { + keyDownEventFlagsRawValue |= CGEventFlags.maskAlternate.rawValue + keyUpEventFlagsRawValue |= CGEventFlags.maskAlternate.rawValue + } + // + // hold down command key (if applicable) + if keyOptions.contains(.withCommandKey) { + keyDownEventFlagsRawValue |= CGEventFlags.maskCommand.rawValue + keyUpEventFlagsRawValue |= CGEventFlags.maskCommand.rawValue + } + // + keyDownEvent.flags = CGEventFlags(rawValue: keyDownEventFlagsRawValue) + keyUpEvent.flags = CGEventFlags(rawValue: keyUpEventFlagsRawValue) + + let processIdAsPid = pid_t(processId) + + // press the key + keyDownEvent.postToPid(processIdAsPid) + // then release the key + keyUpEvent.postToPid(processIdAsPid) + + return true + } +} diff --git a/MorphicMacOS/NativeClasses/Language/MorphicLanguage.swift b/MorphicMacOS/NativeClasses/Language/MorphicLanguage.swift new file mode 100644 index 0000000..8cf73bb --- /dev/null +++ b/MorphicMacOS/NativeClasses/Language/MorphicLanguage.swift @@ -0,0 +1,127 @@ +// +// MorphicLanguagee.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +import Foundation + +// NOTE: the MorphicLanguage class contains the functionality used by Obj-C and Swift applications + +public class MorphicLanguage { + // MARK: - Functions to get/set the preferred languages + + // NOTE: this function gets the current list of preferred languages (even if a key is not set in the global domain); this is the recommended approach + public static func getPreferredLanguages() -> [String]? { + let preferredLanguages = CFLocaleCopyPreferredLanguages() + return preferredLanguages as? [String] + } + + // NOTE: this function gets the property in the global domain (AnyApplication), but only for the current user + public static func getAppleLanguagesFromGlobalDomain() -> [String]? { + guard let propertyList = CFPreferencesCopyValue("AppleLanguages" as CFString, kCFPreferencesAnyApplication, kCFPreferencesCurrentUser, kCFPreferencesAnyHost) else { + return nil + } + let result = propertyList as? [String] + + return result + } + + // NOTE: this function sets the property in the global domain (AnyApplication), but only for the current user + public static func setAppleLanguagesInGlobalDomain(_ languages: [String]) -> Bool { + CFPreferencesSetValue("AppleLanguages" as CFString, languages as CFArray, kCFPreferencesAnyApplication, kCFPreferencesCurrentUser, kCFPreferencesAnyHost) + let success = CFPreferencesSynchronize(kCFPreferencesAnyApplication, kCFPreferencesCurrentUser, kCFPreferencesAnyHost) + + return success + } + + public static func setPrimaryAppleLanguageInGlobalDomain(_ primaryLanguage: String) -> Bool { +// // implementation option 1: get our current list of Apple Languages from the global domain (scoped to the current host) +// guard var appleLanguages = MorphicLanguage.getAppleLanguagesFromGlobalDomain() else { +// return false +// } + +// // implementation option 2: get our current list of Apple Languages from UserDefaults +// guard var appleLanguages: [String] = UserDefaults.standard.array(forKey: "AppleLanguages") as? [String] else { +// return +// } + + // implementation option 2: get our current list of Apple Languages from Core Foundation; this is the preferred method + guard var appleLanguages = MorphicLanguage.getPreferredLanguages() else { + return false + } + + // verify that the specified 'primaryLanguage' is contained within the list of installed languages + guard appleLanguages.contains(primaryLanguage) == true else { + return false + } + + // remove the desired primary language from the list of apple languages (since we want to push it to the top of the list) + appleLanguages = appleLanguages.filter() { $0 != primaryLanguage } +// // alternate approach +// appleLanguages.removeAll(where: { $0 == primaryLanguage }) + + // prepend the desired primary language to the full list + appleLanguages.insert(primaryLanguage, at: 0) + + // re-set the apple languages list (with the desired primary language now at the top of the list) + let success = MorphicLanguage.setAppleLanguagesInGlobalDomain(appleLanguages) + return success + } + + // MARK: - functions to translate locale/language/country codes to human-readable format + + // NOTE: getCurrentLocale() may not reflect changes in the current locale until after a reboot, etc. + public static func getCurrentLocale() -> CFLocale? { + return CFLocaleCopyCurrent() + } + + public static func createLocale(from languageAndCountryCode: String) -> CFLocale? { + guard let canonicalLocaleIdentifier = CFLocaleCreateCanonicalLocaleIdentifierFromString(kCFAllocatorDefault, languageAndCountryCode as CFString) else { + return nil + } + return createLocale(from: canonicalLocaleIdentifier) + } + + public static func createLocale(from canonicalLocaleIdentifier: CFLocaleIdentifier) -> CFLocale? { + return CFLocaleCreate(kCFAllocatorDefault, canonicalLocaleIdentifier) + } + + public static func getLanguageAndCountryCode(for locale: CFLocale) -> String? { + guard let iso639LanguageCode = getIso639LanguageCode(for: locale), + let iso3166CountryCode = getIso3166CountryCode(for: locale) else { + return nil + } + return iso639LanguageCode + "-" + iso3166CountryCode + } + + public static func getIso639LanguageCode(for locale: CFLocale) -> String? { + guard let iso639LanguageCodeAsCFTypeRef = CFLocaleGetValue(locale, CFLocaleKey.languageCode) else { + return nil + } + let iso639LanguageCodeAsCFString = iso639LanguageCodeAsCFTypeRef as! CFString + return iso639LanguageCodeAsCFString as String + } + + public static func getLanguageName(for iso639LanguageCode: String, translateTo translateToLocale: CFLocale) -> String { + return CFLocaleCopyDisplayNameForPropertyValue(translateToLocale, CFLocaleKey.languageCode, iso639LanguageCode as CFString) as String + } + + public static func getIso3166CountryCode(for locale: CFLocale) -> String? { + guard let iso3166CountryCodeAsCFTypeRef = CFLocaleGetValue(locale, CFLocaleKey.countryCode) else { + return nil + } + let iso3166CountryCodeAsCFString = iso3166CountryCodeAsCFTypeRef as! CFString + return iso3166CountryCodeAsCFString as String + } + + public static func getCountryName(for iso3166CountryCode: String, translateTo translateToLocale: CFLocale) -> String { + return CFLocaleCopyDisplayNameForPropertyValue(translateToLocale, CFLocaleKey.countryCode, iso3166CountryCode as CFString) as String + } +} diff --git a/MorphicMacOS/NativeClasses/Process/MorphicLaunchDaemonsAndAgents.swift b/MorphicMacOS/NativeClasses/Process/MorphicLaunchDaemonsAndAgents.swift new file mode 100644 index 0000000..b3e0c4b --- /dev/null +++ b/MorphicMacOS/NativeClasses/Process/MorphicLaunchDaemonsAndAgents.swift @@ -0,0 +1,70 @@ +// +// MorphicLaunchDaemonsAndAgents.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +public class MorphicLaunchDaemonsAndAgents { + public struct MorphicLaunchDaemonOrAgentInfo { + public let name: String + public let serviceName: String + + public init(name: String, serviceName: String) { + self.name = name + self.serviceName = serviceName + } + } + + // list of launch (system) daemons and (user) agents +// public static let controlStrip = MorphicLaunchDaemonOrAgentInfo( +// name: "Control Strip", +// serviceName: "com.apple.controlstrip" +// ) +// // + public static let dock = MorphicLaunchDaemonOrAgentInfo( + name: "Dock", + serviceName: "com.apple.Dock.agent" + ) + // + public static let finder = MorphicLaunchDaemonOrAgentInfo( + name: "Finder", + serviceName: "com.apple.Finder" + ) + // + public static let notificationCenter = MorphicLaunchDaemonOrAgentInfo( + name: "Notification Center", + serviceName: "com.apple.notificationcenterui.agent" + ) + // + public static let spotlight = MorphicLaunchDaemonOrAgentInfo( + name: "Spotlight", + serviceName: "com.apple.Spotlight" + ) + // + public static let systemUIServer = MorphicLaunchDaemonOrAgentInfo( + name: "System UI Server", + serviceName: "com.apple.SystemUIServer.agent" + ) + // + public static let textInputMenuBarExtra = MorphicLaunchDaemonOrAgentInfo( + name: "Text Input Menubar Extra", + serviceName: "com.apple.TextInputMenuAgent" + ) + + public static let allCases: [MorphicLaunchDaemonOrAgentInfo] = [ +// LaunchDaemonsAndAgents.controlStrip, + MorphicLaunchDaemonsAndAgents.dock, + MorphicLaunchDaemonsAndAgents.finder, + MorphicLaunchDaemonsAndAgents.notificationCenter, + MorphicLaunchDaemonsAndAgents.spotlight, + MorphicLaunchDaemonsAndAgents.systemUIServer, + MorphicLaunchDaemonsAndAgents.textInputMenuBarExtra + ] + +} diff --git a/MorphicMacOS/NativeClasses/Process/MorphicProcess.swift b/MorphicMacOS/NativeClasses/Process/MorphicProcess.swift new file mode 100644 index 0000000..79fe113 --- /dev/null +++ b/MorphicMacOS/NativeClasses/Process/MorphicProcess.swift @@ -0,0 +1,37 @@ +// +// MorphicProcess.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +import Foundation + +// NOTE: the MorphicProcess class contains the functionality used by Obj-C and Swift applications + +public class MorphicProcess { + // MARK: - Process/service restarting functionality + + public static func restartViaLaunchctl(serviceNames: [String]) { + // get the current user's ID + // NOTE: in future releases of macOS we may need to consider using SCDynamicStoreCopyConsoleUser or other methods + let userId = getuid() + + for serviceName in serviceNames { + let domainTarget = "gui/" + String(userId) + "/" + serviceName + MorphicProcess.restartViaLaunchctl(domainTarget: domainTarget) + } + } + + public static func restartViaLaunchctl(domainTarget: String) { + let launchctlProcess = Process() + launchctlProcess.launchPath = "/bin/launchctl" + launchctlProcess.arguments = ["kickstart", "-k", domainTarget] + launchctlProcess.launch() + } +} diff --git a/MorphicMacOS/NativeClasses/Window/MorphicWindow.swift b/MorphicMacOS/NativeClasses/Window/MorphicWindow.swift new file mode 100644 index 0000000..ed9ec5d --- /dev/null +++ b/MorphicMacOS/NativeClasses/Window/MorphicWindow.swift @@ -0,0 +1,71 @@ +// +// MorphicWindow.swift +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +import Foundation + +// NOTE: the MorphicWindow class contains the functionality used by Obj-C and Swift applications + +public class MorphicWindow { + + // MARK: - Window search/enumeration functions + + // NOTE: when calculating which window is topmost, this function ignores the top and also the current process's window(s) + public static func getWindowOwnerNameAndProcessIdOfTopmostWindow() -> (windowOwnerName: String, processId: Int)? { + // get a list of window info for all on-screen windows + // NOTE: these windows will be ordered in top-to-bottom order + guard var windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly], kCGNullWindowID) as? [[String: AnyObject]] else { + return nil + } + // NOTE: when porting to languages outside of Swift, determine if "windows" needs to be manually released + + // filter out only windows which have a windowLevel ("kCGWindowLayer") of zero (meaning they are not part of the topbar etc.) + windows = windows.filter { (window) -> Bool in + let windowLayer = window[kCGWindowLayer as String] as? NSNumber + return windowLayer == 0 + } + + // capture the process ID of our current process (so that we don't consider ourselves to be in the "topmost windows" + let applicationProcessId = Int(getpid()) + + // retrieve the topmost window (which is NOT this application) + var topmostWindow: [String: AnyObject]! = nil + for window in windows { + guard let windowProcessId = window[kCGWindowOwnerPID as String] as? Int else { + // if we cannot get the process id of the window, skip the window + continue + } + + if windowProcessId == applicationProcessId { + // do not include current process in search for topmost window + continue + } + + // we have found the topmost window + topmostWindow = window + break + } + // + if topmostWindow == nil { + return nil + } + + // retrieve the window ID and process ID of the topmost window + guard let topmostWindowOwnerName = topmostWindow[kCGWindowOwnerName as String] as? String else { + return nil + } + guard let topmostWindowProcessId = topmostWindow[kCGWindowOwnerPID as String] as? Int else { + return nil + } + + return (windowOwnerName: topmostWindowOwnerName, processId: topmostWindowProcessId) + } +} diff --git a/MorphicMacOS/main.c b/MorphicMacOS/main.c new file mode 100644 index 0000000..a72db2a --- /dev/null +++ b/MorphicMacOS/main.c @@ -0,0 +1,21 @@ +// +// main.c +// Morphic support library for macOS +// +// Copyright © 2020 Raising the Floor -- US Inc. All rights reserved. +// +// The R&D leading to these results received funding from the +// Department of Education - Grant H421A150005 (GPII-APCP). However, +// these results do not necessarily represent the policy of the +// Department of Education, and you should not assume endorsement by the +// Federal Government. + +// NOTE: we specify NAPI_VERSION 4 (for Node.js 10.16 or newer); for Node.js 12.11 or newer, we could use NAPI_VERSION 5 instead +#define NAPI_VERSION 4 +#import "./N-API/include/node_api.h" + +// signature prototype for node.js addon's init function; implementation is in Main.swift +napi_value Init(napi_env env, napi_value exports); + +// NOTE: the following line must be commented out when importing this library into a non-Node.js application +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) diff --git a/README.md b/README.md index 4da0e54..8058ffb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ -# GPII for OS X +# GPII for macOS + +Contains platform-specific components of the GPII architecture for macOS. See [http://gpii.net/](http://gpii.net/) for +overall details of the GPII project. After checkout using git, this project will require node.js and npm to be +installed - please consult [Setting Up Your Development Environment](http://wiki.gpii.net/w/Setting_Up_Your_Development_Environment) +for installation instructions. + +## Building the macOS-specific components + +This package depends on node 10.x (LTS). + +To build the platform-specific GPII components for macOS, perform the following: + + git clone https://github.com/GPII/gpii-macos.git + cd gpii-macos + npm install -This is it. The GPII for OS X has arrived! diff --git a/gpii.js b/gpii.js new file mode 100644 index 0000000..8d5434b --- /dev/null +++ b/gpii.js @@ -0,0 +1,31 @@ +/* + * GPII macOS Personalization Framework Node.js Bootstrap + * + * Copyright 2020 Raising the Floor -- US Inc. All rights reserved. + * Copyright 2012 OCAD University + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + */ + +"use strict"; + +var fluid = require("gpii-universal"), + gpii = fluid.registerNamespace("gpii"); + +require("./index.js"); + +gpii.start(); diff --git a/gpii/node_modules/MacOSUtilities/MacOSUtilities.js b/gpii/node_modules/MacOSUtilities/MacOSUtilities.js new file mode 100644 index 0000000..d303e36 --- /dev/null +++ b/gpii/node_modules/MacOSUtilities/MacOSUtilities.js @@ -0,0 +1,71 @@ +/* + * GPII macOS Personalization Framework + * + * Copyright 2020 Raising the Floor -- US Inc. All rights reserved. + * Copyright 2012 Raising the Floor - International + * Copyright 2012 Antranig Basman + * Copyright 2012 Astea Solutions AD + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + */ + +"use strict"; + +var gpii = fluid.registerNamespace("gpii"); +var macos = fluid.registerNamespace("gpii.macos"); + +/** + * Gets the USB drives that are available. + * + * @param {Array} drivePaths [optional] Array of drives (mounting paths) to check. Omit to check all drives on the system. + * @return {Array} Array of mounting paths to each USB drive. + */ +gpii.macos.getUsbDrives = function (drivePaths) { + var drives = macos.native.getAllUsbDriveMountPaths(); + + // if drivePaths was provided as a filter, remove any drives from our array which are not in the passed-in list + if (drivePaths) { + drives = drives.filter(function (diskMountPath) { + return drivePaths.includes(diskMountPath); + }); + } + + return drives; +}; + +/** + * Ejects USB drives + * + * @param {Array} drivePaths - Array of drives (mounting paths) to eject. + */ +gpii.macos.ejectUsbDrives = function (drivePaths) { + macos.native.safelyEjectUsbDrives(drivePaths, function(success, failedDrivePaths) { + // TODO: in the future, consider capturing this information to return asynchronous successs/failure (perhaps via a Promise) + // console.log("success: " + success); + // // NOTE: if success is false, failedDrivePaths will contain a list of the drivePaths which could not be ejected + // console.log("failedDrivePaths: " + failedDrivePaths); + }); +} + +/** + * Opens directory paths + * + * @param {Array} paths - Array of directory paths to open. + */ +gpii.macos.openDirectories = function (paths) { + macos.native.openDirectories(paths); +} diff --git a/gpii/node_modules/displaySettingsHandler/index.js b/gpii/node_modules/displaySettingsHandler/index.js new file mode 100644 index 0000000..da79ddf --- /dev/null +++ b/gpii/node_modules/displaySettingsHandler/index.js @@ -0,0 +1,3 @@ +"use strict"; + +require("./src/displaySettingsHandler.js"); diff --git a/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js b/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js new file mode 100644 index 0000000..94264a4 --- /dev/null +++ b/gpii/node_modules/displaySettingsHandler/src/displaySettingsHandler.js @@ -0,0 +1,468 @@ +/* + * macOS Display Settings Handler + * + * Copyright 2016-2020 Raising the Floor -- US Inc. All rights reserved. + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + */ + +"use strict"; + +var fluid = require("gpii-universal"); + +var gpii = fluid.registerNamespace("gpii"); +var macos = fluid.registerNamespace("gpii.macos"); + +fluid.registerNamespace("gpii.macos.display"); +fluid.registerNamespace("gpii.macos.displaySettingsHandler"); + +// The OS specific files can only be loaded from within the OS is it intended for. +require("./dpiMacOS.js"); + +// /* +// * Returns if a screen resolution is invalid +// * +// * @param (Object) +// * @returns {Boolean} true if invalid +// */ +// macos.display.isInvalid = function (screenRes) { +// var isInvalid = true; +// if (typeof(screenRes.width) === "number" && typeof(screenRes.height) === "number" && screenRes.width > 0 && screenRes.height > 0) { +// fluid.each(macos.display.getAvailableResolutions(), function (validScreenRes) { +// if (validScreenRes.width === screenRes.width && validScreenRes.height === screenRes.height) { +// isInvalid = false; +// }; +// }); +// } +// return isInvalid; +// }; + +/* + * Gets the current display's screen resolution + * + * @return {Object) The width and height of the screen. + */ +macos.display.getScreenResolution = function () { + var currentDisplayMode = gpii.macos.native.getCurrentDisplayMode(); + if (currentDisplayMode) { + return { + width: currentDisplayMode.widthInVirtualPixels, + height: currentDisplayMode.heightInVirtualPixels + }; + } else { + fluid.fail("Couldn't retrieve the current screen resolution"); + } +}; + +// /* +// * Gets available display modes (including display mode id, resolution and virtual resolution info, etc.) +// * +// * @return {list} of display modes +// */ +macos.display.getAvailableDisplayModes = function() { + // get a list of all valid display modes for the main display + var allDisplayModes = gpii.macos.native.getAllDisplayModes(); + + // filter out any display modes which are marked as not usable for a desktop GUI + var usableForDesktopGuiDisplayModes = macos.display.filterOutUnusableForDesktopGuiDisplayModes(allDisplayModes); + + // sort list of display modes (by width primarily, then height secondarily) in order of increasing size + var sortedDisplayModes = macos.display.sortDisplayModesByResolutionAscending(usableForDesktopGuiDisplayModes); + + // remove duplicate display mode entries + var deduplicatedDisplayModes = macos.display.filterOutDuplicateDisplayModes(sortedDisplayModes); + + // remove all non-retina display mode options which have a corresponding retina display mode option + var retinaPreferredDisplayModes = macos.display.filterOutNonRetinaScaleAlternativeDisplayModes(deduplicatedDisplayModes); + + // remove all retina "native" resolution options where a pixel-doubled retina option exists + var pixelDoublePreferredDisplayModes = macos.display.filterOutRetinaNativeScaleDisplayModes(retinaPreferredDisplayModes); + + // return the filtered list of display modes + return pixelDoublePreferredDisplayModes; +} + +// /* +// * Gets the default display mode +// * +// * @return the default display mode +// */ +macos.display.getDefaultDisplayMode = function() { + // NOTE: this function assumes that the retina variant of the highest screen resolution is the "default" resolution, as Apple no longer + // provides non-deprecated APIs or fixed critera for what they consider the "default" resolution (as shown in System Preferences) + // in the alternate, we could call CGDisplayScreenSize and do some DPI calculations ourselves...based on custom criteria + + // get a list of all valid display modes for the main display + var allDisplayModes = gpii.macos.native.getAllDisplayModes(); + + // filter out any display modes which are marked as not usable for a desktop GUI + var usableForDesktopGuiDisplayModes = macos.display.filterOutUnusableForDesktopGuiDisplayModes(allDisplayModes); + + // retrieve the highest-resolution native display mode from our list of available display modes + var highestResolutionNativeDisplayMode = macos.display.getHighestResolutionNativeDisplayMode(usableForDesktopGuiDisplayModes); + + // get a filtered list of all display modes (minus the unusable-for-gui, non-retina, etc. modes) + var availableDisplayModes = macos.display.getAvailableDisplayModes() + + var defaultDisplayMode = null; + + // find the available display mode which matches our highestResolutionNativeDisplayMode's native resolution (widthInPixels/heightInPixels) + availableDisplayModes.forEach(function(displayMode) { + if (displayMode.widthInPixels == highestResolutionNativeDisplayMode.widthInPixels && + displayMode.heightInPixels == highestResolutionNativeDisplayMode.heightInPixels) { + // + defaultDisplayMode = displayMode; + } + }); + + // failsafe: if our highest-resolution native display mode was somehow filtered, then return the final available display mode instead + if (defaultDisplayMode == null && availableDisplayModes.length > 0) { + defaultDisplayMode = availableDisplayModes[availableDisplayModes.length - 1]; + } + + return defaultDisplayMode; +} + +/* + * Gets available resolutions + * + * @return {list} of resolutions + */ +macos.display.getAvailableResolutions = function () { + var index = 0; + var availableResolutions = []; + + var availableDisplayModes = macos.display.getAvailableDisplayModes(); + + availableDisplayModes.forEach(function(entry) { + var displayResolution = { + width: entry.widthInPixels, + height: entry.heightInPixels + }; + availableResolutions.push(displayResolution); + }); + + fluid.log("macos.display.getAvailableResolutions got available screen resolutions ", availableResolutions); + + return availableResolutions; +}; + +/* + * Sets the current display's screen resolution if possible + * + * @param {Object} The new screen resolution width and height + * @return {Boolean} true if successful + */ +macos.display.setScreenResolution = function (newRes) { + var allScreenDpisWithDisplayModes = macos.display.getAllScreenDpisWithDisplayModes(adaper); + // sanity check + if (!allScreenDpisWithDisplayModes || allScreenDpisWithDisplayModes.dpiDisplayModeMap.length == 0) { + // NOTE: this code should never be executed + return false; + } + + // get the dpiOffset associated with this screen resolution + var newResDpiOffset = null; + for(var dpiOffset = allScreenDpisWithDisplayModes.minimum; dpiOffset <= allScreenDpisWithDisplayModes.maximum; dpiOffset++) { + var dpiOffsetAsString = dpiOffset + ""; + var displayModeAtDpiOffset = allScreenDpisWithDisplayModes.dpiDisplayModeMap[dpiOffsetAsString]; + if(displayModeAtDpiOffset.widthInVirtualPixels == newRes.width && + displayModeAtDpiOffset.heightInVirtualPixels == newRes.height) { + // + newResDpiOffset = dpiOffset; + break; + } + } + + if(!newResDpiOffset) { + fluid.fail("Received an invalid screen resolution: ", newRes); + return false; + } else { + macos.display.setScreenDpi(newResDpiOffset); + return true; + } +}; + +macos.display.setImpl = function (payload) { + var results = {}; + + var targetRes = payload.settings["screen-resolution"]; + if (targetRes) { + var oldRes = macos.display.getScreenResolution(); + var newRes = oldRes; + + if (typeof(targetRes.width) !== "number" || typeof(targetRes.height) !== "number") { + fluid.fail("Incorrect payload for screen resolution: " + + JSON.stringify(payload, null, 4)); + } + else if (macos.display.setScreenResolution(targetRes)) { + newRes = targetRes; + } + + results["screen-resolution"] = { oldValue: oldRes, newValue: newRes }; + } + + var targetDpi = payload.settings["screen-dpi"]; + if (Number.isInteger(targetDpi)) { + var oldDpi = macos.display.getScreenDpi(); + macos.display.setScreenDpi(targetDpi); + results["screen-dpi"] = { oldValue: oldDpi.configured, newValue: targetDpi }; + } + + fluid.log("display settings handler SET returning results ", results); + + return results; +}; + +macos.display.getImpl = function () { + var curRes = macos.display.getScreenResolution(); + var curDpi = macos.display.getScreenDpi(); + + var results = { + "screen-resolution": curRes, + "screen-dpi": curDpi.configured + }; + + return results; +}; + +macos.displaySettingsHandler.get = function (payload) { + return gpii.settingsHandlers.invokeSettingsHandler(macos.display.getImpl, payload); +}; +macos.displaySettingsHandler.set = function (payload) { + return gpii.settingsHandlers.invokeSettingsHandler(macos.display.setImpl, payload); +}; + +// Sorting/filtering utilities + +macos.display.sortDisplayModesByResolutionAscending = function(displayModes) { + return displayModes.slice().sort(function(a,b) { + // sort index: widthInVirtualPixels + if (a.widthInVirtualPixels > b.widthInVirtualPixels) { + return 1; + } else if (a.widthInVirtualPixels < b.widthInVirtualPixels) { + return -1; + } + + // sort index: heightInVirtualPixels + if (a.heightInVirtualPixels > b.heightInVirtualPixels) { + return 1; + } else if (a.heightInVirtualPixels < b.heightInVirtualPixels) { + return -1; + } + + // sort index: widthInPixels + if (a.widthInPixels > b.widthInPixels) { + return 1; + } else if (a.widthInPixels < b.widthInPixels) { + return -1; + } + + // sort index: heightInPixels + if (a.heightInPixels > b.heightInPixels) { + return 1; + } else if (a.heightInPixels < b.heightInPixels) { + return -1; + } + + // otherwise, the values are equal + return 0; + }); +} + +macos.display.filterOutDuplicateDisplayModes = function(displayModes) { + // copy the whole displayModes array into a working set + let workingSet = displayModes.slice(); + + // NOTE: this filter routine would be much faster if we pre-sorted the array; for simplicity we do not do so here + + let iFirstElement = 0; + while (iFirstElement < workingSet.length) { + let iSecondElement = iFirstElement + 1; + while (iSecondElement < workingSet.length) { + if (macos.display.displayModesAreEqual(workingSet[iFirstElement], workingSet[iSecondElement]) == true) { + workingSet.splice(iSecondElement, 1); + } else { + iSecondElement += 1; + } + } + + iFirstElement += 1; + } + + // return the remaining (non-duplicate) working set entries + return workingSet; +} + +macos.display.filterOutUnusableForDesktopGuiDisplayModes = function(displayModes) { + // remove all display modes where ".isUsableForDesktopGui" is set to false + + return displayModes.slice().filter(function(a) { + return a.isUsableForDesktopGui == true; + }); +} + +macos.display.filterOutNonRetinaScaleAlternativeDisplayModes = function(displayModes) { + // remove all non-retina display mode options which have a corresponding retina display mode option + + // copy the whole displayModes array into a working set + let workingSet = displayModes.slice(); + + let iFirstElement = 0 + while (iFirstElement < workingSet.length) { + let firstElementWasRemoved = false; + + let iSecondElement = iFirstElement + 1; + while (iSecondElement < workingSet.length) { + let secondElementWasRemoved = false; + + if (workingSet[iFirstElement].widthInVirtualPixels == workingSet[iSecondElement].widthInVirtualPixels && + workingSet[iFirstElement].heightInVirtualPixels == workingSet[iSecondElement].heightInVirtualPixels) { + // the two entries are the same virtual resolution (i.e. what the user sees in the resolution options) + + // remove the option which is non-retina + if (workingSet[iFirstElement].widthInPixels > workingSet[iSecondElement].widthInPixels && + workingSet[iFirstElement].heightInPixels > workingSet[iSecondElement].heightInPixels) { + // the first entry is the higher-resolution entry; remove the second element + workingSet.splice(iSecondElement, 1); + + // mark "secondElementWasRemoved" as true so it won't be incremented at the end of the current loop iteration + secondElementWasRemoved = true; + } else { + // otherwise, remove the first element + workingSet.splice(iFirstElement, 1); + + // mark "firstElementWasRemoved" as true so that it won't be incremented at the end fo the current (outer) loop iteration + firstElementWasRemoved = true; + // break out the of the loop so that we start re-seeking at the next "first element" position + break; + } + } + + if (secondElementWasRemoved == false) { + // NOTE: we only increment iSecondElement if the second element was NOT removed; otherwise we need to continue processing at the same position + iSecondElement += 1; + } + } + + if (firstElementWasRemoved == false) { + // NOTE: we only increment iFirstElement if the first element was NOT removed; otherwise we need to continue processing at the same position + iFirstElement += 1; + } + } + + // return the remaining ("non-retina mode option removed where matching retina mode was present") working set entries + return workingSet; +} + +macos.display.getHighestResolutionNativeDisplayMode = function(displayModes) { + // get the display mode with the highest resolution which is based on a native display mode + // NOTE: this distinction basically means we are filtering out any display modes which use a fake "widthInPixels/heightInPixels" pair with + // a virtual resolution which is less than the actual resolution; on macOS these are the "more space" options which shrink the + // "Retina" display to a less-than-pixel-doubled virtual resoluiton + + var highestResolutionNativeDisplayMode = null; + + // copy the whole displayModes array into a working set + let workingSet = displayModes.slice(); + + workingSet.forEach(function(element) { + if (element.widthInPixels == element.widthInVirtualPixels && + element.heightInPixels == element.heightInVirtualPixels) { + // the entry is a native resolution + if (highestResolutionNativeDisplayMode == null) { + highestResolutionNativeDisplayMode = element + } else { + // NOTE: we are calculating a resolution as "higher" than the previous ones if both its width and height are greater than + // or equal; we use >= rather than > in case two resolutions have the same width but different heights, etc. + // NOTE: this is an imperfect calculation (if the display offers non-native aspect ratios), but it is practical; we may + // choose to revise this (and the sort routines) using a common "isHigherResolution" function in the future + if (element.widthInPixels >= highestResolutionNativeDisplayMode.widthInPixels && + element.heightInVirtualPixels >= highestResolutionNativeDisplayMode.heightInPixels) { + // + highestResolutionNativeDisplayMode = element + } + } + } + }); + + return highestResolutionNativeDisplayMode; +} + +macos.display.filterOutRetinaNativeScaleDisplayModes = function(displayModes) { + // remove all retina display mode options that are "native resolution" (versus the ones that use pixel doubling) + + // copy the whole displayModes array into a working set + let workingSet = displayModes.slice(); + + let iFirstElement = 0; + while (iFirstElement < workingSet.length) { + let firstElementWasRemoved = false; + + let iSecondElement = iFirstElement + 1; + while (iSecondElement < workingSet.length) { + let secondElementWasRemoved = false; + + if (workingSet[iFirstElement].widthInPixels == workingSet[iSecondElement].widthInPixels && + workingSet[iFirstElement].heightInPixels == workingSet[iSecondElement].heightInPixels) { + // the two entries are the same physical resolution + + // remove the option which is retina's "native" resolution (because the dots would be TOO small) + if (workingSet[iFirstElement].widthInVirtualPixels < workingSet[iSecondElement].widthInVirtualPixels && + workingSet[iFirstElement].heightInVirtualPixels < workingSet[iSecondElement].heightInVirtualPixels) { + // the first entry is the non-native (pixel doubled) entry; remove the second element + workingSet.splice(iSecondElement, 1); + + // mark "secondElementWasRemoved" as true so it won't be incremented at the end of the current loop iteration + secondElementWasRemoved = true; + } else { + // otherwise, remove the first element + workingSet.splice(iFirstElement, 1); + + // mark "firstElementWasRemoved" as true so that it won't be incremented at the end fo the current (outer) loop iteration + firstElementWasRemoved = true; + // break out the of the loop so that we start re-seeking at the next "first element" position + break; + } + } + + if (secondElementWasRemoved == false) { + // NOTE: we only increment iSecondElement if the second element was NOT removed; otherwise we need to continue processing at the same position + iSecondElement += 1; + } + } + + if (firstElementWasRemoved == false) { + // NOTE: we only increment iFirstElement if the first element was NOT removed; otherwise we need to continue processing at the same position + iFirstElement += 1; + } + } + + // return the remaining ("non-retina mode option removed where matching retina mode was present") working set entries + return workingSet; +} + +macos.display.displayModesAreEqual = function(displayMode1, displayMode2) { + if (displayMode1.ioDisplayModeId != displayMode2.ioDisplayModeId || + displayMode1.widthInPixels != displayMode2.widthInPixels || + displayMode1.heightInPixels != displayMode2.heightInPixels || + displayMode1.widthInVirtualPixels != displayMode2.widthInVirtualPixels || + displayMode1.heightInVirtualPixels != displayMode2.heightInVirtualPixels || + displayMode1.refreshRateInHertz != displayMode2.refreshRateInHertz) { + // + return false; + } + + // otherwise, the arguments are equal + return true; +} \ No newline at end of file diff --git a/gpii/node_modules/displaySettingsHandler/src/dpiMacOS.js b/gpii/node_modules/displaySettingsHandler/src/dpiMacOS.js new file mode 100644 index 0000000..2d8525c --- /dev/null +++ b/gpii/node_modules/displaySettingsHandler/src/dpiMacOS.js @@ -0,0 +1,239 @@ +/* + * DPI support for macOS + * + * Copyright 2016-2020 Raising the Floor -- US Inc. All rights reserved. + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + */ + +"use strict"; + +/* The "DpiOffset" scheme here is modeled after Windows 10. + * + * Imagine that the screen resolution options on macOS are a "zoom level", like how Windows has zoom levels: + * [ 100, 125, 150, 175, 200, 225, 250, 300, 350, 400, 450, 500 ] + * + * In this scenario, as an example, if the "default" DPI's % is 175%, then a dpiOffset of 0 is 175, 2 is 225, and -1 is 150. + */ + +var fluid = require("gpii-universal"); + +var gpii = fluid.registerNamespace("gpii"); +var macos = fluid.registerNamespace("gpii.macos"); + +/** + * Sets the DPI scale of the primary display, by specifying the number of values away from an from the recommended + * setting. + * + * @param {Number} offset The offset from the recommended setting. + * @return {DpiConfig} The newly configured, actual, and minimum/maximum DPI offsets (see getScreenDpi). + */ +macos.display.setScreenDpi = function (offset) { + var allScreenDpisWithDisplayModes = macos.display.getAllScreenDpisWithDisplayModes(/* adapter */); + // sanity check + if (!allScreenDpisWithDisplayModes || allScreenDpisWithDisplayModes.dpiDisplayModeMap.length == 0) { + // NOTE: this code should never be executed + return; + } + + // capture the DisplayMode associated with this offset + var dpiOffsetAsString = offset + ""; + var newDisplayMode = allScreenDpisWithDisplayModes.dpiDisplayModeMap[dpiOffsetAsString]; + if(!newDisplayMode) { + fluid.fail("Screen DPI offset " + dpiOffsetAsString + " is invalid; could not set Screen DPI."); + return; + } + + gpii.macos.native.setCurrentDisplayMode(newDisplayMode); + + // Return the new configuration. + return macos.display.getScreenDpi(/* adapter */); +}; + +/** + * Get a list of all bounded screen DPI values (with associated screen modes) + * + * @return {DpiConfig} The full range of available screen DPIs (with associated screen modes) + */ +macos.display.getBoundedScreenDpisWithDisplayModes = function (adapter) { + // NOTE: we ignore the adapter parameter (as we intentionally always reference the main display) + + // NOTE: if we want to change the allowed range of "screen DPIs" in the future (via filtering or otherwise): + // this function's logic should be used by all other DPI mapping functions + // NOTE: it is _not safe_ to remove DPIs in the middle of the range (i.e. only ones at the top or bottom) due to the fact that DPI + // offsets are tracked in absolute values (related to the default display resolution); any bounding must be simply "top and bottom" + + // for bounds purposes, establish a maximum number of Dpi Ssettings + var maximumNumberOfDpiSettings = 8; + + var allScreenDpisWithDisplayModes = macos.display.getAllScreenDpisWithDisplayModes(adapter); + // sanity check + if (!allScreenDpisWithDisplayModes || allScreenDpisWithDisplayModes.dpiDisplayModeMap.length == 0) { + // NOTE: this code should never be executed + return; + } + + var minimumDpiOffset = allScreenDpisWithDisplayModes.minimum; + var maximumDpiOffset = allScreenDpisWithDisplayModes.maximum; + + // bounds enforcement: our DPI range cannot exceed eight values, so lower the maximum DPI as necessary + // NOTE: this is not the same behavior as on Windows; on Windows the "minimumDpiOffset" must be >= -3 (and "maximumDpiOffset" is not well-defined) + // NOTE: minimumDpiOffset is always <= 0 + if (maximumDpiOffset - minimumDpiOffset > maximumNumberOfDpiSettings) { + maximumDpiOffset = maximumNumberOfDpiSettings + minimumDpiOffset - 1; + } + + var result = { + minimum: minimumDpiOffset, + maximum: maximumDpiOffset, + dpiDisplayModeMap: {} + }; + + // add each available display mode to the map + for (var dpiOffset = minimumDpiOffset; dpiOffset <= maximumDpiOffset; dpiOffset++) { + var dpiOffsetAsString = dpiOffset + ""; + result.dpiDisplayModeMap[dpiOffsetAsString] = allScreenDpisWithDisplayModes.dpiDisplayModeMap[dpiOffsetAsString]; + } + + return result; +} + +/** + * Get a list of all screen DPI values (with associated screen modes)...including ones that are "out of bounds" + * + * @return {DpiConfig} The full range of available screen DPIs (with associated screen modes) + */ +macos.display.getAllScreenDpisWithDisplayModes = function (adapter) { + // NOTE: we ignore the adapter parameter (as we intentionally always reference the main display) + + // NOTE: if we want to change the allowed range of "screen DPIs" in the future (via filtering or otherwise): + // this function's logic should be used by all other DPI mapping functions + + // retrieve a filtered list of all available screen modes + var availableDisplayModes = macos.display.getAvailableDisplayModes(); + // sanity check + if (!availableDisplayModes || availableDisplayModes.length == 0) { + // NOTE: this code should never be executed + return; + } + + // sort the list of available display modes in reverse + availableDisplayModes = macos.display.sortDisplayModesByResolutionAscending(availableDisplayModes).reverse(); + + // retrieve the default display mode + var defaultDisplayMode = macos.display.getDefaultDisplayMode(); + // calculate the index of the default display mode + var indexOfDefaultDisplayMode = null; + for(var index = 0; index < availableDisplayModes.length; index++) { + if (macos.display.displayModesAreEqual(availableDisplayModes[index], defaultDisplayMode)) { + indexOfDefaultDisplayMode = index; + break; + } + } + // fail-safe: if index was not found, then assume index[0] + if (indexOfDefaultDisplayMode == null) { + // NOTE: this code should never be executed + indexOfDefaultDisplayMode = availableDisplayModes[availableDisplayModes.length - 1]; + } + + // calculate the minimum, maximum, current and default "DPI" values as offsets from "default" + var minimumDpiOffset = 0 - indexOfDefaultDisplayMode; + var maximumDpiOffset = availableDisplayModes.length - 1 - indexOfDefaultDisplayMode; + + var result = { + minimum: minimumDpiOffset, + maximum: maximumDpiOffset, + dpiDisplayModeMap: {} + }; + + // add each available display mode to the map + for (var index = 0; index < availableDisplayModes.length; index += 1) { + var dpiAsString = (index - indexOfDefaultDisplayMode) + ""; + result.dpiDisplayModeMap[dpiAsString] = availableDisplayModes[index]; + } + + return result; +} + +/** + * Get the configured, maximum, and actual DPI values of the primary display. + * + * The value is the number of "notches" away from the recommended setting of the display. + * + * The configured scale is what DPI should be if the resolution is high enough, the maximum scale is the highest DPI + * scale that the current screen resolution supports. The actual scale (what the user is looking at) is the configured + * scale, capped at the maximum. + * + * @param {Object} adapter [optional] The adapter id pair (from getAdapter()). + * @return {DpiConfig} The newly configured, actual, and minimum/maximum DPI offsets + */ +macos.display.getScreenDpi = function (adapter) { + // NOTE: we ignore the adapter parameter (as we intentionally always reference the main display) + + // retrieve a list of all allowable screenDpis with their associated DisplayModes. + var allScreenDpisWithDisplayModes = macos.display.getAllScreenDpisWithDisplayModes(); + // sanity check + if (!allScreenDpisWithDisplayModes || allScreenDpisWithDisplayModes.dpiDisplayModeMap.length == 0) { + // NOTE: this code should never be executed + return; + } + + // retrieve a list of all bounded screenDpis with their associated DisplayModes. [This list is a subset of the "all" list] + var boundedScreenDpisWithDisplayModes = macos.display.getBoundedScreenDpisWithDisplayModes(); + // sanity check + if (!boundedScreenDpisWithDisplayModes || boundedScreenDpisWithDisplayModes.dpiDisplayModeMap.length == 0) { + // NOTE: this code should never be executed + return; + } + + // retrieve the current display mode + var currentDisplayMode = macos.native.getCurrentDisplayMode(); + // + // calculate the dpiOffset of the current display mode + var dpiOfCurrentDisplayMode = null; + for(var dpiOffset = allScreenDpisWithDisplayModes.minimum; dpiOffset <= allScreenDpisWithDisplayModes.maximum; dpiOffset++) { + var dpiOffsetAsString = dpiOffset + ""; + if (macos.display.displayModesAreEqual(allScreenDpisWithDisplayModes.dpiDisplayModeMap[dpiOffsetAsString], currentDisplayMode)) { + dpiOfCurrentDisplayMode = dpiOffset; + break; + } + } + + // calculate the minimum, maximum, current and default "DPI" values as offsets from "default" + var minimumDpiOffset = boundedScreenDpisWithDisplayModes.minimum; + // var defaultDpiOffset = 0; // default DPI offset is always zero + var currentDpiOffset = dpiOfCurrentDisplayMode; + var maximumDpiOffset = boundedScreenDpisWithDisplayModes.maximum; + + return { + configured: currentDpiOffset, // current resolution // NOTE: we do not use 'Math.max(currentDpiOffset, minimumDpiOffset)' here so that we are logically compatible with the Windows implementation + minimum: minimumDpiOffset, + maximum: maximumDpiOffset, + actual: Math.min(currentDpiOffset, maximumDpiOffset) // current resolution (bound by maximum DPI setting) + }; +}; + +// /** +// * DPI Configuration. +// * @typedef {Object} DpiConfig +// * @property {Number} configured The desired DPI setting. +// * @property {Number} minimum The minimum available DPI setting. +// * @property {Number} maximum The maximum available DPI setting. +// * @property {Number} actual The actual DPI setting - the same as configured, but clamped to minimum and maximum. +// */ + + +fluid.defaults("gpii.macos.display.getScreenDpi", { + gradeNames: "fluid.function", + argumentMap: {} +}); diff --git a/gpii/node_modules/gpii-localisation/index.js b/gpii/node_modules/gpii-localisation/index.js new file mode 100644 index 0000000..c6daf26 --- /dev/null +++ b/gpii/node_modules/gpii-localisation/index.js @@ -0,0 +1,3 @@ +"use strict"; + +require("./src/language.js"); diff --git a/gpii/node_modules/gpii-localisation/src/language.js b/gpii/node_modules/gpii-localisation/src/language.js new file mode 100644 index 0000000..b1d1894 --- /dev/null +++ b/gpii/node_modules/gpii-localisation/src/language.js @@ -0,0 +1,310 @@ +/* + * macOS Language + * + * Copyright 2020 Raising the Floor -- US Inc. All rights reserved. + * Copyright 2018 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + */ + +"use strict"; + +var fluid = require("gpii-universal"); + +var gpii = fluid.registerNamespace("gpii"); +var macos = fluid.registerNamespace("gpii.macos"); + +fluid.registerNamespace("gpii.macos.language"); + +fluid.defaults("gpii.macos.language", { + gradeNames: ["fluid.component", "fluid.modelComponent"], + invokers: { + getInstalledLanguages: { + funcName: "gpii.macos.language.getInstalled", + args: [ "{that}" ] + }, + getLanguageNames: { + funcName: "gpii.macos.language.getLanguageNames", + args: [ "{that}", "{arguments}.0" ] + }, + getDisplayLanguage: { + funcName: "gpii.macos.language.getDisplayLanguage" + }, +// startMessages: "{gpii.macos.messages}.start({that})", +// stopMessages: "{gpii.macos.messages}.stop({that})" + }, + listeners: { + "onCreate.update": "{that}.getInstalledLanguages()", +// "onCreate.messages": "{that}.startMessages()", +// "{gpii.macos.messages}.events.onMessage": { +// funcName: "gpii.macos.language.windowMessage", +// // that, hwnd, msg, wParam, lParam +// args: [ "{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "{arguments}.3" ] +// } + }, + // The model gets updated whenever getInstalledLanguages is called. + model: { + /** @type {Map} */ + installedLanguages: null, + /** Currently configured display language */ + configuredLanguage: null + }, + members: { + /** code=>name map of language names in english */ + englishNames: {} + } +}); + +// /** +// * Language names. +// * @typedef {Object} LanguageNames +// * @property {String} english The language name in English. +// * @property {String} local The language name (and country), in the current display language. +// * @property {String} native The language name (and country), in its native language. +// */ + +// /** +// * An installed language +// * @typedef {LanguageNames} InstalledLanguage +// * @property {String} code The language code, `lang[-COUNTRY]`. +// * @property {Boolean} current true if this is the current display language. +// */ + +/** + * Gets the display languages that are installed on the system, and updates the model. + * + * These are listed in HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\MUI\UILanguages + * + * @param {Component} that The gpii.macos.language instance. + * @return {Promise>} A promise, resolving with either the language names if the list has + * changed, or null if there was no change. + */ +gpii.macos.language.getInstalled = function (that) { + var langCodes = gpii.macos.native.getInstalledAppleLanguages(); + + // NOTE: on Windows, we call "gpii.windows.fixCodeCase" to ensure that the language code portion is lowercase and the country code portion + // is uppercase; on macOS this should not be necessary so we neither include nor call that function to postprocess "langCodes" + + var current = gpii.macos.language.getDisplayLanguage(); + + // Because this function gets called in the off-chance that the language has changed (maybe multiple times during + // key-in), and getting the translated languages requires a new process, perform some checks upfront. + var changed = current !== fluid.get(that.model, "configuredLanguage"); + that.applier.change("configuredLanguage", current); + + if (!changed) { + var knownLanguages = Object.keys(fluid.get(that.model, "installedLanguages") || {}); + changed = langCodes.length !== knownLanguages.length; + if (!changed) { + changed = !knownLanguages.every(function (elem) { + return langCodes.includes(elem); + }); + } + } + + var promise; + if (changed) { + // Update the language names only if required. + promise = that.getLanguageNames(langCodes).then(function (languages) { + that.applier.change("installedLanguages", languages); + }); + } else { + promise = fluid.promise().resolve(); + } + + return promise; +}; + +/** + * Gets the language names of the given languages, identified by their IETF language codes (`en`, `es-MX`). + * + * It returns an object containing the name in English, the current display language, and native language. + * + * If only the language identifier (first 2 characters) are passed, then the language name is returned. + * If the country code is also given, then the country is also returned in brackets: + * - If the country is code is unknown, or the country-specific language isn't recognised, then the language code is + * used instead of the country. + * - If the language is only spoken in a single country (eg, Bulgarian), then the country is not returned, unless a + * different country was passed (eg, bg-GB). + * If the language is unknown, then an empty string is used. If the language code is invalid, null each field is null. + * + * Examples: + *``` + * "es-MX" => { english: "Spanish (Mexico)", local: "Spanish (Mexico)", native: "Español (México)" } + * "en" => { english: "English", local: "English", native: "English" } + * "en-GB" => { "english": "English (United Kingdom)", "local": "English (United Kingdom)", "native": "English (United Kingdom)" } + *``` + * When the current display language is French: + * ``` + * "nl-NL" => { english: "Dutch (Netherlands)", local: "Néerlandais (Pays-Bas)", native: "Nederlands (Nederland)" } + * ``` + * @param {Component} that The gpii.macos.language instance. + * @param {String|Array} langCodes The language code(s), in the form of `lang[-COUNTRY]`. + * @return {Promise} A promise, resolving with the language names. + */ +gpii.macos.language.getLanguageNames = function (that, langCodes) { + var promise = fluid.promise(); + + var currentLangCode = gpii.macos.language.getDisplayLanguage(); + + // resolve each langCode to its english, local and native names + var languages = {}; + langCodes.forEach(langCode => { + let languageNameInEnglishLocale = gpii.macos.native.getLanguageName(langCode, "en-US"); + let languageNameInCurrentLocale = gpii.macos.native.getLanguageName(langCode, currentLangCode); + let languageNameInNativeLocale = gpii.macos.native.getLanguageName(langCode, langCode); + // + let countryNameInEnglishLocale = gpii.macos.native.getCountryName(langCode, "en-US"); + let countryNameInCurrentLocale = gpii.macos.native.getCountryName(langCode, currentLangCode); + let countryNameInNativeLocale = gpii.macos.native.getCountryName(langCode, langCode); + // + let languageAndCountryNameInEnglishLocale = languageNameInEnglishLocale + " (" + countryNameInEnglishLocale + ")" + let languageAndCountryNameInCurrentLocale = languageNameInCurrentLocale + " (" + countryNameInCurrentLocale + ")" + let languageAndCountryNameInNativeLocale = languageNameInNativeLocale + " (" + countryNameInNativeLocale + ")" + + languages[langCode] = { + code: langCode, + english: languageAndCountryNameInEnglishLocale, + local: languageAndCountryNameInCurrentLocale, + native: languageAndCountryNameInNativeLocale + }; + + if (langCode == currentLangCode) { + languages[langCode].currentLangCode = true; + }; + + // Save the English name + that.englishNames[langCode] = languageAndCountryNameInEnglishLocale; + }); + + promise.resolve(languages); + + return promise; +}; + +// /** +// * Called when an event has been received by the message window. +// * +// * When a relevant message is received, the installed languages model will be updated. The current language can't be +// * changed during a session, however the drop-down list in control panel still broadcasts WM_SETTINGCHANGE. +// * +// * @param {Component} that The gpii.macos.language component. +// * @param {Number} hwnd The window handle of the message window. +// * @param {Number} msg The message identifier. +// * #param {Number} wParam Message specific data. (unused) +// * #param {Buffer} lParam Additional message specific data. (unused) +// */ +// gpii.macos.language.windowMessage = function (that, hwnd, msg) { +// if (msg === gpii.macos.API_constants.WM_SETTINGCHANGE +// || msg === gpii.macos.API_constants.WM_INPUTLANGCHANGE) { +// that.getInstalledLanguages(); +// } +// }; + +/** + * Gets the currently configured display language. + * + * This is the language which new processes will use. + * + * @return {String} The language code of the currently configured display language. + */ +gpii.macos.language.getDisplayLanguage = function () { + var langCode = macos.native.getPrimaryInstalledAppleLanguage(); + + // NOTE: on Windows, we call "gpii.windows.fixCodeCase" to ensure that the language code portion is lowercase and the country code portion + // is uppercase; on macOS this should not be necessary so we neither include nor call that function to postprocess "langCode" + + return langCode; +}; + +/** + * Updates the macOS display language, by restarting Finder and other system launch daemons and agents if the language has + * changed since the last time this was called. + * + * @param {String} currentLanguage [optional] The current (new) language. + * @return {Promise|undefined} Resolves when all daemons/agents have restarted, or null if the language has not changed. + */ +gpii.macos.updateLanguage = function (currentLanguage) { + var lang = currentLanguage || gpii.macos.language.getDisplayLanguage(); + + if (gpii.macos.updateLanguage.lastLanguage !== lang) { + // Update the state. + var languageInstances = fluid.queryIoCSelector(fluid.rootComponent, "gpii.macos.language"); + fluid.each(languageInstances, gpii.macos.language.getInstalled); + + var lastLanguage = gpii.macos.updateLanguage.lastLanguage; + gpii.macos.updateLanguage.lastLanguage = lang; + + // update the system language (i.e. primary preferred language) + // NOTE: in the future, if we have any failure scenarios for settting the primary language, we should return null to indicate failure + macos.native.setPrimaryInstalledAppleLanguage(lang); + + // restart all appropriate launch daemons and agents + // NOTE: since restartService is effectively asynchronous, we do not chain its execution via promises + var restartServices = function() { + var serviceNames = macos.native.getAllLaunchDaemonsAndAgentsAsServiceNames(); + macos.native.restartServicesViaLaunchctl(serviceNames); + }; + restartServices(); + + var promise = fluid.promise(); + var result = { + newValue: lang, + oldValue: lastLanguage + } + // NOTE: as of 2020-02-22, the Windows setting handler does not return the new language name; we may want to consider resolving with no argument instead + promise.resolve(result); + return promise; + } +}; + +// NOTE: in the future, we may want to consider using fluid.defaults to abstractly call our updateLanguage function instead (or use .setImpl, etc. for the language handler) +gpii.macos.updateLanguage.set = function (args) { + var settingHandlerName = 'com.apple.macos.language'; + + // verify that our settings handler's name was passed in correctly (and that it contains setttings) + var primaryArg = args[settingHandlerName]; + if (!primaryArg || !primaryArg[0] || !primaryArg[0].settings) { + // if our settings handler payload is missing or invalid, return null; this should not happen. + return null; + } + + var currentLanguage = primaryArg[0].settings.currentLanguage; + if (!currentLanguage) { + // if no current language was provided, return null; this should not happen. + return null; + } + + var result = gpii.macos.updateLanguage(currentLanguage); + + if (result.value == null) { + return null; + } else { + // populate our result with a successful response (according to the standard Fluid response format) + // NOTE: as of 2020-02-22, the Windows setting handler does not return the newValue or oldValue in currentLanguage (and technically we could probably just omit "settings" completely) + // NOTE: in the future, we may want to consider using fluid.set(...), etc. to populate this result (i.e. follow a same pattern as in nativeSettingsHandler.js) + var promise = fluid.promise(); + let wrappedResult = {}; + wrappedResult[settingHandlerName] = [ + { + settings: { + curentLanguage: result.value + } + } + ]; + promise.resolve(wrappedResult); + return promise; + } +} + +gpii.macos.updateLanguage.lastLanguage = gpii.macos.language.getDisplayLanguage(); diff --git a/gpii/node_modules/nativeSettingsHandler/index.js b/gpii/node_modules/nativeSettingsHandler/index.js new file mode 100644 index 0000000..e6a0cbe --- /dev/null +++ b/gpii/node_modules/nativeSettingsHandler/index.js @@ -0,0 +1,3 @@ +"use strict"; + +require("./src/nativeSettingsHandler.js"); diff --git a/gpii/node_modules/nativeSettingsHandler/src/nativeSettingsHandler.js b/gpii/node_modules/nativeSettingsHandler/src/nativeSettingsHandler.js new file mode 100644 index 0000000..1715d7f --- /dev/null +++ b/gpii/node_modules/nativeSettingsHandler/src/nativeSettingsHandler.js @@ -0,0 +1,285 @@ +/* + * macOS Native Settings Handler + * + * Copyright 2020 Raising the Floor -- US Inc. All rights reserved. + * Copyright 2018 Raising the Floor - International + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + */ + +"use strict"; + +var fluid = require("gpii-universal"), + gpii = fluid.registerNamespace("gpii"), + macos = fluid.registerNamespace("gpii.macos"); + +fluid.registerNamespace("gpii.macos.nativeSettingsHandler"); + +/** + * Function that handles calling the native executable for volume control. + * + * @param {String} mode The operation mode, could be either "Set" or "Get". + * @param {Number} [num] The value to set as the current system volume. Range is normalized from 0 to 1. + * @return {Promise} A promise that resolves on success or holds a object with the error information. + */ +macos.nativeSettingsHandler.VolumeHandler = function (mode, num) { + var promise = fluid.promise(); + + try { + var value; + + if (mode === "Get") { + value = macos.native.getAudioVolume(); + // + // patch for compatibility with Windows (since Windows does not distinguish between "mute" and "volume level") + var muteState = macos.native.getAudioMuteState(); + if (muteState === true) { + value = 0.0; + } + } else { + // out of an abundance of caution, establish floor and ceiling values on "num" + if (num < 0.0) { + num = 0.0; + } else if (num > 1.0) { + num = 1.0; + } + + // NOTE: since Windows does not distinguish between "mute" and "volume level", we treat a volume of 0.0 as "mute" for compatibility and best user-experience + if (num === 0.0) { + macos.native.setAudioMuteState(true); + } else { + // NOTE: there is an edge case of moving to volume "0.0" to indicate an actual volume of 0.0 (instead of "mute"); at this time gpii-app does not provide us enough information to distinguish this condition so we are attempting to provide best-effort here + macos.native.setAudioVolume(num); + // + // patch for compatibility with Windows (since Windows does not distinguish between "mute" and "volume level")...we turn mute off if the user has set a non-zero volume level + macos.native.setAudioMuteState(false); + } + // NOTE: for compatibility with the reference Windows implementation, we return 0.0 here (instead of re-requesting the volume via macos.native.getAudioVolume()) + // option 1 (returning 0.0 to match the Windows implementation) + value = 0.0; + // option 2 (returning the actual new volume value) + // value = macos.native.getAudioVolume(); + } + + promise.resolve(value); + } catch (err) { + promise.reject(err); + } + + return promise; +}; + +/** + * Gets the current system volume + * + * @return {Promise} A promise that resolves on success or holds a object with the error information. + */ +macos.nativeSettingsHandler.GetVolume = function () { + return macos.nativeSettingsHandler.VolumeHandler("Get"); +}; + +/** + * Sets the current system volume + * + * @param {Number} num The value to set as the current system volume. Range is normalized from 0 to 1. + * @return {Promise} A promise that resolves on success or holds a object with the error information. + */ +macos.nativeSettingsHandler.SetVolume = function (num) { + return macos.nativeSettingsHandler.VolumeHandler("Set", num); +}; + +macos.nativeSettingsHandler.functions = { + Volume: { + set: macos.nativeSettingsHandler.SetVolume, + get: macos.nativeSettingsHandler.GetVolume + } +}; + +/** + * Helper function that converts a error to string, so the rejection from the + * settings handler only holds a message. + * + * @param {Object} err The error to be stringified. + * @return {String} The error message. + */ +macos.nativeSettingsHandler.errorInfo = function (err) { + return "With error - '" + JSON.stringify(err) + "'"; +}; + +/** + * Helper function for creating a custom message for error rejection. + * + * @param {Promise} promise The promise to be rejected. + * @param {String} settingKey The name of the requested setting for which an operation failed. + * @param {Object} err The error object. + * @param {String} message A message with information about the operation. + */ +macos.nativeSettingsHandler.reject = function (promise, settingKey, err, message) { + var operation = message + settingKey + "' failed. "; + var errMsg = "nativeSettingsHandler" + operation + macos.nativeSettingsHandler.errorInfo(err); + + promise.reject(errMsg); +}; + +/** + * Setter for the nativeSettingsHandler. + * + * The payload should have the following format: + * + * { + * "functionName": "FunctionName", + * "setParam": val + * } + * + * The first two items: + * - function: Function name for the function to be used to set/get the value. + * - setParam: Parameter to be passed to the 'set' function. + * + * @param {Object} payload The payload. + * @return {Promise} Resolves with the response. + */ +macos.nativeSettingsHandler.setImpl = function (payload) { + var pRes = fluid.promise(); + + var fnName = fluid.get(payload, "options.functionName"); + var setFn = fluid.get(macos.nativeSettingsHandler.functions, fnName + ".set"); + var getFn = fluid.get(macos.nativeSettingsHandler.functions, fnName + ".get"); + var fnNotDef = [": Failed due to '", " " + fnName + "' function not being defined."]; + + if (setFn === undefined) { + pRes.reject("nativeSettingsHandler" + fnNotDef[0] + "set" + fnNotDef[1]); + } else if (getFn === undefined) { + pRes.reject("nativeSettingsHandler" + fnNotDef[0] + "get" + fnNotDef[1]); + } else { + var results = {}; + var settingsArray = fluid.makeArray(payload.settings); + + if (settingsArray.length === 0) { + pRes.reject("nativeSettingsHandler: Failed due to empty payload."); + } else { + var uniqueSetting = settingsArray[0]; + // Get the payload value to set. + var settingKey = fluid.keys(uniqueSetting)[0]; + var valueToSet = fluid.values(uniqueSetting)[0].value; + + // Create object for storing setting results. + results[settingKey] = {}; + // Get the current values for the setting. + var pGetOld = getFn(); + + pGetOld.then( + function (oldVal) { + // Store the current value in results. + fluid.set(results[settingKey], "oldValue.value", oldVal); + + var pSet = setFn(valueToSet); + + pSet.then( + function () { + var pGetNew = getFn(); + + pGetNew.then( + function (newVal) { + fluid.set(results[settingKey], "newValue.value", newVal); + + pRes.resolve(results); + }, + function (err) { + macos.nativeSettingsHandler.reject(pRes, settingKey, err, ": Getting new value for setting '"); + } + ); + }, + function (err) { + macos.nativeSettingsHandler.reject(pRes, settingKey, err, ": Setting new value for setting '"); + } + ); + }, + function (err) { + macos.nativeSettingsHandler.reject(pRes, settingKey, err, ": Getting current value for setting '"); + } + ); + } + } + + return pRes; +}; + +/** + * Getter for the nativeSettingsHandler. + * + * @param {Object} payload The payload. + * @return {Promise} Resolves with the response. + */ +macos.nativeSettingsHandler.getImpl = function (payload) { + var pRes = fluid.promise(); + + var fnName = fluid.get(payload, "options.functionName"); + var getFn = fluid.get(macos.nativeSettingsHandler.functions, fnName + ".get"); + var fnNotDef = [": Failed due to '", " " + fnName + "' function not being defined."]; + + if (getFn === undefined) { + pRes.reject("nativeSettingsHandler" + fnNotDef[0] + "get" + fnNotDef[1]); + } else { + var settingsArray = fluid.makeArray(payload.settings); + + if (settingsArray.length === 0) { + pRes.reject("nativeSettingsHandler: Failed due to empty payload."); + } else { + var uniqueSetting = settingsArray[0]; + var results = {}; + + // Get payload setting and prepare results for returning it + var settingKey = fluid.keys(uniqueSetting)[0]; + results[settingKey] = {}; + + var pGetRes = getFn(); + + pGetRes.then( + function (curVal) { + results[settingKey].value = curVal; + + pRes.resolve(results); + }, + function (err) { + macos.nativeSettingsHandler.reject(pRes, settingKey, err, ": Getting current value for setting '"); + } + ); + } + } + + return pRes; +}; + +/** + * Invoke the settings handler. + * + * @param {Object} payload The payload + * @return {Promise} Resolves with the response. + */ +macos.nativeSettingsHandler.get = function (payload) { + return gpii.settingsHandlers.invokeSettingsHandler(macos.nativeSettingsHandler.getImpl, payload); +}; + +/** + * Invoke the settings handler. + * + * @param {Object} payload The payload + * @return {Promise} Resolves with the response. + */ +macos.nativeSettingsHandler.set = function (payload) { + return gpii.settingsHandlers.invokeSettingsHandler(macos.nativeSettingsHandler.setImpl, payload); +}; diff --git a/index.js b/index.js new file mode 100644 index 0000000..570218f --- /dev/null +++ b/index.js @@ -0,0 +1,38 @@ +/* + * GPII Universal Personalization Framework GPII macOS Index + * + * Copyright 2020 Raising the Floor -- US Inc. All rights reserved. + * Copyright 2014 Lucendo Development Ltd. + * + * Licensed under the New BSD license. You may not use this file except in + * compliance with this License. + * + * You may obtain a copy of the License at + * https://github.com/GPII/universal/blob/master/LICENSE.txt + * + * The R&D leading to these results received funding from the + * Department of Education - Grant H421A150005 (GPII-APCP). However, + * these results do not necessarily represent the policy of the + * Department of Education, and you should not assume endorsement by the + * Federal Government. + * + * The research leading to these results has received funding from the European Union's + * Seventh Framework Programme (FP7/2007-2013) + * under grant agreement no. 289016. + */ + +"use strict"; + +var fluid = require("gpii-universal"); + +var gpii = fluid.registerNamespace("gpii"); +var macos = fluid.registerNamespace("gpii.macos"); + +macos.native = require('./build/MorphicMacOS.node'); + +require("./gpii/node_modules/MacOSUtilities/MacOSUtilities.js"); +require("./gpii/node_modules/displaySettingsHandler"); +require("./gpii/node_modules/nativeSettingsHandler"); +require("./gpii/node_modules/gpii-localisation"); + +module.exports = fluid; diff --git a/package.json b/package.json new file mode 100644 index 0000000..9b51535 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "gpii-macos", + "description": "Components of the GPII personalization infrastructure for use on Apple's macOS", + "version": "0.3.0", + "author": "Raising the Floor -- US Inc.", + "bugs": { + "url": "http://issues.gpii.net/browse/GPII" + }, + "homepage": "http://gpii.net/", + "scripts": { + "build": "xcodebuild clean -configuration Release && xcodebuild build -configuration Release && mv build/Release/libMorphicMacOS.dylib build/MorphicMacOS.node", + "build:dev": "xcodebuild clean -configuration Release && xcodebuild build -configuration Debug && mv build/Debug/libMorphicMacOS.dylib build/MorphicMacOS.node", + "postinstall": "npm run build", + "test": "exit 0" + }, + "dependencies": { + "gpii-universal": "github:christopher-rtf/universal#9a15229e" + }, + "os": [ + "darwin" + ], + "license": "BSD-3-Clause", + "keywords": [ + "gpii" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/GPII/gpii-macos.git" + }, + "main": "./gpii.js", + "engines": { + "node": ">=10.16.3" + } +}