On Xcode’s Rising Barrier to Entry

Though I don’t think Apple should charge for Xcode, $5.00 isn’t a lot of money to Cocoa developers… I suspect most would pay a lot more. It is a lot of money to someone who just wants to try a gem, to someone who has to justify a business expense, to someone who isn’t old enough for a credit card. It’s the act of throwing up such a huge barrier to a casual, interested user that is offensive.

Zach Holman in OS X Isn’t for Developers

Got comments? Use reddit and/or poke me on twitter.

Writing AppleScripts That Dynamically Target Either Safari or WebKit

If that title sounds eerily familiar, it’s because I stole it from Gruber. I did that because 1) I’m doing the exact same thing here, 2) there is a small bug in his solution, and 3) there’s a big ass bug in the processes collection in System Events’s dictionary, which combined, lead his solution to double failure.

The small bug is really simple, Gruber returns the name of _app. But name will return “Safari” for both Safari and WebKit. The property that we want is short name.

Now for the Fun Part

Gruber’s code, corrected to use short name, ends up as:

tell application "System Events"
    set _app to item 1 of (every process whose frontmost is true)
    return short name of _app
end tell

And that will generally work. Sometimes.

If we have both Safari and WebKit open, System Events gets confused. As a general rule, it’ll always return the process for the application which was started first. Here’s a quick example in Ruby.

First I started WebKit, then Safari, then ran this:

>> Appscript::app('System Events').processes['Safari'].short_name.get
=> "WebKit"

But that’s not the particular way in which System Events gets confused in Gruber’s script.

Superfluous Assignments Considered Harmful

It’s a big pet peeve of mine that people spend way too many lines of code assigning stuff to variables, instead of just doing whatever is needed to be done with the values right away. For once — however incidentally — I’m validated.

Here’s Gruber’s code, refactored yet again for brevity:

tell application "System Events" to set _app to the first process whose frontmost is true
get the short name of _app

When he gets the first process and assigns it to the _app variable, that’s where System Events can get confused and return the wrong process. Funny enough, this code works perfectly:

tell application "System Events" to get the short name of the first process whose frontmost is true

Somehow, skipping the intermediate assignment and going straight for the kill makes System Events behave sanely.

Now, I went through great lengths to confirm my observed behavior. I wrote a script that gets the short name for the process that is frontmost, testing it with different launch orders and varying the frontmost browser, using both code variants: with temporary assignment, and with direct access. I reformatted the output into the following pretty ASCII table:

+----------------+-----------+----------+----------+
| Launch order   | Frontmost | Directly | Assigned |
+----------------+-----------+----------+----------+
| Safari         |           | Safari   | Safari   |
| WebKit         |           | WebKit   | WebKit   |
| Safari, WebKit | Safari    | Safari   | Safari   |
| Safari, WebKit | WebKit    | WebKit   | Safari * |
| WebKit, Safari | WebKit    | WebKit   | WebKit   |
| WebKit, Safari | Safari    | Safari   | WebKit * |
+----------------+-----------+----------+----------+

The two entries marked with an * are where we the script returned the incorrect browser name.

TL;DR Already

So if you just love plain AppleScript and want a fix for Gruber’s framework, here’s his amended GetCurrentApp:

on GetCurrentApp()
  tell application "System Events" to ¬
    get short name of first process whose frontmost is true
end GetCurrentApp

Simple enough.

Ruby Roolz AppleScript Droolz

It wouldn’t be me without a Ruby solution, especially since all the past scripts I posted here were written in Ruby.

The following method will return an Appscript::Application for the preferred browser, according to similar rules as originally defined in Gruber’s article, but with a bit of extra smarts:

First, it’ll look at the frontmost app; if it’s one of the valid browsers (WebKit or Safari), it’ll return that. Then it’ll look into the running browser processes. It’ll return the running browser, giving preference to the default browser if both are running. If no valid browser is running, it’ll just return the default. It’ll only return a valid browser. If it has to fallback to the default browser and it is, say, Chrome, it’ll just return nil.

