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.

Script: Open Application’s Scripting Dictionary

Today’s script is a bit of a meta-script. It comes in handy if you’re writing scripts of your own, be them traditional AppleScripts or Ruby scripts with rb-appscript.

  • Script: Open Application’s Scripting Dictionary
  • Application: Any
  • Suggested key binding: None

Peek:

processes                  = Appscript::app("System Events").processes
is_frontmost_condition     = Appscript.its.frontmost.eq(true)
frontmost_application      = processes[is_frontmost_condition].first
frontmost_application_path = frontmost_application.application_file.get(:result_type => :file_ref).path
system('open', '-a', 'AppleScript Editor', frontmost_application_path)

This script opens the scripting dictionary for the frontmost application in AppleScript Editor.

Installation is a bit different for this one. Because you want it to be available to all applications, you put it right in ~/Library/Scripts, not in a subfolder.

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

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

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

Script: iTunes/Edit Track Names in TextMate

This is the fourth in a series of posts about the scripts I use with FastScripts, and it’s yet another iTunes script.

  • Script: Edit Track Names in TextMate
  • Application: iTunes
  • Suggested key binding: ⌃⌘E

This one lets you edit the names of the selected tracks in TextMate.

When triggered, it’ll open a document in TextMate with the name of each track on a new line. You can edit them to your heart’s desire. When you’re done, save and close, and the track metadata will be updated in iTunes for the names that changed.

It’s hardcoded for TextMate. If you’d prefer to use another editor, change the mate -w part to use the command line tool of your editor. I know BBEdit and TextWrangler offer compatible tools. Just use edit -w for them.

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

And here it is, in all its glory:

itunes          = Appscript::app("iTunes")
tracks          = itunes.selection.get
old_track_names = tracks.map { |track| track.name.get }
new_track_names = Open3::popen3("mate -w") do |stdin, stdout, stderr|
  stdin.puts old_track_names.join("\n")
  stdin.close
  stdout.readlines.map(&:strip)
end

tracks.zip(old_track_names, new_track_names)
  .select { |*, old_name, new_name| old_name != new_name    }
  .each   { |track, *,    new_name| track.name.set new_name }

Notice the awesome ruby 1.9 argument splices in the blocks.

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

Script: iTunes/Renumber Selected Tracks to Current Order

This is the third in a series of posts about the scripts I use with FastScripts.

  • Script: Renumber Selected Tracks to Current Order
  • Application: iTunes
  • Suggested key binding: None

Peek:

tracks      = Appscript.app("iTunes").selection.get
track_count = tracks.length

tracks.each_with_index do |track, ix|
  track.track_number.set ix + 1
  track.track_count.set track_count if track_count > track.track_count.get
end

This script acts on the the selected tracks in iTunes, setting their track number metadata according to their current sort order.

It’s great to quickly fix albums that come with less-than-stellar metadata (and you’d not be surprised at all by how many independent labels selling digital downloads botch this up).

This will also set the track count (the y in “track x of y”) to the number of tracks currently selected, except when this value is already set to a greater value.

This last trait makes this script also useful to simply set the track count info, and this is paid even less attention to by content providers. It’s sadly common to get an album with proper track numbers, but without the track count info.

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

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