Emacs上のターミナルを最強に: term+.el

第6回関西Emacs勉強会で, Emacs上で最強のターミナル(端末)環境を実現する話をしてきました. 以下がそのとき用いたスライドです.

このスライドだけでは, とりあえず使ってみるのではなく常用したい場合にどうしたらよいかわかりにくいと思うので, その辺りを補足しながら, きちんとしたドキュメントを書くまでの暫定の使い方を書いておこうと思います.

更新履歴

2012-11-07
2012-10-24
  • リポジトリ構成の変更に伴い配布場所とインストール方法の説明を変更
  • term+mux-newのセッションを訊く条件が変更されたのを反映

これは何?

Emacs上の端末エミュレータです. もともとterm.elというものがEmacsに標準添付されていてM-x termで利用できますが, かなり古いものということもあり, 機能面で不満がありました. また, GNU screenなどのマルチプレクサを用いている場合は, その機能も持った端末でなければ乗り換え対象として挙げづらいという事情もあります.

これらの問題を解消して, Emacs上に開いた端末を積極利用できるように, term.elを大幅に強化するのがterm+.elです.

サポート情報

Emacs 22に対応する予定はありません. 開発はEmacs 24.1.50で行なったため, おそらくEmacs 24.2が一番安定して動作します. Emacs 23は対応が甘くバグが残っているかもしれません.

何かバグを見つけたり意見・要望がある場合はissue trackerにお願いします. 既に自分でいくつかissueを英語で登録していますが, 海外ユーザがすごく増えたりしない間は英語が苦手なら日本語で書いてもらっても構わないと思います.

ソースリポジトリの構成と依存パッケージ

配布場所ファイル外部依存内容
github:tarao/term-plus-elterm+.elなし必須ファイル. Emacsに標準で入っているパッケージのみに依存しています.
term+vars.el
term+input.el
term+edit.el
term+file-transfer.el
term+logging.el
term+shell-history.el
xterm-256color.el256色対応のためのファイル. これをrequireしなくても他の機能は使えます.
github:tarao/term-plus-ki-elterm+key-intercept.elkey-intercept.elESCC-cを端末に送る/Emacsで解釈するという2通りをうまく使いわけたい場合に必要です.
github:tarao/term-plus-mode-elterm+mode.elmulti-mode-util.el編集モードの入力フィールドの部分だけterm-mode以外のメジャーモード(たとえばsh-mode)にしたい場合に必要です.
multi-mode.el
github:tarao/term-plus-mux-elterm+mux.eltab-group.elマルチプレクサとしての機能を使いたい場合は必要です.
github:tarao/term-plus-evil-elterm+evil.elEvilEvilユーザのためのパッチです.
github:tarao/term-plus-ash-elterm+anything-shell-history.elanything-complete.elterm+shell-hisotry.elによるシェルの履歴検索をanythingでやるようにするためのファイルです.

とりあえず試してみる

term+.elの設定以外は何も読み込まない状態で試すには以下のようにします. ただし, 外部依存ファイルもすべてダウンロードするため時間がかかります.

$ git clone git://github.com/tarao/term-plus-all.git
$ cd term-plus-all
$ git submodule update --init
$ make emacs    # ウィンドウを開く場合
$ make term     # emacs -nwで開く場合
$ make EMACS=emacs-snapshot term   # emacs-snapshot -nwで開く場合

他の個人設定(.emacsやinit.el)といっしょに使う場合は, term+.el本体と外部依存ファイルを適切に配置し, term+.elのロード設定をする必要があります.

インストール

まず, 上記の配布元から必要なファイルと外部依存ファイルをダウンロードします.

必須ファイルのみを使う場合は, 必要なファイルをロードパスのどこかに置いた上で次のようにします.