VALID_BROWSERS = %w[ WebKit Safari ]
def browser
  processes = Appscript::app("System Events").processes
  target,   = VALID_BROWSERS & processes[Appscript.its.frontmost.eq(true)].short_name.get
  target  ||= begin
    default_browser  = VALID_BROWSERS & [%x(VERSIONER_PERL_PREFER_32_BIT=yes /usr/bin/perl -MMac::InternetConfig -le 'print +(GetICHelper "http")[1]').chomp]
    running_browsers = VALID_BROWSERS & processes.short_name.get
    (running_browsers & default_browser)[0] || running_browsers[0] || default_browser[0]
  end
  Appscript::app(target) if target
end

Here’s the full thing as a standalone Ruby file.

Objective-C Supplement

In a follow-up article, Gruber talks about premature optimization, how shelling out to Perl is potentially expensive, but fast enough for practical purposes. He gives an execution time for his browser detection script of “less than 0.1 seconds”.

Now, I tend to agree with him. That’s fast enough. Hell, I write my stuff in rb-appscript, which has a significant overhead over compiled AppleScripts. But still, just for fun, here’s a solution that clocks in at ~0.023s on my machine.

It’s an Objective-C file, which has to be compiled. The compiler command is listed as a comment at the beginning of the source file. It relies on AppKit to get the frontmost process, and LaunchServices to get the default browser. If anything, LaunchServices should stand the test of time longer than Perl’s Mac::InternetConfig.

The semantics for this are pretty much identical to Gruber’s original GetCurrentApp/GetDefaultWebBrowser solution, without any of the extra smarts of my Ruby version, because writing Objective-C is nearly as boring as writing AppleScript, and this was just a gimmick anyway.

Next Up

In the next weeks I intend to post my Safari/WebKit appscript scripts so we can actually put my browser.rb to some use.

Got comments? Use reddit and/or poke me on twitter.

MacRuby solutions to the exercises in Aaron Hillegass’s “Cocoa Programming for Mac OS X, 3rd edition”

I have been writing the exercises in Aaron Hillegass’s Cocoa Programming for Mac OS X in MacRuby.

I’m up to chapter 24.

You can find the solutions on GitHub. Do pay attention to the README. Each application is in its own branch. You’ll find nothing in the master branch.

Got comments? Use reddit and/or poke me on twitter.

TextMate’s Go to Symbol, `#pragma mark`, Ruby, and you

In Objective-C code you’ll often see blocks of code preceded by something like this:

#pragma mark Foobar methods

That’s a directive that helps you organize code.

In Xcode and TextMate (and probably every other editor), you’ll find in the editor’s status bar a menu pop up that allows you to jump to symbols in your code. In TextMate you can also get a Go to Symbol window by pressing ⇧⌘T.

When you have #pragma marks directives in your Objective-C code, your methods appear grouped in the symbol pop up under the name of their preceding directive.

I find I naturally do the same in Ruby: I group related methods under a comment header. It would trouble me deeply, however, that TextMate did not know about this. So I fixed that.

As far as I know there’s no de facto official #pragma mark syntax in Ruby. I went ahead with the syntax I was already using. A comment starting with three hash characters:

### Foobar methods

