Progress bars, login dialogs and more...

[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):

  1. 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.

  2. 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.

Very neat!

Thanks! I think this is quite cool stuff and the potential here is pretty big. I’d be very interested to get more feedback on people’s thoughts :slight_smile:

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


Glad you like it!

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 :confused:
If you really wanted, you could build curl+libssh2 yourself and bundle the binary with the script…

At a certain point, this falls into the “when your splashing around with alligators you forget the original mission was to drain the swamp” territory.

Do you have any demo code for the login window dialog?

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?

Simon.

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.

Here’s one for ffmpeg:


	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! :smiley: 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

Anyone know a way around this?

Aesthir

Hey Aesthir,

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…:frowning: 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…

Aesthir

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 :wink:

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?