├── .gitignore ├── Examples └── Scriptable │ ├── Scriptable.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── Scriptable │ ├── AppDelegate.swift │ ├── AppDocument+Scripting.swift │ ├── AppDocument.swift │ ├── AppScriptContext.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ └── Main.storyboard │ ├── Info.plist │ ├── Scriptable.entitlements │ └── ViewController.swift ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── ScriptLib │ ├── Error.swift │ ├── Exports │ ├── JSApplication.swift │ ├── JSDocument.swift │ ├── JSURL.swift │ └── JSWindow.swift │ ├── Extensions │ ├── JSContext+Extensions.swift │ └── JSValue+Extensions.swift │ ├── Platform │ ├── Application.swift │ ├── Document.swift │ └── Window.swift │ └── ScriptContext.swift └── Tests └── ScriptLibTests └── ScriptLibTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /Examples/Scriptable/Scriptable.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4350E3E627B569B500520233 /* ScriptLib in Frameworks */ = {isa = PBXBuildFile; productRef = 4350E3E527B569B500520233 /* ScriptLib */; }; 11 | 4350E3EA27B56D3C00520233 /* AppDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4350E3E927B56D3C00520233 /* AppDocument.swift */; }; 12 | 4350E3EC27B57FD000520233 /* AppDocument+Scripting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4350E3EB27B57FD000520233 /* AppDocument+Scripting.swift */; }; 13 | 43A8E80327B565DD00D5CC2A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A8E80227B565DD00D5CC2A /* AppDelegate.swift */; }; 14 | 43A8E80527B565DD00D5CC2A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A8E80427B565DD00D5CC2A /* ViewController.swift */; }; 15 | 43A8E80927B565DE00D5CC2A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43A8E80827B565DE00D5CC2A /* Assets.xcassets */; }; 16 | 43A8E80C27B565DE00D5CC2A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 43A8E80A27B565DE00D5CC2A /* Main.storyboard */; }; 17 | 43AB8F5127B5686F00784154 /* AppScriptContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AB8F5027B5686F00784154 /* AppScriptContext.swift */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | 4350E3E927B56D3C00520233 /* AppDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDocument.swift; sourceTree = ""; }; 22 | 4350E3EB27B57FD000520233 /* AppDocument+Scripting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDocument+Scripting.swift"; sourceTree = ""; }; 23 | 43A8E7FF27B565DD00D5CC2A /* Scriptable.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Scriptable.app; sourceTree = BUILT_PRODUCTS_DIR; }; 24 | 43A8E80227B565DD00D5CC2A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 25 | 43A8E80427B565DD00D5CC2A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 26 | 43A8E80827B565DE00D5CC2A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 27 | 43A8E80B27B565DE00D5CC2A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 28 | 43A8E80D27B565DE00D5CC2A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 29 | 43A8E80E27B565DE00D5CC2A /* Scriptable.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Scriptable.entitlements; sourceTree = ""; }; 30 | 43A8E81527B5662300D5CC2A /* ScriptLib */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = ScriptLib; path = ../..; sourceTree = ""; }; 31 | 43AB8F5027B5686F00784154 /* AppScriptContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppScriptContext.swift; sourceTree = ""; }; 32 | /* End PBXFileReference section */ 33 | 34 | /* Begin PBXFrameworksBuildPhase section */ 35 | 43A8E7FC27B565DD00D5CC2A /* Frameworks */ = { 36 | isa = PBXFrameworksBuildPhase; 37 | buildActionMask = 2147483647; 38 | files = ( 39 | 4350E3E627B569B500520233 /* ScriptLib in Frameworks */, 40 | ); 41 | runOnlyForDeploymentPostprocessing = 0; 42 | }; 43 | /* End PBXFrameworksBuildPhase section */ 44 | 45 | /* Begin PBXGroup section */ 46 | 4350E3E427B569B500520233 /* Frameworks */ = { 47 | isa = PBXGroup; 48 | children = ( 49 | ); 50 | name = Frameworks; 51 | sourceTree = ""; 52 | }; 53 | 43A8E7F627B565DD00D5CC2A = { 54 | isa = PBXGroup; 55 | children = ( 56 | 43A8E80127B565DD00D5CC2A /* Scriptable */, 57 | 43A8E81427B5662300D5CC2A /* Packages */, 58 | 43A8E80027B565DD00D5CC2A /* Products */, 59 | 4350E3E427B569B500520233 /* Frameworks */, 60 | ); 61 | sourceTree = ""; 62 | }; 63 | 43A8E80027B565DD00D5CC2A /* Products */ = { 64 | isa = PBXGroup; 65 | children = ( 66 | 43A8E7FF27B565DD00D5CC2A /* Scriptable.app */, 67 | ); 68 | name = Products; 69 | sourceTree = ""; 70 | }; 71 | 43A8E80127B565DD00D5CC2A /* Scriptable */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | 43A8E80227B565DD00D5CC2A /* AppDelegate.swift */, 75 | 4350E3E927B56D3C00520233 /* AppDocument.swift */, 76 | 4350E3EB27B57FD000520233 /* AppDocument+Scripting.swift */, 77 | 43AB8F5027B5686F00784154 /* AppScriptContext.swift */, 78 | 43A8E80427B565DD00D5CC2A /* ViewController.swift */, 79 | 43A8E80827B565DE00D5CC2A /* Assets.xcassets */, 80 | 43A8E80A27B565DE00D5CC2A /* Main.storyboard */, 81 | 43A8E80D27B565DE00D5CC2A /* Info.plist */, 82 | 43A8E80E27B565DE00D5CC2A /* Scriptable.entitlements */, 83 | ); 84 | path = Scriptable; 85 | sourceTree = ""; 86 | }; 87 | 43A8E81427B5662300D5CC2A /* Packages */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | 43A8E81527B5662300D5CC2A /* ScriptLib */, 91 | ); 92 | name = Packages; 93 | sourceTree = ""; 94 | }; 95 | /* End PBXGroup section */ 96 | 97 | /* Begin PBXNativeTarget section */ 98 | 43A8E7FE27B565DD00D5CC2A /* Scriptable */ = { 99 | isa = PBXNativeTarget; 100 | buildConfigurationList = 43A8E81127B565DE00D5CC2A /* Build configuration list for PBXNativeTarget "Scriptable" */; 101 | buildPhases = ( 102 | 43A8E7FB27B565DD00D5CC2A /* Sources */, 103 | 43A8E7FC27B565DD00D5CC2A /* Frameworks */, 104 | 43A8E7FD27B565DD00D5CC2A /* Resources */, 105 | ); 106 | buildRules = ( 107 | ); 108 | dependencies = ( 109 | 4350E3EE27B590D700520233 /* PBXTargetDependency */, 110 | ); 111 | name = Scriptable; 112 | packageProductDependencies = ( 113 | 4350E3E527B569B500520233 /* ScriptLib */, 114 | ); 115 | productName = Scriptable; 116 | productReference = 43A8E7FF27B565DD00D5CC2A /* Scriptable.app */; 117 | productType = "com.apple.product-type.application"; 118 | }; 119 | /* End PBXNativeTarget section */ 120 | 121 | /* Begin PBXProject section */ 122 | 43A8E7F727B565DD00D5CC2A /* Project object */ = { 123 | isa = PBXProject; 124 | attributes = { 125 | BuildIndependentTargetsInParallel = 1; 126 | LastSwiftUpdateCheck = 1320; 127 | LastUpgradeCheck = 1320; 128 | TargetAttributes = { 129 | 43A8E7FE27B565DD00D5CC2A = { 130 | CreatedOnToolsVersion = 13.2.1; 131 | }; 132 | }; 133 | }; 134 | buildConfigurationList = 43A8E7FA27B565DD00D5CC2A /* Build configuration list for PBXProject "Scriptable" */; 135 | compatibilityVersion = "Xcode 13.0"; 136 | developmentRegion = en; 137 | hasScannedForEncodings = 0; 138 | knownRegions = ( 139 | en, 140 | Base, 141 | ); 142 | mainGroup = 43A8E7F627B565DD00D5CC2A; 143 | productRefGroup = 43A8E80027B565DD00D5CC2A /* Products */; 144 | projectDirPath = ""; 145 | projectRoot = ""; 146 | targets = ( 147 | 43A8E7FE27B565DD00D5CC2A /* Scriptable */, 148 | ); 149 | }; 150 | /* End PBXProject section */ 151 | 152 | /* Begin PBXResourcesBuildPhase section */ 153 | 43A8E7FD27B565DD00D5CC2A /* Resources */ = { 154 | isa = PBXResourcesBuildPhase; 155 | buildActionMask = 2147483647; 156 | files = ( 157 | 43A8E80927B565DE00D5CC2A /* Assets.xcassets in Resources */, 158 | 43A8E80C27B565DE00D5CC2A /* Main.storyboard in Resources */, 159 | ); 160 | runOnlyForDeploymentPostprocessing = 0; 161 | }; 162 | /* End PBXResourcesBuildPhase section */ 163 | 164 | /* Begin PBXSourcesBuildPhase section */ 165 | 43A8E7FB27B565DD00D5CC2A /* Sources */ = { 166 | isa = PBXSourcesBuildPhase; 167 | buildActionMask = 2147483647; 168 | files = ( 169 | 4350E3EA27B56D3C00520233 /* AppDocument.swift in Sources */, 170 | 43A8E80527B565DD00D5CC2A /* ViewController.swift in Sources */, 171 | 4350E3EC27B57FD000520233 /* AppDocument+Scripting.swift in Sources */, 172 | 43A8E80327B565DD00D5CC2A /* AppDelegate.swift in Sources */, 173 | 43AB8F5127B5686F00784154 /* AppScriptContext.swift in Sources */, 174 | ); 175 | runOnlyForDeploymentPostprocessing = 0; 176 | }; 177 | /* End PBXSourcesBuildPhase section */ 178 | 179 | /* Begin PBXTargetDependency section */ 180 | 4350E3EE27B590D700520233 /* PBXTargetDependency */ = { 181 | isa = PBXTargetDependency; 182 | productRef = 4350E3ED27B590D700520233 /* ScriptLib */; 183 | }; 184 | /* End PBXTargetDependency section */ 185 | 186 | /* Begin PBXVariantGroup section */ 187 | 43A8E80A27B565DE00D5CC2A /* Main.storyboard */ = { 188 | isa = PBXVariantGroup; 189 | children = ( 190 | 43A8E80B27B565DE00D5CC2A /* Base */, 191 | ); 192 | name = Main.storyboard; 193 | sourceTree = ""; 194 | }; 195 | /* End PBXVariantGroup section */ 196 | 197 | /* Begin XCBuildConfiguration section */ 198 | 43A8E80F27B565DE00D5CC2A /* Debug */ = { 199 | isa = XCBuildConfiguration; 200 | buildSettings = { 201 | ALWAYS_SEARCH_USER_PATHS = NO; 202 | CLANG_ANALYZER_NONNULL = YES; 203 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 204 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 205 | CLANG_CXX_LIBRARY = "libc++"; 206 | CLANG_ENABLE_MODULES = YES; 207 | CLANG_ENABLE_OBJC_ARC = YES; 208 | CLANG_ENABLE_OBJC_WEAK = YES; 209 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 210 | CLANG_WARN_BOOL_CONVERSION = YES; 211 | CLANG_WARN_COMMA = YES; 212 | CLANG_WARN_CONSTANT_CONVERSION = YES; 213 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 214 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 215 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 216 | CLANG_WARN_EMPTY_BODY = YES; 217 | CLANG_WARN_ENUM_CONVERSION = YES; 218 | CLANG_WARN_INFINITE_RECURSION = YES; 219 | CLANG_WARN_INT_CONVERSION = YES; 220 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 221 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 222 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 223 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 224 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 225 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 226 | CLANG_WARN_STRICT_PROTOTYPES = YES; 227 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 228 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 229 | CLANG_WARN_UNREACHABLE_CODE = YES; 230 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 231 | COPY_PHASE_STRIP = NO; 232 | DEBUG_INFORMATION_FORMAT = dwarf; 233 | ENABLE_STRICT_OBJC_MSGSEND = YES; 234 | ENABLE_TESTABILITY = YES; 235 | GCC_C_LANGUAGE_STANDARD = gnu11; 236 | GCC_DYNAMIC_NO_PIC = NO; 237 | GCC_NO_COMMON_BLOCKS = YES; 238 | GCC_OPTIMIZATION_LEVEL = 0; 239 | GCC_PREPROCESSOR_DEFINITIONS = ( 240 | "DEBUG=1", 241 | "$(inherited)", 242 | ); 243 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 244 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 245 | GCC_WARN_UNDECLARED_SELECTOR = YES; 246 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 247 | GCC_WARN_UNUSED_FUNCTION = YES; 248 | GCC_WARN_UNUSED_VARIABLE = YES; 249 | MACOSX_DEPLOYMENT_TARGET = 12.1; 250 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 251 | MTL_FAST_MATH = YES; 252 | ONLY_ACTIVE_ARCH = YES; 253 | SDKROOT = macosx; 254 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 255 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 256 | }; 257 | name = Debug; 258 | }; 259 | 43A8E81027B565DE00D5CC2A /* Release */ = { 260 | isa = XCBuildConfiguration; 261 | buildSettings = { 262 | ALWAYS_SEARCH_USER_PATHS = NO; 263 | CLANG_ANALYZER_NONNULL = YES; 264 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 265 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 266 | CLANG_CXX_LIBRARY = "libc++"; 267 | CLANG_ENABLE_MODULES = YES; 268 | CLANG_ENABLE_OBJC_ARC = YES; 269 | CLANG_ENABLE_OBJC_WEAK = YES; 270 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 271 | CLANG_WARN_BOOL_CONVERSION = YES; 272 | CLANG_WARN_COMMA = YES; 273 | CLANG_WARN_CONSTANT_CONVERSION = YES; 274 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 275 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 276 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 277 | CLANG_WARN_EMPTY_BODY = YES; 278 | CLANG_WARN_ENUM_CONVERSION = YES; 279 | CLANG_WARN_INFINITE_RECURSION = YES; 280 | CLANG_WARN_INT_CONVERSION = YES; 281 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 282 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 283 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 284 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 285 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 286 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 287 | CLANG_WARN_STRICT_PROTOTYPES = YES; 288 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 289 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 290 | CLANG_WARN_UNREACHABLE_CODE = YES; 291 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 292 | COPY_PHASE_STRIP = NO; 293 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 294 | ENABLE_NS_ASSERTIONS = NO; 295 | ENABLE_STRICT_OBJC_MSGSEND = YES; 296 | GCC_C_LANGUAGE_STANDARD = gnu11; 297 | GCC_NO_COMMON_BLOCKS = YES; 298 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 299 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 300 | GCC_WARN_UNDECLARED_SELECTOR = YES; 301 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 302 | GCC_WARN_UNUSED_FUNCTION = YES; 303 | GCC_WARN_UNUSED_VARIABLE = YES; 304 | MACOSX_DEPLOYMENT_TARGET = 12.1; 305 | MTL_ENABLE_DEBUG_INFO = NO; 306 | MTL_FAST_MATH = YES; 307 | SDKROOT = macosx; 308 | SWIFT_COMPILATION_MODE = wholemodule; 309 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 310 | }; 311 | name = Release; 312 | }; 313 | 43A8E81227B565DE00D5CC2A /* Debug */ = { 314 | isa = XCBuildConfiguration; 315 | buildSettings = { 316 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 317 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 318 | CODE_SIGN_ENTITLEMENTS = Scriptable/Scriptable.entitlements; 319 | CODE_SIGN_STYLE = Automatic; 320 | COMBINE_HIDPI_IMAGES = YES; 321 | CURRENT_PROJECT_VERSION = 1; 322 | DEVELOPMENT_TEAM = 64A5CLJP5W; 323 | ENABLE_HARDENED_RUNTIME = YES; 324 | GENERATE_INFOPLIST_FILE = YES; 325 | INFOPLIST_FILE = Scriptable/Info.plist; 326 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 327 | INFOPLIST_KEY_NSMainStoryboardFile = Main; 328 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 329 | LD_RUNPATH_SEARCH_PATHS = ( 330 | "$(inherited)", 331 | "@executable_path/../Frameworks", 332 | ); 333 | MARKETING_VERSION = 1.0; 334 | PRODUCT_BUNDLE_IDENTIFIER = com.hogbaysoftware.Scriptable; 335 | PRODUCT_NAME = "$(TARGET_NAME)"; 336 | SWIFT_EMIT_LOC_STRINGS = YES; 337 | SWIFT_VERSION = 5.0; 338 | }; 339 | name = Debug; 340 | }; 341 | 43A8E81327B565DE00D5CC2A /* Release */ = { 342 | isa = XCBuildConfiguration; 343 | buildSettings = { 344 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 345 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 346 | CODE_SIGN_ENTITLEMENTS = Scriptable/Scriptable.entitlements; 347 | CODE_SIGN_STYLE = Automatic; 348 | COMBINE_HIDPI_IMAGES = YES; 349 | CURRENT_PROJECT_VERSION = 1; 350 | DEVELOPMENT_TEAM = 64A5CLJP5W; 351 | ENABLE_HARDENED_RUNTIME = YES; 352 | GENERATE_INFOPLIST_FILE = YES; 353 | INFOPLIST_FILE = Scriptable/Info.plist; 354 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 355 | INFOPLIST_KEY_NSMainStoryboardFile = Main; 356 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 357 | LD_RUNPATH_SEARCH_PATHS = ( 358 | "$(inherited)", 359 | "@executable_path/../Frameworks", 360 | ); 361 | MARKETING_VERSION = 1.0; 362 | PRODUCT_BUNDLE_IDENTIFIER = com.hogbaysoftware.Scriptable; 363 | PRODUCT_NAME = "$(TARGET_NAME)"; 364 | SWIFT_EMIT_LOC_STRINGS = YES; 365 | SWIFT_VERSION = 5.0; 366 | }; 367 | name = Release; 368 | }; 369 | /* End XCBuildConfiguration section */ 370 | 371 | /* Begin XCConfigurationList section */ 372 | 43A8E7FA27B565DD00D5CC2A /* Build configuration list for PBXProject "Scriptable" */ = { 373 | isa = XCConfigurationList; 374 | buildConfigurations = ( 375 | 43A8E80F27B565DE00D5CC2A /* Debug */, 376 | 43A8E81027B565DE00D5CC2A /* Release */, 377 | ); 378 | defaultConfigurationIsVisible = 0; 379 | defaultConfigurationName = Release; 380 | }; 381 | 43A8E81127B565DE00D5CC2A /* Build configuration list for PBXNativeTarget "Scriptable" */ = { 382 | isa = XCConfigurationList; 383 | buildConfigurations = ( 384 | 43A8E81227B565DE00D5CC2A /* Debug */, 385 | 43A8E81327B565DE00D5CC2A /* Release */, 386 | ); 387 | defaultConfigurationIsVisible = 0; 388 | defaultConfigurationName = Release; 389 | }; 390 | /* End XCConfigurationList section */ 391 | 392 | /* Begin XCSwiftPackageProductDependency section */ 393 | 4350E3E527B569B500520233 /* ScriptLib */ = { 394 | isa = XCSwiftPackageProductDependency; 395 | productName = ScriptLib; 396 | }; 397 | 4350E3ED27B590D700520233 /* ScriptLib */ = { 398 | isa = XCSwiftPackageProductDependency; 399 | productName = ScriptLib; 400 | }; 401 | /* End XCSwiftPackageProductDependency section */ 402 | }; 403 | rootObject = 43A8E7F727B565DD00D5CC2A /* Project object */; 404 | } 405 | -------------------------------------------------------------------------------- /Examples/Scriptable/Scriptable.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/Scriptable/Scriptable.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/Scriptable/Scriptable/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | @main 4 | class AppDelegate: NSObject, NSApplicationDelegate { 5 | 6 | func applicationDidFinishLaunching(_ aNotification: Notification) { 7 | _ = AppScriptContext.shared 8 | } 9 | 10 | func applicationWillTerminate(_ aNotification: Notification) { 11 | } 12 | 13 | func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 14 | return true 15 | } 16 | 17 | } 18 | 19 | -------------------------------------------------------------------------------- /Examples/Scriptable/Scriptable/AppDocument+Scripting.swift: -------------------------------------------------------------------------------- 1 | import JavaScriptCore 2 | import ScriptLib 3 | 4 | // This is how AppDocument exposes it's `text` field to scripting. 5 | 6 | @objc protocol JSAppDocumentExport: JSExport { 7 | var text: String { get set } 8 | } 9 | 10 | @objc class JSAppDocument: JSDocument, JSAppDocumentExport { 11 | 12 | init(inner: AppDocument, context: ScriptContext) { 13 | super.init(inner: inner, context: context) 14 | } 15 | 16 | var text: String { 17 | get { 18 | (inner as? AppDocument)?.text ?? "" 19 | } 20 | set { 21 | (inner as? AppDocument)?.text = newValue 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Examples/Scriptable/Scriptable/AppDocument.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import ScriptLib 3 | 4 | class AppDocument: NSDocument { 5 | 6 | var text: String = "Hello world" 7 | 8 | override init() { 9 | super.init() 10 | } 11 | 12 | override class var autosavesInPlace: Bool { 13 | return true 14 | } 15 | 16 | override func makeWindowControllers() { 17 | let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil) 18 | self.addWindowController(storyboard.instantiateController( 19 | withIdentifier: NSStoryboard.SceneIdentifier("Document Window Controller") 20 | ) as! NSWindowController) 21 | } 22 | 23 | override func data(ofType typeName: String) throws -> Data { 24 | text.data(using: .utf8)! 25 | } 26 | 27 | override func read(from data: Data, ofType typeName: String) throws { 28 | text = String(data: data, encoding: .utf8)! 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Examples/Scriptable/Scriptable/AppScriptContext.swift: -------------------------------------------------------------------------------- 1 | import ScriptLib 2 | 3 | // Custom scripting is injected into ScriptLib by subclassing `JS` wrappers. 4 | 5 | class AppScriptContext: ScriptContext { 6 | 7 | public static let shared = AppScriptContext() 8 | 9 | public init() { 10 | super.init( 11 | wrapDocument: { inner, context in 12 | (inner as? AppDocument).map { JSAppDocument(inner: $0, context: context) } 13 | } 14 | ) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Examples/Scriptable/Scriptable/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/Scriptable/Scriptable/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Examples/Scriptable/Scriptable/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/Scriptable/Scriptable/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | Default 530 | 531 | 532 | 533 | 534 | 535 | 536 | Left to Right 537 | 538 | 539 | 540 | 541 | 542 | 543 | Right to Left 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | Default 555 | 556 | 557 | 558 | 559 | 560 | 561 | Left to Right 562 | 563 | 564 | 565 | 566 | 567 | 568 | Right to Left 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | -------------------------------------------------------------------------------- /Examples/Scriptable/Scriptable/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDocumentTypes 6 | 7 | 8 | CFBundleTypeRole 9 | Editor 10 | LSHandlerRank 11 | Default 12 | LSItemContentTypes 13 | 14 | com.example.plain-text 15 | 16 | NSDocumentClass 17 | $(PRODUCT_MODULE_NAME).AppDocument 18 | 19 | 20 | UTImportedTypeDeclarations 21 | 22 | 23 | UTTypeConformsTo 24 | 25 | public.plain-text 26 | 27 | UTTypeDescription 28 | Example Text 29 | UTTypeIdentifier 30 | com.example.plain-text 31 | UTTypeTagSpecification 32 | 33 | public.filename-extension 34 | 35 | exampletext 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Examples/Scriptable/Scriptable/Scriptable.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-write 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Examples/Scriptable/Scriptable/ViewController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class ViewController: NSViewController { 4 | 5 | @IBOutlet weak var scriptConsole: NSTextView! 6 | 7 | override func viewDidLoad() { 8 | super.viewDidLoad() 9 | } 10 | 11 | func runScript() { 12 | let textStorage = scriptConsole.textStorage! 13 | var selectionRange = scriptConsole.selectedRange() 14 | 15 | if selectionRange.length == 0 { 16 | selectionRange = scriptConsole.selectionRange( 17 | forProposedRange: selectionRange, 18 | granularity: .selectByParagraph 19 | ) 20 | } 21 | 22 | let script = textStorage.attributedSubstring(from: selectionRange).string 23 | 24 | do { 25 | let result = try AppScriptContext.shared.evaluateScript(script: script) 26 | textStorage.append(NSAttributedString(string: "\n\(result)\n")) 27 | } catch { 28 | textStorage.append(NSAttributedString(string: "\n\(error)\n")) 29 | } 30 | 31 | scriptConsole.scrollToEndOfDocument(nil) 32 | } 33 | 34 | } 35 | 36 | extension ViewController: NSTextViewDelegate { 37 | 38 | func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { 39 | if commandSelector == #selector(insertNewline(_:)) { 40 | runScript() 41 | return true 42 | } 43 | return false 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jesse Grosjean 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "ScriptLib", 8 | platforms: [ 9 | .macOS(.v10_15), 10 | .iOS(.v14) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, and make them visible to other packages. 14 | .library( 15 | name: "ScriptLib", 16 | targets: ["ScriptLib"]), 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | // .package(url: /* package url */, from: "1.0.0"), 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "ScriptLib", 27 | dependencies: []), 28 | .testTarget( 29 | name: "ScriptLibTests", 30 | dependencies: ["ScriptLib"]), 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScriptLib 2 | 3 | Early and in progress scripting suite for Swift apps. 4 | 5 | The goal is to create an API like AppleScript's standard suite, except implement using an embedded JavaScriptCore context. Manny macOS apps use this approach, but it looks like everyone is building their own solution. (Please let me know if there are open source solutions that I've missed). Here are some apps and related JavaScript API's to be inspired by: 6 | 7 | - [Drafts](https://docs.getdrafts.com/docs/actions/scripting) 8 | - [Noteplan](https://help.noteplan.co/article/65-commandbar-plugins) 9 | - [Nova](https://docs.nova.app) 10 | - [Omni Automation](https://omni-automation.com/) 11 | - [Paw](https://paw.cloud/docs/reference/ExtensionContext) 12 | - [Proxyman](https://docs.proxyman.io/scripting/script) 13 | - [Scriptable](https://docs.scriptable.app) 14 | - [Sketch](https://developer.sketch.com/reference/api/) 15 | - [TaskPaper](https://www.taskpaper.com/guide/reference/scripting/) 16 | 17 | To try out what's here: 18 | 19 | 1. Open and run the Example app. 20 | 21 | Each app window has a text area that acts as a very simple script console. You can type the follow scripts into that window. On the other hand you'll have a much better experience if you setup Safari's script console to do this instead of using the in app script console. You setup Safari's script console like this: 22 | 23 | 1. Open Safari 24 | 2. Open the preferences and enable under "Advanced" the "Develop menu bar 25 | 3. You should now see a new "Develop" menu item in Safari. Check the options "Automatically Show Web Inspector for JSContexts" and "Automatically Pause Connecting to JSContexts". With these options a new Safari window will open when you open the example app. 26 | 27 | You can also choose to open an existing context manually from Safari. When the example app is running there will be a context named "Scriptable ScriptContext" in Safari's "Develop" menu. Choose that to open the console. 28 | 29 | 4. Now when you run the example app Safari should open a JavaScript console for the example apps script context. The following instructions should work about the same, but you'll have a nicer experience with autocomplete and better ways to inspect objects. 30 | 31 | 2. Type `app.version` in the apps window (on Safari Console) and press Return. You should see `1.0`. 32 | 33 | 2. Enter `app.beep()`. It should beep! 34 | 35 | 3. Enter `app.documents[0].fileURL`. You should see "undefined" if you haven't yet saved, or a URL object if you have saved. 36 | 37 | 4. Enter `app.documents[0].text`. You should see "Hello World". 38 | 39 | 5. Enter `app.documents[0].text = "Hello, Hello World"`. You have assigned a new text value to the document. 40 | 41 | That's about it! :) 42 | 43 | But there are a number of basic design decision made. 44 | 45 | 1. ScriptLib provides some behavior for free. For example the demo app didn't have to implement `app.version`, `app.documents`, `app.windows`, etc. 46 | 47 | 2. ScriptLib makes it possible to extend this default scripting support. For example `document.text` is a custom property added by the demo app. 48 | 49 | Generally scripting support is added by: 50 | 51 | 1. Wrapping "Swift" objects inside a "JS" wrapper object. (Generally the "JS" object should weakly hold big reference types such as windows and documents so scripts don't create leaks). 52 | 53 | 2. The scripting API is then exposed on that wrapper object by implementing protocol rooted at `JSExport`. 54 | 55 | ## Todos 56 | 57 | - Implement features from AppleScript Standard Suite, combined with whatever useful standards can be understood by looking at existing app scripting APIs. 58 | 59 | - Add features for interacting with user. For example `showAlert`, `getInput`, `chooseFromList`, etc. These features consist of define JavaScript API and then native Swift implementation. 60 | 61 | - Add support for plugins. Generally plugins are just scripts that are automatically loaded and run. 62 | 63 | - Define plugin format 64 | - API to find plugins in common locations 65 | - API to load those plugins into a ScriptContext and call lifecycle events 66 | - API to allow plugins to subscribe to events such as documents and windows opening and closing. 67 | - API for plugins to contribute "commands". Maybe even standard UI that apps can use to browse and execute plugin contributed commands. 68 | - Note sure these ideas all make sense, but it would be pretty great to just include a lib and get all this for free. 69 | 70 | - Figure out some sane way to generate documentation. Ideally each `JSExport` protocol can be documented in Swift and then a script can extract that documentation and export in form suitable for JavaScript development. 71 | 72 | ## Notes 73 | 74 | - There is mention in the code of iOS support. This would be nice eventually, but goal right now is just macOS. 75 | 76 | - This is all in exploratory phase. Maybe better way to do things. Let me know! 77 | -------------------------------------------------------------------------------- /Sources/ScriptLib/Error.swift: -------------------------------------------------------------------------------- 1 | import JavaScriptCore 2 | 3 | public enum Error: Swift.Error { 4 | case scriptException(JSValue) 5 | } 6 | -------------------------------------------------------------------------------- /Sources/ScriptLib/Exports/JSApplication.swift: -------------------------------------------------------------------------------- 1 | import JavaScriptCore 2 | 3 | @objc protocol JSApplicationExport: JSExport { 4 | var name: String { get } 5 | var version: String { get } 6 | var windows: [JSWindow] { get } 7 | var documents: [JSDocument] { get } 8 | func documentForURL(_ url: JSURL) -> JSDocument? 9 | func openURL(_ url: JSURL, _ callback: JSValue) 10 | func beep() 11 | } 12 | 13 | @objc public class JSApplication: NSObject, JSApplicationExport { 14 | 15 | public weak var context: ScriptContext? 16 | 17 | init(context: ScriptContext) { 18 | self.context = context 19 | } 20 | 21 | public var name: String { 22 | Application.name 23 | } 24 | 25 | public var version: String { 26 | Application.version 27 | } 28 | 29 | public var build: UInt { 30 | Application.build 31 | } 32 | 33 | public var windows: [JSWindow] { 34 | guard let context = context else { 35 | return [] 36 | } 37 | 38 | return Application.shared.windows.compactMap { window in 39 | return context.wrapWindow(window) 40 | } 41 | } 42 | 43 | public var documents: [JSDocument] { 44 | guard let context = context else { 45 | return [] 46 | } 47 | 48 | return Application.shared.documents.compactMap { document in 49 | context.wrapDocument(document) 50 | } 51 | } 52 | 53 | public func documentForURL(_ url: JSURL) -> JSDocument? { 54 | guard 55 | let context = context, 56 | let document = Application.shared.document(for: url.inner) 57 | else { 58 | return nil 59 | } 60 | return context.wrapDocument(document) 61 | } 62 | 63 | public func openURL(_ url: JSURL, _ callback: JSValue) { 64 | Application.shared.open(url.inner) { result in 65 | callback.call(withArguments: [result]) 66 | } 67 | } 68 | 69 | public func beep() { 70 | Application.beep() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/ScriptLib/Exports/JSDocument.swift: -------------------------------------------------------------------------------- 1 | import JavaScriptCore 2 | 3 | @objc protocol JSDocumentExport: JSExport { 4 | var fileURL: JSURL? { get } 5 | var windows: [JSWindow]? { get } 6 | } 7 | 8 | @objc open class JSDocument: NSObject, JSDocumentExport { 9 | 10 | public weak var inner: Document? 11 | public weak var context: ScriptContext? 12 | 13 | public init(inner: Document, context: ScriptContext) { 14 | self.inner = inner 15 | self.context = context 16 | } 17 | 18 | public var fileURL: JSURL? { 19 | inner?.fileURL.map { JSURL(inner: $0 ) } 20 | } 21 | 22 | public var windows: [JSWindow]? { 23 | guard 24 | let windows = inner?.windows, 25 | let context = context 26 | else { 27 | return nil 28 | } 29 | 30 | return windows.compactMap { context.wrapWindow($0) } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Sources/ScriptLib/Exports/JSURL.swift: -------------------------------------------------------------------------------- 1 | import JavaScriptCore 2 | 3 | @objc public protocol JSURLExport: JSExport { 4 | static func new(_: String) -> JSURL? 5 | 6 | var lastPathComponent: String? { get } 7 | } 8 | 9 | @objc public class JSURL: NSObject, JSURLExport { 10 | 11 | let inner: URL 12 | 13 | init(inner: URL) { 14 | self.inner = inner 15 | } 16 | 17 | public class func new(_ string: String) -> JSURL? { 18 | URL(string: string).map { JSURL(inner: $0) } 19 | } 20 | 21 | public var lastPathComponent: String? { 22 | inner.lastPathComponent 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Sources/ScriptLib/Exports/JSWindow.swift: -------------------------------------------------------------------------------- 1 | import JavaScriptCore 2 | 3 | @objc public protocol JSWindowExport: JSExport { 4 | var title: String? { get } 5 | var document: JSDocument? { get } 6 | } 7 | 8 | @objc open class JSWindow: NSObject, JSWindowExport { 9 | 10 | public weak var inner: Window? 11 | public weak var context: ScriptContext? 12 | 13 | public init(inner: Window, context: ScriptContext) { 14 | self.inner = inner 15 | self.context = context 16 | } 17 | 18 | public var title: String? { 19 | inner?.title 20 | } 21 | 22 | public var document: JSDocument? { 23 | guard 24 | let document = inner?.document, 25 | let context = context 26 | else { 27 | return nil 28 | } 29 | return context.wrapDocument(document) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Sources/ScriptLib/Extensions/JSContext+Extensions.swift: -------------------------------------------------------------------------------- 1 | import JavaScriptCore 2 | 3 | extension JSContext { 4 | 5 | public subscript(_ key: NSString) -> JSValue? { 6 | get { return objectForKeyedSubscript(key) } 7 | } 8 | 9 | public subscript(_ key: NSString) -> Any? { 10 | get { return objectForKeyedSubscript(key) } 11 | set { setObject(newValue, forKeyedSubscript: key) } 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Sources/ScriptLib/Extensions/JSValue+Extensions.swift: -------------------------------------------------------------------------------- 1 | import JavaScriptCore 2 | 3 | extension JSValue { 4 | 5 | public subscript(_ key: NSString) -> JSValue? { 6 | get { return objectForKeyedSubscript(key) } 7 | } 8 | 9 | public subscript(_ key: NSString) -> Any? { 10 | get { return objectForKeyedSubscript(key) } 11 | set { setObject(newValue, forKeyedSubscript: key) } 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Sources/ScriptLib/Platform/Application.swift: -------------------------------------------------------------------------------- 1 | #if os(OSX) 2 | 3 | import AppKit 4 | public typealias Application = NSApplication 5 | 6 | extension NSApplication { 7 | 8 | public var documents: [Document] { 9 | orderedDocuments 10 | } 11 | 12 | public func document(for url: URL) -> Document? { 13 | NSDocumentController.shared.document(for: url) 14 | } 15 | 16 | public static func beep() { 17 | NSSound.beep() 18 | } 19 | 20 | public func open(_ url: URL, _ callback: @escaping (Bool) -> ()) { 21 | let configuration = NSWorkspace.OpenConfiguration() 22 | configuration.promptsUserIfNeeded = true 23 | NSWorkspace.shared.open( 24 | url, 25 | configuration: configuration) { app, error in 26 | if error != nil { 27 | callback(false) 28 | } else { 29 | callback(true) 30 | } 31 | } 32 | } 33 | } 34 | 35 | #else 36 | 37 | import UIKit 38 | public typealias Application = UIApplication 39 | 40 | extension UIApplication { 41 | 42 | public var documents: [AppDocument] { 43 | fatalError() 44 | } 45 | 46 | public func document(for url: URL) -> AppDocument? { 47 | fatalError() 48 | } 49 | 50 | } 51 | 52 | #endif 53 | 54 | extension Application { 55 | 56 | public static var name: String { 57 | Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "Missing CFBundleName" 58 | } 59 | 60 | public static var version: String { 61 | Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "Missing CFBundleShortVersionString" 62 | } 63 | 64 | public static var build: UInt { 65 | UInt(Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "") ?? 0 66 | } 67 | 68 | public static var isPreview: Bool { 69 | let bundleInfo = Bundle.main.infoDictionary! 70 | let currentShortVersion = bundleInfo["CFBundleShortVersionString"] as! String 71 | return currentShortVersion.range(of: "Preview") != nil 72 | } 73 | 74 | public static var buildDate: Date? { 75 | guard 76 | let executablePath = Bundle.main.executablePath, 77 | let attributes = try? FileManager.default.attributesOfItem(atPath: executablePath), 78 | let date = attributes[.creationDate] as? Date 79 | else { 80 | return nil 81 | } 82 | return date 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /Sources/ScriptLib/Platform/Document.swift: -------------------------------------------------------------------------------- 1 | #if os(OSX) 2 | 3 | import AppKit 4 | public typealias Document = NSDocument 5 | 6 | extension NSDocument { 7 | 8 | var windows: [Window] { 9 | windowControllers.compactMap { $0.window } 10 | } 11 | 12 | } 13 | 14 | #else 15 | 16 | import UIKit 17 | public typealias AppDocument = UIDocument 18 | 19 | extension UIDocument { 20 | 21 | } 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /Sources/ScriptLib/Platform/Window.swift: -------------------------------------------------------------------------------- 1 | #if os(OSX) 2 | 3 | import AppKit 4 | public typealias Window = NSWindow 5 | 6 | extension Window { 7 | 8 | var document: Document? { 9 | windowController?.document as? NSDocument 10 | } 11 | 12 | } 13 | 14 | #else 15 | 16 | import UIKit 17 | public typealias Window = UIWindow 18 | 19 | extension UIWindow { 20 | 21 | public var title: String { 22 | "" 23 | } 24 | 25 | } 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /Sources/ScriptLib/ScriptContext.swift: -------------------------------------------------------------------------------- 1 | import JavaScriptCore 2 | 3 | open class ScriptContext { 4 | 5 | let context: JSContext 6 | let wrapWindowInner: ((Window, ScriptContext) -> JSWindow?) 7 | let wrapDocumentInner: ((Document, ScriptContext) -> JSDocument?) 8 | var lastException: JSValue? 9 | 10 | public init( 11 | wrapWindow: ((Window, ScriptContext) -> JSWindow?)? = nil, 12 | wrapDocument: ((Document, ScriptContext) -> JSDocument?)? = nil 13 | ) { 14 | context = JSContext()! 15 | context.name = "\(Application.name) ScriptContext" 16 | wrapWindowInner = wrapWindow ?? { JSWindow(inner: $0, context: $1) } 17 | wrapDocumentInner = wrapDocument ?? { JSDocument(inner: $0, context: $1) } 18 | 19 | context.exceptionHandler = { [weak self] context, exception in 20 | self?.lastException = exception 21 | } 22 | 23 | context.setObject(JSURL.self, forKeyedSubscript: "JSURL" as NSString) 24 | context["app"] = JSApplication(context: self) 25 | } 26 | 27 | func wrapWindow(_ window: Window) -> JSWindow? { 28 | wrapWindowInner(window, self) 29 | } 30 | 31 | func wrapDocument(_ document: Document) -> JSDocument? { 32 | wrapDocumentInner(document, self) 33 | } 34 | 35 | public func evaluateScript(script: String, withSourceURL: URL? = nil) throws -> JSValue { 36 | lastException = nil 37 | 38 | let script = "(function() { return eval(`\(script)`) })()" 39 | 40 | let result: JSValue = { 41 | if let url = withSourceURL { 42 | return context.evaluateScript(script, withSourceURL: url) 43 | } else { 44 | return context.evaluateScript(script) 45 | } 46 | }() 47 | 48 | if let lastException = lastException { 49 | self.lastException = nil 50 | throw Error.scriptException(lastException) 51 | } 52 | 53 | return result 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Tests/ScriptLibTests/ScriptLibTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ScriptLib 3 | 4 | final class ScriptLibTests: XCTestCase { 5 | 6 | func testExample() throws { 7 | } 8 | 9 | } 10 | --------------------------------------------------------------------------------