Here’s how you do it:

  1. In TextMate, open the Bundle Editor using the Bundles menu, or by pressing ⌃⌥⌘B
  2. Expand the Ruby item, and find its language definition. A language definition is designated by the letter “L” in a gray circle. You’re going to edit this grammar. I find it easier to copy the entire thing into a new TextMate document, edit it, and paste it back into the Bundle Editor.

    Around the line 590 you should see the grammar rule for a line comment. Just search for name = 'comment.line.number-sign.ruby'; to get there. Before this line, add the following code:

    { name     = 'comment.line.number-sign.pragma-mark.ruby';
      match    = '(?:^[ \t]+)?(###) *(.*\S)[ \t]*$\n?';
      captures = { 1 = { name = 'punctuation.definition.comment.ruby'; }; };
    },
    
  3. Back in the Bundle Editor, under Ruby, create a new Preference. Preference items are those with the letter “P” in a gray circle. Name it “Symbol List: Pragma Comment”, and paste this bit into it:

    { showInSymbolList     = 1;
      symbolTransformation = 's/^\s*###\s+//; s/\s+$//';
    }
    
  4. Set the Scope Selector for this Preference to source.ruby comment.line.number-sign.pragma-mark.

  5. Close the Bundle Editor.

That’s it. Now try adding a comment starting with ### to a Ruby file and check the Go to Symbol menu to see if it’s there.

Got comments? Use reddit and/or poke me on twitter.

Script: Finder/New Plain Text File

  • Script: New Plain Text File
  • Application: Finder
  • Suggested key binding: ⌃⌘N

This script tackles a major oversight in the Finder UI; namely, the ability to create a new blank text file.

It behaves similarly to the Finder’s New Folder command: It defaults the name to “untitled.txt”, it enters rename mode immediately after creation, and it appends numbers to the name to avoid name clashes, so you can create many new files at once.


A TL;DR Interlude Spliced With Delicious Bits of Otherwise Irrelevant Code

require 'appscript'
require 'fileutils'
require 'pathname'

Now, there are many tools like this out there, but this one is mine, and mine is better. Here’s why:

A good chunk of such tools bury the option to create a new file deep within context menus, pretty much missing the point. If I have to do mouse gymnastics to get my file, I might well use the text editor’s menus instead. A particularly egregious manifestation of this burial is when this feature is implemented as a Service: the Finder does not make available the menu items for Services in the situation where one would need “New Plain Text File” most commonly: in the context menu for the blank area of folder window, where you trigger it without selecting any of its items.

TFF        = Appscript::app("Finder")
TFFProcess = Appscript::app("System Events").application_processes["Finder.app"]

More significantly, most tools try to do too much. The ability to create files of different types is king amongst them. This is definitely influenced by the way this feature exists on Windows. It’s also—unsurprisingly, given the source—terribly lame; I never ever need to create a file from the Finder that’s not a text file. I have a theory about that:

path_to_desktop             = Pathname.new "#{ENV['HOME']}/Desktop"
path_of_insertion_location  = Pathname.new TFF.insertion_location.get(:result_type => :file_ref).get(:result_type => :alias).path
path_of_first_finder_window = Pathname.new TFF.Finder_windows.first.target.get(:result_type => :alias).path rescue nil
is_desktop_the_active_view  = path_to_desktop == path_of_insertion_location && path_of_first_finder_window != path_to_desktop

Often the need for a text file arises while moving things around in the Finder. You’re organizing your files and you need to put down some notes along with them. In that case it’s great that you’re already in the folder where you want your new file. It makes more sense to create it right there and then than to open an editor and use the save dialog to navigate all over again to where you’re already at.

basename  = "untitled.txt".tap { |s| s.sub!(/( (\d+))?(?=\.txt\z)/) { " #{($2||1).to_i.succ}" } while path_of_insertion_location.join(s).exist? }
file_path = path_of_insertion_location.join(basename)
FileUtils.touch(file_path)

TFF.activate
TFF.insertion_location.update
sleep 0.3 # increase the chances that the \r keystroke that follows will hit the proper selection

Even when that’s not the case, text files crave to be named. I submit that the name of a text file is much more important, than, say, the name of a spreadsheet or a Photoshop document. With text files, it’s integral to the text. It’s often the case that it comes to me even before the text itself. Hence the workflow where I create it, name it, and then edit it makes a lot more sense. With a Photoshop comp, the image you want to draw comes to you first, so you go ahead and draw it. The file name is an afterthought. It is, truly, metadata.

