tty: the switch chord is a setting, because only the host knows the answer
Third guess at a chord, and the third one would have been wrong too: a display
manager that grabs Ctrl+Alt+F<n> matches loosely, so Ctrl+Alt+Shift+F2 never
reaches the guest either. Extra modifiers do not get past a loose grab.
The real problem is that this is not knowable from inside. A key the host
swallowed is indistinguishable from a key nobody pressed, so the guest cannot
probe for a free combination, and there is no combination that is free
everywhere:
the HOST takes Alt+F4, Alt+F2, Ctrl+Alt+F1..F6 (loosely matched),
Ctrl+Alt+digit (QEMU's own console tabs)
APPLICATIONS want Alt+anything (Meta -- zsh binds Meta-digit as
digit-argument), Ctrl+letter, Ctrl+Shift+digit
(Ctrl+Shift+2 types ^@), bare F-keys
the LAYOUT wants AltGr+letter
So it is a setting. VT_SETCHORD/VT_GETCHORD, and in userspace:
chvt -c ctrl+shift+F Ctrl+Shift+F1..F6 (the new default)
chvt -c ctrl+alt+shift+F
chvt -c alt+1
chvt -c off no chord; chvt N is then the only way
chvt report the active terminal and the chord
`off` is a legitimate setting rather than a failure mode. On a machine whose
host grabs everything worth binding, `chvt N` is the honest answer.
The default moved to Ctrl+Shift+F1..F6: display managers bind Ctrl+Alt+F<n>,
not Ctrl+Shift+F<n>, and Ctrl+Shift with a *function* key is not a character
on any layout -- unlike Ctrl+Shift+digit, which is.
Caught while writing it: VTC_RALT was 0x08 while kbd.h's RALT is (1<<7), since
(1<<3) is CAPSLOCK. A chord set from userspace with AltGr would simply never
have matched, and the only symptom would have been a key that does nothing.
A _Static_assert now holds the two headers together.
Verified: Ctrl+Alt+F2 leaves the screen byte-identical, Ctrl+Shift+F2 switches,
Ctrl+Shift+F1 returns to a pixel-identical tty1, and chvt -c round-trips
through alt+1 and off and back. 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:
parent
fe2f5e1bf8
commit
7c0f7c7d01
8 changed files with 256 additions and 60 deletions
|
|
@ -1,8 +1,9 @@
|
|||
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
||||
# Virtual terminals
|
||||
|
||||
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
|
||||
Six of them, `/dev/tty1` through `/dev/tty6`, reached with `chvt N` or with
|
||||
a keyboard chord — **Ctrl+Shift+F1 .. F6** by default, and settable, for
|
||||
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.
|
||||
|
|
@ -107,49 +108,67 @@ 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 this chord
|
||||
### The chord is a setting, and that is the point
|
||||
|
||||
**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:
|
||||
**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** | |
|
||||
| `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 |
|
||||
| **Applications want it** | |
|
||||
| `Alt+`anything | Meta. zsh's own line editor binds Meta-digit as `digit-argument`; so does every readline editor |
|
||||
| **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 layout wants it** | |
|
||||
| `AltGr+`letter | Polish diacritics — ą ć ę ł ń ó ś ź ż — and on some setups AltGr *is* Ctrl+Alt |
|
||||
| **The keyboard layout wants it** | |
|
||||
| `AltGr+`letter | Polish diacritics — ą ć ę ł ń ó ś ź ż — and on some setups AltGr arrives 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 right for something done rarely and on
|
||||
purpose — `chvt N` covers everything else.
|
||||
So the kernel ships a default and lets it be moved:
|
||||
|
||||
Which chords actually reach a guest was **measured, not assumed**: a temporary
|
||||
```sh
|
||||
chvt # report the active terminal and the current chord
|
||||
chvt -c ctrl+shift+F # Ctrl+Shift+F1..F6 (the default)
|
||||
chvt -c ctrl+alt+shift+F # if your host leaves that alone
|
||||
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 `Ctrl+Shift+F1..F6` because display managers bind `Ctrl+Alt+F<n>`
|
||||
rather than `Ctrl+Shift+F<n>`, and because `Ctrl+Shift` with a *function key*
|
||||
is not a character on any layout — unlike `Ctrl+Shift+`digit, which is.
|
||||
|
||||
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 reached the guest, including `Ctrl+Alt+F2` —
|
||||
so the host conflict is the host's display manager, not QEMU.
|
||||
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 ((shift & (CTL | SHIFT | ALT | RALT)) == (CTL | ALT | SHIFT) && ...)
|
||||
if (vt_chord_mods != 0 &&
|
||||
(shift & (CTL | SHIFT | ALT | RALT)) == vt_chord_mods && ...)
|
||||
```
|
||||
|
||||
A mask would fire on any superset, so a chord aimed at the host would switch
|
||||
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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue