<feed xmlns='http://www.w3.org/2005/Atom'>
<title>9line.git, branch master</title>
<subtitle>random plan9 experiements
</subtitle>
<id>https://git.ceux.org/9line.git/atom?h=master</id>
<link rel='self' href='https://git.ceux.org/9line.git/atom?h=master'/>
<link rel='alternate' type='text/html' href='https://git.ceux.org/9line.git/'/>
<updated>2026-08-22T23:12:15+00:00</updated>
<entry>
<title>pim: a window for each kind of thing, and a picker for dates</title>
<updated>2026-08-22T23:12:15+00:00</updated>
<author>
<name>Calvin Morrison</name>
<email>calvin@pobox.com</email>
</author>
<published>2026-08-22T23:12:15+00:00</published>
<link rel='alternate' type='text/html' href='https://git.ceux.org/9line.git/commit/?id=c16c778c60deaafa0c9e5bbc1db9a10f64fec9b2'/>
<id>urn:sha1:c16c778c60deaafa0c9e5bbc1db9a10f64fec9b2</id>
<content type='text'>
cal9 kept an event in a panel and a day in another, with the state that
implies: a selection, a picked day, rectangles for the buttons on it,
and a hundred-odd lines to draw it. Opening a window instead deletes all
of that rather than tidying it away. Clicking an event opens cal9 -v
event, clicking a day opens cal9 -v day, and cal9 is three grids again.

The event window earns being cal9 rather than pim/showwin: it carries
Accept, Tentative and Declined, lit to show where you stand, and only
when the calendar's ctl says it can rsvp. cal9 -v invite is a form that
writes the calendar's new file in the tree's key: value form, so the
composer knows nothing about iCalendar either.

