はじめに

この記事は Emacs Advent Calendar 2017 - Qiita の 17 日目の記事です。

想定する読者

  • 簡単な Emacs Lisp プログラミングができる
  • 特定のデータを一覧できるメジャーモードをシュッっと作りたい

tabulated-list-mode のご紹介

Emacs には tabulated-list-mode というメジャーモードがあります。

tabulated-list-mode は簡単にいうと表形式の表示を行い、ソートなどの基本的な操作を提供するメジャーモードです。また、直接呼び出して使うものではなく、特定のメジャーモードのベースとして使うものになります。

例えば M-x list-processes と打つとプロセス一覧のバッファが、 M-x list-packages とするとパッケージ一覧が表示されますが、これらは tabulated-list-mode を親モードとしたメジャーモードです。

  • list-processes
  • list-packages

他にも、Gist を扱う defunkt/gist.el というパッケージなども tabulated-list-mode をベースにしています。

tabulated-list-mode のカラム表示で、 [v] と表示されているカラムが現在ソートされているカラムになります(この場合は昇順)。

ソートを行いたいカラムまでカーソル移動して S を押すとそのカラムでソートされます。また、ソート済みカラムで S を押すと昇順・降順がトグル表示されます(降順の場合は [^])

詳細はマニュアルの Tabulated List Mode - GNU Emacs Lisp Reference Manual に書いてありますので、必要に応じてご一読ください。


tabulated-list-mode をベースとしたメジャーモードの作成方法

ここでは例として process-menu-mode (Emacs 25.3 版) での設定内容を説明してみます。ソースを抜粋すると、

(define-derived-mode process-menu-mode tabulated-list-mode "Process Menu"
  "Major mode for listing the processes called by Emacs."
  (setq tabulated-list-format [("Process" 15 t)
                               ("Status"   7 t)
                               ("Buffer"  15 t)
                               ("TTY"     12 t)
                               ("Command"  0 t)])
  (make-local-variable 'process-menu-query-only)
  (setq tabulated-list-sort-key (cons "Process" nil))
  (add-hook 'tabulated-list-revert-hook 'list-processes--refresh nil t)
  (tabulated-list-init-header))

define-derived-mode は次のような引数を取る関数なので、

(define-derived-mode CHILD PARENT NAME &optional DOCSTRING &rest BODY)
(define-derived-mode メジャーモード名 tabulated-list-mode 表示名
  説明
  :
)

のように記述します。


tabulated-list 関連の変数・関数

マニュアル により詳しく書いてありますが、ざっと説明すると以下のような変数・関数を利用します。

Variable: tabulated-list-format

カラム表示の設定になります。 (カラム名 幅 ソート可否) のベクターをセットします。

Variable: tabulated-list-sort-key

初期ソートを行うカラム(のリスト)をセットします。

Variable: tabulated-list-revert-hook

再描画を行う際の hook です。ここでは list-processes--refresh が登録されています。

list-processes--refresh は次のような関数です。 ちょっと長いのですが、ここで tabulated-list-entries をセットしているのにご注目ください。

(defun list-processes--refresh ()
  "Recompute the list of processes for the Process List buffer.
Also, delete any process that is exited or signaled."
  (setq tabulated-list-entries nil)
  (dolist (p (process-list))
    (cond ((memq (process-status p) '(exit signal closed))
           (delete-process p))
          ((or (not process-menu-query-only)
               (process-query-on-exit-flag p))
           (let* ((buf (process-buffer p))
                  (type (process-type p))
                  (name (process-name p))
                  (status (symbol-name (process-status p)))
                  (buf-label (if (buffer-live-p buf)
                                 `(,(buffer-name buf)
                                   face link
                                   help-echo ,(format-message
                                               "Visit buffer `%s'"
                                               (buffer-name buf))
                                   follow-link t
                                   process-buffer ,buf
                                   action process-menu-visit-buffer)
                               "--"))
                  (tty (or (process-tty-name p) "--"))
                 (cmd
                   (if (memq type '(network serial))
                       (let ((contact (process-contact p t)))
                         (if (eq type 'network)
                             (format "(%s %s)"
                                     (if (plist-get contact :type)
                                         "datagram"
                                       "network")
                                     (if (plist-get contact :server)
                                         (format "server on %s"
                                                 (or
                                                  (plist-get contact :host)
                                                  (plist-get contact :local)))
                                       (format "connection to %s"
                                               (plist-get contact :host))))
                           (format "(serial port %s%s)"
                                   (or (plist-get contact :port) "?")
                                   (let ((speed (plist-get contact :speed)))
                                     (if speed
                                         (format " at %s b/s" speed)
                                       "")))))
                     (mapconcat 'identity (process-command p) " "))))
             (push (list p (vector name status buf-label tty cmd))
                   tabulated-list-entries))))))

Variable: tabulated-list-entries

これが表示対象のメインデータです。 (id [tabulated-list-format に設定したカラム順のベクター]) という形式のリストになります。

また、マニュアル には出てきませんが、カラム表示画面で (tabulated-list-get-id) を呼び出すと現在行の id を取得することができます。

Function: tabulated-list-init-header

カラムのヘッダを初期化します。 tabulated-list-format のセットより後に呼び出される必要があります。


作成したメジャーモードの呼び出し

list-processes 本体は次のような関数です。同様の関数を定義して provide すればオリジナルメジャーモードの完成です。

(defun list-processes (&optional query-only buffer)
  "Display a list of all processes that are Emacs sub-processes.
If optional argument QUERY-ONLY is non-nil, only processes with
the query-on-exit flag set are listed.
Any process listed as exited or signaled is actually eliminated
after the listing is made.
Optional argument BUFFER specifies a buffer to use, instead of
\"*Process List*\".
The return value is always nil.

This function lists only processes that were launched by Emacs.  To
see other processes running on the system, use `list-system-processes'."
  (interactive)
  (or (fboundp 'process-list)
      (error "Asynchronous subprocesses are not supported on this system"))
  (unless (bufferp buffer)
    (setq buffer (get-buffer-create "*Process List*")))
  (with-current-buffer buffer
    (process-menu-mode)
    (setq process-menu-query-only query-only)
    (list-processes--refresh)
    (tabulated-list-print))
  (display-buffer buffer)
  nil)

関数 list-processes では、バッファ "*Process List*" を作成して、メジャーモード (process-menu-mode) の呼び出しを行い、 (list-processes--refresh)tabulated-list-entries にデータをセットしたのち、 (tabulated-list-print) で描画を行い、最後にそのバッファへ切り替えを行っています。

Function: tabulated-list-print

事前に設定された tabulated-list-entriestabulated-list-sort-key によってソートして出力します。引数については マニュアル をご参照ください。


おわりに

以上が tabulated-list-mode のご紹介でした。

表形式データを扱うことはよくあると思われるので、お手軽にメジャーモードを作る時に参考になれば幸いです。


余談

実は、本エントリは tabulated-list-mode をベースにした自作パッケージの紹介に繋げるつもりだったのですが、本エントリ公開までに細かい修正&公開が間に合わなかったので、そのご紹介は次回に延期することにしました…