Nexoniarz is the sole author and SchOS was never published under Apache-2.0, so this is a straight relicence. LICENSE is the FSF GPLv2 text, every first-party SPDX header moves to GPL-2.0-or-later, and NOTICE, README.md, docs/licensing.md and the site say so. projects/schfs-linux follows the rest of the tree. The GPL gives copyleft: a fork has to ship its source. It also lifts the old "Linux drivers, never" rule, since GPL-2.0-only code combines with GPL-2.0-or-later. "Or later" leaves room for GPLv3, but one binary still has to pick one version, so docs/licensing.md now keeps Apache-2.0 and GPLv3 code out of the kernel to keep Linux code possible there. BSD 4-clause code goes from "avoid" to "no", because the advertising clause is GPL-incompatible. LDLS still ships no .ko. That boundary is now project policy rather than a legal requirement, and ldls.md, devices.md and licensing.md section 6 say so. refmod declares license=GPL, and ldls-check passes (27 checks). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
398 lines
19 KiB
Markdown
398 lines
19 KiB
Markdown
<!-- SPDX-License-Identifier: GPL-2.0-or-later -->
|
|
# Virtual terminals
|
|
|
|
Six of them, `/dev/tty1` through `/dev/tty6`.
|
|
|
|
| | |
|
|
|---|---|
|
|
| **AltGr+F1 .. F6** | go to that terminal |
|
|
| **AltGr+-** / **AltGr+=** | previous / next terminal *in use* |
|
|
| `chvt N` | the same, from a script or a serial console |
|
|
|
|
The chord is settable — `chvt -c` — for the reasons below. Each one is an independent terminal: its own screen, its own
|
|
cursor, its own line discipline, its own foreground process group and its own
|
|
session. Two shells on two terminals are two unrelated processes that happen to
|
|
share a keyboard and a screen.
|
|
|
|
A seventh is deliberately not bound. That is where a display server goes,
|
|
and it will claim switching with `VT_SETMODE` rather than a chord.
|
|
|
|
---
|
|
|
|
## What makes switching possible
|
|
|
|
A backing store, and nothing else.
|
|
|
|
Before this, the console drew straight into the framebuffer and scrolled by
|
|
moving pixels. That works exactly as long as nothing else ever wants the
|
|
screen: there was nowhere to put terminal 2 while terminal 1 was visible, and
|
|
nothing to repaint terminal 1 from when it came back.
|
|
|
|
So each terminal owns a grid of cells and the framebuffer shows whichever grid
|
|
is active. `vt_switch()` is three lines — set the active index, repaint from
|
|
the grid, return — because nothing has to be *saved* on the way out. The
|
|
outgoing terminal's grid has been current all along.
|
|
|
|
The rule the whole layer is built on:
|
|
|
|
> **A terminal does not know whether it is visible.**
|
|
|
|
Writes land in the grid, the cursor moves, the grid scrolls, all of it
|
|
unconditionally. Drawing is a separate question, asked once, at the bottom of
|
|
`kernel/dev/vt.c`, answered by comparing against `vt_active`. Every alternative
|
|
spreads "am I on screen" through the escape parser, and then a terminal
|
|
switched away from mid-sequence comes back wrong.
|
|
|
|
## A cell
|
|
|
|
Eight bytes:
|
|
|
|
| | |
|
|
|---|---|
|
|
| `ch` | the Unicode codepoint, 32 bits |
|
|
| `fg`, `bg` | palette indices, 0-15 ANSI plus two theme defaults |
|
|
| `attr` | reverse video; room for underline and the rest |
|
|
| `pad` | to eight, so the grid indexes without a multiply |
|
|
|
|
`ch` was one byte holding a CP437 glyph, not a codepoint: the console mapped
|
|
UTF-8 down to CP437 on the way in, because that is what `include/font.h`
|
|
contained. Widening it to the codepoint is what made Ą, Ę, Ś, Ω and the rest
|
|
possible, and no font change could have done it alone — a byte cannot *hold*
|
|
U+0104 whatever is drawing it. Mapping on the way in also meant the decision
|
|
was made once, at write time, and a later font could not revisit it.
|
|
|
|
The cost is real: the grid went from four bytes per cell to eight, and six
|
|
terminals at 240x75 is 864 KB of BSS rather than 432 KB. Paid once at link
|
|
time, which is the only way to pay it — there is no contiguous multi-page
|
|
allocator to ask.
|
|
|
|
The colours stay indices rather than RGB, for the same reason they always
|
|
were: a cell holding two 32-bit colours would be sixteen bytes.
|
|
|
|
## What is per-terminal
|
|
|
|
Everything that used to be a single global:
|
|
|
|
| Was | Now |
|
|
|---|---|
|
|
| `int tty_fg_pgrp = 1;` | `v->fg_pgrp`, one per terminal |
|
|
| one `struct termios tio` | `v->tio` |
|
|
| one 256-byte input ring | `v->in` |
|
|
| one escape-parser state | `v->esc_*` |
|
|
| one UTF-8 decoder | `v->u8_*` |
|
|
| one cursor, in pixels, in `gfx.c` | `v->cx`, `v->cy`, in cells |
|
|
|
|
`TIOCSCTTY` used to be `return 0` — it claimed to make a terminal the caller's
|
|
controlling terminal and did nothing, because there was one terminal and
|
|
nothing to choose between. It records `proc->ctty` now, which is what
|
|
`/dev/tty` resolves through.
|
|
|
|
## The devices
|
|
|
|
| Path | Minor | What |
|
|
|---|---|---|
|
|
| `/dev/console` | 1 | the **active** terminal, so kernel messages follow a switch |
|
|
| `/dev/tty` | 10 | the caller's **controlling** terminal, via `proc->ctty` |
|
|
| `/dev/tty1`..`tty6` | 11-16 | that terminal, whether or not it is on screen |
|
|
|
|
`/dev/tty` was an alias for `/dev/console` with the same major and minor. They
|
|
are different questions and now have different answers.
|
|
|
|
## The ioctls
|
|
|
|
Linux's numbers, because they do Linux's job.
|
|
|
|
| | |
|
|
|---|---|
|
|
| `VT_ACTIVATE` | switch. Argument is a 1-based number **by value**, not a pointer |
|
|
| `VT_WAITACTIVE` | returns immediately: `vt_switch` repaints synchronously, so there is nothing left to wait for |
|
|
| `VT_GETSTATE` | active terminal and a bitmask of those in use |
|
|
| `VT_OPENQRY` | the first terminal nobody has open |
|
|
| `VT_GETMODE`/`VT_SETMODE` | accepted and ignored — the kernel drives switching and there is no process mode to set |
|
|
| `VT_GETCHORD`/`VT_SETCHORD` | SchOS's own: which key combination switches. See below |
|
|
| `KB_GETLAYOUT`/`KB_SETLAYOUT`/`KB_LISTLAYOUT` | SchOS's own: the keyboard layout, by name |
|
|
|
|
## The keyboard
|
|
|
|
`kbd.c` turns scancodes into characters and chords. `keymap.c` holds the
|
|
layouts. Between them is one rule, and it is the rule the whole keycode space
|
|
is built around:
|
|
|
|
> **A key produces either a Unicode codepoint or a value above `0x10FFFF`.
|
|
> Nothing in between means anything.**
|
|
|
|
The chord codes always obeyed it. The navigation keys did not: `KEY_UP` was
|
|
`0xE2`, and `0xE0..0xE9` are exactly the UTF-8 lead bytes for three-byte
|
|
characters. Typing any of U+0800..U+FFFF — most punctuation, every arrow, every
|
|
maths symbol, the whole of CJK — produced a value the console read as an arrow
|
|
key and turned into `ESC [ A`. `∞`, `→`, `≤` and `'` vanished from the input
|
|
line while `ą` and `ę` came through, because those are two-byte sequences whose
|
|
lead byte is `0xC4` and nothing claimed `0xC4`. They are `KBD_*` now and sit
|
|
above every codepoint there is.
|
|
|
|
What the chord *means* is decided in `console.c`, where the terminals are. The
|
|
keyboard driver names it and stops.
|
|
|
|
### Layouts
|
|
|
|
Three, in the kernel: `us`, `pl` (Polish programmers), `de` (QWERTZ). `loadkeys`
|
|
lists and switches them; `/etc/vconsole.conf` records the choice and the
|
|
`os.schos.keymap` job applies it at boot.
|
|
|
|
**In the kernel, not in files.** Linux's `loadkeys` compiles a text keymap and
|
|
uploads it a key at a time through `KDSKBENT`, which puts the data in userspace
|
|
— and a machine whose `/usr` has not mounted then has no layout at all. Here
|
|
the kernel always has a working keyboard and userspace only names which one.
|
|
|
|
A layout is four tables of codepoints — plain, Shift, AltGr, Shift+AltGr —
|
|
indexed by scancode. Two things are deliberately *not* tables:
|
|
|
|
- **Ctrl is derived from the plain codepoint**, not looked up. The table it
|
|
replaced was indexed by scancode, which quietly assumed US: on QWERTZ the Z
|
|
and Y keys swap, so Ctrl+Z would have suspended a job only on some layouts.
|
|
- **Caps Lock is a case toggle on the result**, so `ą` from the AltGr level
|
|
becomes `Ą` — which no second table could have said, because Caps Lock
|
|
combines with AltGr rather than replacing it.
|
|
|
|
Codepoints and not bytes is the entire point: `ó` fits in Latin-1 and `ą` does
|
|
not, and half a Polish alphabet is not a Polish layout. `kbdgetc()` encodes to
|
|
UTF-8 and hands the bytes to `consoleintr()` one call at a time, because that
|
|
function's contract is bytes — the serial line delivers UTF-8 already and
|
|
nothing could tell an encoded codepoint from a byte of the same value.
|
|
|
|
**No dead keys.** On a real German layout the acute and circumflex keys wait
|
|
for the next key and combine; here they type the accent alone. It needs
|
|
per-keyboard pending state that this interface has no room for, so layouts that
|
|
depend on it — French AZERTY, Spanish — are not shipped rather than shipped
|
|
wrong.
|
|
|
|
### Locale
|
|
|
|
`/etc/locale.conf`, read by `/etc/profile` and `/etc/zsh/zshenv`, default
|
|
`LANG=C.UTF-8`.
|
|
|
|
**Only the charset half of it is real.** musl decodes and encodes UTF-8
|
|
whatever the locale says and ships no locale data, so collation stays codepoint
|
|
order and messages stay English whichever language is named. What `LANG` does
|
|
is tell the programs that ask that the terminal is UTF-8 — an unset `LANG`
|
|
reads as "ASCII only", and that is what makes a program fall back to `?`.
|
|
|
|
### The chord is a setting, and that is the point
|
|
|
|
**There is no combination that is free for everyone on every host**, and a
|
|
guest cannot find out what its host takes: a key the host swallowed is
|
|
indistinguishable from a key nobody pressed. Three parties have a claim on
|
|
every candidate.
|
|
|
|
| | |
|
|
|---|---|
|
|
| **The host takes it, and the guest never sees it** | |
|
|
| `Alt+F4`, `Alt+F2` | window manager |
|
|
| `Ctrl+Alt+F1..F6` | display manager — and it matches **loosely**, so adding Shift does not get past it |
|
|
| `Ctrl+Alt+`digit | QEMU's own console tabs |
|
|
| **Applications want it, and lose it permanently if the kernel eats it** | |
|
|
| `Alt+`anything | Meta. zsh binds Meta-digit as `digit-argument`; so does every readline editor |
|
|
| `Ctrl+`letter | control characters |
|
|
| `Ctrl+Shift+`digit | types a character — `Ctrl+Shift+2` is `^@` |
|
|
| `F1..F6` alone | help, and whatever a full-screen program wants |
|
|
| **The keyboard layout wants it** | |
|
|
| `AltGr+`letter | Polish diacritics — ą ć ę ł ń ó ś ź ż — and on some setups AltGr arrives as Ctrl+Alt |
|
|
|
|
So the kernel ships a default and lets it be moved:
|
|
|
|
```sh
|
|
chvt # report the active terminal and the current chord
|
|
chvt -c altgr+F # AltGr+F1..F6 (the default)
|
|
chvt -c ctrl+shift+F # Ctrl+Shift+F1..F6
|
|
chvt -c alt+1 # Alt+1..6
|
|
chvt -c off # no chord; chvt N is then the only way
|
|
```
|
|
|
|
`off` is a legitimate setting, not a failure mode. On a machine whose host
|
|
grabs everything worth binding, `chvt N` from a shell is the honest answer.
|
|
|
|
The default is **AltGr+F1..F6**, and the `AltGr+`letter row above needs reading
|
|
carefully: what a Polish layout wants is AltGr with a **letter**. AltGr with a
|
|
**function key** produces nothing on any layout, no window manager binds it,
|
|
and no application asks for it. It is the one combination all three parties
|
|
leave alone.
|
|
|
|
That distinction is only expressible because Right Alt is tracked as its own
|
|
modifier bit. It used to share `ALT`, which is harmless for typing — neither is
|
|
used to look up a character — and made this impossible to say.
|
|
|
|
Verified rather than argued: with the default set, `AltGr+F2` switches,
|
|
`AltGr+F1` returns to a pixel-identical tty1, and `AltGr+a` still reaches the
|
|
shell, so ą is untouched.
|
|
|
|
Whatever the modifiers are, they also bind **minus** and **equals** to the
|
|
previous and next terminal. Those step only through terminals that are *in
|
|
use* — the same `opened` count `VT_GETSTATE` reports — because six terminals
|
|
with two shells on them means four blank screens in between, and cycling past
|
|
them is worse than not having the key.
|
|
|
|
Which chords reach a guest at all was **measured, not assumed**: a temporary
|
|
probe in `kbdgetc` logged every modified keypress, and QEMU's `sendkey` fired
|
|
each candidate at it. Everything arrived, `Ctrl+Alt+F2` included — so that
|
|
conflict is the host's display manager, not QEMU, and no amount of extra
|
|
modifiers gets around a grab that matches loosely.
|
|
|
|
The modifier test is an **equality**, not a mask:
|
|
|
|
```c
|
|
if (vt_chord_mods != 0 &&
|
|
(shift & (CTL | SHIFT | ALT | RALT)) == vt_chord_mods && ...)
|
|
```
|
|
|
|
A mask fires on any superset, so a chord aimed at the host would switch
|
|
terminals behind your back. Right Alt is tracked as its own bit (`RALT`) for
|
|
the same reason; it used to share `ALT`, which is harmless for typing — neither
|
|
is used to look up a character — and wrong for chords.
|
|
|
|
`include/posix.h`'s `VTC_*` bits mirror `include/kbd.h`'s modifier bits, and a
|
|
`_Static_assert` in `kbd.c` holds them together. A mismatch would be silent:
|
|
a chord set from userspace would simply never match, and the only symptom
|
|
would be a key that does nothing.
|
|
|
|
### Taking the chord back
|
|
|
|
Three layers, in increasing order of how much a program is claiming:
|
|
|
|
| | |
|
|
|---|---|
|
|
| **`VT_SETMODE(VT_PROCESS)`** | the terminal's owner handles switching itself. The kernel stops acting on the chord entirely. This is what a display server uses. |
|
|
| **`EVIOCGRAB` on `/dev/input/event0`** | an exclusive keyboard grab. Checked in `kbdgetc` *before* the chord is even recognised, so a grabbing program receives every scancode including this one. |
|
|
| nothing | the kernel switches, which is the default and right for a plain terminal |
|
|
|
|
`VT_SETMODE` used to be accepted and ignored — a program could ask for the
|
|
chord, be told yes, and still not get it. It works now.
|
|
|
|
What `VT_PROCESS` does **not** do yet is the release handshake: Linux signals
|
|
the owning process on every switch request and blocks until it answers
|
|
`VT_RELDISP`, which is how a display server releases the GPU before the screen
|
|
changes hands. Nothing here has anything to release, so the kernel simply
|
|
stops switching rather than negotiating. `struct vt_mode` already carries
|
|
`relsig`/`acqsig` for when Schcom needs it.
|
|
|
|
Input goes to whichever terminal is on screen. That is what makes a terminal a
|
|
terminal: the keyboard follows the eyes.
|
|
|
|
## Two rules that are easy to get wrong
|
|
|
|
**The grid is the only source of truth for the size.** `TIOCGWINSZ` reports
|
|
`v->cols`/`v->rows`, not a fresh `gfx_cols()`/`gfx_rows()`. When it computed
|
|
its own answer there were two, and they disagreed: the grids are sized at
|
|
`vt_init()`, which runs before the display driver is found on the PCI bus, so
|
|
they were 80x24 while `TIOCGWINSZ` said 160x50. Text stopped two thirds of the
|
|
way down the screen, `ls` formatted for a width the grid did not have and split
|
|
words across the wrap, and zsh's cursor tracking was wrong on every line --
|
|
three symptoms, one disagreement. `vt_resize()` now corrects the grids when a
|
|
scanout arrives, and the ioctl reads them.
|
|
|
|
**A glyph in the last column does not wrap.** It sets `wrap_pending`, and the
|
|
wrap happens when the *next* glyph arrives. Every real terminal behaves this
|
|
way and none of them document it clearly. Wrapping immediately makes a line
|
|
exactly as wide as the screen take two rows instead of one, and makes it
|
|
impossible to write the bottom-right cell without scrolling -- and zsh's
|
|
partial-line marker depends on the rule directly: it writes exactly
|
|
`COLUMNS - 1` spaces and expects the cursor to still be on that line. With
|
|
immediate wrap, a reverse-video `#` appeared above every prompt.
|
|
|
|
Anything that moves the cursor deliberately -- CR, LF, backspace, an escape
|
|
sequence -- clears the debt.
|
|
|
|
## Ctrl-C used to kill the shell
|
|
|
|
Worth its own note, because the cause was one character and the effect was a
|
|
machine you could not use.
|
|
|
|
When Ctrl-C arrives, `consoleread()` discards the partial line and breaks off
|
|
the read in progress. It returned **`-1`** to do so — and `-1` is `-EPERM`. So a
|
|
program that handles `SIGINT` by carrying on saw a permanent error instead of
|
|
an interrupted call, and exited:
|
|
|
|
```
|
|
zsh: error on TTY read: operation not permitted
|
|
schinitd: restarting os.schos.console-shell
|
|
```
|
|
|
|
and then the next Ctrl-C did the same thing to the replacement. A shell that
|
|
cannot survive Ctrl-C is a shell nobody can use, and the supervisor faithfully
|
|
restarting it made it a loop.
|
|
|
|
It returns **`EINTR`** now, which is what a signal-interrupted read is required
|
|
to give: the whole convention — retry, or return to the caller — is built on
|
|
receiving that and not something else.
|
|
|
|
## Editing a line you have already typed
|
|
|
|
Two bugs, both in the same half-second of using a shell, and both worth
|
|
recording because the symptom pointed away from the cause.
|
|
|
|
**Arrow keys corrupted the next two keystrokes.** They arrive from the keyboard
|
|
as single keycodes and were pushed into the input buffer exactly like that. No
|
|
terminal has ever worked that way. `consoleintr()` translates them to the
|
|
DEC/xterm sequences now -- `ESC[A`, `ESC[D`, `ESC[5~` and the rest -- which is
|
|
what every terminfo entry and every line editor already expects.
|
|
|
|
The *value* those keycodes had was a second bug wearing the same coat, and it
|
|
outlived the fix: `KEY_UP` was `0xE2`, a UTF-8 lead byte, so a character typed
|
|
from the other direction was eaten just as thoroughly. See the keycode rule
|
|
above.
|
|
|
|
**Then the end of the line vanished as you typed into it.** With the arrows
|
|
working, editing mid-line left the buffer perfectly correct and the screen
|
|
wrong, which is a maddening pair. The cause is one line in `vt_putc()`:
|
|
backspace *erased the cell it moved onto*.
|
|
|
|
That is what a line-discipline erase does. It is not what the BS control
|
|
character does — a program that wants to rub a character out sends `\b \b`,
|
|
backspace, space, backspace, and the separation matters because programs also
|
|
use plain `\b` simply to go back. zsh does, on every mid-line edit:
|
|
|
|
```
|
|
echo alpha \b\b zha \b\b
|
|
```
|
|
|
|
Move back two, rewrite the tail, move back two again to leave the cursor where
|
|
the user expects it. Against a destructive backspace that last pair deleted the
|
|
`ha` it had just drawn.
|
|
|
|
`\b` moves and nothing else now; `0x7f` is ignored, as it is on a real
|
|
terminal; and `consputc()` spells its own erase out as `\b \b`, exactly as it
|
|
already did for the serial line. `ESC[@`, `ESC[P` and `ESC[X` — insert, delete
|
|
and erase characters — were added at the same time, because `TERM=linux`
|
|
declares them and a terminal that drops them silently is the same class of bug
|
|
waiting for a different editor.
|
|
|
|
## What it does not do
|
|
|
|
- **No scrollback.** The grid is exactly the visible screen. Shift+PageUp does
|
|
nothing because there is nothing above the top row to show.
|
|
- **No reflow on resize.** The geometry is read once at `vt_init()`. Nothing
|
|
resizes a framebuffer console today; a compositor will, and that is when this
|
|
has to grow.
|
|
- **No `VT_RELDISP` handshake.** `VT_PROCESS` stops the kernel switching, but
|
|
the kernel does not signal the owning process and wait for an answer. A
|
|
display server that needs to release the GPU *before* the screen changes
|
|
hands will need that, and `struct vt_mode` already has the fields.
|
|
- **No terminal past 6.** Linux allows 63. Six is what the chords reach and
|
|
what the grids are sized for.
|
|
- **Only 240x75 of any screen.** Larger framebuffers work; the grid is clamped
|
|
and the remainder stays background. Raising `VT_MAXCOLS`/`VT_MAXROWS` is the
|
|
fix, and costs BSS.
|
|
- **No UTF-8 the font lacks.** The console draws through the TrueType
|
|
rasteriser now, so the limit is the font's `cmap` rather than a 256-entry
|
|
table — Latin, Latin Extended, Greek, Cyrillic and the common punctuation and
|
|
maths symbols all render. Anything genuinely absent still draws as `?`, which
|
|
is deliberate: drawing a *different* character would be worse.
|
|
- **No shaping, and no wide characters.** One codepoint is one cell of one
|
|
advance. Combining marks land on top of their own cell rather than the
|
|
preceding one, and CJK draws at half the width it wants. Both need a
|
|
width table and a cell that can span two columns.
|
|
- **No dead keys**, so French and Spanish layouts are absent rather than
|
|
broken. See above.
|
|
- **No per-terminal layout.** One keyboard, one layout. Switching to tty3 does
|
|
not move anybody's fingers.
|
|
- **Only three layouts.** Adding one is a table in `keymap.c` and a line in the
|
|
catalogue; the mechanism is not the limit, the typing is.
|