diff --git a/cmd/chvt/main.c b/cmd/chvt/main.c index 83683bc..57e02cc 100644 --- a/cmd/chvt/main.c +++ b/cmd/chvt/main.c @@ -3,12 +3,26 @@ // chvt N -- show virtual terminal N. // // The same name and the same argument as the Linux tool, because it does the -// same thing and nobody should have to learn a second one. Alt+F1..F6 does +// same thing and nobody should have to learn a second one. The chord does // this from the keyboard; this does it from a script, and from a serial // console where there are no function keys to press. // -// chvt report which terminal is active and which are in use -// chvt 2 switch to /dev/tty2 +// chvt report the active terminal, those in use, and the chord +// chvt 2 switch to /dev/tty2 +// chvt -c set the switch chord +// chvt -c off unbind it entirely; chvt stays the only way to switch +// +// The chord is settable because the right value is a property of the *host*, +// not of SchOS. A display manager that grabs Ctrl+Alt+F takes it before the +// guest sees anything, and it matches loosely -- extra modifiers do not get +// past it -- so a chord that works on one machine is dead on the next. The +// guest cannot detect this: a key the host swallowed is indistinguishable from +// a key nobody pressed. So it is a setting. +// +// chvt -c ctrl+shift+F Ctrl+Shift+F1..F6 (the default) +// chvt -c ctrl+alt+shift+F Ctrl+Alt+Shift+F1..F6 +// chvt -c alt+1 Alt+1..6 +// chvt -c off no chord at all #include "types.h" #include "user.h" @@ -44,6 +58,73 @@ num(const char *s) return v; } +static int +same(const char *a, const char *b, int n) +{ + for (int i = 0; i < n; i++) + if (a[i] != b[i]) return 0; + return 1; +} + +static int +eq(const char *a, const char *b) +{ + while (*a && *a == *b) { a++; b++; } + return *a == 0 && *b == 0; +} + +// "ctrl+shift+F", "alt+1", "off". Modifiers by name, then a base: F for the +// function keys, 1 for the number row. +static int +parse_chord(const char *spec, struct vt_chord *out) +{ + out->mods = 0; + out->base = 0; + + if (eq(spec, "off") || eq(spec, "none")) + return 1; // no modifiers: the chord is disabled + + const char *p = spec; + while (*p) { + const char *start = p; + while (*p && *p != '+') p++; + int n = (int)(p - start); + + if (n == 4 && same(start, "ctrl", 4)) out->mods |= VTC_CTRL; + else if (n == 5 && same(start, "shift", 5)) out->mods |= VTC_SHIFT; + else if (n == 3 && same(start, "alt", 3)) out->mods |= VTC_ALT; + else if (n == 5 && same(start, "altgr", 5)) out->mods |= VTC_RALT; + else if (n == 1 && (*start == 'F' || *start == 'f')) out->base = VTC_BASE_F1; + else if (n == 1 && *start == '1') out->base = VTC_BASE_1; + else return 0; + + if (*p == '+') p++; + } + // A base with no modifiers would bind a bare function key, which every + // full-screen program wants. Refuse it here rather than in the kernel, so + // the message can say why. + if (out->base && !out->mods) + return 0; + if (out->mods && !out->base) + out->base = VTC_BASE_F1; + return 1; +} + +static void +show_chord(const struct vt_chord *c) +{ + if (c->mods == 0) { + puts("off (use chvt N)\n"); + return; + } + if (c->mods & VTC_CTRL) puts("Ctrl+"); + if (c->mods & VTC_ALT) puts("Alt+"); + if (c->mods & VTC_SHIFT) puts("Shift+"); + if (c->mods & VTC_RALT) puts("AltGr+"); + if (c->base == VTC_BASE_F1) puts("F1..F6\n"); + else puts("1..6\n"); +} + int main(int argc, char *argv[]) { @@ -56,6 +137,21 @@ main(int argc, char *argv[]) exit(); } + if (argc == 3 && argv[1][0] == '-' && argv[1][1] == 'c') { + struct vt_chord c; + if (!parse_chord(argv[2], &c)) { + puts("chvt: bad chord -- try ctrl+shift+F, alt+1, or off\n"); + exit(); + } + if (ioctl(fd, VT_SETCHORD, &c) < 0) { + puts("chvt: kernel refused that chord\n"); + exit(); + } + puts("chord set to "); + show_chord(&c); + exit(); + } + if (argc == 1) { struct vt_stat st; if (ioctl(fd, VT_GETSTATE, &st) < 0) { @@ -71,6 +167,11 @@ main(int argc, char *argv[]) putn(i); } puts("\n"); + struct vt_chord c; + if (ioctl(fd, VT_GETCHORD, &c) == 0) { + puts("chord: "); + show_chord(&c); + } exit(); } diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 13c7563..4f2682d 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -18,8 +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`, -Ctrl+Alt+Shift+F1..F6 -- +Six virtual terminals sit under all of it -- `/dev/tty1`..`tty6`, `chvt N` +or a settable chord -- 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 37af0f0..2c4f0c3 100644 --- a/docs/architecture/terminals.md +++ b/docs/architecture/terminals.md @@ -1,8 +1,9 @@ # 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` +rather than `Ctrl+Shift+F`, 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: diff --git a/include/defs.h b/include/defs.h index 3b86adc..fee92b9 100644 --- a/include/defs.h +++ b/include/defs.h @@ -44,6 +44,8 @@ void cons_flush_input(int minor); int cons_fg_pgrp(int minor); void cons_set_fg_pgrp(int minor, int pg); int console_vt_hotkey(int key); +extern uint vt_chord_mods; // kernel/dev/kbd.c -- the switch chord +extern uint vt_chord_base; struct vt *vt_for_minor(int minor); extern uint lapic_timer_hz; // measured, kernel/arch/x86_64/lapic.c extern uint64 tsc_hz; diff --git a/include/kbd.h b/include/kbd.h index 6f65f98..7984861 100644 --- a/include/kbd.h +++ b/include/kbd.h @@ -36,40 +36,43 @@ #define KEY_INS 0xE8 #define KEY_DEL 0xE9 -// Ctrl+Alt+Shift+F1..F6 -- switch virtual terminal. +// 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. // -// 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: +// *Which* chord is a runtime setting, not a constant here, and that is the +// interesting part. Three separate parties have a claim on every candidate: // -// 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 +// the HOST, which sees the keystroke first and may never pass it on +// Alt+F4, Alt+F2 window manager +// Ctrl+Alt+F1..F6 display manager, and it matches loosely -- +// adding Shift does not get past it +// Ctrl+Alt+ QEMU's own console tabs // -// 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 +// APPLICATIONS, which lose the key permanently if the kernel eats it +// Alt+ Meta. zsh binds Meta-digit as digit-argument +// Ctrl+ control characters +// Ctrl+Shift+ types a character -- Ctrl+Shift+2 is ^@ +// F1..F6 alone help, and full-screen programs // -// THE KEYBOARD LAYOUT wants it -// AltGr+ Polish diacritics, and on some setups AltGr is -// delivered as Ctrl+Alt +// the KEYBOARD LAYOUT +// AltGr+ 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 correct for something done rarely -// and deliberately -- and `chvt N` is there for everything else. +// There is no combination that is free for everyone on every host, and the +// guest cannot find out what its host grabs -- the keystroke simply never +// arrives, which is indistinguishable from a chord nobody pressed. So the +// kernel ships a default and lets it be changed: VT_SETCHORD, and `chvt +// --chord` in userspace. Setting it to no modifiers disables the chord +// entirely and leaves `chvt N` as the only way to switch, which is a +// legitimate choice on a machine whose host takes everything. #define KEY_VT_SWITCH 0x120 + #define SCAN_F1 0x3B // F1..F6 are scancodes 0x3B..0x40 +#define SCAN_1 0x02 // 1..6 are scancodes 0x02..0x07 // C('A') == Control-A #define C(x) (x - '@') diff --git a/include/posix.h b/include/posix.h index 829d792..fce5e13 100644 --- a/include/posix.h +++ b/include/posix.h @@ -140,6 +140,32 @@ struct rlimit { #define VT_PROCESS 0x01 #define VT_ACKACQ 0x02 +// VT_SETCHORD / VT_GETCHORD -- which key combination the kernel switches on. +// SchOS's own, not Linux's: Linux hardcodes Ctrl+Alt+F and nobody has ever +// needed to move it, because Linux is the host. A guest does need to move it, +// because the right answer depends entirely on what the host grabs first -- +// and that is not something the guest can discover. +#define VT_GETCHORD 0x5620 +#define VT_SETCHORD 0x5621 + +// Modifier bits, matched exactly. These mirror kernel/dev/kbd.h's values so +// that userspace and the keyboard driver cannot drift apart. +#define VTC_SHIFT 0x01 // kbd.h SHIFT (1<<0) +#define VTC_CTRL 0x02 // kbd.h CTL (1<<1) +#define VTC_ALT 0x04 // kbd.h ALT (1<<2) +#define VTC_RALT 0x80 // kbd.h RALT (1<<7) -- NOT (1<<3); that is + // CAPSLOCK, and bits 3..6 are the other locks + // and the E0 prefix flag + +// Well-known base scancodes for the `base` field below. +#define VTC_BASE_F1 0x3B // F1..F6 +#define VTC_BASE_1 0x02 // the number row, 1..6 + +struct vt_chord { + uint16 mods; // exactly these modifiers held, and no others + uint16 base; // scancode of the key meaning terminal 1 +}; + struct vt_mode { char mode; // VT_AUTO or VT_PROCESS char waitv; // unused here diff --git a/kernel/dev/kbd.c b/kernel/dev/kbd.c index 85d6925..71a2942 100644 --- a/kernel/dev/kbd.c +++ b/kernel/dev/kbd.c @@ -2,9 +2,20 @@ #include "x86.h" #include "defs.h" #include "kbd.h" +#include "posix.h" #include "vt.h" /* NVT -- how many terminals the chords reach */ #include "input.h" +// The chord, settable at run time through VT_SETCHORD. Defaults to +// Ctrl+Shift+F1..F6: the host's display manager binds Ctrl+Alt+F and +// matches it loosely, so anything containing Ctrl+Alt never reaches a guest, +// whatever else is held with it. Ctrl+Shift is not a VT chord on any host and +// is not a character on any layout. +// +// Zero modifiers disables the chord entirely; `chvt N` still works. +uint vt_chord_mods = CTL | SHIFT; +uint vt_chord_base = SCAN_F1; + int kbdgetc(void) { @@ -60,19 +71,22 @@ kbdgetc(void) shift |= shiftcode[data]; shift ^= togglecode[data]; - // 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(). + // The switch chord, before the character tables, because there is no + // character to look up: it is a combination 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(). + // + // The modifier test is an equality, not a mask. A mask fires on any superset, + // so a chord aimed at the host would switch terminals behind your back. // // The modifier test is an equality, not a mask. `shift & ALT` would fire on // 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)) == (CTL | ALT | SHIFT) && - data >= SCAN_F1 && data < SCAN_F1 + NVT) - return KEY_VT_SWITCH + (data - SCAN_F1); + if (vt_chord_mods != 0 && + (shift & (CTL | SHIFT | ALT | RALT)) == vt_chord_mods && + data >= vt_chord_base && data < vt_chord_base + NVT) + return KEY_VT_SWITCH + (data - vt_chord_base); c = charcode[shift & (CTL | SHIFT)][data]; if(shift & CAPSLOCK){ @@ -89,3 +103,10 @@ kbdintr(void) { consoleintr(kbdgetc); } + +// The two headers describe the same bits to two different audiences, and 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. +_Static_assert(VTC_SHIFT == SHIFT && VTC_CTRL == CTL && + VTC_ALT == ALT && VTC_RALT == RALT, + "include/posix.h VTC_* must mirror include/kbd.h modifiers"); diff --git a/kernel/sys/sys_fs.c b/kernel/sys/sys_fs.c index 9d80abc..e6a69af 100644 --- a/kernel/sys/sys_fs.c +++ b/kernel/sys/sys_fs.c @@ -1187,6 +1187,30 @@ addr_t sys_ioctl(void) vt_for_minor(f->ip->minor)->mode = m->mode; return 0; } + // Which chord the kernel switches on. SchOS's own ioctl -- see the long + // comment in include/kbd.h for why this has to be settable at all. + if (request == VT_SETCHORD) { + struct vt_chord *c = (struct vt_chord*)user_ptr(argp, sizeof(*c)); + if (!c) return -EFAULT; + // Only real modifier bits, and a base that leaves room for NVT keys + // below the release flag. A base of 0 with no modifiers is the + // documented way to turn the chord off, so it is not an error. + if (c->mods & ~(VTC_SHIFT | VTC_CTRL | VTC_ALT | VTC_RALT)) + return -EINVAL; + if (c->mods && (c->base == 0 || c->base + NVT > 0x80)) + return -EINVAL; + vt_chord_mods = c->mods; + vt_chord_base = c->base; + return 0; + } + if (request == VT_GETCHORD) { + struct vt_chord *c = (struct vt_chord*)user_ptr(argp, sizeof(*c)); + if (!c) return -EFAULT; + c->mods = (uint16)vt_chord_mods; + c->base = (uint16)vt_chord_base; + return 0; + } + if (request == VT_GETMODE) { struct vt_mode *m = (struct vt_mode*)user_ptr(argp, sizeof(*m)); if (!m) return -EFAULT;