Introduction
One can poll various acme(1) events via its 9P file system, documented in acme(4). For some general ideas about acme(1), see this article; for some extra tools, see this article.
Regarding events, some aspects are unclear from the man pages, hence this little article.
There are two sources of events, that we’ll explore in the following two sections. I’ll conclude by a third and final section with an example, demonstrating how to use events to implement shortcuts.
Note: The implementation hasn’t been battle-tested yet; I saw acme(1) crash a few times too, so use with caution.
Log events
Those are the events one can access by reading acme/log
; here’s a sample
output:
% 9p read acme/log
64 focus /home/mb/acme-events.md
62 focus /home/mb/-earth
64 focus /home/mb/acme-events.md
65 focus /man/acme(4)
62 focus /home/mb/-earth
64 focus /home/mb/acme-events.md
65 focus /man/acme(4)
64 focus /home/mb/acme-events.md
62 focus /home/mb/-earth
66 new
66 focus
62 focus /home/mb/-earth
66 focus
66 del
62 focus /home/mb/-earth
Here’s the associated documentation from acme(4):
log reports a log of window operations since the opening of
the log file. Each line describes a single operation
using three fields separated by single spaces: the dec-
imal window ID, the operation, and the window name.
Reading from log blocks until there is an operation to
report, so reading the file can be used to monitor edi-
tor activity and react to changes. The reported opera-
tions are `new' (window creation), `zerox' (window cre-
ation via zerox), `get', `put', and `del' (window dele-
tion). The window name can be the empty string; in
particular it is empty in `new' log entries correspond-
ing to windows created by external programs.
Note that this is a read-only file, which means, by comparison with the other source of event we’ll talk about in the next section, that you can’t generate events by writing to this file:
% 9p ls -l acme/log
--r-------- M 0 mb mb 0 Jan 30 19:28 log
Note: You can still “generate” the corresponding
events (e.g. opening/closing windows), by other means (e.g.
echo -n '' | 9p write acme/new/ctl
to create a new window as
if clicking with button 2 on New
).
This won’t be covered here (actually, I somehow assume that you’re already familiar with that kind of stuff). You may want to read acme(4), this article and the related code on GitHub for more on the topic.
Buffer/window events
Each window is identified by a unique (numeric) id; there are multiple ways to access them, at least:
9p ls acme | grep '^[0-9]+*'
9p read acme/index | awk '{ print $1 }'
;echo $winid
;- B2 (mouse click with middle-button/second button) on
ID
(within acme(1) then, for instance on the tagline).
To each window/buffer (I’ll use both terms interchangeably)
is associated a directory, e.g. acme/64
. In this directory,
there’s, among others, a file event
, that can both be read
and written to:
% 9p ls -l acme/64/event
--rw------- M 0 mb mb 0 Jan 30 19:33 event
Now if you try to read this file, you will observe essentially (not always, there’s a special case, but I’ll ignore it) one line per-event:
% 9p read acme/64/event
...
KI2853 2854 0 1 ,
KI2854 2855 0 1
KI2855 2856 0 1 f
KI2856 2857 0 1 i
KI2857 2858 0 1 l
KI2858 2859 0 1 e
Mx43 43 2 0
Mx36 47 0 11 echo $winid
...
Once this file is opened, you should still be able to type in the
corresponding window, cut/paste text with the mouse, which should all
generate events printed to acme/<winid>/event
. But you
might observe that other actions are “broken”, in particular,
executing commands from the tag line, such as B2 on Put
.
The behavior is documented; let’s open acme(4) and carefully read it:
event
When a window's event file is open, changes to the win-
dow occur as always but the actions are also reported
as messages to the reader of the file. Also, user
actions with buttons 2 and 3 (other than chorded Cut
and Paste, which behave normally) have no immediate
effect on the window; it is expected that the program
reading the event file will interpret them. The mes-
sages have a fixed format: a character indicating the
origin or cause of the action, a character indicating
the type of the action, four free-format blank-
terminated decimal numbers, optional text, and a new-
line. The first and second numbers are the character
addresses of the action, the third is a flag, and the
final is a count of the characters in the optional
text, which may itself contain newlines. The origin
characters are E for writes to the body or tag file, F
for actions through the window's other files, K for the
keyboard, and M for the mouse. The type characters are
D for text deleted from the body, d for text deleted
from the tag, I for text inserted to the body, i for
text inserted to the tag, L for a button 3 action in
the body, l for a button 3 action in the tag, X for a
button 2 action in the body, and x for a button 2
action in the tag.
If the relevant text has less than 256 characters, it
is included in the message; otherwise it is elided, the
fourth number is 0, and the program must read it from
the data file if needed. No text is sent on a D or d
message.
For D, d, I, and i the flag is always zero. For X and
x, the flag is a bitwise OR (reported decimally) of the
following: 1 if the text indicated is recognized as an
acme built-in command; 2 if the text indicated is a
null string that has a non-null expansion; if so,
another complete message will follow describing the
expansion exactly as if it had been indicated
explicitly (its flag will always be 0); 8 if the com-
mand has an extra (chorded) argument; if so, two more
complete messages will follow reporting the argument
(with all numbers 0 except the character count) and
where it originated, in the form of a fully-qualified
button 3 style address.
For L and l, the flag is the bitwise OR of the follow-
ing: 1 if acme can interpret the action without loading
a new file; 2 if a second (post-expansion) message fol-
lows, analogous to that with X messages; 4 if the text
is a file or window name (perhaps with address) rather
than plain literal text.
For messages with the 1 bit on in the flag, writing the
message back to the event file, but with the flag,
count, and text omitted, will cause the action to be
applied to the file exactly as it would have been if
the event file had not been open.
It’s a bit disconcerting that writing back messages “as-is” doesn’t work:
% echo Mx48 51 0 3 Put | 9p write acme/64/event
9p: write error: bad event syntax
Not only is this the format we read message from, but it clearly matches the man page’s description:
The messages have a fixed format: a character indicating the
origin or cause of the action, a character indicating
the type of the action, four free-format blank-
terminated decimal numbers, optional text, and a new-
line.
But if you look closely:
For messages with the 1 bit on in the flag, writing the
message back to the event file, but with the flag,
count, and text omitted, will cause the action to be
applied to the file exactly as it would have been if
the event file had not been open.
And indeed, if you skip the flag/count/extra text, you can generate some events:
% echo Mx38 38 | 9p write acme/64/event
%
You’d still have issues for others:
% echo KD10828 10829 | 9p write acme/64/event
9p: write error: bad event syntax
But alright, it’s expected from the man page, as the flag for such messages didn’t had its first bit set. Nevertheless, some events who still lack this characteristic can still be forwarded:
# This one was a right click (B3) on a man page string, acme(4);
# the event is forwarded, and ends up opening the corresponding
# man page, as expected.
#
# Here are the two events from which it's been drawn:
# Ml83 83 2 0
# Ml80 84 0 4 acme
% echo Ml83 83 | 9p write acme/57/event
%
Let’s head to the sources for confirmation.
% cd $PLAN9
% grep -Rn 'bad event syntax'
src/cmd/acme/xfid.c:25:char Ebadevent[] = "bad event syntax";
grep: bin/acme: binary file matches
% grep -n Ebadevent *.[ch]
xfid.c:25:char Ebadevent[] = "bad event syntax";
xfid.c:925: err = Ebadevent;
Here’s the relevant code from xfid.c
:
void
xfideventwrite(Xfid *x, Window *w)
{
...
for(n=0; n<x->fcall.count; n+=m){
p = x->fcall.data+n;
w->owner = *p++; /* disgusting */
c = *p++;
while(*p == ' ')
p++;
q0 = strtoul(p, &q, 10);
if(q == p)
goto Rescue;
p = q;
while(*p == ' ')
p++;
q1 = strtoul(p, &q, 10);
if(q == p)
goto Rescue;
p = q;
while(*p == ' ')
p++;
if(*p++ != '\n')
goto Rescue;
m = p-(x->fcall.data+n);
if('a'<=c && c<='z')
t = &w->tag;
else if('A'<=c && c<='Z')
t = &w->body;
else
goto Rescue;
if(q0>t->file->b.nc || q1>t->file->b.nc || q0>q1)
goto Rescue;
qlock(&row.lk); /* just like mousethread */
switch(c){
case 'x':
case 'X':
execute(t, q0, q1, TRUE, nil);
break;
case 'l':
case 'L':
look3(t, q0, q1, TRUE);
break;
default:
qunlock(&row.lk);
goto Rescue;
}
qunlock(&row.lk);
}
Out:
...
return;
Rescue:
err = Ebadevent;
goto Out;
}
Example: ^s
to write the current buffer to disk
Let’s see how we can “implement” a basic shortcut to save (Put
)
a file on ^s
/ctrl-s
(note that there are other, perhaps better, ways
to handle this, without relying on acme(1)’s events, e.g. via X11).
Pressing ^s
in acme is interpreted as typing a “weird”
character (, that is likely not to be displayed by your
browser’s font); it’s actually the case for a bunch of
ctrl-
combinations, which means that you can implement other “shortcuts”
using this technique.
If you try to snoop around the /event
file at what happens
when you press ^S
you’ll see that, as expected, it generates
a keyboard event, similar to when we type any other character:
% 9p read acme/64/event
...
KI10165 10166 0 1
...
We’ll need to write two shell scripts for this to work:
register-hook.sh <path/to/hook.sh>
, which will register a script processing the<winid>/event
file for all existing acme(1) buffers. It’ll also need to keep track of newly created buffers, so as to register the hook for them too, which implies that it’ll need to listen onacme/log
;hook-shortcut.sh <winid>
, which will be the hook to be registered by the previous script for the given window id.
Let’s look at them in more details, starting with the latest. A link to a more polished implementation of those scripts will be provided later.
hook-shortcut.sh
We need to proceed as such:
- Open the
event
file for a given buffer and poll it; - Write back relevant events (e.g.
Mx
); - Look for keyboard insertion events with a text of
;
- Remove the symbol
from the file;
- Store the file to disk.
We’ll do this in awk(1), wrapped in a sh(1) script. Event redirection can be performed as follow:
# Note that this will naturally die when the corresponding
# window is closed.
9p read acme/$id/event 2>/dev/null | awk '
{
# debug
# print $0
# (flag & 1) || (flag & 2)
if ($3 % 2 || ($3 / 10) % 2) {
printf("%s %d\n", $1, $2) | "9p write acme/'$id'/event"
}
}
Note: As already discussed, we need to redirect more events than just those with a flag with bit one set. Perhaps this will need to be adjusted even further later on.
If you try the previous code for a bit, you’ll discover
that the automatic menu management gets thrown away once the
event
file is opened, which isn’t documented; see xfid.c
:
void
xfidopen(Xfid *x)
{
...
case QWevent:
if(w->nopen[q]++ == 0){
if(!w->isdir && w->col!=nil){
w->filemenu = FALSE;
winsettag(w);
}
}
break;
...
}
And, well, the “automatic menu management” itself is yet another
poorly documented feature too: writing menu
/nomenu
to
<winid>/ctl
files enables/disables it; it’s responsible e.g.
to automatically show/hides Put
in the tag when the files is
modified/unmodified).
Note: For the record, I’m confident that most of the
code related to external programs polling <winid>/event
was
written for win(1)
. There are almost no other (published) external
programs.
Still in xfid.c
:
void
xfidctlwrite(Xfid *x, Window *w)
{
...
if(strncmp(p, "nomenu", 6) == 0){ /* turn off automatic menu */
w->filemenu = FALSE;
settag = TRUE;
m = 6;
}else
if(strncmp(p, "menu", 4) == 0){ /* enable automatic menu */
w->filemenu = TRUE;
settag = TRUE;
m = 4;
...
}
So we’ll want to adjust our previous piece of awk(1)
to re-enable the automatic management once we’ve opened the event
file, e.g.
# For some reason, automatic menu management is thrown
#Â away once /event is opened; do it here so as to avoid
# any dead-lock.
if (!menu) {
printf("menu") | "9p write acme/'$id'/ctl"
menu = 1
}
Regarding the symbol removal: observe that we’ll be reading
an event, and that this event will contain exactly the position
of this character (the first two numbers). For instance, in
the following it would be
10165
and 10166
% 9p read acme/64/event
...
KI10165 10166 0 1
...
We can then use the following idiomatic pattern to programmatically alter a buffer:
- First, write the address to
<winid>/addr
in the format#m,#n
wherem
andn
are the position we get from the event (e.g. in our previous example, we’d need to write#10165,#10166
to “select” the character to remove). - Then, write an empty string to
<winid>/data
to remove the portion of the text we’ve just selected.
We can then write put
to <winid>/ctl
to store the file. This
gives the following final script:
#!/bin/sh
if [ -z "$1" ]; then
echo `basename $0` '<winid>' 1>&2
exit 1
fi
id=$1
# Note that this will naturally die when the corresponding
# window is closed.
9p read acme/$id/event 2>/dev/null | awk '
{
# For some reason, automatic menu management is thrown
#Â away once /event is opened; do it here so as to avoid
# any dead-lock.
if (!menu) {
printf("menu") | "9p write acme/'$id'/ctl"
menu = 1
}
# Debug
# print $0
# (flag & 1) || (flag & 2)
if ($3 % 2 || ($3 / 10) % 2) {
printf("%s %d\n", $1, $2) | "9p write acme/'$id'/event"
}
}
# $4 == 1 because otherwise this will be sometimes
# triggered e.g. on copy/pasting stuff containing a
/K.*/ && $4 == 1 {
q0 = substr($1, 3)
q1 = $2
# flush the previous message before starting to work with /data,
# for otherwise, we may start writing stuff before
# the address where we want to write has been registered.
printf("#%d,#%d", q0, q1) | "9p write acme/'$id'/addr"
close("9p write acme/'$id'/addr")
printf("") | "9p write acme/'$id'/data"
close("9p write acme/'$id'/data")
printf("put") | "9p write acme/'$id'/ctl"
}'
Note: There’s still an inconvenient issue with this,
again poorly documented: once there’s a reader for <winid>/event
,
an ad-hock dumping mechanism is required: it’s expected that the
reader will provide a dumping command via <winid>/ctl
. Otherwise,
the windows simply won’t be dumped, see rows.c
:
void
rowdump(Row *row, char *file)
{
...
/* windows owned by others get special treatment */
if(w->nopen[QWevent] > 0)
if(w->dumpstr == nil)
continue;
...
}else if(w->dumpstr){
dumped = FALSE;
Bprint(b, "e%11d %11d %11d %11d %11.7f %s\n", i, t->file->dumpid,
0, 0,
100.0*(w->r.min.y-c->r.min.y)/Dy(c->r),
fontname);
...
if(w->dumpstr){
if(w->dumpdir)
Bprint(b, "%s\n%s\n", w->dumpdir, w->dumpstr);
else
Bprint(b, "\n%s\n", w->dumpstr);
}
...
}
OK, we still have to register this hook systematically, for all buffers.
register-hook.sh
The main difficulty here is to provide a cleanup mechanism, so as to avoid having a potentially broken hooks hijacking events on all acme(1) windows.
This can be implemented with a basic trap(1) in sh(1).
Besides the fact that we cleverly avoids hooking win(1)
buffers,
the code is rather straightforward, so without further ado:
#!/bin/sh
if [ -z "$1" ]; then
echo `basename $0` '<path/to/hook.sh>' 1>&2
exit 1
fi
hook="$1"
pids=`mktemp`
cleanup() {
while read pid; do
pkill $pid
done < $pids
rm $pids
}
trap cleanup SIGINT
9p read acme/index | awk '$6 !~ /-[a-z]+$/ { print $1 }' | while read id; do
echo hooking $id
sh $hook $id &
echo $! >> $pids
done
9p read acme/log | awk '/^[0-9]+ new / {
system("sh '$hook' " $1 " & echo $! >> '$pids'")
}'
Note: A more polished implementation can be found on GitHub, alongside other acme(1)-related tools. The trap(1) based mechanism has been removed; termination is handled by a specific script which kills all processes in the group of the process who initiated the hook registration.
There’s also a workaround for the dumping issue.
Still, use with caution, unless you have time to fix some long-lasting acme(1) bugs.
Comments
By email, at mathieu.bivert chez: