You can use AppleScript, JavaScript or Objective C to control apps on the Mac. Since Free Pascal has an Objective Pascal dialect, you can use it in place of Objective C. You can also execute AppleScript and JavaScript scripts under Pascal control.
The sections below include code examples in several languages for controlling the Numbers spreadsheet app. All Macs should already have Numbers. If not, just install it via the Mac App Store.
Source code for the examples is available here.
Any app that can be scripted includes a scripting "dictionary". You can open any dictionary in Script Editor to see the AppleScript, JavaScript or Objective C syntax for the classes and properties available to your scripts with that app. And since the dictionary is just an XML file, you can also open it with a text editor. For example, Numbers' dictionary is file Numbers.sdef in the Numbers.app/Contents/Resources folder.
Here's an example script that manipulates the Numbers app. In AppleScript, code comments are indicated by double hyphens (--).
try
tell application "Numbers"
log name & " " & version --output app's name and version
set numDoc to make new document with properties {name:"My Doc"} --create new document
set name of sheet 0 of numDoc to "My Sheet" --change sheet's tab
set numCell to cell 2 of row 2 of table 0 of sheet 0 of numDoc
--get upper left-most non-header cell of doc's first sheet
tell numCell --set cell's properties
set value to 1.23
set font size to 15
end tell
activate --move app to foreground
end tell
on error errMsg number errorNumber
return errMsg
end try
To run this example script yourself, you can open file testnumbers.applescript
in Script Editor, then click the Run button. You can also run it from
the command line in a Terminal window like this:
osascript testnumbers.applescript
Here's the Numbers example as JavaScript:
try {
var numApp = Application("Numbers");
console.log(numApp.name() + " " + numApp.version());
var numDoc = numApp.Document({name:"My Doc"}).make();
numDoc.sheets[0].name = "My Sheet";
var numCell = numDoc.sheets[0].tables[0].rows[1].cells[1];
numCell.value = 1.23;
numCell.fontSize = 15;
numApp.activate();
} catch(e) {
e.message;
}
Like the AppleScript example above, you can open file testnumbers.scpt
in Script Editor and click the Run button. Or you can run it from the
command line:
osascript testnumbers.scpt
Here's an example Pascal program that can run any of the example script files.
program TestScriptFile;
{$mode objfpc}{$H+}
{$modeswitch objectivec2}
uses
SysUtils,
{$IFDEF NoCocoaAll}
Foundation;
{$ELSE}
CocoaAll;
{$ENDIF}
function RunScript(const ScriptFName : string;
out ErrorMsg : string) : Boolean;
var
ScriptURL : NSURL;
ScriptObj : NSAppleScript;
ErrorInfo : NSDictionary;
EventDesc : NSAppleEventDescriptor;
begin
Result := False;
ErrorMsg := '';
if (Copy(ScriptFName, 1, 5) = 'http:') or
(Copy(ScriptFName, 1, 6) = 'https:') then
ScriptURL := NSURL.URLWithString(NSSTR(PAnsiChar(ScriptFName)))
else
ScriptURL := NSURL.fileURLWithPath(NSSTR(PAnsiChar(ScriptFName)));
ScriptObj := NSAppleScript.alloc.initWithContentsOfURL_error(
ScriptURL, @ErrorInfo);
if not Assigned(ScriptObj) then
begin
ErrorMsg := 'Unable to load script:'#10 +
NSString(ErrorInfo.objectForKey(NSAppleScriptErrorMessage)).UTF8String;
Exit;
end;
try
EventDesc := ScriptObj.ExecuteAndReturnError(@ErrorInfo);
if not Assigned(EventDesc) then
begin
ErrorMsg := 'Unable to execute script:'#10 +
NSString(ErrorInfo.objectForKey(NSAppleScriptErrorMessage)).UTF8String;
Exit;
end;
if (EventDesc.stringValue.UTF8String <> '') and
(EventDesc.stringValue.UTF8String <> 'true') then
begin
ErrorMsg := 'Error executing script:'#10 +
EventDesc.stringValue.UTF8String;
Exit;
end;
Result := True;
finally
ScriptObj.release;
end;
end;
var
ErrorMsg : string;
begin
if ParamStr(1) = '' then
begin
WriteLn('No script file specified.');
Halt(1);
end;
if not RunScript(ParamStr(1), ErrorMsg) then
begin
WriteLn(ErrorMsg);
Halt(1);
end;
end.
After compiling testscriptfile.pas, you can use this program to run a script from the command line like this:
./testscriptfile testnumbers.applescript
Here's a fragment of code from the testscriptembed.pas example that shows how it's done to control the Pages app:
ScriptStr :=
'try'#10 +
' tell application "Pages"'#10 +
' log name & " " & version'#10 +
' set pagesDoc to make new document'#10 +
' tell pagesDoc'#10 +
' set size of body text to 15'#10 +
' set body text to "Howdy there!"'#10 +
' end tell'#10 +
' activate'#10 +
' end tell'#10 +
'on error errMsg number errorNumber'#10 +
' return errMsg'#10 +
'end try';
ScriptObj := NSAppleScript.alloc.initWithSource(NSSTR(PAnsiChar(ScriptStr)));
The first step is to create a header file for the app you want to script. For example, to create Numbers.h, enter this at the command line:
sdef "/Applications/Numbers.app" | sdp -fh --basename Numbers
The resulting Numbers.h header file can then be used with Objective C and Swift code.
Since Pascal can't work directly with a header file, you also have to create an interface unit by parsing the header file. You can use the framework parser from here to do that.
Finally, since the framework parser typically doesn't do a perfect job, you'll probably need to manually tweak the resulting Pascal interface unit to get it to compile. This has already been done for you with Numbers, Pages and Keynote; Pascal interface units for those apps are included with the examples.
Here's the Numbers example as Pascal (testnumbers.pas):
program TestNumbers;
{$mode objfpc}{$H+}
{$modeswitch objectivec2}
uses
SysUtils,
{$IFDEF NoCocoaAll}
Foundation,
{$ELSE}
CocoaAll,
{$ENDIF}
ScriptingBridge,
Numbers;
var
NumApp : NumbersApplication;
NumDoc : NumbersDocument;
NumCell : NumbersCell;
begin
try
NumApp := SBApplication.applicationWithBundleIdentifier(NSSTR('com.apple.iWork.Numbers'));
if not Assigned(NumApp) then
begin
WriteLn('Unable to start Numbers.');
Halt(1);
end;
{Output app's name and version}
WriteLn(NumApp.name.UTF8String, ' ', NumApp.version.UTF8String);
{Create and add a document}
NumDoc := class_createInstance(
NumApp.classForScriptingClass(NSSTR('document')), 0).
initWithProperties(NSDictionary.dictionaryWithObject_forKey(
NSSTR('My Doc'), NSSTR('name'))).autorelease;
NumApp.documents.addObject(NumDoc);
{Change sheet's tab}
NumbersSheet(NumDoc.sheets.objectAtIndex(0)).setName(NSSTR('My Sheet'));
{New document will contain one sheet with one table. Get second cell
of second row in table.}
NumCell := NumDoc.sheets.objectAtIndex(0).tables.objectAtIndex(0).
rows.objectAtIndex(1).cells.objectAtIndex(1);
{Set cell's properties}
NumCell.setValue(NSNumber.numberWithDouble(1.23));
NumCell.setFontSize(15);
{Move app to foreground}
NumApp.activate;
except on E:Exception do
begin
WriteLn(E.Message);
Halt(1);
end;
end;
end.
macpgmr (at) icloud (dot) com
First posted Feb. 20, 2018; last edited Feb. 20, 2018.
Code syntax highlighting done with highlight.js.