# attempt to select the newly created file
is_desktop_the_active_view                      ?   # When you call `select` for an item on the Desktop, a new Finder window is opened and the
  TFFProcess.keystroke(file_path.basename.to_s) :   # file is selected in that window. We don't want that, so if the Desktop is active, we try
  TFF.select(MacTypes::Alias.path(file_path.to_s))  # to type out the file name to cause its selection directly on the Desktop. /hack

To be perfectly strict, there’s another case where I want to create a new, named file in the current Finder window, then open and edit it. When I need a new script. But what is a script if not a particular case of a text file, maybe with a different extension? So, as I trigger New Plain Text File and it enters rename mode, I just type ⌘A to include the extension in the selection, and type over the entire name.

# press return to enter rename mode
TFFProcess.keystroke("\r")

Recapitulating it, this is one is better because:

  • It’ll only create one type of file: empty plain text files. The UI indirection and the mental tax that come with having to pick a file type are blissfully nowhere to be found.
  • It’s goddamn keyboard driven. Even neurosurgeons and miniature artists get frustrated to death when trying to navigate menu trees. No so much with pressing key combinations.
  • I wrote it.

An Epilogue About the Officially Suggested Key Binding

While ⌃⌘N is nice because it follows the New Window / Folder / Smart Folder pattern of ⌘N / ⇧⌘N / ⌥⌘N, it actually sucks because you can’t uncontortedly trigger it with a single hand. If you’re like me and probably pretty much everyone else, you never invoke the New Smart Folder command, so you might well use ⌥⌘N for this script.


ATTN: Skip Here for the Goods

As usual: get the standalone script file, or git pull if you’re keeping a git clone of the scripts.

You may also check it it in all its colorful glory (and in a single piece) on github.


This post of part of series on the scripts I use with FastScripts.

Got comments? Use reddit and/or poke me on twitter.

Script: Finder/Paste as Symlink

  • Script: Paste as Symlink
  • Application: Finder
  • Suggested key binding: ⌥⌘V

This script creates symlinks to the paths in the clipboard in the active Finder window. The target path used for these symlinks is relative to the location where we’re creating the link.

It supports paths copied via ⌘C in the Finder, or a list of plain text paths as you’d get from my previous Copy Paths to Selected Items script. The pasting behaviour is generally similar to what you’d get with file copies using ⌘V. E.g. the name used is the same as the source’s, numbers are appended to prevent name collisions, etc.

Unfortunately, AppleScript pretty much sucks ass, so in order to be able to read multiple paths from the clipboard I had to resort to some good old Cocoa. But hold your brackets; I used MacRuby.

If you’re not using MacRuby for anything already (shame on you), you probably don’t have it installed. MacRuby 0.7 was just released right after I initially posted this, so go ahead and grab it while it’s fresh. It’s a standard OS X Installer package, no neck beard or dark chanting needed.


Seemingly, anything touched by Cocoa becomes automatically verbose; so I’ll not be showing the script in its entirety here. However, here’s a peek at the Cocoa bit:

paths_from_clipboard = Array.new NSPasteboard.generalPasteboard.pasteboardItems
  .map { |pbi | pbi.stringForType('public.file-url') }.compact
  .map { |url | NSURL.URLWithString(url).path }
  .map { |path| Pathname.new(path) }

You can see it in all its colorful glory here.

(In all fairness, Cocoa is not really to blame for the length, it just turned out longish.)

As usual: get the standalone script file, or git pull if you’re keeping a git clone of the scripts.


This post of part of series on the scripts I use with FastScripts.

Got comments? Use reddit and/or poke me on twitter.

Script: Finder/Edit in TextMate

  • Script: Edit in TextMate
  • Application: Finder
  • Suggested key binding: ⇧⌘E

This script edits the Finder selection in TextMate. If there’s no selection, it’ll use the folder for the active Finder window. If there are no open windows, that’ll mean the Desktop.

