Faster man pages rendering

December 1, 2022

If you try to open rclone or gcc man pages with Emacs, you'd be surprised how unpleasant that experience can be. Those pages are over 30K lines and Emacs will became unresponsive easily. For how long will depend on your CPU.

To speed up things, setting Man-fontify-manpage-flag to nil can alleviate this problem - it will disable highlighting of a man page and buffer can be somehow usable. Surprisingly, running woman, an alternative man pager written in elisp, will render the rclone man page faster, but it will leave a lot of garbage around.

The fastest option for me was running a shell command man <page> | col -b and reading that output directly as plain text in the Emacs buffer.

However, running it with (shell-command-to-string) would still temporarily block Emacs until man finish rendering the page. But we can do it better.

Let's use (async-shell-command) instead and leave a man rendering the content in the background, but at the same time, capture the output in our buffer.

Here is an elisp function for that:

(defun faster-man (page)
  "Get a Un*x manual page and put it in a buffer.
Faster alternative to (man) and (woman)."
  (interactive
   (list
    ;; autocompletion machinery stolen from (man)
    (let* ((default-entry (Man-default-man-entry))
           (completion-ignore-case t)
           ; no cache across calls for completion table
           Man-completion-cache
           (input (completing-read
                   (format "Manual entry%s"
                           (if (string= "" default-entry)
                             ": "
                             (format " (default %s): " default-entry)))
                   'Man-completion-table
                   nil nil nil 'Man-topic-history default-entry)))
      (if (string= "" input)
        (error "No args given")
        input))))
  (let* ((buffer (pop-to-buffer (format "*Faster Man - %s*" page))))
    (with-current-buffer buffer
      (erase-buffer)
      (let ((proc
             (progn
               ;; Actual shell command. Redirect troff warnings & errors to /dev/null
               ;; so it doesn't pollute the output. Also, quote man page so it can display
               ;; things like "printf(3)"
               (async-shell-command (format "man \"%s\" 2> /dev/null | col -b" page) buffer)
               (get-buffer-process buffer))))
        (when (process-live-p proc)
          ;; wait for process to finish, then apply fundamental-mode on it
          ;; and jump to the beginning of buffer
          (set-process-sentinel proc (lambda (process signal)
                                       (when (memq (process-status process) '(exit signal))
                                         (with-current-buffer buffer
                                           (fundamental-mode)
                                           (beginning-of-buffer))))))))))

The hardest part was figuring out when (async-shell-command) was finished so that it could apply fundametal-mode for snappier scrolling and jump to the beginning of the buffer. Thanks to this code, I was able to get the desired behavior.

Running (fundamental-mode) immediately after (async-shell-command) didn't work well for me, making Emacs unresponsive, probably because the external command is still altering the buffer. That is why changing mode is done after the command is completed.

After evaluating the above code, run M-x faster-man and enjoy (a little bit) faster man pages :)