syst
sysk describes; whatever shows it lays out. The shell says what its answers are: text, a value, or a word of its own such as the prompt. The terminal carries them. A host on the person's machine draws them: a run of records as a table, at the width the person's terminal really has. On a plain serial line nothing changes, so sysk is never unreadable.
Three places, one job each
Unix puts a tty between a program and the person: a line discipline in the kernel that echoes, edits and turns Ctrl-C into a signal, with escape codes for anything more. sysk's terminal does almost none of that (sysk 015): the person's end edits the line and sends it whole. syst takes the next step. Programs no longer format for a screen at all. The shell knows what each answer is, the terminal task carries it, and the host decides how it looks.
The shell and the terminal run in sysk, on the RISC-V machine; the host runs on the person's machine, at the other end of the serial line: tools/attach.rkt, which make run starts. Select a box.
What the shell says
An answer is a line of 64 bytes, as since sysk 013: byte 0 is the length of the text that follows. The length is below 64, so its six bits leave two over, and since sysk 044 they say what the text is. The shell's say sets them: a line that is exactly one s-expression is a value (sx-one, a parenthesis inside a string counting for nothing since sysk 090), anything else is text. The prompt and the region numbers are the shell's own, control. A value longer than a line goes in pieces that the next answer continues, so a value is always whole by the time anything shows it.
| Kind | Bits | Text | Shown on a plain line | Sent to a host |
|---|---|---|---|---|
| text | 00 | any line | as it is | (text "…"), quoted |
| value | 01 | one s-expression, or its last piece | wrapped between its fields; a word longer than a line fills it and goes on, indented, on the next (sysk 082) | (out VALUE) |
| more | 11 | a piece of a value; the next answer goes on | held until the value is whole | held until the value is whole |
| control | 10 | (prompt "/ % "), (dot "/"), (region 3), (region 3 (view long)), (frame 3) | the prompt, unfinished; dot, regions and frames not at all | as it is |
Nothing a program prints can pass for the shell. The kind is in byte 0, never in the text. A job's lines are clamped to text when the shell relays them, and a file that holds (region 9) is said as a value, so a host receives (out (region 9)): data, never control. The same rule will carry the trusted path later (docs/authority.md): only the terminal task can write outside these messages.
On the wire
A host says (syst 1) as its first line. The terminal answers it and from then on writes one message a line, and reads one: what the person types comes as (line "…"), in pieces of at most 63 bytes as a plain line longer than a message is, the end of input as (eof), and Ctrl-C as the byte 0x03, as on a plain line, so the terminal sees it even among keys that a waiting line holds up (sysk 069, 084). Right after its answer it sends the views, a line each (sysk 066, below). Before that, or with no host at all, the terminal shows everything as it always has. One session, both ways: the person types cat /sys/task/*/status, then uptime (abridged to four of the thirteen tasks).
on the serial line
what the person sees
What the person types is shown by their own terminal, not sent back by sysk. The messages are what the boot tests check (tests/boot-test.rkt); the layout is a function of the messages alone, tested without a machine (tests/syst-test.rkt).
Values, a *, and where
Three small things in the shell make values worth having. cat says a whole line that is one s-expression as one value, however long. A path may have one component that is *: cat /sys/task/*/status is cat of each task's status, in the directory's order. And where keeps the values that have a field: (where (state live) (cat /sys/task/*/status)). Each is the shell's, because each is about what a command means; none knows how anything looks.
/ % (where (from terminal) (cat /sys/chan/*)) name bytes depth queued ended from to screen 64 2 0 no terminal uart lines 64 1 0 no terminal sugar
where filters what cat shows, for now: around anything else it says so. A path with a * is made in the job table's buffer, free while a file is shown, and the directory is read again for each name, since showing a file uses the reply buffers; a directory of 64 names at most.
Views
A program says every field it knows, and a person rarely wants them all (sysk 066; docs/commands.md). So how a record is shown is its view, declared once in the manifest by the record's head, whoever says it: the fields shown unless the person asks for more, what a number measures, and a mark after the first field. The terminal sends the views to the host when it answers (syst 1):
(views (entry (show name) (units (length bytes)) (mark (type dir) "/"))) ; system.sysk (view entry (show name) (units (length bytes)) (mark (type dir) "/")) ; on the wire, after (syst 1)
The person picks another view with a word at the line's end: @long, every field, a table or, when that is wider than the terminal or a field holds records, a list; @list, a field to a line; @raw, the values as said. Everything before the @ is the program's, everything after it the host's, so no switch means one thing to a program and another to the shell. Sugar makes ls /net @long into (view long (ls /net)), the shell sends the view with the command's region, and the host keeps it for that region, so a job in the background keeps the view it began with. ls -l is ls @long, a habit from Unix kept. A head with no view is shown as before, so a new program looks right before anyone writes one.
on the serial line
what the person sees
The same two records each time; only the region message and the host's layout differ. The hint under the first is shown once a session for each head whose view hides fields. Lengths are bytes: a length of 12345 would show as 12.1 K, and still sort as the number.
Over a plain serial line, with no host, the shell's values are written as they are: every view is @raw there, and sysk itself never lays anything out. A view only names fields, so a field a record lacks is skipped and a view that has drifted from its records shows less, never something wrong.
The host
tools/syst.rkt is the layout, and tools/attach.rkt the relay that uses it: QEMU's serial line on one side, the person's terminal on the other. It holds a run of values in one region. When the run ends (text, a prompt, another region, a value of another shape), it is laid out: in the region's view, or by the records' own (Views); with neither, two or more records with the same head and keys are a table if it fits the terminal's width, numbers to the right; anything else is written as it is, wrapped between fields, and inside a string between words, so a long string is never cut (sysk 057). A field may hold values as well as atoms, as a holding's chain of steps does (sysk 093): the record is still a record, a view that does not show the field leaves it folded away, @long and @list write it out, the field's name on a line and each record in it below, written out the same way two spaces deeper, to any depth (sysk 114: cat /sys/wire/job/ping @long shows every step of each chain), and Tab after (where ( offers only the fields with an atom to match. A line from a region older than the newest, a job in the background, carries that region's number. make run speaks syst; tools/attach.rkt --plain relays the bytes as before; --pt hands the line to postterm's pt-attach.
The prompt
Before a prompt that waits for a command the shell sends (dot "/net/eth"), its directory; the prompt itself, (prompt "eth % "), is the short form (the shell). With no more the host shows the shell's prompt. make run PROMPT='%D %% ' (tools/attach.rkt --prompt) gives it a template instead: %d the last name, %D the whole directory, %% a %; a prompt while a command is open, (prompt " 1+ "), is moved to the right of one as wide as the last. A template is a format language, and it lives here, in Racket on the person's machine, rather than in the proven shell, which sends only what is true (sysk 068).
Frames: a watch drawn in place
watch shows a file again each time it changes, and top is a watch of every task's status (sysk 060). After each showing the shell says (frame N): what region N has shown since its last frame is one frame, whole, and a next frame in region N replaces it. It comes after the showing, not before the next, because the host holds a run of values until something ends it: a mark before each redraw would end each frame only when the next began, a round late. Only the shell can say it; a file that holds (frame 9) is a value, as (region 9) is.
The host lays out a frame as soon as its mark comes, and counts the lines it wrote since the last (region) or (frame). When the next frame in that region comes and nothing else was written in between (a background job's line, a prompt), and the last frame is shorter than the screen, it moves up that many lines and clears to the end (ESC [ n A, ESC [ J) before writing the new one; otherwise below. In a table drawn over another, a number that grew since then shows by how much, its row found by its first field: this is any watch's, so watch /sys/chan/* shows bytes and queued growing the same way. The rows keep their order: rows that jump from frame to frame are harder to follow than a column that says what grew. sysk's plain terminal shows no control line but the prompt, so there each frame is written below the last, as watch always did. top 300 2, abridged to four of the thirteen tasks:
on the serial line
what the person sees
Tab
At a terminal the host edits the line itself (sysk 050): sysk never edits, and the canonical mode of a terminal cannot see a Tab. On Tab, while sysk waits for a line, it sends (complete WORD ...), the line so far as words, and takes the shell's choices out of what comes back before the next prompt, without showing them as output. One choice is filled in whole; several fill in what they share; when they share nothing more, they are shown in columns under the line, and the line comes back. Inside (where (FIELD VALUE) ...) the host does not ask: a value describes itself, so the records it last laid out are the fields there are and their values, and Tab offers those (sysk 051): cat /sys/task/*/status, then (where (st⇥ is (where (state , and l⇥ is live. Up and down walk the lines typed before; Ctrl-U empties the line. Lines a job prints in the background come above the line being typed, which is drawn again under them. With --plain, or with no terminal, the host edits nothing, as before.
/ % cat /sys/task/s⇥ /sys/task/sugar/ /sys/task/shell/ /sys/task/slot-0/ /sys/task/slot-1/ /sys/task/slot-2/ /sys/task/slot-3/ / % cat /sys/task/sl⇥ / % cat /sys/task/slot- / % follow /log --q⇥ / % follow /log --quiet
The choices are values the shell said, (choice (word /sys/task/sugar/) (kind dir)); their words are what the host shows. What a program accepts and what Tab offers come from one place, the catalogue the job check judges by.
Not yet: results kept as files, /res/3; a line only the terminal task may write, for the trusted path; and postterm as a host, through the upgrade its line form designs.