finder = Appscript::app('Finder')
paths  = finder.selection.get(:result_type => :alias).map(&:path)
paths << finder.insertion_location.get(:result_type => :file_ref).get(:result_type => :alias).path if paths.empty?
system "mate", *paths unless paths.empty?

The effect is generally the same as if you dragged the selection onto the TextMate icon in the Dock or Finder toolbar. I.e. it’ll create a project window with all the files.

If you’d rather use another editor, replace mate with whatever command your editor provides, e.g. edit for TextWrangler.


Get the standalone script file, or git pull if you’re keeping a git clone of the scripts.

This post of part of series on the scripts I use with FastScripts.

Got comments? Use reddit and/or poke me on twitter.

Script: Finder/Make Executable

  • Script: Make Executable
  • Application: Finder
  • Suggested key binding: ⇧⌘X

Peek:

finder      = Appscript::app("Finder")
paths       = finder.selection.get(:result_type => :alias).map(&:path)
file_paths  = paths.select { |path| File.file? path }
chmod_flags = ARGV[0] || "ugo+x"
system "chmod", chmod_flags, *file_paths unless file_paths.empty?

While you can change the read/write permissions for a file from the Finder via the Get Info pane, the execute permission is nowhere to be found.

This script hopes to mend that. It sets the executable bits for the selected files in the Finder. It does not affect the existing read/write permissions. (I.e., it calls chmod ugo+x.)


Today’s script comes with a free counterpart, Make Not Executable, which I suggest you bind to ⌥⌘X.

It simply piggybacks on the former, passing a parameter that modifies what is passed down to chmod:

system Pathname.new(__FILE__).dirname.realpath.join("Make Executable.rb").to_s, "ugo-x"

Get the standalone script files for Make Executable and for Make Not Executable, or git pull if you’re keeping a git clone of the scripts.

This post of part of series on the scripts I use with FastScripts.

Got comments? Use reddit and/or poke me on twitter.

Rakefile for my Library/Scripts

If you’re keeping a clone of my Scripts repository for use with FastScripts, you may have noticed a couple of annoying details in FastScripts:

  1. Menu items appear with the .rb extension
  2. Non-script files also show up on the menu

FastScripts respects the same file attributes as the Finder to decide which extensions and which files to show/hide. So, to hide an extension, just Get Info on the file in the Finder, and check Hide Extension. And to hide a file…

Well, actually to do either you can just use SetFile, which ships with the Developer Tools. Passing -a E will set the file’s extension as hidden, -a V will set the file as invisible.

I added a Rakefile with these two tasks to the repository:

  • hide-extensions, which will set the hidden extension for all .rb files, and
  • hide-nonscripts, which will hide the files that I don’t want in the menu.

They are both dependencies for the (empty) default task, so if you just run rake from within ~/Library/Scripts, they’ll both execute.

Got comments? Use reddit and/or poke me on twitter.

Script: Mail/Copy URLs for Selected Messages

  • Script: Copy URLs for Selected Messages
  • Application: Mail
  • Suggested key binding: ⇧⌘C

Peek:

mail = Appscript::app("Mail")
urls = mail.selection.get.map { |msg| "message://" + CGI.escape("<#{msg.message_id.get}>") }
open('|pbcopy', 'w') { |io| io.write urls.join("\n") }

This script copies the URLs for the selected messages in Mail to the clipboard.

Mail URLs are local to your Mail library. When opened, they’ll launch Mail and open the message. They’re very useful when referring to Mail documents (i.e. emails) elsewhere, like in Things or Notational Velocity.

No, not all my scripts are about copying stuff to the clipboard, but hey, it’s sunday.

Get the standalone script file, or git pull if you’re keeping a git clone of the scripts.

This post of part of series on the scripts I use with FastScripts.

Got comments? Use reddit and/or poke me on twitter.