Lovely UI toolkits (Was: Re: eXene)

Luke Gorrie luke@REDACTED
Tue Mar 25 00:38:03 CET 2003


Jimmie Houchin <jhouchin@REDACTED> writes:

> Anybody who is interested in looking at what Alan Kay and Smalltalk
> are doing try out Squeak.
> 
> http://www.squeak.org

Yeah, Squeak is pretty amazing.

BTW, I just tried out web browser written in Common Lisp under the
free CLIM implementation. It isn't yet up to the standard of
mozilla/opera/etc, but it's bloody impressive.

Screenshot:
  http://www.bluetail.com/~luke/misc/closure.png

Homepage:
  http://www.stud.uni-karlsruhe.de/~unk6/closure/

Took quite a bit of fiddling to get it up and running. Unfortunately
it can't handle forms yet, so I can't google with it. However, CLIM
does seem to be emerging as a candidate to replace/complement the only
UI toolkit that I currently really like - Emacs.

It's high time for a description of how lovely Emacs is. I haven't
seen its virtues as a UI toolkit written up anywhere, and it's by far
the most convenient one I've ever used - both as a programmer and as a
suitably-brainwashed user. (Of course, I've never tried a Lisp
machine..)

To illustrate how convenient it is, here is a simple file-browsing
program in the spirit of Windows Explorer. You tell it to browse a
directory, and it presents a buffer listing the files and
subdirectories. If you press return on one of them, it either opens it
for editing (if it's a file) or "recursively" browses it (if it's a
directory.) The program is less than 30 lines of code.

It's implemented in two functions: one that generates a buffer showing
the files in a directory and tagging each entry with useful semantic
information; and one function that selects an entry in that buffer.

Here's the main function for generating a directory list and recording
semantic information:

01   (defun dirlist (dir)
02     "Display a buffer showing the contents of DIR.
03   Each filename is tagged with two text properties:
04           type - a symbol, either 'dir or 'file
05           path - the fully qualified path"
06     (interactive "DDirectory: ")
07     (unless (file-directory-p dir)
08       (error "Not a directory!"))
09     (pop-to-buffer (get-buffer-create "*Dirlist*"))
10     (erase-buffer)
11     (setq header-line-format (format "Directory listing of: %s" dir))
12     (use-local-map dirlist-keymap)
13     (dolist (path (directory-files dir t))
14       (let ((line (format "%s\n" (file-name-nondirectory path))))
15         (insert (propertize line
16                             'type (if (file-directory-p path) 'dir 'file)
17                             'path path))))
18     (goto-char (point-min)))

Lines 1-5 are the function header. The function takes one argument:
the directory to examine, called DIR. Note that the documentation
string can be queried online with commands like describe-function,
apropos, etc.

Line 6 declares that it's an interactively callable command, i.e. that
you can bind it to a key or invoke it with M-x. The first character
"D" says that its argument is a directory, so Emacs will give you
appropriate prompting and completion. The rest of the string is the
prompt to display.

Lines 7-8 make sure that the specified path is really a directory, and
raise an error otherwise.

Line 9 gets a buffer called *Dirlist* (creating it if necessary), then
displays and selects it in "somewhere" on the screen - Emacs has
heuristics to make it display in a good place.

Line 10 nukes any contents of that buffer, which we may have from
previous uses of this command.

Line 11 sets up a banner across the top of the of the buffer's window,
saying which directory it is listing. This banner is always displayed
at the top line, even if you scroll in the buffer.

Line 12 makes the buffer use a special local keymap, in addition to
the default mappings. The keymap is defined below.

Line 13 starts a loop over all the files in DIR - for each file it
evaluates the enclosed expressions with "path" bound to the
filename. The second argument to directory-files, 't', tells it to
return absolute filenames.

Line 14 creates the line of text that we will insert into this buffer:
the filename (sans directory) followed by a newline.

Lines 15-17 insert the line after added two "text properties": one
called `type' containing `dir' or `file' as appropriate; one called
`path' containing the fully qualified path. These properties are for
our other command, which will extract them from the text to find out
which file to act on.

Line 18 jumps back to the beginning of the buffer, so that it will be
displayed from the start.

When you call that command with "M-x dirlist", you get a buffer on the
screen showing all the files in the directory you specify, and each
line is tagged with information for this next command which "visits" a
directory entry:

19   (defun dirlist-visit ()
20     "Visit the file or directory on the current line of a dirlist buffer."
21     (interactive)
22     (let ((path (get-text-property (point) 'path)))
23       (case (get-text-property (point) 'type)
24         (file (find-file path))
25         (dir  (dirlist (file-truename path))))))

Lines 19-20 are the header, and line 21 declares the command as
interactive but taking no formal arguments.

Line 22 retrieves the `path' text property at the current buffer
position and puts it in a local variable.

Lines 23-25 check the type of the entry: if it's a file, it calls the
builtin `find-file' function (aka C-x C-f), if it's a directory it
calls our `dirlist' command to display its contents - `file-truename'
is used to canonicalise "." and "..". Pretty easy.

Finally, here is the keymap that `dirlist' uses so that pressing
return will visit an entry:

26   (defvar dirlist-keymap (make-sparse-keymap))
27   (define-key dirlist-keymap [return] 'dirlist-visit)

That's a pretty short and simple program. I personally wouldn't be
able to write a similarly concise one with any other
language/toolkit. (If people on this list are familiar with some
toolkits that let you write such a program so easily, I'd really love
to check out some code!)

So, that is my case for Emacs being nice for the programmer.

For the user, this `dirlist' program also has some desirable
properties: all motion commands work perfectly with it - arrow keys,
isearch, {start,end}-of-buffer, and even homemade ones. If you like
you can show different parts of the buffer in different windows using
standard window-manipulation commands, and do any of the other
billion-or-two things that Emacs knows how to do with buffers.

The universality [1] of Emacs commands is very nice for users. For
example, I have some home-made motion commands for doing a manual
binary-search to a particular line on the screen. These commands work
with every Emacs program - mail readers, web browsers, text editing,
etc. It doesn't matter that the authors of those programs might think
the commands are arcane or silly - I wrote them, I like them, and I
can use them in every single Emacs program. Truly, this is the Tao of
programming.

[1]:  "universality" is a real word, according to the universally
      available Emacs spellchecker.

NB 1: See also http://ww.telent.net/diary/2003/1/#14.28515

NB 2: Emacs already comes with a directory-listing program called
      "dired". It's about 5,000 lines of code, and I don't know many
      of its features. One day, when I learn all about it, I probably
      won't believe I ever lived without it.

NB 3: I didn't get a chance to mention many of Emacs's other wonderful
      UI features, like builtin undo and "abort" (none of this
      "bugger, I can't click Cancel because I have an
      hourglass-pointer!")

NB 4: To number the lines in the dired program, I wrote a small Emacs
      command. Now I can conveniently number source lines in emails,
      HTML or TeX documents, etc. So can you if you want, here is the
      command:

(defun number-lines (start end)
  "Number lines in the region. Hardcoded for 2-digit numbering."
  (interactive "r")
  (goto-char start)
  (let ((end-marker (set-marker (make-marker) end))
        (line 0))
    (unwind-protect
         (while (< (point) end-marker)
           (incf line)
           (when (< line 10) (insert "0"))
           (insert (format "%S   " line))
           (next-line 1)
           (beginning-of-line))
      (delete-marker end-marker))))

If you want to run the dired program, you can easily strip off the
line numbers using the Emacs `kill-rectangle' (C-x r k) command.

Cheers,
Luke




More information about the erlang-questions mailing list