Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

Acmeをプログラムから操作する

Acmeは、Plan 9のために書かれたテキストエディタです。現在はPlan 9 from User SpaceUnix環境にも移植されています。

このエディタはプログラムから操作するための機能がいくつか用意されていて、それを使うとキーボードやマウスなどの入力イベントを外部のプログラムが受け取り、加工してエディタに反映するなどといったことができます。標準で用意されているのはメーラーですが、最近Russ CoxがTodoを公開していました。

プログラマ向けの情報はAcmeのマニュアルにあります。

また、Goから扱うためのパッケージもあります。いろいろ複雑なeventファイルを適切に扱ってくれるので便利です。

基本

Acmeは水色の領域(tag)と黄色の領域(body)を合わせてwindowとして扱います。Acmeはwindow毎に1つディレクトリを用意していて(以下の実行例では1という名前のディレクトリ)、windowを開くたびに2,3...と増えていきます。プログラムからAcmeを操作する場合は、これらのディレクトリを使って行います。

$ 9p ls acme | mc
1       cons    draw    index   log
acme    consctl editout label   new

$ 9p ls acme/1 | mc
addr    ctl     editout event   tag     xdata
body    data    errors  rdsel   wrsel

開いているファイルの取得

acme/indexファイルにも、現在開いているファイルやディレクトリのリストが記録されています。acmeディレクトリを直接読み込んでも同じことはできますが、ほとんどの場合はこちらを使ったほうが簡単でしょう。

$ 9p read acme/index
          1          35         318           1           0 /Users/lufia/ Del Snarf Get | Look 
          2          39          20           0           1 /Users/lufia/.inputrc Del Snarf | Look 
          4          39          26           1           0 /Users/lufia/lib/ Del Snarf Get | Look 
          5          40         101           0           0 /Users/lufia/lib/npmrc Del Snarf | Look 

1行が1つのwindowを表し、左から順番に固定幅で次のような情報が書かれています。

  1. windowのID
  2. タグ行の文字数(rune)
  3. ファイルの文字数(rune)
  4. 開いているファイルがディレクトリかどうか(ディレクトリなら1)
  5. 開いているファイルが編集中(未保存)かどうか(編集中なら1)
  6. タグ行の内容

テキストの編集

windowディレクトリ(1/など)配下のaddrdataは、編集しているテキストを読み書きするために使います。

addrAcmeが扱うアドレスを書くとテキストが選択された状態になります。不要な改行を加えるとエラーになるので気をつけましょう。Acmeは以下のような書式をアドレスとして扱います。

  • 2 - 2行目を選択
  • 2,4 - 2〜4行目を選択
  • /func.*\n/ - funcから改行までを選択
  • #0,#2 - ファイルの先頭から2文字選択

範囲選択が成功すると、dataxdataの内容が範囲で選択された部分に置きかわります。datareadすると、範囲選択部分が終わってもテキストが続けば最後まで読んでしまいますが、xdataは範囲の終わりでEOFとなります。同様にwriteすると選択した範囲を新しいテキストで置き換えます。

$ 9p read acme/2/body
1行目
2行目
3行目
$ echo -n '2' | 9p write acme/2/addr
$ 9p read acme/2/data
2行目
3行目
$ echo -n '2' | 9p write acme/2/addr
$ 9p read acme/2/xdata
2行目

ただし、addrを更新しても内部的に選択された状態になるだけで、画面とはリンクしていません。画面を更新する場合はctlを使います。

$ echo -n '#30,#45' | 9p write acme/1/addr
$ echo -n 'dot=addr' | 9p write acme/1/ctl

dot=addrは変わったコマンドですが、dotは画面で選択されている範囲、addraddrファイルの内容を表すので、addrファイルの内容で画面の選択範囲を更新する意味になります。画面で選択した範囲をaddrに設定するaddr=dotもあります。

イベントの取得

Acmeのイベントは大きく2種類あります。ひとつはエディタ全体でのイベント、もうひとつはwindow単位で発生するイベントです。

