tty: six virtual terminals, and the plumbing that makes them terminals

/dev/tty1..tty6, Alt+F1..F6 or `chvt N`. Each is an independent terminal --
its own screen, cursor, line discipline, foreground process group and
session. Two shells on two terminals are unrelated processes that share a
keyboard and a screen.

What made it possible is a backing store. The console drew straight into the
framebuffer and scrolled by moving pixels, which works exactly as long as
nothing else wants the screen: there was nowhere to put terminal 2 while
terminal 1 was visible, and nothing to repaint terminal 1 from. Each terminal
owns a grid of cells now and the framebuffer shows whichever grid is active,
so vt_switch() is set-index, repaint, return -- nothing is saved on the way
out because the outgoing grid has been current all along.

The rule the layer is built on: a terminal does not know whether it is
visible. Writes land in the grid, the cursor moves, the grid scrolls, all
unconditionally; drawing is one question asked at the bottom of vt.c. Every
alternative spreads "am I on screen" through the escape parser, and then a
terminal switched away from mid-sequence comes back wrong.

Everything that was a single global is per-terminal now: tty_fg_pgrp (deleted),
termios, the input ring, the escape parser, the UTF-8 decoder, and the cursor
-- which also stops being pixels in gfx.c and becomes cells in the terminal.
TIOCSCTTY was `return 0` and now records proc->ctty, which is what /dev/tty
resolves through; /dev/tty was an alias for /dev/console with the same minor,
and they are different questions.

kbd.c names Alt+F1..F6 as KEY_ALT_F1+n, above 0xFF on purpose -- every byte
value is already a character or a UTF-8 continuation, and a chord colliding
with one would be typed into the terminal instead of switching away from it.
What the chord means is decided in console.c, where the terminals are.

VT_ACTIVATE / VT_WAITACTIVE / VT_GETSTATE / VT_OPENQRY, Linux's numbers.
The argument to ACTIVATE is by value, not a pointer -- same shape as TCFLSH.

Two bugs found by screenshotting the result rather than reading the log:

  - zsh emits 0x0F to leave alt-charset mode, because that is what the `linux`
    terminfo says rmacs is, and the console drew CP437 glyph 0x0F: a sun, in
    the prompt, on every line. A terminal never draws a C0 control character.
  - SGR 7 was unimplemented, so zsh's partial-line marker rendered as a bare
    '#' instead of a reversed block. Reverse video swaps the colours at draw
    time, not at store time, so turning it off restores what was underneath.

Verified by screendump: tty1 and tty2 show different screens, and switching
back repaints tty1's earlier content from its grid. schfs-check passes.

docs/architecture/terminals.md, including what it does not do -- no
scrollback, no reflow, no VT_PROCESS mode for a display server to veto a
switch, which Schcom will need before it owns Alt+F7.

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 00:08:00 +02:00
commit 1c1b0c9570
24 changed files with 1460 additions and 458 deletions

View file

