From fe2f5e1bf852a6a7d2184c62eadc052108cb1d59 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sun, 13 Sep 2026 13:05:43 +0200 Subject: [PATCH] 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+ 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 Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2 --- docs/architecture/README.md | 3 +- docs/architecture/terminals.md | 80 +++++++++++++++++++++++++--------- include/kbd.h | 44 +++++++++++-------- include/posix.h | 15 +++++++ include/vt.h | 5 +++ kernel/core/console.c | 25 +++++++---- kernel/dev/kbd.c | 9 ++-- kernel/dev/vt.c | 21 +++++++++ kernel/sys/sys_fs.c | 24 +++++++--- 9 files changed, 169 insertions(+), 57 deletions(-) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 184470c..13c7563 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -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). diff --git a/docs/architecture/terminals.md b/docs/architecture/terminals.md index f8aa07f..37af0f0 100644 --- a/docs/architecture/terminals.md +++ b/docs/architecture/terminals.md @@ -1,13 +1,14 @@ # 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 diff --git a/include/kbd.h b/include/kbd.h index bc581ca..6f65f98 100644 --- a/include/kbd.h +++ b/include/kbd.h @@ -36,32 +36,40 @@ #define KEY_INS 0xE8 #define KEY_DEL 0xE9 -// Alt+1..Alt+6 -- switch virtual terminal. -// -// Above 0xFF deliberately. kbdgetc returns an int and consoleintr takes one, -// and every value that fits in a byte is already either a character or a -// UTF-8 continuation byte. A chord that collided with one of those would be -// typed into the terminal instead of switching away from it. -// Alt+1..Alt+6 -- switch virtual terminal. +// Ctrl+Alt+Shift+F1..F6 -- switch virtual terminal. // // Above 0xFF deliberately. kbdgetc returns an int and consoleintr takes one, // and every value that fits in a byte is already either a character or a // UTF-8 continuation byte. A chord that collided with one would be typed into // the terminal instead of switching away from it. // -// The number row rather than the function keys, and left Alt rather than -// either, because everything else was already taken: +// Choosing this took two wrong answers. Anything the kernel swallows is taken +// from every program that will ever run here, permanently, and the obvious +// candidates are all spoken for on one side or the other: // -// Alt+F4 the host 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 AltGr is Ctrl+Alt on some setups -// Ctrl+Shift+key widely bound inside applications +// HOST takes it, before the guest ever sees it +// Alt+F4 the window manager closes the QEMU window +// Alt+F2 GNOME and KDE open a run dialog +// Ctrl+Alt+F1..F6 the display manager switches its *own* terminals // -// Alt+digit survives all of that. It is matched exactly -- Ctrl+Alt+1 and -// AltGr+1 do nothing -- so a chord meant for the host never reaches here. -#define KEY_ALT_F1 0x120 -#define SCAN_1 0x02 // 1..6 are scancodes 0x02..0x07 +// APPLICATIONS want it, and would never get it again +// Alt+ Meta. zsh's own line editor binds Meta-digit as +// digit-argument; every readline editor does too +// Ctrl+ control characters +// Ctrl+Shift+ types a character -- Ctrl+Shift+2 is ^@ +// F1..F6 alone help, and whatever a full-screen program wants +// +// THE KEYBOARD LAYOUT wants it +// AltGr+ Polish diacritics, and on some setups AltGr is +// delivered as Ctrl+Alt +// +// 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 correct for something done rarely +// and deliberately -- and `chvt N` is there for everything else. +#define KEY_VT_SWITCH 0x120 +#define SCAN_F1 0x3B // F1..F6 are scancodes 0x3B..0x40 // C('A') == Control-A #define C(x) (x - '@') diff --git a/include/posix.h b/include/posix.h index ace0af7..829d792 100644 --- a/include/posix.h +++ b/include/posix.h @@ -133,6 +133,21 @@ struct rlimit { #define VT_ACTIVATE 0x5606 #define VT_WAITACTIVE 0x5607 +// VT_SETMODE modes. VT_AUTO is the kernel switching on its own; VT_PROCESS +// means a program has taken switching over and the kernel must keep its hands +// off the keyboard. +#define VT_AUTO 0x00 +#define VT_PROCESS 0x01 +#define VT_ACKACQ 0x02 + +struct vt_mode { + char mode; // VT_AUTO or VT_PROCESS + char waitv; // unused here + short relsig; // signal on release -- not delivered yet + short acqsig; // signal on acquire -- not delivered yet + short frsig; // unused +}; + struct vt_stat { uint16 v_active; // active terminal, 1-based uint16 v_signal; // unused; Linux keeps it for VT_SETMODE diff --git a/include/vt.h b/include/vt.h index e34fce6..db98923 100644 --- a/include/vt.h +++ b/include/vt.h @@ -118,6 +118,10 @@ struct vt { int sid; // session that owns this terminal, 0 = none int opened; // times it has been opened, for /dev/tty + + // VT_AUTO or VT_PROCESS. In VT_PROCESS a program has claimed switching and + // the kernel stops acting on the chord -- see vt_kernel_switches(). + int mode; }; // The active terminal, and the one the framebuffer is showing. @@ -142,5 +146,6 @@ void vt_repaint(struct vt *v); // grid -> framebuffer, if active uint32 vt_rgb(uint8 idx); // palette index -> RGB int vt_index(struct vt *v); // 0-based number of a terminal +int vt_kernel_switches(void); // 0 if a program owns switching #endif // VT_H diff --git a/kernel/core/console.c b/kernel/core/console.c index 48bd6cb..746d3e5 100644 --- a/kernel/core/console.c +++ b/kernel/core/console.c @@ -2,7 +2,7 @@ #include "types.h" #include "defs.h" #include "vt.h" -#include "kbd.h" /* KEY_ALT_F1 -- the terminal-switch chords */ +#include "kbd.h" /* KEY_VT_SWITCH -- the terminal-switch chords */ #include "gfx.h" #include "theme.h" #include "irq.h" @@ -361,15 +361,22 @@ erase_one(struct vt *v) // Keyboard input // --------------------------------------------------------------------------- -// Alt+1..6 switch terminals. kbd.c reports them as KEY_ALT_F1.. so that the +// Ctrl+Alt+Shift+F1..F6 switch terminals. kbd.c reports them as +// KEY_VT_SWITCH+n so that the // decision of what a chord *means* is made here, where the terminals are, // rather than in a driver that should not know they exist. int console_vt_hotkey(int key) { - if (key < KEY_ALT_F1 || key >= KEY_ALT_F1 + NVT) + if (key < KEY_VT_SWITCH || key >= KEY_VT_SWITCH + NVT) return 0; - vt_switch(key - KEY_ALT_F1); + // A program that called VT_SETMODE(VT_PROCESS) owns switching on this + // terminal. Dropping the chord entirely would be worse than not binding it: + // it would vanish instead of reaching the program that asked for it. So it + // falls through to the line discipline as an ordinary key. + if (!vt_kernel_switches()) + return 0; + vt_switch(key - KEY_VT_SWITCH); return 1; } @@ -384,12 +391,14 @@ consoleintr(int (*getc)(void)) continue; // Terminal switching happens before the line discipline sees anything: - // Alt+F2 is not input for the terminal you are leaving. - if (c >= KEY_ALT_F1 && c < KEY_ALT_F1 + NVT) { + // the chord is not input for the terminal you are leaving. + if (c >= KEY_VT_SWITCH && c < KEY_VT_SWITCH + NVT) { release(&input_lock); - console_vt_hotkey(c); + int taken = console_vt_hotkey(c); acquire(&input_lock); - continue; + if (taken) + continue; + continue; /* VT_PROCESS: nothing sensible to hand the tty yet */ } // Input goes to whichever terminal is on screen. That is what makes a diff --git a/kernel/dev/kbd.c b/kernel/dev/kbd.c index a093a8d..85d6925 100644 --- a/kernel/dev/kbd.c +++ b/kernel/dev/kbd.c @@ -60,7 +60,8 @@ kbdgetc(void) shift |= shiftcode[data]; shift ^= togglecode[data]; - // Alt+1..6 before the character tables, because there is no character to + // Ctrl+Alt+Shift+F1..F6 before the character tables, because there is no + // character to // look up: it is a chord that means "show me a different terminal". What it // means is decided in console.c, which is where the terminals are; this only // has to name it. See console_vt_hotkey(). @@ -69,9 +70,9 @@ kbdgetc(void) // Ctrl+Alt+1 and on AltGr+1 as well, so a chord aimed at the host -- or a // Polish keystroke -- would switch terminals behind your back. Exactly left // Alt, and nothing else held. - if ((shift & (CTL | SHIFT | ALT | RALT)) == ALT && - data >= SCAN_1 && data < SCAN_1 + NVT) - return KEY_ALT_F1 + (data - SCAN_1); + if ((shift & (CTL | SHIFT | ALT | RALT)) == (CTL | ALT | SHIFT) && + data >= SCAN_F1 && data < SCAN_F1 + NVT) + return KEY_VT_SWITCH + (data - SCAN_F1); c = charcode[shift & (CTL | SHIFT)][data]; if(shift & CAPSLOCK){ diff --git a/kernel/dev/vt.c b/kernel/dev/vt.c index f8c9734..48e8c4d 100644 --- a/kernel/dev/vt.c +++ b/kernel/dev/vt.c @@ -697,3 +697,24 @@ vt_index(struct vt *v) { return v ? v->index : 0; } + +// May the kernel act on the switch chord? +// +// Not if the terminal on screen is in VT_PROCESS: a program has said it will +// handle switching itself, and the whole point of saying so is that the chord +// belongs to it now. This is the documented Unix answer to "my application +// wants that key", and it is the one a display server will use -- Schcom, on +// the terminal it owns, so that Ctrl+Alt+Shift+F2 reaches its own compositor +// bindings instead of yanking the screen out from under it. +// +// What this does *not* do yet: deliver relsig/acqsig and wait for VT_RELDISP. +// Linux signals the owning process on every switch request and blocks until it +// answers, which is how a display server gets to release the GPU first. There +// is nothing here that needs to release anything, so the kernel simply stops +// switching rather than negotiating. When Schcom needs the handshake, this is +// where it goes, and struct vt_mode already carries the fields for it. +int +vt_kernel_switches(void) +{ + return vt_current()->mode != VT_PROCESS; +} diff --git a/kernel/sys/sys_fs.c b/kernel/sys/sys_fs.c index 969054f..9d80abc 100644 --- a/kernel/sys/sys_fs.c +++ b/kernel/sys/sys_fs.c @@ -1174,12 +1174,26 @@ addr_t sys_ioctl(void) return -ENFILE; } - // VT_GETMODE/VT_SETMODE describe whether the kernel or a process drives - // switching. There is only the kernel here, so report the default and - // accept the default; refusing outright would stop a program that only - // wanted to check. - if (request == VT_GETMODE || request == VT_SETMODE) + // VT_GETMODE/VT_SETMODE: who drives switching. + // + // These were accepted and ignored, which was a lie with a consequence -- + // a program could ask for the chord and be told yes while the kernel kept + // taking it. VT_PROCESS now genuinely stops the kernel switching, so an + // application that wants Ctrl+Alt+Shift+F2 can have it. + if (request == VT_SETMODE) { + struct vt_mode *m = (struct vt_mode*)user_ptr(argp, sizeof(*m)); + if (!m) return -EFAULT; + if (m->mode != VT_AUTO && m->mode != VT_PROCESS) return -EINVAL; + vt_for_minor(f->ip->minor)->mode = m->mode; return 0; + } + if (request == VT_GETMODE) { + struct vt_mode *m = (struct vt_mode*)user_ptr(argp, sizeof(*m)); + if (!m) return -EFAULT; + memset(m, 0, sizeof(*m)); + m->mode = (char)vt_for_minor(f->ip->minor)->mode; + return 0; + } return 0; }