Scheme から Emacs のコマンドを実行

私はよく Scheme のプログラムを Emacs 上で実行するんですが、elscreen で裏に回していたりして *scheme* バッファが見えない時に、何かユーザーの注意や介入が必要な事態が起こっていることがあります (入力を促す、等)。

そんな時、外部プログラムを起動するとか、ffi で CD トレイを開くとか、その旨を知らせる方法はいくらでも有るわけですが、せっかく Emacs がそこにあるんだから (beep) の一つも eval 出来ないでどうする、ということで考えてみました。

SchemeEmacs の間には接続が張られているわけですから、そこにコマンドを流し込み、フック関数でモニターすれば良い、ということで、出来たのがこちら:

(add-hook 'inferior-scheme-mode-hook
          (lambda ()
            (add-hook 'comint-preoutput-filter-functions
                      (lambda (s)
                        (if (string-match "^(tell-emacs \\(.*\\))" s)
                            (save-excursion
                              (prin1
                               (eval
                                (car
                                 (read-from-string (match-string 1 s)))))
                              "> ")
                          s))
                      nil t)))

目的のフックがすぐ見つかったので案外易しかったです。"(tell-emacs " で始まる行を見つけたらS式を文字列として取り出し、read-from-string で S式化してから eval します。

さらに、Scheme 以外でも使えるようにマクロ化してみました (elisp にはクロージャが無いので):

(eval-when (load)
  (defmacro connect-to-emacs (prompt)
    `(add-hook 'comint-preoutput-filter-functions
               (lambda (s)
                 (if (string-match "^(tell-emacs \\(.*\\))" s)
                     (save-excursion
                       (prin1
                        (eval
                         (car
                          (read-from-string (match-string 1 s)))))
                       ,prompt)
                   s))
               nil t)))

プロンプトを言語ごとに設定できるようになっています:

(add-hook 'inferior-scheme-mode-hook
          (lambda () (connect-to-emacs "> ")))
(add-hook 'shell-mode-hook (lambda () (connect-to-emacs "")))

上記を .emacs に追加しておくと、Scheme プログラム中で

(display '(tell-emacs (beep)))

のように出力するか、あるいはプロンプトで

> '(tell-emacs (beep))

と評価することで、任意の Lisp 式を Emacs に送信できるようになります。

当然ながら

'(tell-emacs
  (comint-send-string (scheme-proc)
                      "'(tell-emacs (beep))"))

てなことも出来ます。

なお、Lisp 系以外の場合は式を文字列としてプリントする方法を取ってください。シェルモードの例:

echo "(tell-emacs (comint-send-string (scheme-proc) \"'(tell-emacs (beep))\"))" | cat

もっと騒がしい通知の例 (Scheme + Cygwin):

'(tell-emacs
  (comint-send-string (save-window-excursion (shell))
                      "cat /dev/urandom > /dev/audio\n"))

一見回りくどいですが、Windows 上の MzScheme からシェルを呼び出すよりちょっと楽だったりします。

ぜひお試しあれ。