Zsh and Ruby: Toward a universal directory/file opener

This post has been deprecated. Please read this post instead.

I’ve been using Linux for a while now (almost 2 years?), and have learned quite a bit about it. However, ever since I switched to Xmonad as my window manager, I’ve always been tied to the “urxvt + zsh” GUI. Don’t get me wrong — I really enjoy this setup (a plain old terminal has a beauty all of its own in how things get done), but it could be even better.

Lately, I’ve noticed my ~/.zshrc file grow larger and larger, with the addition of new aliases to open up common programs, like “m” for mplayer (audio, movies) and “e” for evince (PDF’s). To get rid of this clutter, and also to automate things, I decided to make a new, simple alias to handle pretty much everything I throw at it. The new functionality lets Zsh, with the help of Ruby, open up just about any file I throw at it, while also integrating the common “cd” command for directories — all from the command line.

The required files/code are as follows: (1) a custom zsh function (autoloaded into your zsh session) and (2) a custom Ruby script. I’ve only tested this on Ruby 1.9.1p243 (2009-07-16 revision 24175) [x86_64-linux].

For the custom zsh function, here is how I autoload it from ~/.zshrc:

fpath=(~/.zsh/func $fpath) # add ~/.zsh/func to $fpath
autoload -U ~/.zsh/func/*(:t) # load all functions in ~/.zsh/func

The function itself is as follows:

 1 # Author: Shinobu (https://zuttobenkyou.wordpress.com)
 2 # Date: December 2009
 3 # License: PUBLIC DOMAIN
 4 # Program Name: univ_open()
 5 # Description: zsh function to open up either directories or files
 6 univ_open() {
 7     ls_pretty="ls -Ahs --color=auto"
 8     if [[ ! -e $@ ]]; then
 9         # go to the parent directory if given path does not exist
10         /home/shinobu/syscfg/shellscripts/sys/univ_handler.rb $@ "parent" | read parent
11         cd $parent && ${=ls_pretty}
12     elif [[ -d $@ ]]; then
13         cd $@ && ${=ls_pretty}
14     else
15         # if it does exist, but is not a directory, open it up with the
16         # universal readerscript, to execute an appropriate program for the
17         # extension
18         /home/shinobu/syscfg/shellscripts/sys/univ_handler.rb $@ "extension" | read outputcmd
19         # since zsh does not split words by default, we need to manually do
20         # this by invoking outputcmd with the ${=VAR} syntax; this is because
21         # univ_handler will sometimes give a multi-word command where we use
22         # one or more commandline options, such as "mplayer -loop 0" or
23         # "soffice -writer" -- to prevent zsh from interpreting the entire
24         # string as the name of a command, we have to split it up into an array
25         ${=outputcmd} $@
26     fi
27 }

The file that contains this function is also called “univ_open”, which resides in ~/.zsh/func/. Anyway, this function is quite simple — it checks to see if the given argument does not exist (line 8), does exist and is a directory (line 12), or does exist and is a file (line 14).

In the “if” statement at line 8, it calls our custom Ruby script, univ_handler.rb, with an argument called “parent”. For lines 12-13, we don’t need to intelligently handle anything since the given argument is a valid directory path, so we just “cd” into it. Lastly, if the argument is a valid file (line 14), we call univ_handler.rb with the “extension” argument to determine which command to use to open up that file.

The univ_handler.rb file looks like this:

 1 #!/usr/bin/ruby
 2 # Author: Shinobu (https://zuttobenkyou.wordpress.com)
 3 # Date: December 2009
 4 # License: PUBLIC DOMAIN
 5 # Program Name: univ_handler.rb
 6 # Description: with the help of smart input from zsh, this script helps zsh
 7 # open up directories and files intelligently
 9 class Ext
10     @doc = %w{doc odf odt rtf}
11     @web = %w{htm html}
12     @pdf = %w{pdf ps}
13     @img = %w{bmp gif jpg jpeg png svg tiff}
14     @img2 = %w{psd xcf}
15     @audio = %w{flac mp3 wma}
16     @midi = %w{mid midi}
17     @movie = %w{avi flv ogg mkv mov mp4 mpg mpeg wmv}
19     class<<self; attr_reader :doc end
20     class<<self; attr_reader :web end
21     class<<self; attr_reader :pdf end
22     class<<self; attr_reader :img end
23     class<<self; attr_reader :img2 end
24     class<<self; attr_reader :audio end
25     class<<self; attr_reader :midi  end
26     class<<self; attr_reader :movie end
27 end
29 file = ARGV.shift
30 arg = ARGV.shift
32 # zsh will read whatever we ultimately "print" to STDOUT!
33 if file.nil? # if the user does not specify an argument
34     print ""
35 else # figure out file extension, or if directory, the parent directory
36     if arg == "extension"
37         case file.split(".").last.downcase
38         when *Ext.doc
39             print "soffice -writer"
40         when *Ext.web
41             print "firefox"
42         when *Ext.pdf
43             print "evince"
44         # image files -- typical simple files for plain viewing
45         when *Ext.img
46             print "eog"
47         # image files -- ones meant to be edited (strange looking ones and also
48         # gimp's native format)
49         when *Ext.img2
50             print "gimp"
51         # audio files
52         when *Ext.audio
53             print "mplayer -loop 0"
54         # midi files
55         when *Ext.midi
56             print "timidity"
57         # movies
58         when *Ext.movie
59             print "mplayer -loop 0"
60         else
61             # use vim to access files by default, including those without an
62             # extension
63             print "vim"
64         end
65     elsif arg == "parent" # find the closest valid parent directory
66         if file.scan("/").empty? # if there is no "/" in the argument
67             print "." # let's keep the user in the same directory
68         else # if we have a filename (at least the user thinks so), but it is
69              # not a valid file path = file.split("/")
70             path.pop
71             # return 1 directory higher than this one -- we assume that the
72             # reader used tab completions enough to avoid having more than 1
73             # bad directory/file level (that only the text following the last
74             # "/" is invalid)
75             print path.join("/")
76         end
77     end
78 end

You don’t even need to know Ruby to see what’s going on. The important thing is what’s inside the curly braces — we simply group common extensions together based on the file they usually represent. We don’t bother with uppercased combinations, since we use Ruby’s String class’s “downcase” method on the original user-given argument before handling it. Since I love vim, I use it as the “catch-all” to open up any other kind of file (such as files that don’t have extensions).

Now, the most important part — the one alias to rule them all (in your ~/.zshrc):

alias d='univ_open' # alias to use univ_open()

And… that’s it! All you have to do is now do d [file, directory, path, symlink to either a directory or path] to open it up intelligently! I personally use “d” since historically I’ve used it to cd into a directory (and used “k” to go back up); I also find using my middle finger much easier to press keys than my index finger. Probably the best benefit of this setup is that you can do “d TAB TAB …” to go deep into directories, and then just press ENTER when you find the file you want to edit — no more cd-ing deep into a subdirectory first, and then doing “ls” on it to see what’s inside, and deciding which command to use to open up that file. Now it’s just “d” and away you go.

It took me about half a day to get everything working, with help from googling, of course. I hope you can make use of it as it is, or even better, to customize it to your own needs. There’s enough “foundation” code in there with enough complexity to let anyone customize it a great deal and add more extensions to it.

As an added bonus, notice how the code here does not mention symlinks at all — and yet they are handled properly (don’t you love symlinks?)).

UPDATE December 6, 2009: Apparently, there is a similar command line script to open anything called “gnome-open”: http://embraceubuntu.com/2006/12/16/gnome-open-open-anything-from-the-command-line/. You can open up URLs in addition to directories and files. I don’t really see the advantages of opening up a URL from the command line, though — I’m already on Xmonad and going to my supremely powerful Firefox + Vimperator setup is at most 2 or 3 keystrokes away. Still, it looks interesting.

UPDATE December 31, 2009: This post has been deprecated. Please read this post instead.

Clean Japanese and Korean Characters in URXVT

I have files and directories using Japanese and Korean. Unfortunately, urxvt by default does not display these characters nicely (the hangul looks especially ugly) by default. The way to fix this is to have a good set of TrueType fonts for both Japanese and Korean, and to have URXVT use these as defaults.

If you’re on Arch Linux like me, you can install from the AUR the ttf-kochi-substitute and ttf-baekmuk fonts for Japanese and Korean fonts, respectively. (There is also ttf-sazanami and ttf-unfonts-core for more Japanese and Korean fonts, respectively — but here I’m going to use Kochi Gothic and Baekmuk Gulim in URXVT). Now, in your ~/.Xdefaults file, put this in:

urxvt*font: xft:Terminus:pixelsize=14,\
            xft:Kochi Gothic:antialias=false,\
            xft:Baekmuk Gulim

This makes it so that Terminus is used, then Kochi Gothic, then Baekmuk Gulim. It’s good to have Kochi Gothic on a higher priority than Baekmuk Gulim, since Kochi Gothic’s kanji glyphs look much better than Baekmuk’s (and since Japanese words often have kanji in them, whereas Korean files almost always have just hangul in them). Also, the pixelsize defined with Terminus is used for all succeeding fonts below. Now my URXVT looks really nice!

Xfce4-Terminal + Console Vim Glitch Workaround

I’ll try to keep this short. I’m using Xubuntu 8.04.1 + vim 7.1.138. (Xfce4-terminal is version 0.2.8).

The problem: If you have the lines and columns options set in your .vimrc file for gvim‘s default window size upon startup, you run into a strange graphics glitch if you attempt to run simple console vim inside xfce4-terminal. What you type does not get displayed, and a new line appears after every keystroke, deleting the previous line — although the data of what you typed in is still there. It is the same problem discussed here. I encountered this problem while executing the git commit -an command from xfce4-terminal, when git resorted to vim as the default text editor. (NOTE: You might need to edit xfce4-terminal’s default window size in ~/.config/Terminal/termnalrc (the MiscDefaultGeometry option) to reproduce the glitch — I had mine set to 80×19).

The solution: Make the lines and columns options in .vimrc dependent on the “gui_running” variable, like so (copied from here):

if has("gui_running")
"GUI is running or is about to start.
"Maximize gvim window.
set lines=69 columns=100
" if we're in console Vim, then we just want to leave the window size alone --
" let it simply be whatever the window size of the terminal is before vim is
" launched
"  "This is console Vim.
"  if exists("+lines")
"    set lines=69
"  endif
"  if exists("+columns")
"    set columns=100
"  endif

This works. You do not get the strange graphical glitch behavior described above, by making vim only resize the window if it is gvim, and not console vim. I have commented out the console vim settings because, as stated above, I already have a default window size of 80×19 for xfce4-terminal.

Note: If you use xterm, it doesn’t matter — even if you don’t have the special “gui_running” option in your .vimrc, console vim will work properly. So run git commands in xterm if you don’t like xfce4-terminal.