Illusory stillness

mumble mumble techblog

Replace Less With Emacs

I use shell-mode in Emacs a lot (and have never been that taken with Eshell or term-mode). I got tired of seeing this sort of thing:

1
2
3
dekkera:htlatex-maximal peter$ git log
WARNING: terminal is not fully functional
-  (press RETURN)

It’s reasonable for commands that generate a lot of output to go through less, but less works poorly in an Emacs buffer.

However, Emacs itself is quite good at paging. So I wondered if I could make Emacs take over for less. I wrote this shell script emacs-pager and placed it on my $PATH:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/bin/bash
#A script for $PAGER that redirects to an Emacs buffer

# get the command line whose output I am paging
COMMAND=$(echo "$(ps -o 'command=' -p $(ps -o 'ppid=' $$))" | sed -e 's/["\\]/\\&/g' -e 's/["\\]/\\&/g')

# make a named fifo
FIFO=$(mktemp -ut pager.$$)
mkfifo $FIFO

# tell emacs to to read that fifo into a buffer
read -d '' LISP <<EOF
(let* ((buf (generate-new-buffer "*${COMMAND}*"))
       (proc (start-process "PAGER" buf "cat" "$FIFO")))
   (view-buffer buf 'kill-buffer)
   ; make sure point stays at top of window while process output accumulates
   (set-process-filter proc
     (lambda (proc string)
       (let ((buf (process-buffer proc))
             (mark (process-mark proc)))
       (with-current-buffer buf
         (let ((buffer-read-only nil))
           (save-excursion
             (goto-char mark)
             (insert string)
             (ansi-color-apply-on-region mark (point))
             ; TODO also support backspace overstriking
             (set-marker mark (point)))))))))
EOF

# echo "$LISP"

emacsclient -e "$LISP"

cat > "$FIFO"

Then I made sure the following was in my init.el:

1
2
3
4
(server-start)
(unless (getenv "TERM_PROGRAM")
  (setenv "TERM" "xterm"))
(setenv "PAGER" "emacs-pager")

Now when I invoke git log or man, the output appears in a new paging buffer.

  • Note use of “ps” to figure out what to call the buffer.
  • Note use of printf '%q' to double escape the string that is substituted into Lisp code. (Why did I need double escaping? Shell scripting is terrible is why.)
  • The 'kill-buffer means that the paging buffer will be deleted when you press “q”. This is not standard Emacs behavior but I don’t want a whole gaggle of temporary buffers laying around.
  • The process filter is there just to keep point at the top of the buffer as you receive input.
  • There was a weird interaction with Emacs’ builtin M-x man command. I worked around it with:
1
2
3
4
5
6
(defadvice man (around reset-pager activate)
  "reset PAGER to `less' when getting man pages."
  (let ((old (getenv "PAGER")))
    (setenv "PAGER" "less")
    ad-do-it
    (setenv "PAGER" old)))
  • There is a further problem when running man from the shell, in that man outputs backspace codes for bold/underline and so on. (TODO)