@ -1,6 +1,8 @@
#include <stdarg.h>
#include "types.h"
#include "defs.h"
#include "vt.h"
#include "kbd.h" /* KEY_ALT_F1 -- the terminal-switch chords */
#include "gfx.h"
#include "theme.h"
#include "irq.h"
@ -44,7 +46,6 @@ extern void gfx_screen_reset(void);
// This was initialised to 7, a leftover from when it was a VGA text-mode
// attribute byte. gfx_putc takes a 32-bit RGB value, so 7 meant "very dark
// blue" -- it looked black against the cream background purely by accident.
static uint32 cons_fg = SCH_FG;
static struct {
struct spinlock lock;
@ -170,12 +171,6 @@ console_takeover(void)
gfx_screen_reset();
}
// Foreground process group of the console, i.e. the controlling terminal.
// tcgetpgrp()/tcsetpgrp() read and write this through TIOCGPGRP/TIOCSPGRP in
// sys_fs.c. Initialised to 1 -- init's process group -- so the first shell,
// which inherits that group, already sees itself as the foreground job and
// does not immediately stop itself with SIGTTIN.
int tty_fg_pgrp = 1;
#define BACKSPACE 0x100
@ -193,296 +188,9 @@ int tty_fg_pgrp = 1;
// only for the framebuffer. Two consumers, two right answers.
// ---------------------------------------------------------------------------
// The sixteen ANSI colours as RGB, for a linear framebuffer.
//
// consoleinit() used to program these into the VGA DAC with set_vga_palette(),
// which does nothing at all in a 32bpp linear mode -- there is no palette to
// index. The colours had to live somewhere real, and this is it.
//
// That DAC code is now gone rather than merely unused, along with the blink
// disable next to it: both were writes to VGA hardware registers made from the
// console, which is neither a driver nor allowed to know it was running on a
// VGA. Nothing observable changed, because nothing observable depended on it.
static const uint32 ansi_rgb[16] = {
0x000000, 0xAA0000, 0x00AA00, 0xAA5500, // black red green yellow(brown)
0x0000AA, 0xAA00AA, 0x00AAAA, 0xAAAAAA, // blue magenta cyan white
0x555555, 0xFF5555, 0x55FF55, 0xFFFF55, // bright versions
0x5555FF, 0xFF55FF, 0x55FFFF, 0xFFFFFF,
};
#define CONS_DEFAULT_FG SCH_FG // see include/theme.h
#define CONS_DEFAULT_BG SCH_BG
static uint32 cons_bg = CONS_DEFAULT_BG;
static int cons_bold = 0;
// Escape parser state.
// E_OSC and E_OSC_ESC exist for hyperlinks. A terminal that does not follow
// them must still *consume* them, or the URL is printed as text -- which is
// what "Disk (]8;;file:///\]8;;\): 4.6 MiB" was: fastfetch wrapping a path in
// OSC 8, and this console spelling it out.
enum { E_NORMAL, E_ESC, E_CSI, E_OSC, E_OSC_ESC };
static int esc_state = E_NORMAL;
static int esc_parm[8];
static int esc_nparm;
static int esc_private; // saw '?' after CSI
static int saved_col, saved_row;
// UTF-8 decoder state. A codepoint arrives as up to four bytes and has to be
// reassembled before it can be turned into a glyph.
static uint32 u8_cp;
static int u8_need;
// ---------------------------------------------------------------------------
// Unicode -> glyph
//
// The font's upper half is CP437, not Latin-1, so a codepoint cannot be used
// as an index. It looks like it can -- font.h's own comments say so, labelling
// each glyph with the Unicode character of the same number -- but the bitmaps
// disagree: index 0xE9 draws a capital theta, and rendering U+00E9 there turns
// "cafe" into "caf" followed by a theta. Every accented character came out as
// an unrelated symbol, consistently enough to look almost deliberate.
//
// So: a table. Entry i covers codepoint 0x80 + i, and 0 means "this font has
// no glyph for that". The C1 range (0x80-0x9F) has no printable characters at
// all, hence the leading run of zeros.
// ---------------------------------------------------------------------------
static const uchar latin1_to_cp437[128] = {
/* 0x80-0x9F: C1 controls, nothing to draw */
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
/* 0xA0 */ 0x20, 0xAD, 0x9B, 0x9C, 0, 0x9D, 0, 0x15,
/* 0xA8 */ 0, 0, 0xA6, 0xAE, 0xAA, 0x2D, 0, 0,
/* 0xB0 */ 0xF8, 0xF1, 0xFD, 0, 0, 0xE6, 0x14, 0xFA,
/* 0xB8 */ 0, 0, 0xA7, 0xAF, 0xAC, 0xAB, 0, 0xA8,
/* 0xC0 */ 0, 0, 0, 0, 0x8E, 0x8F, 0x92, 0x80,
/* 0xC8 */ 0, 0x90, 0, 0, 0, 0, 0, 0,
/* 0xD0 */ 0, 0xA5, 0, 0, 0, 0, 0x99, 0,
/* 0xD8 */ 0, 0, 0, 0, 0x9A, 0, 0, 0xE1,
/* 0xE0 */ 0x85, 0xA0, 0x83, 0, 0x84, 0x86, 0x91, 0x87,
/* 0xE8 */ 0x8A, 0x82, 0x88, 0x89, 0x8D, 0xA1, 0x8C, 0x8B,
/* 0xF0 */ 0, 0xA4, 0x95, 0xA2, 0x93, 0, 0x94, 0xF6,
/* 0xF8 */ 0, 0x97, 0xA3, 0x96, 0x81, 0, 0, 0x98,
};
// The handful of characters above U+00FF that CP437 does have. Box drawing
// and block elements matter because they are what every text-mode program
// draws frames and progress bars out of.
struct cp_pair { uint32 cp; uchar glyph; };
static const struct cp_pair cp437_extra[] = {
{ 0x2022, 0x07 }, /* bullet */
{ 0x2500, 0xC4 }, { 0x2502, 0xB3 }, /* light horizontal/vertical */
{ 0x250C, 0xDA }, { 0x2510, 0xBF },
{ 0x2514, 0xC0 }, { 0x2518, 0xD9 },
{ 0x251C, 0xC3 }, { 0x2524, 0xB4 },
{ 0x252C, 0xC2 }, { 0x2534, 0xC1 }, { 0x253C, 0xC5 },
{ 0x2550, 0xCD }, { 0x2551, 0xBA }, /* double */
{ 0x2554, 0xC9 }, { 0x2557, 0xBB },
{ 0x255A, 0xC8 }, { 0x255D, 0xBC },
{ 0x2580, 0xDF }, { 0x2584, 0xDC }, { 0x2588, 0xDB },
{ 0x258C, 0xDD }, { 0x2590, 0xDE },
{ 0x2591, 0xB0 }, { 0x2592, 0xB1 }, { 0x2593, 0xB2 },
{ 0x25A0, 0xFE }, /* black square */
{ 0x221A, 0xFB }, { 0x221E, 0xEC }, /* root, infinity */
{ 0x2260, 0, }, { 0x2264, 0xF3 }, { 0x2265, 0xF2 },
{ 0x2248, 0xF7 }, { 0x2261, 0xF0 },
};
// Returns the glyph to draw for a codepoint, or 0 if there is none.
static int
glyph_for(uint32 cp)
{
if (cp < 0x80)
return (int)cp; // ASCII is CP437's lower half
if (cp < 0x100)
return latin1_to_cp437[cp - 0x80];
for (unsigned i = 0; i < NELEM(cp437_extra); i++)
if (cp437_extra[i].cp == cp)
return cp437_extra[i].glyph;
return 0;
}
static void
sgr(void)
{
// "ESC[m" with no parameters means ESC[0m -- reset -- and ECMA-48 says so for
// every CSI: an omitted parameter takes its default, and SGR's default is 0.
//
// This loop used to start at a count of zero and therefore do nothing, so a
// bare reset was silently dropped. Programs emit it constantly: fastfetch
// writes "\x1b[32m1%\x1b[m", and with the reset ignored everything after the
// first coloured character stayed green forever. Worse with background
// colours -- its palette swatch sets a white background and resets, so the
// whole console went white-on-white and stayed there, which looks like a
// corrupted framebuffer rather than a missing branch.
int nparm = esc_nparm ? esc_nparm : 1; // esc_parm[0] is already 0
for (int i = 0; i < nparm; i++) {
int p = esc_parm[i];
if (p == 0) {
cons_fg = CONS_DEFAULT_FG;
cons_bg = CONS_DEFAULT_BG;
cons_bold = 0;
} else if (p == 1) {
cons_bold = 1;
// Re-apply brightness to a colour already chosen.
for (int j = 0; j < 8; j++)
if (cons_fg == ansi_rgb[j]) { cons_fg = ansi_rgb[j + 8]; break; }
} else if (p == 22) {
cons_bold = 0;
} else if (p >= 30 && p <= 37) {
cons_fg = ansi_rgb[(p - 30) + (cons_bold ? 8 : 0)];
} else if (p == 39) {
cons_fg = CONS_DEFAULT_FG;
} else if (p >= 40 && p <= 47) {
cons_bg = ansi_rgb[p - 40];
} else if (p == 49) {
cons_bg = CONS_DEFAULT_BG;
} else if (p >= 90 && p <= 97) {
cons_fg = ansi_rgb[(p - 90) + 8];
} else if (p >= 100 && p <= 107) {
cons_bg = ansi_rgb[(p - 100) + 8];
}
}
gfx_set_bg(cons_bg);
}
// One final byte of a CSI sequence, with esc_parm[] already filled in.
static void
csi_dispatch(int final)
{
int col, row;
int n = esc_nparm > 0 ? esc_parm[0] : 0;
int n1 = n ? n : 1; // most sequences default to 1
gfx_cursor_get(&col, &row);
switch (final) {
case 'A': gfx_cursor_set(col, row - n1); break;
case 'B': gfx_cursor_set(col, row + n1); break;
case 'C': gfx_cursor_set(col + n1, row); break;
case 'D': gfx_cursor_set(col - n1, row); break;
case 'E': gfx_cursor_set(0, row + n1); break;
case 'F': gfx_cursor_set(0, row - n1); break;
case 'G': gfx_cursor_set(n1 - 1, row); break;
case 'd': gfx_cursor_set(col, n1 - 1); break;
case 'H': case 'f': {
int r = esc_nparm > 0 && esc_parm[0] ? esc_parm[0] : 1;
int c = esc_nparm > 1 && esc_parm[1] ? esc_parm[1] : 1;
gfx_cursor_set(c - 1, r - 1); // ANSI counts from 1
break;
}
case 'J': gfx_erase_display(n); break;
case 'K': gfx_erase_line(n); break;
case 'm': sgr(); break;
case 's': saved_col = col; saved_row = row; break;
case 'u': gfx_cursor_set(saved_col, saved_row); break;
// Cursor visibility (DECTCEM) and the other private modes. There is no
// hardware cursor being drawn, so there is nothing to show or hide --
// swallowing them silently is the correct behaviour, and far better than
// printing "?25l" into the middle of someone's output.
case 'h': case 'l': break;
default: break; // unknown: drop it, do not print it
}
}
// Feed one output byte to the framebuffer side. Returns nothing; the serial
// side has already had the raw byte.
static void
gfx_feed(int c)
{
switch (esc_state) {
case E_ESC:
if (c == '[') {
esc_state = E_CSI;
esc_nparm = 0;
esc_private = 0;
for (int i = 0; i < 8; i++) esc_parm[i] = 0;
return;
}
if (c == ']') { // OSC: swallow it, see below
esc_state = E_OSC;
return;
}
esc_state = E_NORMAL;
if (c == 'c') { // RIS -- full reset
cons_fg = CONS_DEFAULT_FG;
cons_bg = CONS_DEFAULT_BG;
gfx_screen_reset();
}
return; // other two-byte escapes: ignored
// An operating-system command runs until BEL or until ST (ESC \). Nothing
// here acts on one -- there is no window title to set and no hyperlink to
// follow -- but discarding the payload is not optional: the alternative is
// printing somebody's URL into the middle of their output.
case E_OSC:
if (c == '\a') { esc_state = E_NORMAL; return; }
if (c == '\x1b') { esc_state = E_OSC_ESC; return; }
return;
case E_OSC_ESC:
// ESC \ ends it; ESC anything-else means the OSC was malformed and a new
// escape has started, so hand the byte to the ESC state rather than eating
// it. Dropping it would lose the sequence that follows.
if (c == '\\') { esc_state = E_NORMAL; return; }
esc_state = E_ESC;
gfx_feed(c);
return;
case E_CSI:
if (c == '?') { esc_private = 1; return; }
if (c >= '0' && c <= '9') {
if (esc_nparm == 0) esc_nparm = 1;
if (esc_nparm <= 8)
esc_parm[esc_nparm - 1] = esc_parm[esc_nparm - 1] * 10 + (c - '0');
return;
}
if (c == ';') {
if (esc_nparm < 8) esc_nparm++;
return;
}
esc_state = E_NORMAL;
if (!esc_private)
csi_dispatch(c);
return;
}
// E_NORMAL
if (c == '\x1b') { esc_state = E_ESC; return; }
// UTF-8 continuation. A codepoint the font cannot draw becomes '?' -- the
// one thing it must not become is a *different* character, which is exactly
// what indexing the font by codepoint did.
if (u8_need > 0) {
if ((c & 0xC0) != 0x80) { // malformed: abandon the sequence
u8_need = 0;
gfx_putc('?', cons_fg);
return;
}
u8_cp = (u8_cp << 6) | (c & 0x3F);
if (--u8_need > 0)
return;
int g = glyph_for(u8_cp);
gfx_putc(g ? g : '?', cons_fg);
return;
}
if (c >= 0xF0) { u8_cp = c & 0x07; u8_need = 3; return; }
else if (c >= 0xE0) { u8_cp = c & 0x0F; u8_need = 2; return; }
else if (c >= 0xC0) { u8_cp = c & 0x1F; u8_need = 1; return; }
else if (c >= 0x80) { gfx_putc('?', cons_fg); return; } // stray continuation
gfx_putc(c, cons_fg);
}
// Output goes to the terminal layer now. The escape parser, the palette, the
// cursor and the UTF-8 decoder all moved to kernel/dev/vt.c, because each of
// them is per-terminal and there used to be exactly one of each.
void
consputc(int c)
@ -495,76 +203,92 @@ consputc(int c)
if (c == BACKSPACE) {
uartputc('\b'); uartputc(' '); uartputc('\b');
if (!cons_serial_only)
gfx_putc(c, cons_fg);
vt_putc(vt_current(), '\b');
return;
}
uartputc(c);
if (!cons_serial_only)
gfx_feed(c & 0xff);
vt_feed(vt_current(), c & 0xff);
}
#define INPUT_BUF 256
struct {
struct spinlock lock;
char buf[INPUT_BUF];
uint r;
uint w;
uint e;
uint abort_read;
// Set by EOF (Ctrl-D) on an empty line. Canonical-mode end of input is not
// a character -- it is the *absence* of one -- so it cannot be signalled by
// putting something in the buffer. The old code pushed a literal ^D in and
// had consoleread() pick it back out, which meant a ^D typed mid-line was
// indistinguishable from one typed alone.
uint eof;
} input;
#define C(x) ((x)-'@')
// ---------------------------------------------------------------------------
// The line discipline
// Terminals as devices
//
// TCGETS used to return a hardcoded set of flags and TCSETS threw its argument
// away, so the terminal was permanently in one mode: line-at-a-time, always
// echoing. Anything wanting a keypress at a time -- a pager, an editor, a
// shell doing its own line editing, which is most of what zsh is -- had no way
// to ask for one. This is the real thing: a struct programs can write, that
// changes what the kernel actually does.
// The line discipline below used to operate on one `struct input` and one
// `struct termios`, because there was one terminal. Both live in struct vt
// now (kernel/dev/vt.c) and everything here takes the terminal it is acting
// on as an argument.
//
// One lock still guards every terminal's input rather than one lock each.
// The rings are only touched from the keyboard interrupt and from read(2),
// both of which are short, and a single lock cannot be acquired in the wrong
// order against itself. Per-terminal locks would buy nothing until there is a
// second input source to contend with it.
// ---------------------------------------------------------------------------
static struct termios tio = {
.c_iflag = ICRNL,
.c_oflag = OPOST | ONLCR,
.c_cflag = CS8 | CREAD | CLOCAL,
.c_lflag = ISIG | ICANON | ECHO | ECHOE,
.c_cc = {
[VINTR] = C('C'), [VQUIT] = C('\\'), [VERASE] = '\x7f', [VKILL] = C('U'),
[VEOF] = C('D'), [VSUSP] = C('Z'), [VWERASE] = C('W'), [VMIN] = 1,
},
.c_ispeed = 15, .c_ospeed = 15,
};
static struct spinlock input_lock;
void
cons_get_termios(struct termios *out)
// Which terminal a device minor names.
//
// 1 /dev/console the active terminal, so kernel messages and a
// shell started on the console both follow a switch
// 10 /dev/tty the caller's controlling terminal
// 11..16 /dev/tty1..6 that terminal, whether or not it is on screen
//
// Anything else is the console, which is what an unrecognised minor meant
// before there was more than one terminal to get wrong.
struct vt *
vt_for_minor(int minor)
{
acquire(&input.lock);
*out = tio;
release(&input.lock);
if (minor >= 11 && minor < 11 + NVT)
return vt_get(minor - 11);
if (minor == 10) {
struct proc *p = myproc();
if (p && p->ctty > 0 && p->ctty <= NVT)
return vt_get(p->ctty - 1);
}
return vt_current();
}
// One output byte to a terminal.
//
// The serial line only hears from the terminal that is on screen. Two
// terminals interleaving into one serial log would be unreadable, and the
// serial console is meant to show what the monitor shows.
static void
vt_out(struct vt *v, int c)
{
if (v == vt_current())
uartputc(c);
if (!cons_serial_only)
vt_feed(v, c & 0xff);
}
void
cons_set_termios(const struct termios *in)
cons_get_termios(int minor, struct termios *out)
{
acquire(&input.lock);
tio = *in;
struct vt *v = vt_for_minor(minor);
acquire(&input_lock);
*out = v->tio;
release(&input_lock);
}
void
cons_set_termios(int minor, const struct termios *in)
{
struct vt *v = vt_for_minor(minor);
acquire(&input_lock);
v->tio = *in;
// Switching out of canonical mode has to hand over whatever has been typed
// but not yet submitted, or a shell that turns off ICANON mid-line silently
// eats the characters already in the edit buffer.
if (!(tio.c_lflag & ICANON))
input.w = input.e;
release(&input.lock);
wakeup(&input.r);
if (!(v->tio.c_lflag & ICANON))
v->in.w = v->in.e;
release(&input_lock);
wakeup(&v->in.r);
}
// Discard everything typed but not yet consumed: completed lines waiting in
@ -576,42 +300,77 @@ cons_set_termios(const struct termios *in)
// A pending EOF goes too: end of input is buffer state like any other, and
// leaving it set would end the *next* read instead of the one it was typed at.
void
cons_flush_input(void)
cons_flush_input(int minor)
{
acquire(&input.lock);
input.e = input.w = input.r;
input.eof = 0;
release(&input.lock);
struct vt *v = vt_for_minor(minor);
acquire(&input_lock);
v->in.e = v->in.w = v->in.r;
v->in.eof = 0;
release(&input_lock);
}
// Foreground process group, per terminal. This was one global int for the
// whole machine; tcsetpgrp on tty2 now says nothing about tty1, which is the
// entire point of having more than one.
int
cons_fg_pgrp(int minor)
{
return vt_for_minor(minor)->fg_pgrp;
}
void
cons_set_fg_pgrp(int minor, int pg)
{
vt_for_minor(minor)->fg_pgrp = pg;
}
static void
echo(int c)
echo(struct vt *v, int c)
{
if (c == '\n' || c == '\r') {
if ((tio.c_lflag & ECHO) || (tio.c_lflag & ECHONL))
consputc('\n');
if ((v->tio.c_lflag & ECHO) || (v->tio.c_lflag & ECHONL))
vt_out(v, '\n');
return;
}
if (!(tio.c_lflag & ECHO))
if (!(v->tio.c_lflag & ECHO))
return;
// Control characters echo as ^X, the way every other terminal shows them.
if (c < 32 && c != '\t') {
consputc('^');
consputc(c + '@');
vt_out(v, '^');
vt_out(v, c + '@');
} else {
consputc(c);
vt_out(v, c);
}
}
// Rub out the last character of the current line.
static void
erase_one(void)
erase_one(struct vt *v)
{
if (input.e == input.w)
if (v->in.e == v->in.w)
return;
input.e--;
if (tio.c_lflag & ECHOE)
consputc(BACKSPACE);
v->in.e--;
if (v->tio.c_lflag & ECHOE) {
if (v == vt_current()) { uartputc('\b'); uartputc(' '); uartputc('\b'); }
if (!cons_serial_only)
vt_putc(v, '\b');
}
}
// ---------------------------------------------------------------------------
// Keyboard input
// ---------------------------------------------------------------------------
// Alt+F1..F6 switch terminals. kbd.c reports them as KEY_ALT_F1.. 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)
return 0;
vt_switch(key - KEY_ALT_F1);
return 1;
}
void
@ -619,17 +378,30 @@ consoleintr(int (*getc)(void))
{
int c;
acquire(&input.lock);
acquire(&input_lock);
while((c = getc()) >= 0){
if (c == 0)
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) {
release(&input_lock);
console_vt_hotkey(c);
acquire(&input_lock);
continue;
}
// Input goes to whichever terminal is on screen. That is what makes a
// terminal a terminal: the keyboard follows the eyes.
struct vt *v = vt_current();
// Input mapping, before anything looks at the character.
if (tio.c_iflag & ISTRIP) c &= 0x7f;
if (v->tio.c_iflag & ISTRIP) c &= 0x7f;
if (c == '\r') {
if (tio.c_iflag & IGNCR) continue;
if (tio.c_iflag & ICRNL) c = '\n';
} else if (c == '\n' && (tio.c_iflag & INLCR)) {
if (v->tio.c_iflag & IGNCR) continue;
if (v->tio.c_iflag & ICRNL) c = '\n';
} else if (c == '\n' && (v->tio.c_iflag & INLCR)) {
c = '\r';
}
@ -639,22 +411,22 @@ consoleintr(int (*getc)(void))
// handler is whatever happened to be running on this CPU when the key was
// pressed, which is almost never the program the user is looking at. It
// also set a kill flag rather than raising a signal, so nothing could
// catch it and the shell never learned the job had died. Both wrong;
// both fixed by sending a real signal to the foreground process group,
// which the kernel has been tracking in tty_fg_pgrp all along.
if (tio.c_lflag & ISIG) {
// catch it and the shell never learned the job had died. Both wrong; both
// fixed by sending a real signal to the foreground process group -- which
// is now this terminal's group rather than the machine's only one.
if (v->tio.c_lflag & ISIG) {
int sig = 0;
if (c == tio.c_cc[VINTR]) sig = SIGINT;
else if (c == tio.c_cc[VQUIT]) sig = SIGQUIT;
else if (c == tio.c_cc[VSUSP]) sig = SIGTSTP;
if (c == v->tio.c_cc[VINTR]) sig = SIGINT;
else if (c == v->tio.c_cc[VQUIT]) sig = SIGQUIT;
else if (c == v->tio.c_cc[VSUSP]) sig = SIGTSTP;
if (sig) {
echo(c);
input.e = input.w = input.r; // discard the partial line
input.abort_read = 1;
release(&input.lock);
sigsend_pgrp(tty_fg_pgrp, sig);
acquire(&input.lock);
wakeup(&input.r);
echo(v, c);
v->in.e = v->in.w = v->in.r; // discard the partial line
v->in.abort_read = 1;
release(&input_lock);
sigsend_pgrp(v->fg_pgrp, sig);
acquire(&input_lock);
wakeup(&v->in.r);
continue;
}
}
@ -666,53 +438,53 @@ consoleintr(int (*getc)(void))
continue;
}
if (tio.c_lflag & ICANON) {
if (c == tio.c_cc[VERASE] || c == C('H')) {
erase_one();
if (v->tio.c_lflag & ICANON) {
if (c == v->tio.c_cc[VERASE] || c == C('H')) {
erase_one(v);
continue;
}
if (c == tio.c_cc[VKILL]) {
while (input.e != input.w &&
input.buf[(input.e-1) % INPUT_BUF] != '\n')
erase_one();
if (c == v->tio.c_cc[VKILL]) {
while (v->in.e != v->in.w &&
v->in.buf[(v->in.e-1) % VT_INPUT_BUF] != '\n')
erase_one(v);
continue;
}
if (c == tio.c_cc[VWERASE]) {
while (input.e != input.w &&
input.buf[(input.e-1) % INPUT_BUF] == ' ')
erase_one();
while (input.e != input.w &&
input.buf[(input.e-1) % INPUT_BUF] != ' ')
erase_one();
if (c == v->tio.c_cc[VWERASE]) {
while (v->in.e != v->in.w &&
v->in.buf[(v->in.e-1) % VT_INPUT_BUF] == ' ')
erase_one(v);
while (v->in.e != v->in.w &&
v->in.buf[(v->in.e-1) % VT_INPUT_BUF] != ' ')
erase_one(v);
continue;
}
if (c == tio.c_cc[VEOF]) {
if (input.e == input.w)
input.eof = 1; // empty line: end of input
input.w = input.e; // otherwise just submit what is there
wakeup(&input.r);
if (c == v->tio.c_cc[VEOF]) {
if (v->in.e == v->in.w)
v->in.eof = 1; // empty line: end of input
v->in.w = v->in.e; // otherwise submit what is there
wakeup(&v->in.r);
continue;
}
}
if (input.e - input.r >= INPUT_BUF)
if (v->in.e - v->in.r >= VT_INPUT_BUF)
continue; // buffer full: drop it
// Everything else goes in, including bytes above 127 -- those are UTF-8
// continuation bytes, and the old `c >= 32 && c <= 126` test silently
// refused every non-ASCII character anyone tried to type.
input.buf[input.e++ % INPUT_BUF] = c;
echo(c);
v->in.buf[v->in.e++ % VT_INPUT_BUF] = c;
echo(v, c);
// Canonical mode hands over a line at a time; raw mode hands over
// everything as it arrives.
if (!(tio.c_lflag & ICANON) ||
c == '\n' || input.e == input.r + INPUT_BUF) {
input.w = input.e;
wakeup(&input.r);
if (!(v->tio.c_lflag & ICANON) ||
c == '\n' || v->in.e == v->in.r + VT_INPUT_BUF) {
v->in.w = v->in.e;
wakeup(&v->in.r);
}
}
release(&input.lock);
release(&input_lock);
}
int
@ -720,11 +492,10 @@ consoleread(struct inode *ip, uint64 off, char *dst, int n)
{
uint target;
int c;
static uint32 prng_seed = 0x9A7B3C1D;
static uint32 prng_seed = 0x9A7B3C1D;
if (ip->minor == 2) {
if (ip->minor == 2)
return 0;
}
if (ip->minor == 4) {
for (int i = 0; i < n; i++) {
@ -736,56 +507,59 @@ consoleread(struct inode *ip, uint64 off, char *dst, int n)
return n;
}
struct vt *v = vt_for_minor(ip->minor);
iunlock(ip);
target = n;
acquire(&input.lock);
acquire(&input_lock);
while(n > 0){
while(input.r == input.w){
if (input.eof) {
input.eof = 0;
release(&input.lock);
while(v->in.r == v->in.w){
if (v->in.eof) {
v->in.eof = 0;
release(&input_lock);
ilock(ip);
return 0; // end of input
}
if (myproc()->killed || input.abort_read) {
input.abort_read = 0;
release(&input.lock);
if (myproc()->killed || v->in.abort_read) {
v->in.abort_read = 0;
release(&input_lock);
ilock(ip);
return -1;
}
sleep(&input.r, &input.lock);
sleep(&v->in.r, &input_lock);
}
c = input.buf[input.r++ % INPUT_BUF];
c = v->in.buf[v->in.r++ % VT_INPUT_BUF];
*dst++ = c;
--n;
// Canonical mode returns a line at a time. Raw mode returns whatever has
// arrived: a program that asked for a keypress must not be made to wait
// for a newline that is never coming.
if (tio.c_lflag & ICANON) {
if (v->tio.c_lflag & ICANON) {
if (c == '\n')
break;
} else {
if (target - n >= (uint)(tio.c_cc[VMIN] ? tio.c_cc[VMIN] : 1))
if (target - n >= (uint)(v->tio.c_cc[VMIN] ? v->tio.c_cc[VMIN] : 1))
break;
}
}
release(&input.lock);
release(&input_lock);
ilock(ip);
return target - n;
}
// Nonzero if a completed input line is waiting — poll(2)'s POLLIN answer
// for the console tty. Mirrors the r/w cursors consoleread() blocks on.
// Nonzero if a completed input line is waiting -- poll(2)'s POLLIN answer for
// a terminal. Mirrors the r/w cursors consoleread() blocks on.
int
console_input_ready(void)
console_input_ready(int minor)
{
struct vt *v = vt_for_minor(minor);
int ready;
acquire(&input.lock);
ready = input.r != input.w;
release(&input.lock);
acquire(&input_lock);
ready = v->in.r != v->in.w;
release(&input_lock);
return ready;
}
@ -794,20 +568,50 @@ consolewrite(struct inode *ip, uint64 off, char *buf, int n)
{
int i;
if (ip->minor == 2 || ip->minor == 3 || ip->minor == 4) {
return n;
}
if (ip->minor == 2 || ip->minor == 3 || ip->minor == 4)
return n;
struct vt *v = vt_for_minor(ip->minor);
iunlock(ip);
acquire(&cons.lock);
if (panicked) {
release(&cons.lock);
ilock(ip);
cli();
for(;;) hlt();
}
for(i = 0; i < n; i++)
consputc(buf[i] & 0xff);
vt_out(v, buf[i] & 0xff);
release(&cons.lock);
ilock(ip);
return n;
}
// A descriptor has been opened on a terminal, or the last one closed.
//
// This is the only thing that knows a terminal is in use, which is what
// VT_GETSTATE reports through v_state and what VT_OPENQRY looks for when it
// is asked for a free one. Without it `chvt` says every terminal is idle even
// while two shells are sitting on them.
static void
consoleopen(struct inode *ip)
{
if (ip->minor >= 11 && ip->minor < 11 + NVT)
vt_get(ip->minor - 11)->opened++;
}
static void
consoleclose(struct inode *ip)
{
if (ip->minor >= 11 && ip->minor < 11 + NVT) {
struct vt *v = vt_get(ip->minor - 11);
if (v->opened > 0)
v->opened--;
}
}
// The keyboard handler lives in kbd.c, but the console is what owns the
// keyboard as an input source, so this is where the line gets claimed.
static void
@ -821,12 +625,12 @@ void
consoleinit(void)
{
initlock(&cons.lock, "console");
initlock(&input.lock, "input");
memset(&input, 0, sizeof(input));
initlock(&input_lock, "input");
devsw[CONSOLE].write = consolewrite;
devsw[CONSOLE].read = consoleread;
devsw[CONSOLE].read = consoleread;
devsw[CONSOLE].open = consoleopen;
devsw[CONSOLE].close = consoleclose;
cons.locking = 1;
irq_register(IRQ_KBD, kbd_irq, 0, "ps2-kbd", 0);

View file

@ -1,5 +1,6 @@
#include "types.h"
#include "defs.h"
#include "vt.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
@ -68,6 +69,11 @@ kmain(uint32 fb_addr)
// their handlers are registered.
inputinit();
if (boot_verbose) cprintf("verbose boot: inputinit done\n");
// Before consoleinit, because the console writes to a terminal and there
// has to be one. vt_init reads the framebuffer geometry, so it also has to
// come after whatever attached a scanout.
vt_init();
if (boot_verbose) cprintf("verbose boot: vt_init done\n");
consoleinit();
if (boot_verbose) cprintf("verbose boot: consoleinit done\n");
uartinit();