(require 'term+)

加えて, 256色対応が必要な場合はxterm-256color.elもロードパスのどこかに置いた上で次のようにします.

(require 'xterm-256color)

term+key-intercept.elを利用する場合は, 依存ファイルを含めてロードパスのどこかに置いた上で次のようにします.

(require 'term+key-intercept)

term+mode.elを利用する場合は, 依存ファイルを含めてロードパスのどこかに置いた上で次のようにします.

(require 'term+mode)

term+mux.elを利用する場合は, 依存ファイルを含めてロードパスのどこかに置いた上で次のようにします.

(require 'term+mux)

term+evil.elを利用する場合は, 依存ファイルを含めてロードパスのどこかに置いた上で次のようにします.

(eval-after-load 'evil
  '(progn (require 'term+evil)
          (when (featurep 'term+mode) (require 'multi-mode+evil))))

term+anything-shell-history.elを利用する場合は, 依存ファイルを含めてロードパスのどこかに置いた上で次のようにします.

(require 'term+anything-shell-history)

端末の起動

単一の端末

従来通りM-x termあるいはM-x ansi-termすると, term+.elが有効になった端末バッファを開くことができます. 端末内のプロセス(シェルなど)が終了すると, デフォルトではバッファも閉じられます. この挙動を変更するにはterm+kill-buffer-at-exit変数の値をnilにします.

端末をタブで開く

term+mux.elが必要です. 以下のコマンドで, マルチプレクサで管理された端末のタブを開くことができます.

M-x term+mux-new
新しいタブに端末を開きます. 現在セッションが選択されていればそのセッションのタブを, 選択されていなければ, セッションが複数ある場合はどのセッションのタブを開くか訊いてから, セッションが1つしかない場合はそのセッションのタブを開きます.
M-x term+mux-other-window
既に現在のセッションあるいはデフォルトセッションに端末バッファがあれば, それを別ウィンドウに開きます. 端末バッファがない場合は新しい端末バッファを別ウィンドウに開きます.
M-x term+mux-new-other-window
term+mux-newと同じですが, 別のウィンドウに開きます.
M-x term+mux-noselect
term+mux-newと同じですが, 新しい端末バッファを選択しません.
M-x term+mux-new-command
term+mux-newと同じですが, 端末内で実行するコマンド(デフォルトはシェル)を指定します.
M-x term+mux-new-session
新しいセッションを作成します. そのセッションにタブが1つもなければterm+mux-newを使って1つ開きます.
M-x term+mux-remote-session
term+mux-new-sessionと同じですが, ユーザ名, ホスト名, セッション名を指定してセッションを作成します.

ターミナルモード

最初に端末を開いたときのモードです. このモードではほとんどのキーはそのまま端末内アプリに送られます. Emacs側で解釈されるキーとその意味は次の通りです.

C-c
プレフィックスキー (term.elにもともと用意されているコマンドを利用できます. term+key-intercept.elを利用している場合は, 後続のキーを即座に打たなかった場合にはC-cを端末に送ります.)
C-c C-e
ESCを端末に送る (term+key-intercept.elを利用している場合は単にESCキーを打てば同様のことができます.)
C-c C-c
C-cを端末に送る
C-c h
端末画面のハードコピーを取る
C-c l
端末の出力テキストログのファイルへの保存を開始/終了
C-c r
端末を録画開始/終了
C-q
後続のキーをそのまま端末に送る
C-y
killリング(クリップボード)の中身を端末にyank(ペースト)
C-x
Emacsの通常のC-xキーとして動作
M-x
Emacsの通常のM-xキーとして動作
M-:
Emacsの通常のM-:キーとして動作
M-RET
編集モードに移行

ターミナルモードでEmacs側で解釈されるキーを増やすには, term+char-mapに対してdefine-keyします. たとえば, C-zで他のバッファを前面に出すには以下のように設定します.

(define-key term+char-map (kbd "C-z") #'bury-buffer)

編集モード

ターミナルモードからM-RETで編集モードに入ると, 端末内のカーソル位置以外の領域は読み取り専用になり, カーソル位置に入力フィールドが設定されます. 入力フィールド内はEmacsの通常のバッファと同様に編集でき, RETで入力フィールドの内容が端末に送られます. 入力フィールド内で改行するにはC-jを使います.

入力フィールドでのキーバインドterm+line-mapで設定できます. デフォルトでは次のようになっています.

RET
入力フィールドの内容を端末に送信
C-a
行頭へ移動し, 入力フィールドの先頭で一度止まる
C-e
行末へ移動し, 入力フィールドの末尾で一度止まる
C-k
カーソル位置から行末までを削除し, 入力フィールドの末尾以降は残す
C-c C-u
入力フィールドの内容を削除
C-c C-w
入力フィールドの内容を1単語だけ削除
M-p
1つ前の入力内容を表示
M-n
1つ後の入力内容を表示
M-RET
ターミナルモードに戻る

term+mode.elを利用すると入力フィールドの中だけ別のメジャーモード(たとえばsh-mode)にすることができます. 詳しくはシェル連携についての説明を見て下さい.

入力フィールドの外ではスペースキーで範囲選択を開始できます. もう一度スペースキーを押すと選択された範囲をコピーして編集モードを終了します. RETESCでも編集モードを終了できます. 入力フィールドの外で有効になるキーバインドterm+input-readonly-mapで設定できます.

ログ機能

ターミナルモードでは標準で3種類のログ機能を利用できます.

ハードコピー

GNU screenにおける:hardcopyと同等の機能です.

C-c hまたはM-x term+hardcopyで, 端末の1画面に表示されている内容をテキストファイルに保存できます. term+hardcopy-visible-contentsnilに設定すると, 1画面ではなく端末バッファ内のすべての内容を保存します. term+hardcopy-appendtに設定すると, 保存先のファイルが存在する場合は末尾に追記するようになります. この際, 追記された内容ともともとのファイルの内容との間にはterm+hardcopy-separatorに設定された区切りが挿入されます. term+hardcopy-separatorの詳細はM-x describe-variable, term+hardcopy-separatorを参照して下さい.

出力テキストログ

1画面ではなく, いままでに表示されたバッファ内容をテキストファイルに保存することもできます. C-c lまたはM-x term+start-buffer-logでファイルを指定すると, そのファイルにバッファ内容を記録していきます. 記録をやめるにはもう一度C-c lするか, またはM-x term+stop-buffer-logします. ファイルへの書き込みはEmacsがアイドル状態の時または記録の終了時に行なわれます. ファイルに書き込むまでの待ち時間はterm+buffer-log-intervalで秒単位(小数可)で指定できます.

端末バッファでは本来term-buffer-maximum-sizeで指定した行数を超えた分は古い行から削除されますが, この機能で出力されたファイルには古すぎて削除された分のバッファ内容も記録されています(ただしM-x term+start-buffer-logしたときに既に消えていた分に関してはこの限りではありません).

この機能は内部でtruncate(1)コマンドを使用します. もしtruncate(1)がインストールされていない場合は, 端末バッファの内容を(2048行を超えた分も含めて)別のバッファに保持するようになっています. メモリ効率が気になる場合はtruncate(1)コマンドをインストールするようにして下さい.

録画

C-c rまたはM-x term+start-recordで, 指定したファイルへの端末の録画を開始します. 録画中は右上に「REC」という録画マークが表示されます. 録画を終了するにはもう一度C-c rするか, もしくはM-x term+stop-recordするか, あるいは録画マークをクリックします.

録画される内容は, 後述の特殊制御シーケンスを除いたあらゆる(ふつうの文字列も含む)制御シーケンスと, そのシーケンスが出力された時刻で, ttyrecコマンドと互換性のあるデータ形式で保存されるので, ttyplayコマンドで再生できます.

セッション管理とタブ操作

この機能にはterm+mux.elおよびその依存パッケージtab-group.elが必要です.

セッション

セッションは複数の端末バッファのタブを1つのグループにまとめたものです. ふつうはユーザ名とホスト名が共通する複数の端末バッファを1つにまとめるために使います. あるセッションに属する端末バッファを選択中にterm+mux-newで新たな端末バッファを開くとき, セッションに関連づいたユーザ名とホスト名が使用されます.

セッションがローカルホストのrootのものの場合, デフォルトではsudoした端末を開きます. またセッションがリモートホストのものの場合, デフォルトではsshした端末を開きます. sudosshへの引数はterm+mux-sudo-optionsおよびterm+mux-ssh-optionsで設定できます. sshの場合, ForwardX11ControlMasterなどの設定も適宜行なわれます. これらの挙動を変えたい場合はterm+mux-ssh-*カスタム変数を設定して下さい.

デフォルトでは同じセッション内のバッファはバッファ一覧に1つだけしか表示されません. この設定を変更するにはterm+mux-session-buffer変数の値をnilにします.

タブバー

タブは, ターミナルモードではモードラインに, 編集モードでは通常のモードラインの末尾に表示されます. term+mux-mode-line-tabbar変数の値をnilにすると, タブは常にヘッダラインに表示されます.

タブはドラッグ&ドロップで別の位置に移動できます. これはウィンドウをまたいでいても構いません. ウィンドウをまたぐ移動の場合, ある端末バッファを別のセッションのタブバーに, あるいは端末バッファではないタブバー(tab-group.elの機能で作られた通常のタブグループ)に移動することもできます.

タブ操作

タブに開いた端末バッファでは次のキーでタブを操作できます. ただし編集モードでは先頭にC-xが必要です. プレフィックスキーはterm+mux-char-prefixおよびterm+mux-line-prefixを設定することで変更可能です.

C-t N
新しい端末バッファを開く
C-t o
端末バッファを別ウィンドウに開く
C-t O
新しい端末バッファを別ウィンドウに開く
C-t c
新しい端末バッファを開く
C-t C
新しいコマンドを指定して端末バッファを開く
C-t S
新しいセッションを作る
C-t R
新しいリモートセッションを作る
C-t r
現在のタブの名前を変更する
C-t t
現在のタブの名前を変更する
C-t u
現在のタブの名前を元に戻す
C-t スペース
次のタブに移動
C-t n
次のタブに移動
C-t p
前のタブに移動
C-t s
タブを選択 (選択中はタブ番号が表示され, タブ番号またはタブ名の一部をスペース区切りで入力することで絞り込んで選択します.)
C-t g
タブを表示するグループをスイッチ (これは1つのタブが複数のタブグループに属している場合に, 別のグループのタブバーを表示するために使います.)
C-t N
現在のバッファを指定したタブグループのタブとして追加
C-t P
現在のバッファの(現在のグループの)タブをタブバーから取り除く
C-t l
現在のグループに属するタブ一覧を表示
C-t ←
タブバーを左にスクロール
C-t →
タブバーを右にスクロール
C-t home
タブバーを左いっぱいにスクロール
C-t end
タブバーを右いっぱいにスクロール
C-t <
タブを左に移動
C-t >
タブを右に移動
C-t [
タブを左端に移動
C-t ]
タブを右端に移動

シェル連携

term+.elではシェル連携のための機能を提供しています. シェル側の設定例(zsh用)はhttps://github.com/tarao/dotfiles/blob/master/.zsh/eterm.zshにあります.

実際にはシェルだけでなく端末内で動くすべてのアプリケーションがこの機能を利用できます. シェル連携機能を利用するには, 端末内のアプリケーションは端末(/dev/tty)に対して特殊な制御シーケンスを送ります. 特殊な制御シーケンスに関連づけられたコマンドは端末内アプリケーションからの要求に従った動作をして, 場合によっては端末内アプリケーションへ応答を返します.

定義済みの特殊制御シーケンスについて調べる

M-x describe-function, term-emulate-terminalに詳しい説明があります. またM-x term+control-command-listで現在定義されている特殊制御シーケンスの一覧が表示されます.

編集モード

編集モードに関する特殊制御シーケンスには以下のものがあります.

"\e]51;mode;mode-name\e\\"
次に編集モードに入ったときに入力フィールド内で有効にするメジャーモードをmode-nameに指定
"\e]52;i;text\e\\"
textが入力フィールド内に入った状態で編集モードに移行
"\e]52;n;text\e\\"
textが入力フィールド内に入った状態でevil-normal-stateの編集モードに移行 (Evil利用時のみ可, term+evil.elが必要)

たとえばM-ish-modeが有効になった編集モードに入るようにするzshの設定は次のようになります.

function switch-to-line-mode-insert () {
    local buf="$BUFFER"
    zle kill-buffer
    zle -R
    echo -ne "\e]51;mode;sh-mode\e\\" > /dev/tty
    echo -ne "\e]52;i;$buf\e\\" > /dev/tty
}
zle -N switch-to-line-mode-insert
bindkey '^[i' switch-to-line-mode-insert

右プロンプトにも対応した詳細なバージョンは設定例を参照して下さい.

ユーザ名, ホスト名, カレンドディレクト

ユーザ名, ホスト名およびカレントディレクトリを端末へ通知すると, 端末バッファのdefault-directoryの値を端末内のアプリケーションのものに設定できます. こうしておくと, TRAMPでリモートファイルを開く場合や, 他のシェル連携機能を利用する場合に便利です.

以下の制御シーケンスでこれらの情報を通知できます.

"\e]51;host;host-name\e\\"
ホスト名をhost-nameに設定
"\e]51;user;user-name\e\\"
ユーザ名をuser-nameに設定
"\e]51;cd;path\e\\"
カレントディレクトリをpathに設定

zshでの設定例は以下のようになります.

host=`hostname`
echo -ne "\e]51;host;$host\e\\" > /dev/tty
user=`id -run`
echo -ne "\e]51;user;$user\e\\" > /dev/tty

function precmd_eterm_cwd () {
    local dir; dir=`pwd`
    echo -ne "\e]51;cd;$dir\e\\" > /dev/tty
}
typeset -Uga precmd_functions # これはどこかで一度だけしておく
precmd_functions+=precmd_eterm_cwd
ファイル転送

指定したファイルをEmacsで開く, あるいはコピーするために, 以下の制御シーケンスが利用できます.

"\e]51;open;files\e\\"
filesEmacsで開く
"\e]51;view;files\e\\"
filesEmacsview-modeで開く
"\e]51;get;files\e\\"
filesをミニバッファで指定した場所にコピー
"\e]51;put;\e\\"
ミニバッファで指定したファイルをdefault-directoryにコピー
"\e]51;put;m\e\\"
diredで選択した複数のファイルをdefault-directoryにコピー

files;で区切られたファイル名です. ユーザ名, ホスト名およびカレントディレクトリが正しく通知されていれば(default-directoryが正しく更新されるため)リモートホストのファイルであっても転送できます.

openviewで別ウィンドウで開くようにするにはterm+open-in-other-window変数の値をtに設定します.

view-modeで開いた場合, 通常のview-modeと違い, qで(正確にはView-quitコマンドで)バッファを閉じます.

get/putの向きの覚え方は, FTPの場合と同じです.

シェルで利用する方法は設定例を参照して下さい.

履歴選択

Emacsのインタフェースでシェルのコマンド履歴を参照してコマンドを選択・端末に送信することができます. ただし, あらかじめシェルの履歴ファイルを指定しておく必要があります.

"\e]51;histfile;path\e\\"
履歴ファイルをpathに設定
"\e]52;h;text\e\\"
絞り込みのための初期値をtextに設定した上で履歴選択を開始

リモートホストの端末で正しく動作させるためには, ユーザ名, ホスト名が正しく設定されている必要があります. histfileで指定されたファイルはEmacsのバッファとして開くため, リモートホストの場合には動作速度に問題がある場合もあります.

anything-complete.elとterm+anything-shell-history.elを利用している場合は履歴の選択がzshの履歴検索にanything.elを使う(ターミナル版)に似た, anythingを用いたインタフェースになります.

たとえばC-rで履歴選択できるようにするためのzshでの設定例は以下のようになります.

echo -ne "\e]51;histfile;$HISTFILE\e\\" > /dev/tty

function history-search-eterm () {
    local buf="$BUFFER"
    zle kill-buffer
    echo -ne "\e]52;h;$buf\\e\\" > /dev/tty
}
zle -N history-search-eterm
bindkey '^R' history-search-eterm
マルチプレクサ

マルチプレクサに関する特殊制御シーケンスとしては以下のものが利用可能です(term+mux.elが必要).

"\ekstr\e\\"
タブタイトルをstrに変更 (screen形式)
"\e[2;str\e\\"
タブタイトルをstrに変更 (tmux形式)
"\e]51;cdd;param\e\\"
他のタブのカレントディレクトリを問い合わせる

最初の2つはタブのタイトルを変更するためのもので, それぞれscreen, tmuxのものと同じ制御シーケンスです. たとえばscreenを用いている場合に, カレントディレクトリもしくは現在実行中のコマンドに応じてタブのタイトルを自動設定するスクリプトhttps://github.com/tarao/dotfiles/blob/master/.zsh/screen-title.zshに加えて, 以下の設定をしておくと, term+mux.elでも同様にタブ名を自動設定できます.

typeset -a eterm_options
eterm_options=(${(s:,:)INSIDE_EMACS})
function eterm_has () {
    [[ -n "$eterm_options[(r)$1]" ]] && return
    return 1
}

eterm_has mux && {
    whence precmd_screen_window_title >/dev/null && {
        precmd_functions+=precmd_screen_window_title
    }
    whence preexec_screen_window_title >/dev/null && {
        preexec_functions+=preexec_screen_window_title
    }
    SCREEN_TITLE=auto
}

3つ目は, 他のタブのカレントディレクトリに移動するためのGNU screen用コマンドcdd(id:secondlifeさん作)のterm+mux.el版を実装するために必要な制御シーケンスです. 実際の実装は設定例を参照して下さい. term+mux.el版のcddを引数なしで実行した場合, Emacsのタブ選択UIで他のタブを選択できます.

特殊制御シーケンスを追加する

特殊制御シーケンスは, 開始符号と終了符号の間にはさまれた文字列を, 関連づけられたコマンドの第1引数に渡して実行します. 制御シーケンスとコマンドを関連づけるにはterm+new-control-command関数に開始符号, 終了符号およびコマンド(のシンボル)を渡します.

たとえばファイルを開くための特殊制御シーケンスは次のように定義されています.

(defun term+open (files &optional find-file)
  ...)
(term+new-control-command "\033]51;open;" "\033\\" 'term+open)

その他

Emacs上の端末かどうか判別

環境変数INSIDE_EMACSを調べることで, 端末内のアプリがEmacs上の端末で動作しているかどうか確認できます. この環境変数にはterm+.elかどうか, term+.elのオプショナルなモジュールを使っているかどうかなどの情報も含まれています. INSIDE_EMACS環境変数はリモートセッションやrootセッションでも適切に設定されます.