OK, one for the Mac users. Continuing the theme of user interfaces, here’s a simple but powerful way of using AppleScript to create a user interface for your Python programs and shell scripts and sending the results to just about any application installed on your Mac.
This solution has the advantage over Python’s native Tkinter in that the development time is much faster, and uses the speech synthesis features of OS/X to make your code much easier to use for the non-technical, elderly or visually impaired.
AppleScript can appear quite wordy and imprecise to anyone used to programming languages such as Python or C, but the fact that it can be written to read like English can make it very easy to understand what’s going on, though it can make the grammar difficult to learn, particularly for non-English speakers. It is in fact a true programming language and if you take the time to learn its more advanced features, AppleScript will reward you with some quite powerful tools, allowing your Python programs, scripts and shell files to interact in some very useful ways with commercial applications on your Mac.
The idea behind AppleScript is a good one, but it’s a shame it only works on OS/X. It’s a very high level programming language designed to manipulate not just constants, Booleans, integers, reals, strings (called text), lists, dictionaries (called records) and files, but also commercial applications from different vendors, which can be shepherded and wrangled to cooperate in any way you can devise. With its ability to interact with most applications, as well as your own programs and shell scripts, it should be thought of as a comprehensive top-level platform for bolting together applications, programs and shell scripts for complex task automations that can be astonishingly sophisticated. But only on a Mac. (1)
This post describes a way of using AppleScript to create a graphical user interface (GUI) that interrogates the user via a series of simple dialogue boxes, passing your user’s answers as command line flags to your Python program or script, then using AppleScript’s powerful application control features to direct the output of your program to the application of your choice. In this way, it uses not only AppleScript’s powerful application management tools, but some of its programming language features too.
This method has the advantage over Python’s default user interface Tkinter in that the development time is much faster, it is not limited to working with Python and that it can save the results to (almost) any file format that your Mac will recognise.(2) Unlike Tkinter though, it doesn’t allow you to create a single panel dashboard to select all your program options at once. Instead, the trick is to present your program options to the user as a series of sequential, guided questions.
This may sound clunky, but it’s a great solution if you want to access the power of OS/X to:
-
-
- Create a GUI quickly,
- Get a great deal more access to your Mac’s operating system features and applications than you could just from just using Python,
- Provide your users with spoken instructions,
- Allow people who are technically challenged, elderly or visually impaired to use your program,
- Save your results to a choice of MS Word, email message, TextEditor or any writeable Mac application that AppleScript can open.
-
Here are some screenshots from a demonstration I wrote as a user interface for the Markov random text generator program, which was used as the back-end logic for a previous Tkinter GUI post. Once the template was created, this simple user interface was put together in about half an hour.
I’ve programmed it to use Scottish Fiona as the default voice, but you can turn her off, or change her to any of the available Mac voices. The audio instructions to the user are quite conversational, and the OS/X speech generators sound quite natural.
Suggested defaults can be offered for any variable to be set.
Once the output application is chosen, the program runs, then opens the application the user chooses, copies the output to a new document, giving the user the chance to name or discard it:
The Code
Here is the AppleScript code that was used to create the above user interface:
(* This AppleScript utility creates a user interface with optional voice directions for any
command line-launchable program or script. This demonstration has been written to
launch a Python program that uses Unix-type command line flags/switches, but it
can be adapted to launch any program in the same directory, or any command
recognisable by the operating system. It will guide your user through setting
the options for your chosen program, options which would normally be set at the
command line.
All the lines you need to edit have been marked with '-- XX' for fast GUI development.
Author: author: matta_idlecoder at protonmail dot com
*)
global DebuggingIt, SayingIt, userCanceled
set SayingIt to false
set DebuggingIt to false
set userCanceled to false
--=============CHOOSE THE COMPILER AND PROGRAM YOU'RE RUNNING:
-- change to your scripting language of choice, or leave blank for a shell script-- XX:
set Compiler to "python " -- XX
set YourProgName to "Markov.py" -- Change to the filename of your code or script. -- XX
set homeFolder to (POSIX path of ((path to me as text) & "::"))
set YourProgNameAsPOSIX to homeFolder & YourProgName
set CommandLineText to Compiler & "\"" & YourProgNameAsPOSIX & "\""
set AudioPhrase to "" -- Std string initialisation
--=============HANDLERS/FUNCTIONS:================================
--===== General purpose GUI functions I've written to tailor AppleScript widgets for most situations.
--===== PickAFile is the only one that might need changed:
to InteractWithUser(Comment, Choice)
if DebuggingIt then display dialog Comment & Choice
if SayingIt then say Comment & Choice
end InteractWithUser
-- Widget offering mandatory choice between two, 1st is default, no cancel offered:
to PickAButton(Question, FirstChoice, DefaultChoice)
set AnswerText to "None"
if SayingIt then say Question
set DialogAnswer to display dialog Question buttons {FirstChoice, DefaultChoice} default button 1
set AnswerText to button returned of DialogAnswer
InteractWithUser("", AnswerText)
return AnswerText
end PickAButton
to PickFromList(ListInstruction, ListOfItems, DefaultItem) -- Widget offering list of choices, with cancel offered
set ListPick to "None"
if SayingIt then say ListInstruction
set ListResult to choose from list ListOfItems OK button name "Choose" with prompt ListInstruction default items DefaultItem
if ListResult is false then -- user hit cancel. Historical quirk of 'choose from list'. See AppleScript LanguageGuide 2013 p148
set userCanceled to true
else -- ListResult is not false. Note that there's a result, not that it's true. See LanguageGuide p148
set ListPick to item 1 of ListResult -- 'choose from list' always returns a list, even of one item, if Cancel is not hit.
end if
InteractWithUser("", ListPick)
return ListPick
end PickFromList
to GetUserText(UserQuestion, defaultText) -- Widget that asks the user for input
set UserAnswer to ""
repeat until userCanceled or (UserAnswer is not "")
try
if SayingIt then say UserQuestion
set BoxAnswer to display dialog UserQuestion default answer defaultText
set UserAnswer to the text returned of BoxAnswer
InteractWithUser("", UserAnswer)
on error number -128 -- user hit cancel
set userCanceled to true
end try
end repeat
return UserAnswer
end GetUserText
to PickAFolder(Instruction, DefaultPath) -- Widget that asks the user to choose a folder, offering a default and Cancel
set FolderPathString to ""
try
if SayingIt then say Instruction
set ChosenFolder to choose folder with prompt Instruction default location DefaultPath
on error number -128 -- user hit cancel
set userCanceled to true
end try
if not userCanceled then
if DebuggingIt then display dialog "The HFS path of the chosen folder is: " & ChosenFolder
set PosixFolder to POSIX path of ChosenFolder
set FolderPathString to "\"" & PosixFolder & "\""
if DebuggingIt then display dialog "The POSIX path of the chosen folder is: " & FolderPathString
end if
return FolderPathString
end PickAFolder
to PickAFile(Instruction, DefaultFolderPathAlias) -- Widget that asks the user to choose a file, offering a Cancel
set FilePathString to ""
try
if SayingIt then say Instruction
-- In the next line, you need to insert the correct UTI type for the files you want to open. This can be a list of file types.
-- To open everything in the chosen folder, omit the <of type {""}> command:
set ChosenFile to choose file with prompt Instruction of type {"public.plain-text"} default location DefaultFolderPathAlias --XX
on error number -128 -- user hit cancel
set userCanceled to true
end try
if not userCanceled then
if DebuggingIt then display dialog "The HFS path of the chosen file is: " & ChosenFile
set PosixFile to POSIX path of ChosenFile
set FilePathString to "\"" & PosixFile & "\""
if DebuggingIt then display dialog "The POSIX path of the chosen file is: " & FilePathString
end if
return FilePathString
end PickAFile
to PickFiles(Instruction, DefaultFolderPathAlias) -- Widget that asks the user to choose multiple files, offering a Cancel
set FilePathString to ""
try
if SayingIt then say Instruction
-- In the next line, you need to insert the correct UTI type for the files you want to open. This should be a list of file types,
-- using the Apple definition of UTIs. To open everything in the chosen folder, omit the <of type {""}> command:
set ChosenFiles to choose file with prompt Instruction of type {"public.plain-text"} default location DefaultFolderPathAlias with multiple selections allowed
on error number -128 -- user hit cancel
set userCanceled to true
end try
if not userCanceled then
repeat with FileAlias in ChosenFiles --<====== how you iteratate through a list in AppleScript
if DebuggingIt then display dialog "The HFS path of the chosen files are: " & FileAlias
set PosixFile to POSIX path of FileAlias
set FilePathString to FilePathString & "\"" & PosixFile & "\" " --<==== note the space after the second quote
end repeat
if DebuggingIt then display dialog "The list of POSIX paths of the chosen files is: " & FilePathString
end if
return FilePathString
end PickFiles
to SendOutputTo(ChosenApp, DocBody, DocName) -- Sends the results to the user's chosen app
set the clipboard to the DocBody
tell application ChosenApp
if SayingIt and not DebuggingIt then -- for a faster debug
delay 0.5
say "Here are the results of your analysis."
end if
run
activate
delay 1
if (ChosenApp is "Microsoft Word") then delay 2
if ChosenApp is "Mail" then
tell application "Mail"
set theMessage to make new outgoing message with properties {visible:true, subject:DocName, content:DocBody}
tell theMessage
make new to recipient with properties {name:"Matt", address:"matta_idlecoder at protonmail dot com"}
--send --<===== this line will send the email automatically.
end tell
end tell
else
tell application "System Events" to keystroke "n" using command down -- cmd-n = open new document
tell application "System Events" to keystroke "v" using command down -- cmd-v = paste the clipboard contents
end if
end tell
end SendOutputTo
to ChooseOutputAppFor(Results, Title) -- Widget that asks the user to choose an output app
set OutputApp to ""
try
set OutputQuestion to "Which application do you want to send the results to?"
if SayingIt then say OutputQuestion
set OutputAppAnswer to choose from list {"TextEdit", "Microsoft Word", "Notes", "Mail"} OK button name ¬
"Select" with title "Output file type" default items {"TextEdit"} with prompt OutputQuestion
if OutputAppAnswer is not false then -- user didn't hit cancel, so extensionResult is not false.
-- Note that this means there's a result, not that it's true. See LanguageGuide p148
set OutputApp to item 1 of OutputAppAnswer
InteractWithUser("OK. I'm now saving to a new file of type ", OutputApp)
SendOutputTo(OutputApp, Results, Title)
end if
on error number -128 -- user hit cancel
set userCanceled to true
end try
end ChooseOutputAppFor
--==================================MAIN========================================= --XX
(*
NOTE 1: The code blocks below are used to create sequential, simple GUI input boxes to set most command line variables
your program might need. For each block you decide to use, you only need to modify the lines marked '-- XX'.
NOTE 2: The user interface that you write here for your program needs to take into account all the mandatory command line
flags/switches your program has, plus any optional flags/switches you want to set.
NOTE 3. As the user answers the questions, a valid command line for your program is assembled.
NOTE 4. Finally, before your program is run, the user is asked which application they want to send the output of your program to.
*)
--=========PICK AND MODIFY THE QUESTIONS BELOW TO CREATE THE COMMAND LINE FOR YOUR PROGRAM:
-- Ask if the user wants audio instructions for your program and, if so, play an audio intro for the user:
set FionaAnswer to PickAButton("Do you want Fiona to talk you through it?", "No, thanks.", "Yes, please.")
if FionaAnswer is "Yes, please." then
set SayingIt to true
say "Hi, I'm Fiona. I'm going to walk you through a Markovian random text generation exercise, using the word chains from multiple input text files to create random, grammatically correct but meaningless English."
set AudioPhrase to "I'm now running a "
end if
--== Ask the user to pick a text option from a list, offering a default:
if userCanceled is false then
set UsersText to PickFromList("What Order of Markov do you want to run?", {"First", "Second", "Third", "Fourth"}, "Second") -- XX
if (UsersText is "First") then -- XX
set Order to 1
else if (UsersText is "Second") then
set Order to 2
else if (UsersText is "Third") then
set Order to 3
else if (UsersText is "Fourth") then
set Order to 4
end if
set AudioPhrase to AudioPhrase & UsersText & " order Markov, " -- XX
set CommandLineText to CommandLineText & " -o " & Order -- XX
end if
if (userCanceled is false) then
set UseStartSeq to PickAButton("What about a starting phrase? Do you want to pick one from one of the input files you plan to use?", "No", "Yes") -- XX
if UseStartSeq is "Yes" then
set start_text to GetUserText("OK, now please cut and paste a " & Order & " word phrase from one of your input files. This will be the start of the Markovian paragraph.", "Choose a random phrase.") -- XX
set NumWordsEntered to count words of start_text
--display dialog "The starting phrase you have entered is " & "'" & start_text & "'" & " of length " & NumWordsEntered & " words"
delay 1
if start_text is not "Copy and paste your starting phrase from one of the texts." then -- XX
if NumWordsEntered is equal to Order then
set AudioPhrase to AudioPhrase & " starting with the words you have chosen," -- XX
set CommandLineText to CommandLineText & " -s " & "\"" & start_text & "\" " -- XX
else
display dialog "Your Markov Order does not match the length of the starting phrase."
delay 1
return false
end if
else
set AudioPhrase to AudioPhrase & " from a random starting phrase, " -- XX
end if
end if
end if
--== For completeness, you should check your user input has the correct type and value range:
if userCanceled is false then
set UsersText to ""
set UsersText to GetUserText("How many words long should the output text be?", "300") as text
set AudioPhrase to AudioPhrase & " creating a " & UsersText & " word paragraph, " -- XX
set CommandLineText to CommandLineText & " -w " & UsersText & " " -- Needs an extra space if this is the last arg -- XX
end if
--===========SET COMMAND LINE TO TIME HOW LONG THE PROGRAM TAKES:
if userCanceled is false then
set TimingThis to PickAButton("Do you want to time your program, to see how long each stage of the analysis takes?", "No", "Yes") -- XX
if TimingThis is "Yes" then
set CommandLineText to CommandLineText & " -t " -- XX - if your program has a -t flag
end if
end if
--=== SELECT MULTIPLE FILES:
if userCanceled is false then
set FilePathsAsString to PickFiles("Ok, please select one or more text files to combine word sequences.", homeFolder) -- XX
if Order is greater than 1 then
set AudioPhrase to AudioPhrase & " by finding identical phrases of " & (Order) & " words in the input files, and repeatedly choosing the next word randomly from all the possibilities found. " -- XX
else
set AudioPhrase to AudioPhrase & " by finding identical words in the input files, and randomly choosing the next word from all the possibilities found. " -- XX
end if
set CommandLineText to CommandLineText & FilePathsAsString -- XX
end if
--===========SET AUDIO TO MENTION TIMER:
if userCanceled is false then
if TimingThis is "Yes" then
set AudioPhrase to AudioPhrase & " And I'm going to time it." -- XX
end if
end if
--===========RUN YOUR PROGRAM OR SHELL SCRIPT:
if userCanceled is false then
if SayingIt then say AudioPhrase
if DebuggingIt then display dialog "Sending this command to the cmd line:" & CommandLineText
set ProgramResult to do shell script CommandLineText
end if
if userCanceled is false then
ChooseOutputAppFor(ProgramResult, "The results are as follows: ")
end if
--=====================END OF MAIN========================
How to Run It
Copy the above code into your Markov folder on your Mac, save it as Markov.scpt and launch Apple’s Script Editor (just double-click on the file). The AppleScript editor should open the script and highlight all the AppleScript syntax correctly:
Alternatively, create a new folder containing Markov.py from the earlier post, saving the AppleScript program into the same folder. You should also make sure you have some texts to serve as the input files to build your Markov word chains. For example, if your texts are Ivanhoe and The Odyssey, your folder contents should contain at least these four files:
Markov.scpt Markov.py Ivanhoe.txt Odyssey.txt
To launch the user interface for your program, double-click on the Markov.scpt file and your Mac should reload the AppleScript code into Apple’s Script Editor. Run it by clicking on the run arrow (the third button) at the top of the editor.
How It Works
-
-
- I’ve created wrappers for several different AppleScript user-input widgets, with self-explanatory names like PickAButton, PickFromList, GetUserText, PickFIles. These can be called in any order in your own user interface to build your input variable list. More complex widgets such as Tkinter’s sliders and radioboxes are not available.(3)
- The trick is that after each user answer, both the summary spoken AudioPhrase and the CommandLineText are incrementally assembled, so that they are ready to go when your code is called. The AudioPhrase gives the user an audible summary of what is about to happen just before it calls your code. The CommandLineText makes it happen.
- I’ve used it to run a Python program above, but as far as the AppleScript code is concerned, it is simply launching a shell script. The code above has been generalized, with comments explaining what is happening.
-
If you follow the instructions in the code, you should easily be able to adapt it to serve as a user interface for most shell or Python scripts you have. This should also give your program access to the rich feature set of OS/X, offering voice guidance to users, and allowing your program to direct its output to most applications installed on your Mac.
As always, feel free to borrow, adapt and use. Feedback and success stories welcome.
Notes
-
-
- The best guide to AppleScript is the Apple manual, which is quite good. Here’s a PDF of the 2008 version. And here’s an up to date online version. There are also many good websites with tricks and tips. The best one I find for any language is Rosetta Code. The AppleScript Rosetta Code page explains how to use it to solve typical programming problems and some common algorithms.
- I haven’t yet been able to get it to work with Apple’s own Pages application.
- There may be a way of creating Tk-style radioboxes, sliders, etc, in AppleScript, but I am unaware of it.
-