How Can I Run JXA from Swift? - swift

If I have a string variable with JXA source code, is there a way to run that from swift? It seems NSAppleScript only works with AppleScript source.

Here is an example showing how to use OSAKit from Swift to run a JavaScript for Automation script stored in a string:
import OSAKit
let scriptString = "Math.PI";
let script = OSAScript.init(source: scriptString, language: OSALanguage.init(forName: "JavaScript"));
var compileError : NSDictionary?
script.compileAndReturnError(&compileError)
if let compileError = compileError {
print(compileError);
return;
}
var scriptError : NSDictionary?
let result = script.executeAndReturnError(&scriptError)
if let scriptError = scriptError {
print(scriptError);
}
else if let result = result?.stringValue {
print(result)
}
This Swift code was adapted from the Hammerspoon source code (Objective-C).

Why? JXA is a dog in every respect. If you just want to run JS code, the JavaScriptCore Obj-C API is far cleaner and easier. If you want to control "AppleScriptable" applications then use AppleScript—it's the only officially supported† option that works right.
(† There is SwiftAutomation, but Apple haven't bitten and I'm not inclined to support it myself given Apple's chronic mismanagement of Mac Automation. We'll see what happens with WWDC17.)

Related

NSSavePanel name not user editable

I'm a total beginner to OSX GUI programming, so please be gentle with me.
I'm trying some experiments with adding light GUI elements from appkit to a CLI, so I'm working on a very small program to take the contents of a PDF and save it to a text file.
Here's the code I have
import AppKit
import Foundation
import Quartz
func helperReadPDF(_ filename: String) -> String {
let pdata = try! NSData(contentsOfFile: filename) as Data
let pdf = PDFDocument(data: pdata)
return pdf!.string!
}
func selectFile() -> URL? {
let dialog = NSOpenPanel()
dialog.allowedFileTypes = ["pdf"]
guard dialog.runModal() == .OK else { return nil }
return dialog.url
}
func getSaveLocation() -> URL? {
let sa = NSSavePanel()
sa.nameFieldStringValue = "Untitled.txt"
sa.canCreateDirectories = true
sa.allowedFileTypes = ["txt"]
guard sa.runModal() == .OK else { return nil }
return sa.url
}
let file = selectFile()?.path ?? ""
print("where to save?")
let dest = getSaveLocation()!
try! helperReadPDF(file).write(to: dest, atomically: true, encoding: .utf8)
(I know, there are lots of unidiomatic things in here, like all the forced unwrapping and pointlessly converting URLs to paths. I have obscure reasons...)
So this code mostly works: when I run it from a terminal window with swift guitest.swift it'll pop up a file picker window, let me select a pdf file, and then pop up a save dialogue and let me choose the directory, and then save the extracted text from the pdf into that directory.
But it won't let me change the filename. I can highlight the "Untitled.txt" provided by default, I can even get a cursor into the field... but it doesn't respond to keyboard input.
In this previous SO, someone suggested adding a nameFieldStringValue to make it editable, but, as you can see from the above code, I did that, and it doesn't work.
I see from this very old SO that at least in Objective-C-land, you have to initiate some kind of application object in order to accept keyboard input. Is that true today in Swift-land as well?
(Even though for some weird reason you can accept mouse input without doing any of that?!) If so, how do I do that here?
Edit: I get from the comments to that last prior SO I linked that this is probably a terrible idea, and that if I want to learn Mac GUI programming I should do it the heavy way with XCode and storyboards and all the rest. But could you indulge my doing it the stupid way in an effort to try to learn one thing at a time? (I.e., learn the GUI APIs on offer without also trying to learn XCode and Apple's preferred style of architecture at the same time.)
Thanks!
(Swift 4.2 on latest version of OSX. Not using XCode at all.)
Setting the application's ActivationPolicy will make it work.
// Import statements... (import Quartz)
NSApplication.shared.setActivationPolicy(.accessory)
// Functions and so on... (func helper..)

Converting Swift 2.0 to Swift 3.0

I am currently writing a iOS App using AWS Mobile Hub and the AWS services. I'm in the middle of implementing API Gateway into my app after connecting it with an AWS Lambda function. After doing all that, AWS generated an SDK for me to implement into my project but the problem is, is that the code is in Swift 2.0. I am trying to convert it into Swift 3.0 but I have no idea what this code is supposed to do/trying to do, so I don't know how to convert it.
My question is, how do I convert this line into Swift 3.0?
var URLString: String = "https://XXXXX.execute-api.XXXX.amazonaws.com/prod"
if URLString.hasSuffix("/") {
URLString = URLString.substringToIndex(URLString.startIndex.advancedBy(URLString.lengthOfBytesUsingEncoding(NSUTF8StringEncoding) - 1))
}
The problem is the line of code in the "if" statement.
Thank you.
Swift 3 version:
var URLString: String = "https://XXXXX.execute-api.XXXX.amazonaws.com/prod"
if URLString.hasSuffix("/") {
let index = URLString.index(URLString.startIndex, offsetBy: (URLString.characters.count - 1))
URLString = URLString.substring(to: index)
}
Please use camelcase starting from small letter with naming your variables within Swift. More convenient way:
var urlString: String = "https://XXXXX.execute-api.XXXX.amazonaws.com/prod"
if urlString.hasSuffix("/") {
urlString = String(urlString.characters.dropLast())
}

Calling getsectiondata from Swift

This question and answer describe how to read data from a Mach-O section with Objective-C on modern OS X/macOS versions: Crash reading bytes from getsectbyname
The described answer works. I'm trying to implement the same thing with Swift. I can't make it work.
I have the following in "Other linker flags": -Wl,-sectcreate,__LOCALIZATIONS,__base,en.lproj/Localizable.strings,-segprot,__LOCALIZATIONS,r,r.
This Swift code gets me the a pointer to the embedded data, until I try to run the code outside Xcode and ASLR breaks it:
var size: UInt = 0
let _localizationSection = getsectdata(
"__LOCALIZATIONS",
"__base",
&size)
To get around the ASLR problem, according to the above question and answer, and based on my own testing, I should be using getsectiondata instead. It works great in Objective-C, but I'm having no luck in Swift. The following is the only thing I've managed to get past the compiler, but it returns nil:
var size: UInt = 0
var header = _mh_execute_header
let localizationSection = getsectiondata(
&header,
"__LOCALIZATIONS",
"__base",
&size)
Is taking a copy of _mh_execute_header the problem and is there any way to avoid it? I need an UnsafePointer<mach_header_64>, but using &_mh_execute_header as the first parameter to getsectiondata causes a compilation error.
I'm using Swift 3.0, and running my code on macOS 10.12.
The difference between the linked-to Objective-C code
void *ptr = getsectiondata(&_mh_execute_header, ...);
and your Swift translation
var header = _mh_execute_header
let localizationSection = getsectiondata(&header, ...)
is that the latter passes the address of a copy of the global
_mh_execute_header variable to the function, and apparently that
is not accepted. If you modify the Objective-C code to
struct mach_header_64 header = _mh_execute_header;
void *ptr = getsectiondata(&header, ...);
then it fails as well (and actually crashed in my test).
Now the problem is that _mh_execute_header is exposed to Swift
as a constant:
public let _mh_execute_header: mach_header_64
and one cannot take the address of a constant in Swift. One possible
workaround is to define
#import <mach-o/ldsyms.h>
static const struct mach_header_64 *mhExecHeaderPtr = &_mh_execute_header;
in the bridging header file, and then use it as
let localizationSection = getsectiondata(mhExecHeaderPtr, ...)
in Swift.
Another option is to lookup the symbol via dlopen/dlsym
import MachO
if let handle = dlopen(nil, RTLD_LAZY) {
defer { dlclose(handle) }
if let ptr = dlsym(handle, MH_EXECUTE_SYM) {
let mhExecHeaderPtr = ptr.assumingMemoryBound(to: mach_header_64.self)
var size: UInt = 0
let localizationSection = getsectiondata(
mhExecHeaderPtr,
"__LOCALIZATIONS",
"__base",
&size)
// ...
}
}

How to determine whether a Swift code is running inside XCode Playground

I'm writing a simple application reading CSV file in Swift and I would like to be able to use the same code in Playground and as an input file to the swift command.
To read a file in Playground I have to use this code
let filePath = XCPlaygroundSharedDataDirectoryURL.URLByAppendingPathComponent("data.csv")
I would like to achieve something like:
#if PLAYGROUND
import XCPlayground
let filePath = XCPlaygroundSharedDataDirectoryURL.URLByAppendingPathComponent("data.csv")
#else
let filePath = NSURL.fileURLWithPath("data.csv")
#endif
The test is quite simple:
let bundleId = NSBundle.mainBundle().bundleIdentifier ?? ""
if bundleId.hasPrefix("com.apple.dt"){
//... Your code
}
But I think you have already seen the problem once you've done that... the import will stop the build elsewhere. I suspect you are trying to build a playground for a framework you have built (if not, I'm not quite sure how the code is being shared)... The way I solved it in the framework was to provide an optional call back hook for the value I wanted to get... so for example
In Framework
public defaultUrlHook : (()->NSURL)? = nil
internal var defaultUrl : NSURL {
return defaultUrlHook?() ?? NSURL.fileURLWithPath("data.csv")
}
In playground
import XCPlayground
import YourFramework
defaultUrlHook = { ()->NSURL in
return XCPlaygroundSharedDataDirectoryURL.URLByAppendingPathComponent("data.csv")
}
//Do your thing....