datepick is a program rather than a widget so anything can use it:

	pim/agenda -d `{datepick}

-t asks for a time as well, in stages, because a grid of ninety-six
five-minute slots is not something you want to aim at. A window started
from wctl has nowhere useful to print, so -o and -s hand the answer back
by file or by snarf; the invite form spawns it that way and fills the
field when it returns.

An event you have not answered draws pale, so a glance at the week says
what is still waiting.

Move and Resize are gone from the menus: rio owns the border and does
both, better. Compact is one item showing whichever state you are not
in.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>gui: a widget layer, so the next program does not write one</title>
<updated>2026-08-22T23:12:01+00:00</updated>
<author>
<name>Calvin Morrison</name>
<email>calvin@pobox.com</email>
</author>
<published>2026-08-22T23:12:01+00:00</published>
<link rel='alternate' type='text/html' href='https://git.ceux.org/9line.git/commit/?id=77c7bc9b187450818ec068239a3ae836d01fdebc'/>
<id>urn:sha1:77c7bc9b187450818ec068239a3ae836d01fdebc</id>
<content type='text'>
cal9 had grown its own: borders drawn four fills at a time in two
places, a button-3 menu, hit regions, buttons, text wrapping. None of
that is about calendars, and a second program would have written it
again, differently. It lives in gui/ui now.

It is a kit and not a framework. Every call draws a thing and, where it
makes sense, records where it drew it; the program keeps its own event
loop and its own idea of what to redraw. Menu takes a redraw function
rather than calling back into anything. The moment the library owns the
loop it stops being usable by the next program, which was the problem
being fixed.

Body is the fix for two bugs that turned out to be one. rio draws a
window's border into the client's own image and puts corner cursors in
it, taking those clicks before the client sees them: the border is
rio's resize handle. Filling the whole rectangle erased it, so the
border vanished and so did resizing. Drawing inside Body gives both
back, and the drag-to-resize cal9 had grown -- which could only ever
shrink, since a window stops getting mouse events once the pointer
leaves it -- was deleted rather than fixed.

Field is one line of editable text with a cursor, backspace, ^U and a
paste from /dev/snarf. No selection, no mouse placement, no history: a
form asking for a summary and a time needs none of them and each is
somewhere to be subtly wrong.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>ignore the binaries go build leaves at the module root</title>
<updated>2026-08-22T21:01:16+00:00</updated>
<author>
<name>Calvin Morrison</name>
<email>calvin@pobox.com</email>
</author>
<published>2026-08-22T21:01:16+00:00</published>
<link rel='alternate' type='text/html' href='https://git.ceux.org/9line.git/commit/?id=ba00c7dab8ea982fc3fdbfc7062234adbe1fcc0e'/>
<id>urn:sha1:ba00c7dab8ea982fc3fdbfc7062234adbe1fcc0e</id>
<content type='text'>
"go build ./cmd/x" writes the binary to the current directory, not to
bin/, so pim/caldavfs and gui/spike were showing up as new files a
stray "git add ." would have committed. Anchored with a leading slash
so a source file of the same name deeper in the tree still counts.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>ignore sessions/</title>
<updated>2026-08-22T20:48:52+00:00</updated>
<author>
<name>Calvin Morrison</name>
<email>calvin@pobox.com</email>
</author>
<published>2026-08-22T20:48:52+00:00</published>
<link rel='alternate' type='text/html' href='https://git.ceux.org/9line.git/commit/?id=560712f12a496678a703818dd51a1df9bdab77d2'/>
<id>urn:sha1:560712f12a496678a703818dd51a1df9bdab77d2</id>
<content type='text'>
Transcripts are not part of the source. They were in the initial commit
and have been removed from the history rather than deleted in a later
one, so a clone no longer carries them at all.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>pim: invite, and a way to say what you mean</title>
<updated>2026-08-22T20:48:07+00:00</updated>
<author>
<name>Calvin Morrison</name>
<email>calvin@pobox.com</email>
</author>
<published>2026-08-22T20:48:07+00:00</published>
<link rel='alternate' type='text/html' href='https://git.ceux.org/9line.git/commit/?id=a071bf9a56241aa743e1be21bccbb4cb72b189f2'/>
<id>urn:sha1:a071bf9a56241aa743e1be21bccbb4cb72b189f2</id>
<content type='text'>
pim/invite makes an event and, when it names attendees, sends the
invitation -- which on a caldav server with auto-schedule is the same
act. -p writes the object to standard output instead, so it can go to
upas/marshal as iMIP, into a directory an ical/fs is serving, or
nowhere in particular. There is no separate verb for inviting because
to a calendar there is no separate thing.

It writes the tree's own key: value form rather than icalendar. The
first version spelled SUMMARY: by hand and got "lunch, then a walk"
wrong: a comma is a separator there and wants escaping. Parsing with
go-ical and generating by hand is the wrong asymmetry, so calfs grew
Compose, and now the same shape the tree emits is the shape it accepts.
The one UID we ever mint -- as organiser we own the event -- is minted
there too.

pim/invites lists what is waiting for an answer, which is what makes
pim/rsvp usable: it walks the date tree rather than every event, since
an invitation you never answered last March is not news.

agenda and show take -d date now, via seconds(1), which parses a human
date where date(1) only formats one.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>pim: answer an invitation by writing to a file</title>
<updated>2026-08-22T18:14:59+00:00</updated>
<author>
<name>Calvin Morrison</name>
<email>calvin@pobox.com</email>
</author>
<published>2026-08-22T18:14:59+00:00</published>
<link rel='alternate' type='text/html' href='https://git.ceux.org/9line.git/commit/?id=ebfe721eb065be1d33a13e03e40426c4b3adfad0'/>
<id>urn:sha1:ebfe721eb065be1d33a13e03e40426c4b3adfad0</id>
<content type='text'>
echo ACCEPTED &gt;events/uuid/&lt;uid&gt;/partstat now does it, on a backend
that can. caldav/fs fetches the object, rewrites only our own ATTENDEE
line and PUTs it back with If-Match; fastmail advertises
calendar-auto-schedule, so that single PUT both updates the copy and
sends the iMIP to the organiser. caps says read rsvp schedule and means
all three.

Two things that cost an hour between them. go-webdav strips the quotes
off an ETag when it parses the header, but If-Match wants an
entity-tag: a bare hex string gets 412, which reads exactly like losing
a race to another client. And the tree kept serving the old answer
right after a successful write, because RSVP resynced the backend
without rebuilding anything; a file that lies immediately after you
wrote it is worse than one that is slow.

-R uid:PARTSTAT answers one invitation without serving, which is what a
pim/rsvp wrapper wants to call.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>pim: a caldav backend, and a partstat that refuses to lie</title>
<updated>2026-08-22T18:04:17+00:00</updated>
<author>
<name>Calvin Morrison</name>
<email>calvin@pobox.com</email>
</author>
<published>2026-08-22T18:04:17+00:00</published>
<link rel='alternate' type='text/html' href='https://git.ceux.org/9line.git/commit/?id=08155ead7bf80308b4cb67063cc91e3e3679ce16'/>
<id>urn:sha1:08155ead7bf80308b4cb67063cc91e3e3679ce16</id>
<content type='text'>
caldav/fs is the second backend, and it cost ~330 lines: discovery,
a report for the window, ETags to decide whether anything moved. The
event model, recurrence expansion, the tree, ctl, query, alarm and
changed all came from lib/cal untouched, which is what the split was
for. go-webdav hands back *ical.Calendar from the same library lib/cal
parses with, so FromCalendars takes it straight in.

Two interfaces, both optional. Identity says whose calendar this is,
because in iTIP your identity is the mailto: in your own ATTENDEE line
and nothing else can tell which attendee is you. RSVPer says the
backend can answer an invitation -- meaning both halves of it, updating
your copy and telling the organiser. A backend that can only do one
should not implement it, so partstat is 0444 on a published .ics and
writing to it fails rather than half-working.

Two bugs worth naming. go-webdav resolves paths with path.Join, which
drops a trailing slash: pointed at /dav/ it asks about /dav, and Cyrus
answers 405 for that spelling and 207 for the other, so the first
PROPFIND is done by hand. And an absolute DAV href replaces the
endpoint's path rather than extending it; appending gave
/dav/dav/calendars/..., a 404, and a probe reading 404 as "no" hid
calendar-auto-schedule, which fastmail does in fact offer.

pimup brings mail and the calendars up and mounts them; riostart runs
it before opening any window, since a mount only exists in the
namespace that made it. rio does not run riostart by itself: it is the
argument to -i.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>pim: personal information management, starting with a calendar</title>
<updated>2026-08-22T17:16:49+00:00</updated>
<author>
<name>Calvin Morrison</name>
<email>calvin@pobox.com</email>
</author>
<published>2026-08-22T17:16:49+00:00</published>
<link rel='alternate' type='text/html' href='https://git.ceux.org/9line.git/commit/?id=8a3d2c99bff60cb775ebc42516c0c3912d54ba3d'/>
<id>urn:sha1:8a3d2c99bff60cb775ebc42516c0c3912d54ba3d</id>
<content type='text'>
A calendar as a file tree, and tools that only know the tree:

	events/date/yyyy/mm/dd/hhmm-summary	as lived
	events/uuid/&lt;uid&gt;/			as stored
	ctl query alarm changed

lib/cal owns all of that. A backend supplies events and, where its
protocol allows, takes changes back -- six methods. cmd/icalfs is the
first: it reads .ics files from a directory and nothing else, because
fetching is rc/fetch's job and hget already exists. That keeps
net/http out of the binary and makes a subscribed calendar and a local
one the same thing.

The tools are rc on purpose. If the tree needs a compiled program to be
useful, the tree is the wrong shape. Three things were added to the
tree because the rc port needed them: a path from an occurrence to its
event, epoch seconds beside RFC3339, and a numeric slot for all-day
events so test(1) can compare it.

ctl reports caps, so a tool can say "read only" instead of trying and
failing. A published .ics is read only: nowhere to PUT, and no
METHOD:REQUEST to reply to. CalDAV would be read write rsvp schedule,
and that is the next backend.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>gui: a /dev/draw layer in go, with no cgo and no devdraw</title>
<updated>2026-08-22T17:16:35+00:00</updated>
<author>
<name>Calvin Morrison</name>
<email>calvin@pobox.com</email>
</author>
<published>2026-08-22T17:16:35+00:00</published>
<link rel='alternate' type='text/html' href='https://git.ceux.org/9line.git/commit/?id=9aacce8b3b54060d0037eca897856d7273e6e5e8'/>
<id>urn:sha1:9aacce8b3b54060d0037eca897856d7273e6e5e8</id>
<content type='text'>
9fans.net/go/draw builds for plan9 but shells out to plan9port's
devdraw, which 9front does not have. This talks to /dev/draw itself:
the protocol is file i/o, so a pure-go client is a few hundred lines
under an already-complete idea.

draw/keyboard.go opens /dev/cons and turns the console raw, as
libdraw's initkeyboard does. Without it rio keeps its line editor on
the window and paints what you type over the drawing.

draw/snarf.go writes /dev/snarf, which is what every other program on
the system means by copy.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>doc: 91 checks, and a wire</title>
<updated>2026-08-19T17:00:43+00:00</updated>
<author>
<name>Calvin Morrison</name>
<email>calvin@pobox.com</email>
</author>
<published>2026-08-19T17:00:43+00:00</published>
<link rel='alternate' type='text/html' href='https://git.ceux.org/9line.git/commit/?id=5ef4699d05bc919255449a9af780f216a0589a72'/>
<id>urn:sha1:5ef4699d05bc919255449a9af780f216a0589a72</id>
<content type='text'>
Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
</feed>
