はじめに
"libvte"とだけ聞いてもピンと来ない方もおられるかもしれません。
これは,端末エミュレータのコア(DEC VT端末のエミュレーション及び擬似端末処理等の裏方)機能をGtkWidgetとしてライブラリ化したものであり,要するにgnome-terminalの中身と考えていただければよいと思います。
libvte: https://github.com/GNOME/vte
(VTE is a library (libvte) implementing a terminal emulator widget for GTK+, and a minimal sample application (vte) using that.)
libvteを使うことで,低レイヤーでの端末処理に煩わされることなく,外観や使い勝手などの向上に注力できるようになり,端末エミュレータ開発の敷居が大幅に下がりました。
21世紀初頭にlibvteが登場してからというもの,libvteを利用し様々な特長を備えた端末エミュレータ(gnome-terminal roxterm sakura lxterminal など)が次々と開発されています。
しかし,端末エミュレーションという観点からlibvteを見てみると,Sixel Graphicsなど,VT100以降の上位機種の機能は十分実装されておらず,xtermなどに比べると見劣りしているといわざるを得ません(mltermも,xtermやRLoginに比べるとまだまだですが。)。
一方,折角端末エミュレータのコアとLook&Feelが分離されているわけですから,他の端末エミュレータのソースを基にlibvteとABI互換のライブラリを作って,コアの方を差し替えてしまえば,見た目や使い勝手はそのままに,より高機能な端末エミュレーションを実現するといったこともできるはずです。
mlterm (http://mlterm.sf.net) では,version 3.0.2以降において,libvte 互換ライブラリを提供しており,例えば,外観はgnome-terminalなんだけど,中身はmltermが動いてるといったことができるようになっています(ただし,libvteのAPIのうち未実装の関数もあります。また,おそらく誰も使っていないため,安定性には難があるかもしれません。)。
また,mltermでなくても,既存の端末エミュレータのソースコードをlibvteのABIでラップすれば,同様のことは理論上可能です。
本稿では,libvte互換ライブラリの実装方法や,実装する上で注意する必要のある事項等を概説したいと思います。
とりあえず試してみる
gnome-terminal の中で mlterm が動くといっても,どういうことかイメージが湧かないかもしれませんが,次のようにして試すことができます。
なお,gtk+>=3.0 を前提としています。
※ 最新版のビルド方法は https://github.com/arakiken/mlterm/blob/master/gtk/README を参照してください。(2020/05/04追記)
- libvte及びgnome-terminalをインストール
pkg-config vte-2.90 --modversion を実行し,パッケージが見つからないと言われる場合は,これらをインストールしてください。なお,libvteのヘッダファイル(Debian(Ubuntu)の場合,libvte-2.90-dev パッケージ)もインストールしておく必要があります。
なお,vte-0.38からAPIが変更され,パッケージ名もvte-2.91となっていますが,mltermのlibvte互換ライブラリはvte-2.90以前(vte-0.36以前)でしかテストしていません。
(2015.3.10追記:mlterm-3.4.4においてvte-2.91をサポートしました。) - http://mlterm.sf.net から最新のソースコードのtar ballを取得
- ビルド
$ tar xzvf mlterm-x.x.x.tar.gz
$ cd mlterm-x.x.x
$ ./configure --prefix=~/mlterm --with-gtk=3.0 ...
ここで,gtk/Makefileが生成されているかどうか確認してください。もし生成されていない場合,libvteがインストールされていない可能性があるため,上記1.の手順を確認してください。
また,/usr以下などにインストールすると正規のlibvteライブラリを上書きしてしまうため,--prefixオプションにより,必ず~/mltermなどシステムに影響しないディレクトリにインストールするようにしてください。
なお,mlterm 3.4.3以前ではデフォルトでgtk+-2.xを使用しますが,正規のlibvteライブラリがgtk+-3.xに依存している場合は,--with-gtk=3.0オプションによりgtk+-3.xを使用する必要があります。
$ make ; sudo make install
$ cd gtk ; make ; sudo make install
$ sudo ln -sf ~/mlterm/lib/libvte2_90.so.9 /usr/lib/libvte2_90.so.9
正規のlibvteライブラリであるlibvte2_90.so.9(通常はlibvte2_90.so.9.xxxへのシンボリックリンクになっていると思います。環境によっては,/usr/libでなく/usr/local/libや/usr/pkg/libなどにインストールされています。)を~/mlterm/lib/libvte2_90.so.9へのシンボリックリンクに張り替えることで,gnome-terminal等がlibvte互換ライブラリ上で動作するようになります。正規のlibvteライブラリに戻したい場合には,シンボリックリンクを元に戻してください。 - gnome-terminal を起動し,Sixel Graphics ファイルを cat して画像が表示されれば成功です。
上が通常のgnome-terminalで,下が中身をmltermに差し替えたgnome-terminalです。
(normal gnome-terminal)
(gnome-terminal with mlterm)
中身がmltermなので,gnome-terminalの中で,例えばpcf(ビットマップ)フォントを使用したり,右から左に表記するアラビア文字を表示したり,端末版vimの縦分割スクロール高速化設定 (http://qiita.com/kefir_/items/c725731d33de4d8fb096) が可能になったりと,mltermの持つ機能を利用することができるようになります。
(Arabic in gnome-terminal)
(mplus bitmap font in gnome-terminal)
なお,設定については,まず~/.mlterm/以下の設定ファイルが適用された上で,gnome-terminalでの設定が(上書き)適用されるので注意してください。
libvte互換ライブラリの実装方法
mltermのlibvte互換ライブラリは次のように実装しています。
- GtkWidget(GdkWindow)の上にmltermのWindowを乗っける。
- GtkWidgetの初期化に合わせて,mltermの内部状態も初期化。
- mlterm<=>gtk(gdk)間でイベントをやりとりしながら動作。
- GtkWidgetが破棄されるタイミングで,mltermの内部リソースも解放。
具体的な作業としては,VteTerminal(GtkWidgetを継承),VtePty及びVteReaperをGObjectとして定義し,vte_terminal_xxx(),vte_pty_xxx()及びvte_reaper_xxx()というlibvteのAPI関数の中身をmltermの関数を使ってひたすら実装していくという単純なものです。
libvteのAPI関数については,ドキュメントを読みながら同等のmltermの関数を呼び出す形で淡々と実装していけばいいですが,GtkWidget内部のイベントハンドラについては,libvteのソースコードを読んでその挙動を調査し,GtkWidget内部の状態とmlterm内部の状態を適切に更新していく必要があり,少々面倒です。
ついては,いくつか留意事項をまとめてみました。
なお,以下の項目名は,mlterm-x.x.x/gtk/vte.cにおけるイベントハンドラの関数名です。
実装上の留意事項
vte_terminal_realize()
GtkWidgetClass::realizeに登録し,VteTerminalのGdkリソースを生成すべき時に呼び出されます。
まず,gdk_window_new()によりGdkWindowを生成し,これをVteTerminalに対してgtk_widget_set_window()します。
ここまでは通常のGtkWidgetと同じですが,ここから更に,このGdkWindowのXIDを親とし,同じ属性でmltermのWindowを生成してやります。
こうすることで,gnome-terminalやgtk内部からは通常のGtkWidgetのように見えるVteTerminal上に,mltermの描画する画面を表示することができるようになります。
vte_terminal_filter()
gdk_window_add_filter(NULL, vte_terminal_filter, NULL)のように登録し,gdkが(X Serverから)受け取った全てのイベントをフックします。
GtkWidget(GdkWindow)の上にmltermのWindowを被せるに当たっては,mlterm内部のイベント処理とgtk/gdk内部のイベント処理の橋渡しをしてやらなければなりません。
mltermはXlibのみに依存しておりgtkやgdkのことは知りませんから,vte_terminal_filter()でフックしたイベントをXlibから受け取ったかのようにmltermの関数に渡してやる必要があります。
また,イベントをgtk/gdk内部でも処理させたい場合には,戻り値としてGDK_FILTER_CONTINUEを返す必要があります。
このように両者を橋渡しすることで,例えば,右クリックするとgnome-terminalのメニューが表示され,Ctrl+右クリックするとmltermの設定画面が出る,といったように両者の機能をシームレスに使用することができます。
vte_terminal_size_allocate()
GtkWidgetClass::size_allocateに登録し,リサイズ時に呼び出されます。
リサイズ時には,mlterm内部の画面サイズ情報を更新するだけでなく,VteTerminal構造体の
char_width
char_height
char_ascent
char_descent
row_count
column_count
メンバも更新しておく必要があります。
また,gtk_adjustment_configure()を呼び出して,スクロールバーにも反映しておかなければなりません。
vte_terminal_unrealize()・vte_terminal_finalize()
前者はGtkWidgetClass::unrealizeに登録し,後者はGtkWidgetClass::finalizeに登録します。
synapticのようにコマンドの実行状況を表示するのにlibvteを利用するようなアプリケーションでは,vte_terminal_unrealize()が呼ばれた後,vte_terminal_finalize()されず,再度vte_terminal_realize()される場合があります。
そのため,vte_terminal_unrealize()の中で解放するリソースはWindow関係のみとし,ptyをcloseしてはいけません。
catch_child_exited()
forkした子プロセス(シェルなど)がexitしたかどうかは,通常,SIGCHLDシグナルで検知することができます。
libvteでは,VteReaperという専用のオブジェクトがあります。
子プロセスのpidをvte_reaper_add_child()しておき,"child-exited"シグナルにハンドラを登録しておくことで,その子プロセスの終了を検知することができるという仕組みになっています。
おわりに
mltermのlibvte互換ライブラリについて,その一部を概説してみました。
正直libvteの互換ライブラリを作るとか時間の無駄という感はありますが,しょうもないことにリソースを無駄遣いして「動いたワーイ」を楽しむことができるのが,趣味プログラミングの醍醐味でもあります。というわけで,誰かxtermベースのlibvte互換ライブラリでも作ってみてはいかがでしょうか。