#pre (section zero)
all of a sudden, i wanted something strange: get track info in iTunes completely with keyboard. however, there's one little but annoying point: if you ever made it into "lyrics" tab and your cursor stuck in textarea -- adiós -- you'll never git it out of there without help of mouse. and so, being fed up with that, i finally took some time to deal with problem in top down manner.
##first try
first intension was to make simple AppleScript to interact with UI elements
.
take /Library/Scripts/UI Element Scripts/Probe Window.applescript
as an
example and you are good to go. here's first version of script:
tell application "System Events"
tell process "iTunes"
keystroke "i" using command down
tell front window
tell first tab group
tell first radio button
click
set focused to true
end tell
end tell
end tell
end tell
end tell
something strange happened to synchronization and tab sometimes would not become
selected and script would fall with error. window was not opening quick enough
to become the front
one before AppleScript reaches next tell
blocks.
simple delay between keystroke
and tell
fixed it:
repeat with n from 1 to 10
if (count of tab groups of front window) is not 0 then exit repeat
delay 0.1 -- wait for the info window to appear
end repeat
##second try
- put this together
- save as
~/Library/iTunes/Scripts/info (general).scpt
- go to System Preferences -> Keyboard pane -> Keyboard Shortcuts (right tab) -> Application Shortcuts (bottommost row)
- add shortcut for
- Application: iTunes
- Menu Title: "info (general)", without quotes
- Keyboard Shortcut: ⌥⌘I (Cmd-Alt-I) for example
now go back to iTunes, press ⌥⌘I and... after some time watching spinning
beach ball, get an error!
further investigation and tests revealed that while 'outside' scripts just
telling apps what to do, 'insiders' (e.g. those run from Scripts menu) are
executed by actually iTunes itself, and must obey its threading rules or
something. iTunes suspends scripts execution until Info window is closed.
which is why by the time we get to repeat
block, Info window already
destructed and we're back to the library.
so what do we do now? get it abstract: we need some tool to execute from
inside iTunes but be asynchronous so that it can interact with app while
in 'suspend' mode. more details: tool must be written in AppleScript
as
primary language, we are on the Mac OS X.
##launchd / launchctl
so what does it mean 'we are on the Mac OS X'? it means we have all the power
of launchd with us. basically it's a
stuff that makes all other applications in the system running and restarting
(in case of failure or something). launchd
is the 'daemon' running in
background, and launchctl
is a frontend to manipulate the former. once the
job is sent, launchd
executes it in background, giving you back your terminal
or whatever.
usually they are using property list
based configuration files to let developers specify tasks and rules for
starting them on different occasions. good enough, launchctl
also supports
syntax for sending jobs (tasks) right from command line, on-the-fly, no XML
needed.
so, if we send a job with launchctl
, we'll get control back to us right
away, so that AppleScript can exit immediately (since it's not much useful
in suspended mode). what's that task that we are sending to launchd
? right!
that's another AppleScript which actually does the job.
invocation of shell from AppleScript follows this syntax:
do shell script "..." & (quoted form of "some argument that needs to be escaped")
invocation of launchctl
, in its turn:
launchctl submit -l label [-p executable] [-o path] [-e path] -- command [args]
invocation of AppleScript from shell:
osascript [-e statement | programfile]
now the triangle's complete!
now it's time to do the dirty stuff. at first, i had an idea of putting all
necessary AppleScript in one -e
argument, but them i realized how crazy it
would look like, and how many escaping would need to be done.
##third try
conditional execution. if we are called without arguments -- it's iTunes,
then we fire ourself once more -- from launchd
-- this time with some dumb
argument just to be able to tell.
on run args
if (count of args) is 0 then
set path_to_me to quoted form of the POSIX path of the ((path to me) as string)
do shell script "launchctl osascript " & path_to_me & " run" -- 'run' is that one 'dump argument'
else
tell application "System Events"
...
end tell
end if
end run
good? not yet. if we read manual carefully, we could see this line:
This mechanism also tells launchd to keep the program alive in the event of failure.
but we don't want our script to re-start forever!
##fourth try
that's why i came up with a pretty convenient function launchonce
in my
.zshrc
. what it is doing is taking care of removing a job after it's done.
additionally it writes log file located at ~/Library/Logs/launchonce/
.
launchonce () {
local logdir=~/Library/Logs/launchonce
mkdir -p "$logdir"
while [[ -z "$logfile" ]] || [[ -a "$logfile" ]] ; do # get unique file name
local id="tk.ratijas.$1.$RANDOM"
local logfile="$logdir/$id.log"
done
local script=' "$@" ; launchctl remove "$0" '
1="$(/usr/bin/which "$1")"
launchctl submit -l "$id" -p /bin/sh -o "$logfile" -e "$logfile" -- /bin/sh -c "$script" "$id" "$@"
echo "launchd job id:$id"
echo "log:$logfile"
# to get log:
# launchonce ... | cut -d : -f 2-99
}
now include this function (with all double-quotes escaped) in do shell script
and replace launchctl
invocation with it, like this:
do shell script "
launchonce () {
local logdir=~/Library/Logs/launchonce
mkdir -p \"$logdir\"
while [[ -z \"$logfile\" ]] || [[ -a \"$logfile\" ]] ; do # get unique file name
local id=\"tk.ratijas.$1.$RANDOM\"
local logfile=\"$logdir/$id.log\"
done
local script=' \"$@\" ; launchctl remove \"$0\" '
1=\"$(/usr/bin/which \"$1\")\"
launchctl submit -l \"$id\" -p /bin/sh -o \"$logfile\" -e \"$logfile\" -- /bin/sh -c \"$script\" \"$id\" \"$@\"
echo \"launchd job id:$id\"
echo \"log:$logfile\"
# to get log:
# launchonce ... | sed '2p;d' | cut -d : -f 2-99
}
launchonce osascript " & path_to_me & " run"
save it, go to iTunes and give it a try!
#conclusion
Mac OS X is powerful. AppleScript is mighty. but launchd
rulez 'em all.
____ __ _ _
/ __ \____ _/ /_(_) (_)___ ______
/ /_/ / __ `/ __/ / / / __ `/ ___/
/ _, _/ /_/ / /_/ / / / /_/ (__ )
/_/ |_|\__,_/\__/_/_/ /\__,_/____/
me@ /___/ .tk
https://ratijas.tk/