From 65906639daff548d4e9297ee0c20c160e429c3ae Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sun, 13 Sep 2026 13:22:01 +0200 Subject: [PATCH] tty: AltGr+F1..F6 by default, and AltGr+-/+ to step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sweet spot was available the whole time and I ruled it out by writing "AltGr+anything" when the true statement is "AltGr+*letter*". A Polish layout wants 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 the host, applications and the keyboard layout all leave alone, and the previous three commits talked themselves out of it on a technicality that was mine, not the hardware's. Verified rather than argued, with QEMU sendkey and screendump hashes: AltGr+F2 -> tty2 AltGr+F1 -> back, pixel-identical to the start AltGr+a -> reaches zsh, so ą still types That distinction is only expressible because Right Alt became its own modifier bit two commits ago. Sharing ALT is harmless for typing -- neither is used to look up a character -- and made this impossible to say. Also added, since it was asked for in the same breath: minus and equals step to the previous and next terminal, on whatever the chord's modifiers are. They skip terminals that are not 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 those is worse than not having the key. AltGr+= -> tty2, again -> wraps to tty1 (only two in use) AltGr+- -> tty2 chvt -c still moves all of it, and `chvt` reports both halves. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2 --- cmd/chvt/main.c | 17 +++++++++++---- docs/architecture/README.md | 4 ++-- docs/architecture/terminals.md | 38 +++++++++++++++++++++++++++------- include/kbd.h | 24 +++++++++++++++++---- include/vt.h | 1 + kernel/core/console.c | 10 ++++++--- kernel/dev/kbd.c | 28 ++++++++++++++++--------- kernel/dev/vt.c | 27 ++++++++++++++++++++++++ 8 files changed, 118 insertions(+), 31 deletions(-) diff --git a/cmd/chvt/main.c b/cmd/chvt/main.c index 57e02cc..306ee1c 100644 --- a/cmd/chvt/main.c +++ b/cmd/chvt/main.c @@ -19,10 +19,13 @@ // 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 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 at all +// +// Whatever the modifiers are, they also bind minus and equals to the previous +// and next terminal in use -- AltGr+- and AltGr+= by default. #include "types.h" #include "user.h" @@ -121,8 +124,14 @@ show_chord(const struct vt_chord *c) 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"); + if (c->base == VTC_BASE_F1) puts("F1..F6"); + else puts("1..6"); + puts(", and "); + 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+"); + puts("- / + to step\n"); } int diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 4f2682d..02e3edf 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`, `chvt N` -or a settable chord -- +Six virtual terminals sit under all of it -- `/dev/tty1`..`tty6`, AltGr+F1..F6 +or `chvt N` -- 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 2c4f0c3..155f1ac 100644 --- a/docs/architecture/terminals.md +++ b/docs/architecture/terminals.md @@ -1,9 +1,15 @@ # Virtual terminals -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 +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. @@ -133,8 +139,8 @@ So the kernel ships a default and lets it be moved: ```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 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 ``` @@ -142,9 +148,25 @@ 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. +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 diff --git a/include/kbd.h b/include/kbd.h index 7984861..915afab 100644 --- a/include/kbd.h +++ b/include/kbd.h @@ -65,12 +65,28 @@ // 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. +// kernel ships a default and lets it be changed: VT_SETCHORD, and `chvt -c` +// 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. +// +// The default is AltGr+F1..F6, and the "AltGr wants it" line above needs +// reading carefully: what a Polish layout wants is AltGr+*letter*. AltGr with +// a function key produces nothing on any layout, no window manager binds it, +// and no application asks for it. Right Alt being a separate modifier bit +// here is what makes the distinction expressible at all -- it used to share +// ALT, which is harmless for typing and useless for this. #define KEY_VT_SWITCH 0x120 +// Previous / next terminal, on the same modifiers as the chord above. The +// minus and equals keys, which sit together and read as - and + -- a relative +// move for when you have two terminals and do not care which number they are. +#define KEY_VT_PREV 0x130 +#define KEY_VT_NEXT 0x131 + +#define SCAN_MINUS 0x0C // - / _ +#define SCAN_EQUALS 0x0D // = / + + #define SCAN_F1 0x3B // F1..F6 are scancodes 0x3B..0x40 #define SCAN_1 0x02 // 1..6 are scancodes 0x02..0x07 diff --git a/include/vt.h b/include/vt.h index db98923..7ddb26c 100644 --- a/include/vt.h +++ b/include/vt.h @@ -132,6 +132,7 @@ void vt_resize(void); // a scanout arrived; re-measure struct vt *vt_get(int i); // 0-based, clamped, never null struct vt *vt_current(void); int vt_switch(int i); // 0-based; repaints. -1 if out of range +int vt_cycle(int dir); // -1 previous, +1 next, in-use only // Output, per terminal. Drawing happens only if `v` is the active one. void vt_feed(struct vt *v, int c); // one output byte, through the parser diff --git a/kernel/core/console.c b/kernel/core/console.c index 746d3e5..068a751 100644 --- a/kernel/core/console.c +++ b/kernel/core/console.c @@ -368,7 +368,8 @@ erase_one(struct vt *v) int console_vt_hotkey(int key) { - if (key < KEY_VT_SWITCH || key >= KEY_VT_SWITCH + NVT) + if (key != KEY_VT_PREV && key != KEY_VT_NEXT && + (key < KEY_VT_SWITCH || key >= KEY_VT_SWITCH + NVT)) return 0; // A program that called VT_SETMODE(VT_PROCESS) owns switching on this // terminal. Dropping the chord entirely would be worse than not binding it: @@ -376,7 +377,9 @@ console_vt_hotkey(int key) // falls through to the line discipline as an ordinary key. if (!vt_kernel_switches()) return 0; - vt_switch(key - KEY_VT_SWITCH); + if (key == KEY_VT_PREV) vt_cycle(-1); + else if (key == KEY_VT_NEXT) vt_cycle(+1); + else vt_switch(key - KEY_VT_SWITCH); return 1; } @@ -392,7 +395,8 @@ consoleintr(int (*getc)(void)) // Terminal switching happens before the line discipline sees anything: // the chord is not input for the terminal you are leaving. - if (c >= KEY_VT_SWITCH && c < KEY_VT_SWITCH + NVT) { + if (c == KEY_VT_PREV || c == KEY_VT_NEXT || + (c >= KEY_VT_SWITCH && c < KEY_VT_SWITCH + NVT)) { release(&input_lock); int taken = console_vt_hotkey(c); acquire(&input_lock); diff --git a/kernel/dev/kbd.c b/kernel/dev/kbd.c index 71a2942..a91b5d7 100644 --- a/kernel/dev/kbd.c +++ b/kernel/dev/kbd.c @@ -6,14 +6,19 @@ #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. +// The chord, settable at run time through VT_SETCHORD. // -// Zero modifiers disables the chord entirely; `chvt N` still works. -uint vt_chord_mods = CTL | SHIFT; +// AltGr+F1..F6 by default. Every other candidate is taken by the host, by +// applications, or by the keyboard layout -- see include/kbd.h -- and AltGr +// with a function key is taken by none of the three: a Polish layout wants +// AltGr+*letter*, which still reaches the shell untouched, and no window +// manager binds AltGr with an F-key. +// +// AltGr+minus and AltGr+equals step to the previous and next terminal on the +// same modifiers, for when you have two and do not care which number they are. +// +// Zero modifiers disables all of it; `chvt N` still works. +uint vt_chord_mods = RALT; uint vt_chord_base = SCAN_F1; int @@ -84,9 +89,12 @@ kbdgetc(void) // Polish keystroke -- would switch terminals behind your back. Exactly left // Alt, and nothing else held. 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); + (shift & (CTL | SHIFT | ALT | RALT)) == vt_chord_mods) { + if (data >= vt_chord_base && data < vt_chord_base + NVT) + return KEY_VT_SWITCH + (data - vt_chord_base); + if (data == SCAN_MINUS) return KEY_VT_PREV; + if (data == SCAN_EQUALS) return KEY_VT_NEXT; + } c = charcode[shift & (CTL | SHIFT)][data]; if(shift & CAPSLOCK){ diff --git a/kernel/dev/vt.c b/kernel/dev/vt.c index 48e8c4d..18c02e5 100644 --- a/kernel/dev/vt.c +++ b/kernel/dev/vt.c @@ -718,3 +718,30 @@ vt_kernel_switches(void) { return vt_current()->mode != VT_PROCESS; } + +// Step to the previous or next terminal that is actually in use. +// +// Skipping the idle ones matters: six terminals with two shells on them means +// four blank screens between the things you wanted, and cycling past them is +// worse than not having the key. "In use" is `opened`, the same count +// VT_GETSTATE reports, so what the key does and what `chvt` says are the same +// fact. +// +// If nothing else is open this stays put rather than wrapping to a blank one. +int +vt_cycle(int dir) +{ + if (!vt_ready) + return -1; + + for (int step = 1; step <= NVT; step++) { + int i = vt_active + dir * step; + while (i < 0) i += NVT; + while (i >= NVT) i -= NVT; + if (i == vt_active) + break; + if (vts[i].opened > 0) + return vt_switch(i); + } + return 0; // nowhere else to go; not an error +}