tty: Ctrl+Alt+Shift+F1..F6, and VT_SETMODE actually works

Alt+1..6 was a worse choice than the Alt+F-keys it replaced, and the reason
is the one that matters: anything the kernel swallows is taken from every
program that will ever run here, permanently.

Alt+<digit> is Meta-digit. zsh's own line editor binds it as digit-argument --
Alt+1 Alt+2 x repeats x twelve times -- and every readline editor does the
same. Yesterday's chord broke that system-wide, which is exactly the failure
mode of picking a chord applications want.

Working the whole space instead of one axis of it:

  the HOST takes    Alt+F4, Alt+F2, Ctrl+Alt+F1..F6
  APPLICATIONS want Alt+anything (Meta), Ctrl+letter, Ctrl+Shift+digit
                    (Ctrl+Shift+2 types ^@), bare F-keys
  the LAYOUT wants  AltGr+letter -- Polish diacritics

Ctrl+Alt+Shift+F1..F6 is in none of those sets. Display managers bind the same
chord without Shift, so adding it steps out of the host's way, and no terminal
application binds a triple-modified function key.

Which chords actually reach a guest was measured rather than assumed: a
temporary probe in kbdgetc logged every modified keypress and QEMU's sendkey
fired each candidate at it. Everything reached the guest, Ctrl+Alt+F2
included -- so that conflict is the host's display manager, not QEMU. The same
probe corrected an earlier claim: `sendkey altgr-2` produces no event at all,
so the previous commit's "AltGr+2 did not fire" was never actually tested.

And the escape hatch is real now. VT_SETMODE was accepted and ignored, so a
program could ask to own the chord, be told yes, and still not get it.
VT_PROCESS genuinely stops the kernel switching. Together with the EVIOCGRAB
path -- which kbdgetc already checks before the chord is recognised -- there
are three honest layers: nothing, VT_PROCESS, exclusive grab.

What VT_PROCESS still does not do is the VT_RELDISP handshake; struct vt_mode
carries relsig/acqsig for when Schcom needs to release the GPU before the
screen changes hands.

Verified by screendump hash: Alt+2 reaches zsh as Meta, Ctrl+Alt+F2 and
Ctrl+Shift+F2 leave the screen untouched, Ctrl+Alt+Shift+F2 switches.
schfs-check passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
Nexoniarz 2026-09-13 13:05:43 +02:00
commit fe2f5e1bf8
9 changed files with 170 additions and 58 deletions

View file

@ -18,7 +18,8 @@ its distribution.
| API | **SchAPI** | The native programming interface — *designed, not built* |
| Compositor | **Schcom** | Owns the display; v0 runs |
Six virtual terminals sit under all of it -- `/dev/tty1`..`tty6`, Alt+1..6 --
Six virtual terminals sit under all of it -- `/dev/tty1`..`tty6`,
Ctrl+Alt+Shift+F1..F6 --
each with its own screen, line discipline and foreground process group. See
[terminals.md](terminals.md).

View file

@ -1,13 +1,14 @@
<!-- SPDX-License-Identifier: Apache-2.0 -->
# Virtual terminals
Six of them, `/dev/tty1` through `/dev/tty6`, reached with **Alt+1 .. Alt+6**
or with `chvt N`. Each one is an independent terminal: its own screen, its own
Six of them, `/dev/tty1` through `/dev/tty6`, reached with
**Ctrl+Alt+Shift+F1 .. F6** or with `chvt N`. 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.
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.
---
@ -98,7 +99,7 @@ Linux's numbers, because they do Linux's job.
## The keyboard
`kbd.c` recognises Alt+1..6 and returns `KEY_ALT_F1 + n`, which is above
`kbd.c` recognises Ctrl+Alt+Shift+F1..F6 and returns `KEY_VT_SWITCH + n`, which is above
0xFF on purpose: every value that fits in a byte is already either a character
or a UTF-8 continuation byte, and a chord that collided with one would be typed
into the terminal instead of switching away from it.
@ -106,32 +107,68 @@ into the terminal instead of switching away from it.
What the chord *means* is decided in `console.c`, where the terminals are. The
keyboard driver names it and stops.
### Why the number row, and why left Alt
### Why this chord
Everything else was already spoken for:
**Anything the kernel swallows is taken from every program that will ever run
here, permanently.** That is the whole difficulty, and it took two wrong
answers to see it. The obvious candidates are all spoken for on one side or
the other:
| | |
|---|---|
| `Alt+F4` | the host window manager closes the QEMU window |
| **The host takes it** | |
| `Alt+F4` | the window manager closes the QEMU window |
| `Alt+F2` | GNOME and KDE open a run dialog |
| `Ctrl+Alt+F1..F6` | the host switches its **own** virtual terminals |
| `AltGr+`anything | Polish diacritics — ą ć ę ł ń ó ś ź ż — and on some setups AltGr *is* Ctrl+Alt |
| `Ctrl+Shift+`key | widely bound inside applications |
| Scroll Lock | not present on many keyboards |
| `Ctrl+Alt+F1..F6` | the display manager switches its **own** terminals |
| **Applications want it** | |
| `Alt+`anything | Meta. zsh's own line editor 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 layout wants it** | |
| `AltGr+`letter | Polish diacritics — ą ć ę ł ń ó ś ź ż — and on some setups AltGr *is* Ctrl+Alt |
`Alt+1..6` survives all of it.
`Ctrl+Alt+Shift+F1..F6` is in none of those sets. Display managers bind the
same chord *without* Shift, so adding it steps out of the host's way, and no
terminal application binds a triple-modified function key. Four keys is more
than one wants to press, which is right for something done rarely and on
purpose — `chvt N` covers everything else.
Which chords actually reach a guest was **measured, not assumed**: a temporary
probe in `kbdgetc` logged every modified keypress, and QEMU's `sendkey` fired
each candidate at it. Everything reached the guest, including `Ctrl+Alt+F2` —
so the host conflict is the host's display manager, not QEMU.
The modifier test is an **equality**, not a mask:
```c
if ((shift & (CTL | SHIFT | ALT | RALT)) == ALT && ...)
if ((shift & (CTL | SHIFT | ALT | RALT)) == (CTL | ALT | SHIFT) && ...)
```
`shift & ALT` would fire on `Ctrl+Alt+1` and on `AltGr+1` too, so a chord aimed
at the host — or an ordinary Polish keystroke — would switch terminals behind
your back. Right Alt is tracked as its own bit (`RALT`) for exactly this
reason; it used to share `ALT`, which is harmless for typing, because neither
is used to look up a character, and wrong for chords.
A mask would fire 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.
### 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.
@ -167,9 +204,10 @@ sequence -- clears the debt.
- **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 process-mode switching.** `VT_SETMODE` is accepted and ignored. A
display server that wants to veto a switch — which is what `VT_PROCESS` is
for — cannot yet, and will need to before Schcom owns Alt+F7.
- **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