[NOTE: This trick works on 10.5 and 10.6 only. Please see this post for an alternative solution to progress bars in applets.]
I noticed the other day that AppleScript support in Automator is full Studio-level, and can call methods, access user defaults, load nibs etc. This is quite cool, though most of it only works for applets since otherwise it will be trying to access properties or resources of the Automator Runner. But it got me thinking: is there any way to do this sort of stuff from within a normal applescript? The solution was obvious: tell Automator Runner to do it.
Unlike Automator Runner, the AppleScript Runner is smart enough to access the script bundle rather than itself, so with the combined power of both it becomes easy to, say, add a nib for a progress window to a script bundle and load it up when the script runs. AppleScript Editor is also far nicer to work with than Automator since (among other advantages) it won’t delete all the bundle resources when you save.
So I’ve put together two little demos, just for the sake of example (they don’t do anything useful and you probably shouldn’t try to modify them to be useful):
Lame Demo is a script bundle which you can add to your script menu. It takes a selected mp3 file from iTunes and compresses it with lame, showing a progress bar as it goes.
Upload Demo is an applet. It shows a login dialog to connect to your own computer via ftp (you need to enable ftp in your sharing prefs) and uploads a file, again showing a progress bar (though you’ll need a large file to actually see the progress bar move).
Both scripts also include explanations about displaying progress for shell commands. The nib files are free for anyone to use in their own scripts.
Holy crap gannet. Thanks for showing how to use nibs with applescripts. That’s awesome.
But even without needing a nib, this is extremely useful for just accessing objective-c using “call methods”. I’ve wanted to do this for a long time. By playing with your demos I found that “AppleScript Runner” understands “call methods” (of course Automator Runner does too). This is amazing! Here’s a quick example…
Note: I’m using 10.6 so I can’t say if this works in earlier OS versions. If you find it does please share the info.
set thePath to "path/to/someFile.txt"
tell application "AppleScript Runner"
set fullName to call method "lastPathComponent" of thePath
set fileName to call method "stringByDeletingPathExtension" of fullName
quit
end tell
display dialog "The file name is: " & fileName
Yup, AppleScript Runner has the full ASKit dictionary too but actually can’t do most of the stuff :(. Method calls are (for the most part) fine, though there probably wouldn’t be any advantage to using it instead of Automator Runner (neither program is available on 10.4).
After learning and experimenting more, I’ve updated the demos with a little more info. The login window has some new features but more fun than that, I’ve ditched the Automator Applet for a good ol’ AppleScript Applet after realising it was easy to bring windows to the front when clicking the dock icon.
For nibs though, I thought it might be good to have a collection of useful generic windows, such as these login and progress windows, so feel free to share ideas/nibs for other windows.
Just for fun, here’s a silly little remote chat script. You can’t normally display dialogs on remote machines but Automator Runner is happy to:
set remote to text returned of (display dialog "Connect to:" default answer "")
set myname to computer name of (system info)
set AR to application "Automator Runner" of machine remote
set them to ""
tell application "Automator Runner" to try
repeat
activate
set you to text returned of (display dialog remote & ": " & them default answer "")
tell AR
activate
set them to text returned of (display dialog myname & ": " & you default answer "")
end tell
end repeat
on error
quit AR
quit
end try
I just think this is the coolest thing I have seen in a while. Thank you gannet so much for putting this together and sharing it.
I have a fair number of scripts that I would like to add progress bars to, but they do not use underlying UNIX commands that write a log file with a nice percentage number to use for the display. (SFTP and some proprietary installers, I am looking at you).
I modified the example code gannet provided to create a generic handler for displaying a progress bar. You feed the handler your estimate in seconds of how long you thing a task may take, and the handler will display a bar for that long. You also feed the handler the PID of the process you are waiting for and it will keep on displaying the bar until the process completes, even if it is longer than your estimate.
NO one complains if a progress bar disappears too early, and they frequently stay pegged at 100% so I figured an estimate approach was better than nothing.
Apologies, my code is not as economical as many who post here, but this seems to work.
Don
(* based on code by gannet*)
set pid to do shell script "sleep 10 &> /tmp/sleep.log & echo $!"
showProgressEstimate("hello", 6, pid, "sleep.log")
on showProgressEstimate(MyName, estTime, procID, logName)
set delayInterval to (estTime / 100)
tell application "Automator Runner" to try
set MyBundle to call method "bundleWithPath:" of class "NSBundle" with parameter (POSIX path of (path to me))
-- The MainMenu nib contains just the basic edit and window commands, disabling Automator Runner's Quit and Hide
load nib "MainMenu" in bundle MyBundle
-- Since all our windows are owned by Automator Runner, it's a good idea to activate whenever displaying something
activate
-- Windows are quickly unloaded if they are not used. Prepare any data for the initial setup of a window before loading the nib.
--set mp3Icon to load image mp3Icon
load nib "ProgressWindow" in bundle MyBundle
tell window "progress"
set title to MyName
set content of text field "upper" to "working..."
set content of progress indicator "bar" to 0
show it
repeat with i from 0 to (estTime * 10)
delay 0.1
-- Buttons use the "on off" type so you can test if they were clicked. They may also be set to automatically hide the window.
if state of button "cancel" is 1 then
do shell script "kill -9 " & procID & "; rm /tmp/" & logName & " &"
error number -128
end if
set p to (100 / (estTime * 10)) * (i + 1)
set content of progress indicator "bar" to p
end repeat
-- optionally, you can slightly animate bar if process takes longer than the estimate
repeat while (do shell script "ps o pid= -p " & procID & " &") contains procID
set content of progress indicator "bar" to 99
delay 0.5
if state of button "cancel" is 1 then
do shell script "kill -9 " & procID & "; rm /tmp/" & logName & " &"
error number -128
end if
set content of progress indicator "bar" to 100
end repeat
do shell script "rm /tmp/" & logName
hide it
end tell
on error err number num
activate
-- Note: Alerts and dialogs displayed by Automator Runner always remain in front of all other windows.
-- If this is undesirable you will need to make sure the dialog is displayed outside of Automator Runner.
if num is not -128 then display alert MyName & " encountered an error." message err as warning
end try
end showProgressEstimate
I’m generally against estimating but I realise sometimes there simply isn’t any other way. Your script looks good - just one comment: The log file isn’t being read anywhere so you may as well do away with it. Pipe the command to /dev/null and remove the commands to delete the log (just put “kill -9 " & procID & " &”).
For your own situations though, have you tried using curl instead of sftp? It should work just like in the Upload Demo.
For installers, if you know the size of the payload and where it’s installing it, what you can do is just check the current size of the install with the “du” command.
set installSize to installSize / 100 / 512 -- du command returns size in 512-byte blocks
repeat while (do shell script "ps o pid= -p " & pid & " &") contains pid
delay 0.1
if state of button "cancel" is 1 then
do shell script "kill -9 " & pid & " &"
error number -128
end if
if frontmost of me then activate
set p to (word 1 of (do shell script "du -s " & installPath)) / installSize
set content of progress indicator "bar" to p
end repeat
I agree, I know using a guess is an ugly hack, but since this script will be used by only departments in the same building on the the network, I plan to do some timing tests and then under estimate to make sure the progress bar is not needlessly on screen. And I feel showing something instead of just dead air while a file uploads for 45-60 seconds is better than nothing. And this hack is just too cool not to use.
I would much rather use curl than SFTP, but whenever it does not seem to work on the standard install on OS X 10.6 for SFTP. I have also not been able to make SCP work. I know enough UNIX to get around, but I am not an expert.
Oh, you’re right. Built-in curl doesn’t support sftp while sftp and scp don’t output progress info
If you really wanted, you could build curl+libssh2 yourself and bundle the binary with the script…
Sure, you can see the upload demo for one example. Here’s a slightly different (simpler) one:
tell application "Automator Runner"
set Myself to application (path to me as text)
set MyBundle to call method "bundleWithPath:" of class "NSBundle" with parameter (POSIX path of (path to me))
load nib "MainMenu" in bundle MyBundle
activate
load nib "LoginWindow" in bundle MyBundle
tell window "login"
set content of text field "prompt" to "Enter your name and password."
show it
repeat
repeat while state of button "ok" is 0
delay 0.1
if state of button "cancel" is 1 then error number -128
if frontmost of Myself then activate
end repeat
if content of text field "username" is "name" and content of text field "password" is "pass" then exit repeat
display alert "You entered an invalid username or password." message "Please try again." attached to it
set state of button "ok" to 0
end repeat
hide it
end tell
end tell
Note there are some bindings set up the nib: The text fields will be disabled while the state of “ok” is 1 (ie, it’s been clicked) and will be enabled again as soon as you set the state back to 0. Also, if you want to use the checking indicator (see the upload demo), the text’s visibility is tied to the indicator so you only need to set the visibility of the indicator and the text will go with it.
I just tested this out on 10.5 - the whole concept works fine although the Lame Demo doesn’t work because the lame binary I included requires 10.6. For the Upload Demo I just had to make a small change: Getting properties of ‘me’ (eg, name, frontmost) doesn’t seem to work so instead I set a variable ‘Myself’ to ‘application (path to me as text)’ and get properties of that instead (see the code in my post above).
That’s great! I had looked around a bit for something like Automater Runner that I could use as a ready proxy for this kind of thing. And I don’t think I would’ve ever thought of using the load nib to disable the app’s standard menus.
However, if it is based on the ASS Libraries, will this technique still work on 10.7? A quick look with Activity Monitor seems to indicate the answer is no, it is not based on ASS. Anyone know how to tell definitively if this is the case?
Yup, Automator Runner does use the AppleScriptKit framework, which I believe is the ASS library. Apple isn’t in the habit of just removing frameworks like this though, so unless you’ve heard evidence to the contrary then it should be safe to assume ASS will not stop working in 10.7. I am worried that Xcode 4 won’t support ASS development though…
I just discovered a new shell trick. Some commands, such as cp, support SIGINFO. cp doesn’t normally print progress information but it will if you send it this signal. Here’s an example of how to use this with the progress window.
[edit] This actually prints progress on a per-file basis, so unless you’re only copying one file it’s probably not a good idea.
set pid to do shell script "cp " & theSource & " " & theTarget & " &> /tmp/cp.log & echo $!"
repeat while (do shell script "ps o pid= -p " & pid & " &") contains pid
(* Tell cp to print its current progress by sending it a SIGINFO. This will take a moment so doing it before the delay
should ensure the info is written by the time we come to read it. Note: backgrounding a command by appending it
with "&" is useful for ignoring errors (the exit status). For kill commands, it's possible our pid has already finished by
the time we send it a signal. This isn't a problem except that kill will return an error, so we make sure it is ignored. *)
do shell script "kill -INFO " & pid & " &"
delay 0.1
if state of button "cancel" is 1 then
do shell script "kill " & pid & " &"
error number -128
end if
set p to last word of (read POSIX file "/tmp/cp.log")
set content of progress indicator "bar" to p
end repeat
I think that’s a fair guess. OTOH, if any APIs is uses stop working – and quite a few APIs have been deprecated in the move to 64-bit – it’s probably safe to assume AppleScriptKit won’t be reworked to solve the problem.
set oldDelims to AppleScript's text item delimiters
set text item delimiters to "time="
tell application "Automator Runner"
snip / snap
repeat while (do shell script "ps o pid= -p " & pid & " &") contains pid
delay 0.5
if state of button "cancel" is 1 then
do shell script "kill " & pid & " &"
error number -128
end if
if frontmost of Myself then activate
try
set ffmpegLog to (read POSIX file "/tmp/ffmpeg.log")
set fileDurationHr to (word 2 of (paragraph 23 of ffmpegLog)) * 60 as number
set fileDurationMin to (word 3 of (paragraph 23 of ffmpegLog)) * 60 as number
set fileDurationSec to word 4 of (paragraph 23 of ffmpegLog) as number
set totalDurationSec to fileDurationHr + fileDurationMin + fileDurationSec
set percentageCorrection to 100 / totalDurationSec
set ffmpegProgress to paragraph -2 of ffmpegLog
set progressSec to word 1 of text item 2 of ffmpegProgress as number
set progressPercentage to progressSec * percentageCorrection + 1
set content of progress indicator "bar" to progressPercentage
end try
end repeat
snip / snap
end tell
set AppleScript's text item delimiters to oldDelims
reason for edit
The position of the time value is inconsistent, for example each additional output file results in +3 words.
changed: set progressSec to word 15 of ffmpegProgress as number to: set text item delimiters to “time=”
set progressSec to word 1 of text item 2 of ffmpegProgress as number
Wow! this has amazing potential! I started working on reating my own nib file and ran into a problem immediately…
About Xcode 4 removing ASS development support… it’s been done already. Try opening “ProgressWindow.nib” with Xcode 4 and watch it extract all AppleScript code, giving you the message:
:o
Then, after saving it (which you would have to do if making your own nib) and running one of the demos again, an alert comes up with the message:
Just when a whole new horizon was opened to me, not 2 minutes into my glee, all doors came crashing down… Aesthir wipes away a tear
Sadly this is not possible with Xcode 4, you will need Interface Builder 3. Whether or not you can keep running IB3 with your Xcode 4 install, I don’t know (I think you can but I’ve held off on Xcode 4 just in case).
It is possible to edit the nib’s xml and add in the AppleScript data by hand but I really wouldn’t recommend this :rolleyes:
[edit] Wait, I lie. It is possible to do it without AppleScript data, you just need to reference everything by index. Eg, “window 1” etc. It’ll just be a little confusing if you have multiple items of the same type and aren’t sure which is 1 and which is 2.
Aww… that’s too bad… Let me get this straight to see if I understand you. You say creating nib files that are controlled by AppleScript code can be done only if you reference every object/element by index? So, is this the same as using “System Events” to gui script an unscriptable application? I actually do this all the time but even when I do, I never use ids unless I absolutely have to.
in my experience, referencing by index alone causes problems when two or more items of the same type are randomly loaded every run, or when different window configurations contain different numbers of the element you are trying to target (changing inde numbers constantly). Sure there are ways around it by doing lots of tests, but that’s a bunch of unnecessary coding that causes a performance penalty.
Also, even adding all AppleScript code manually (which I have no idea how to do and wouldn’t want to know how) to a nib file wouldn’t work I’ll bet. Xcode 4 strips out all AppleScript as soon as the nib is opened, there’s no choice or cancel button. You can never open with Xcode I guess but how long until OSX won’t even run these nibs when created with interfact builder 3.
Like I’ve said, it’s too bad as not everyone knows or wants to learn [obj-]C[++]. Does anyone know why AppleScript support was dropped? Also, is there a way to do the same or something similar with shell scripting? Just wondering…
Yes, working with UI in AppleScript Studio is just like System Events, the difference being you can reference elements by their AppleScript name if they’ve been assigned one in Interface Builder 3. The issues with referencing by index shouldn’t really be a problem here though - you create the nib yourself and are in full control of it with your script. Things aren’t going to be changing around without you knowing
AppleScript Studio development is no longer supported but support for running ASS apps is not going to go away any time soon (Automator still relies on AppleScriptKit). AppleScriptObjC is Apple’s way forward now - this is still very much AppleScript but includes an Objective-C bridge to allow full access to cocoa frameworks.
And no, you can’t use UI with shell scripts without some sort of command or framework designed to do so (like osascript ;))
Can you not just install IB3?