acme/log

logはwindowのID、操作、ファイル名の3つが連続したテキストです。ファイル名は省略される場合もあります。実行例を示します。

$ 9p read acme/log
1 focus /Users/lufia/
2 new     # Newコマンドを2ボタンで実行した場合
2 focus 
3 new /Users/lufia/src/    # 3ボタンでファイルを開いた場合
3 focus /Users/lufia/src/
2 del 
3 focus /Users/lufia/src/
3 del /Users/lufia/src/
1 focus /Users/lufia/

スペースで区切った3つのうち、最初のカラムはAcmeのwindowを表す数字です。2つ目は操作を表すコマンドで、これはいくつかあります。

op 意味
new 新しいwindowを作成した
focus windowにマウスポインタが移動した
del windowを削除した
get ファイルを再読み込みした
put ファイルを保存した
zerox windowを複製した

最後のカラムは開いているファイル名またはディレクトリ名です。

acme/<id>/event

eventはwindow単位のイベントが流れてくるファイルです。このファイルをopenしている間は、イベントが流れてくる代わりにAcmeデフォルトの動作は止まります(書き戻すことでデフォルトの動作を起こせる)。

ファイルの内容は1つのイベントが1行になっていて、エディタを操作するたびに1行追加されていきます。eventファイルを開いてからのイベントは読み込みできますが、過去のイベントを読むことはできません。

[origin][type][addr0] [addr1] [flag] [n] [text?]\n

originは何からイベントが発生したかを表す1文字です。

origin どこからイベントが発生したか
E bodyまたはtagファイル
F eventファイルへの書き込みなど
K キーボード入力によるイベント
M マウス操作によるイベント

typeは大きく4つですが、tagまたはbodyのどちらで発生したものかを区別します。また、typeによって続きのaddr0addr1が意味するものが変わります。

type 意味
D bodyからaddr0addr1の範囲が削除された
d tagからaddr0addr1の範囲が削除された
I bodyaddr0addr1textを挿入した
i tagaddr0addr1textを挿入した
L bodytextを検索(マウスボタン3)
l tagtextを検索(マウスボタン3)
X bodytext実行(マウスボタン2)
x tagtextを実行(マウスボタン2)

Dまたはdイベントの場合、addr0addr1の値は削除前の範囲を指します。そのため、イベント発生後に範囲の内容を読み込んでも、元のテキストは消えてしまっているため取り出せません。また、Iまたはiイベントの場合は、addr0addr1はテキストを挿入し終えた後の範囲(新しく増えたテキスト部分)を表します。では2文字以上範囲選択した状態でテキスト入力するとどうなるか、ですが、この場合はDIのイベントに分割されます。従って、addrdataファイルを使って以下の操作を行なった場合、

$ echo -n 2 | 9p write acme/12/addr
$ echo hello | 9p write acme/12/data

eventファイルには分割された次のイベントが届きます。

FD6 12 0 0
FI6 12 0 6 hello

イベントの残り要素、flagtextは複雑なのでacmeのマニュアルを読んでください。textは色々な要因により省略されたりします。

利用例

AcmeのファイルAPIを使った例をいくつか挙げます。

# 4行目から9行目の範囲をaddrに設定
echo -n '4,6' | 9p write acme/<id>/addr

# 選択した範囲(4行目から9行目)を読む
9p read acme/<id>/xdata

# 先頭からのオフセットで範囲選択
echo -n '#34,#54' | 9p write acme/5/addr

# 選択した範囲を更新
echo test | 9p write acme/<id>/data

# addrの内容をwindowに反映(dot)
echo -n 'dot=addr' | 9p write acme/<id>/ctl

# windowで選択した範囲をaddrに設定
echo -n 'addr=dot' | 9p write acme/<id>/ctl

# 変更ありの状態にする
echo -n dirty | 9p write acme/<id>/ctl

# 変更ありフラグを落とす
echo -n clean | 9p write acme/<id>/ctl