Using IBM Swift Sandbox and rangeOfCharacterFromSet

I am trying swift on IBM's new Swift online Sandbox.
The following script is not running on that sandbox: http://swiftlang.ng.bluemix.net/
import Foundation
func palindromTest(s: String) -> Bool{
let lower = s.lowercaseString
let letters = NSCharacterSet.letterCharacterSet()
let onlyLetters = lower.characters.filter({String($0).rangeOfCharacterFromSet(letters) != nil})
let reverseLetters = Array(onlyLetters).reverse()
return String(onlyLetters) == String(reverseLetters)
}
palindromTest("abc")
The sandbox prints the following error message:
/swift-execution/code-tmp.swift:7:48: error: value of type 'String' has no member 'rangeOfCharacterFromSet'
let onlyLetters = lower.characters.filter({String($0).rangeOfCharacterFromSet(letters) != nil})
Did I forget to import something? Hope you can help me.
Thanks.
rangeOfCharacterFromSet is one of the many String methods
which are actually handled by NSString.
From this commit
to NSString.swift, it seems that support for rangeOfCharacterFromSet
was added only recently to the (non-Apple) Foundation library,
so you cannot use it until the IBM Swift Sandbox is updated to use
a new Swift version.
FWIW, this works now in the Sandbox swiftlang.ng.bluemix.net/#/repl/aae5d1caf4e0a6232ff428c3a0160e6e98cba6ed913ce9176f2baee46d30cb1c
Lots of things have changed since these answers were given. I've updated Pat's Sandbox answer for Swift 3.1.1. Below is the code and a summary of the API changes.
Several methods in the standard library and Foundation have since been renamed:
Array: .reverse() → .reversed()
String: .rangeOfCharacterFromSet() → .rangeOfCharacter(from:)
(NS)CharacterSet: .lettersCharacterSet() → .letters
I also made a slight change, perhaps just from personal preference. Rather than use String.rangeOfCharacter(from:) I chose to use CharacterSet.contains(), which required I use String.unicodeScalars instead of .characters. Not sure about the performance tradeoffs in general, but I liked how cleaned things up.
import Foundation
func palindromTest(_ word: String) -> Bool{
let letters = CharacterSet.letters
let onlyLetters = word.lowercased().unicodeScalars.filter {
letters.contains($0)
}.map { Character($0) }
let reverseLetters = onlyLetters.reversed()
print("\(String(onlyLetters)) == \(String(reverseLetters))")
return String(onlyLetters) == String(reverseLetters)
}
print(palindromTest("abc"))
// false
print(palindromTest("dud"))
// true