From 1c1b0c957075b6d74d4a8576af003f72ce770754 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sun, 13 Sep 2026 00:08:00 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2 --- Makefile | 4 +- cmd/chvt/main.c | 93 +++++ docs/Website/docs.js | 1 + docs/architecture/README.md | 4 + docs/architecture/terminals.md | 128 +++++++ etc/schinit/console-shell.job | 8 +- etc/schinit/tty2-shell.job | 25 ++ etc/zsh/zshrc | 5 +- include/defs.h | 13 +- include/gfx.h | 5 + include/kbd.h | 9 + include/posix.h | 15 + include/proc.h | 4 + include/vt.h | 139 +++++++ kernel/core/console.c | 680 ++++++++++++--------------------- kernel/core/main.c | 6 + kernel/dev/cdev.c | 6 + kernel/dev/gfx.c | 49 +++ kernel/dev/kbd.c | 9 + kernel/dev/vt.c | 627 ++++++++++++++++++++++++++++++ kernel/fs/devfs.c | 12 +- kernel/fs/file.c | 2 +- kernel/proc/proc.c | 2 +- kernel/sys/sys_fs.c | 74 +++- 24 files changed, 1461 insertions(+), 459 deletions(-) create mode 100644 cmd/chvt/main.c create mode 100644 docs/architecture/terminals.md create mode 100644 etc/schinit/tty2-shell.job create mode 100644 include/vt.h create mode 100644 kernel/dev/vt.c diff --git a/Makefile b/Makefile index 31d9a52..61fd00e 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ KERN_CORE = $(addprefix $(BUILD)/kernel/core/, \ KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \ bio.o cdev.o device.o disk.o fb.o ide.o input.o irq.o kbd.o mouse.o \ - pci.o text.o ttf.o uart.o gfx.o ramdisk.o procfs.o) + pci.o text.o ttf.o uart.o gfx.o vt.o ramdisk.o procfs.o) # Drivers for hardware that has to be *found* rather than assumed. Anything # discovered by walking a bus belongs here; the fixed-address legacy devices @@ -107,7 +107,7 @@ KERN_NOENTRY = $(filter-out $(BUILD)/kernel/arch/x86_64/entry.o, $(KERNOBJS)) # Third-party software does NOT go here — see ports/. # ----------------------------------------------------------------------- ULIB = $(addprefix $(BUILD)/lib/, start.o glue.o usys.o) -CMDS = init schinit fbinfo shmtest socktest inputtest nettest reflink schattr ping +CMDS = init schinit fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS)) # ----------------------------------------------------------------------- diff --git a/cmd/chvt/main.c b/cmd/chvt/main.c new file mode 100644 index 0000000..83683bc --- /dev/null +++ b/cmd/chvt/main.c @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// 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 +// 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 + +#include "types.h" +#include "user.h" +#include "fcntl.h" +#include "posix.h" + +// No printf in lib/, and this needs one string and two small numbers. +static void +puts(const char *s) +{ + int n = 0; + while (s[n]) n++; + write(1, (void *)s, n); +} + +static void +putn(int v) +{ + char b[12]; + int i = 0; + if (v < 0) { write(1, "-", 1); v = -v; } + if (v == 0) { write(1, "0", 1); return; } + while (v > 0) { b[i++] = '0' + (v % 10); v /= 10; } + while (i-- > 0) write(1, &b[i], 1); +} + +static int +num(const char *s) +{ + int v = 0; + while (*s >= '0' && *s <= '9') + v = v * 10 + (*s++ - '0'); + return v; +} + +int +main(int argc, char *argv[]) +{ + // /dev/console rather than fd 0: the caller may have redirected its input, + // and the terminal being asked about is the machine's, not whichever one + // this process happens to be reading from. + int fd = open("/dev/console", O_RDWR); + if (fd < 0) { + puts("chvt: cannot open /dev/console\n"); + exit(); + } + + if (argc == 1) { + struct vt_stat st; + if (ioctl(fd, VT_GETSTATE, &st) < 0) { + puts("chvt: VT_GETSTATE failed\n"); + exit(); + } + puts("active: tty"); + putn(st.v_active); + puts("\nin use:"); + for (int i = 1; i <= 16; i++) + if (st.v_state & (1 << i)) { + puts(" tty"); + putn(i); + } + puts("\n"); + exit(); + } + + int n = num(argv[1]); + if (n < 1) { + puts("usage: chvt N\n"); + exit(); + } + + // The argument goes by value, not through a pointer -- Linux defines + // VT_ACTIVATE that way and so does SchOS. Passing a pointer here would + // switch to whatever the address happened to be. + if (ioctl(fd, VT_ACTIVATE, (void *)(long)n) < 0) { + puts("chvt: no terminal "); + putn(n); + puts("\n"); + exit(); + } + exit(); +} diff --git a/docs/Website/docs.js b/docs/Website/docs.js index 10061d8..a793acc 100644 --- a/docs/Website/docs.js +++ b/docs/Website/docs.js @@ -29,6 +29,7 @@ ["architecture/syscalls.md", "Syscalls"], ["architecture/filesystem.md", "The VFS"], ["architecture/schfs.md", "SchFS"], + ["architecture/terminals.md", "Virtual terminals"], ["architecture/devices.md", "Device model"], ["architecture/usb.md", "USB"], ["architecture/graphics.md", "Graphics"], diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 2d63ded..d014ead 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -18,6 +18,10 @@ 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+F1..F6 -- +each with its own screen, line discipline and foreground process group. See +[terminals.md](terminals.md). + --- ## The stack, bottom to top diff --git a/docs/architecture/terminals.md b/docs/architecture/terminals.md new file mode 100644 index 0000000..f0ab541 --- /dev/null +++ b/docs/architecture/terminals.md @@ -0,0 +1,128 @@ + +# Virtual terminals + +Six of them, `/dev/tty1` through `/dev/tty6`, reached with **Alt+F1..Alt+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. + +Alt+F7 is deliberately unused. That is where a display server goes. + +--- + +## What makes switching possible + +A backing store, and nothing else. + +Before this, the console drew straight into the framebuffer and scrolled by +moving pixels. That works exactly as long as nothing else ever wants the +screen: there was nowhere to put terminal 2 while terminal 1 was visible, and +nothing to repaint terminal 1 from when it came back. + +So each terminal owns a grid of cells and the framebuffer shows whichever grid +is active. `vt_switch()` is three lines — set the active index, repaint from +the grid, return — because nothing has to be *saved* on the way out. The +outgoing terminal's grid has been current all along. + +The rule the whole 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 of it +unconditionally. Drawing is a separate question, asked once, at the bottom of +`kernel/dev/vt.c`, answered by comparing against `vt_active`. Every alternative +spreads "am I on screen" through the escape parser, and then a terminal +switched away from mid-sequence comes back wrong. + +## A cell + +Four bytes: + +| | | +|---|---| +| `ch` | the glyph the font draws — **not** a codepoint | +| `fg`, `bg` | palette indices, 0-15 ANSI plus two theme defaults | +| `attr` | reverse video; room for underline and the rest | + +`ch` stores the glyph rather than the codepoint because the console maps UTF-8 +to CP437 before it gets here, and that is what `include/font.h` contains. +Storing the codepoint would mean re-running the mapping on every repaint and +getting a different answer for anything the font cannot draw. + +The colours are indices rather than RGB because a cell holding two 32-bit +colours would be twelve bytes instead of four, and the grids are static: six +terminals at 240x75 is 432 KB of BSS, paid once at link time rather than failed +to pay at run time. There is no contiguous multi-page allocator to ask. + +## What is per-terminal + +Everything that used to be a single global: + +| Was | Now | +|---|---| +| `int tty_fg_pgrp = 1;` | `v->fg_pgrp`, one per terminal | +| one `struct termios tio` | `v->tio` | +| one 256-byte input ring | `v->in` | +| one escape-parser state | `v->esc_*` | +| one UTF-8 decoder | `v->u8_*` | +| one cursor, in pixels, in `gfx.c` | `v->cx`, `v->cy`, in cells | + +`TIOCSCTTY` used to be `return 0` — it claimed to make a terminal the caller's +controlling terminal and did nothing, because there was one terminal and +nothing to choose between. It records `proc->ctty` now, which is what +`/dev/tty` resolves through. + +## The devices + +| Path | Minor | What | +|---|---|---| +| `/dev/console` | 1 | the **active** terminal, so kernel messages follow a switch | +| `/dev/tty` | 10 | the caller's **controlling** terminal, via `proc->ctty` | +| `/dev/tty1`..`tty6` | 11-16 | that terminal, whether or not it is on screen | + +`/dev/tty` was an alias for `/dev/console` with the same major and minor. They +are different questions and now have different answers. + +## The ioctls + +Linux's numbers, because they do Linux's job. + +| | | +|---|---| +| `VT_ACTIVATE` | switch. Argument is a 1-based number **by value**, not a pointer | +| `VT_WAITACTIVE` | returns immediately: `vt_switch` repaints synchronously, so there is nothing left to wait for | +| `VT_GETSTATE` | active terminal and a bitmask of those in use | +| `VT_OPENQRY` | the first terminal nobody has open | +| `VT_GETMODE`/`VT_SETMODE` | accepted and ignored — the kernel drives switching and there is no process mode to set | + +## The keyboard + +`kbd.c` recognises Alt+F1..F6 and returns `KEY_ALT_F1 + 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. + +What the chord *means* is decided in `console.c`, where the terminals are. The +keyboard driver names it and stops. + +Input goes to whichever terminal is on screen. That is what makes a terminal a +terminal: the keyboard follows the eyes. + +## What it does not do + +- **No scrollback.** The grid is exactly the visible screen. Shift+PageUp does + nothing because there is nothing above the top row to show. +- **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 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 + and the remainder stays background. Raising `VT_MAXCOLS`/`VT_MAXROWS` is the + fix, and costs BSS. +- **No UTF-8 the font lacks.** CP437 has ~256 glyphs. Anything else draws as + `?`, which is deliberate: drawing a *different* character would be worse. diff --git a/etc/schinit/console-shell.job b/etc/schinit/console-shell.job index 8b8a4e6..04f15bf 100644 --- a/etc/schinit/console-shell.job +++ b/etc/schinit/console-shell.job @@ -1,4 +1,4 @@ -# The interactive shell on the system console. +# The interactive shell on /dev/tty1, the first virtual terminal. Label = os.schos.console-shell # zsh, not /bin/sh. mksh stays as /bin/sh and is what schinit falls back to @@ -23,9 +23,9 @@ Environment = SHELL=/bin/zsh # terminal input at all. Without this the shell echoes and executes nothing. ControllingTerminal = yes -StandardIn = /dev/console -StandardOut = /dev/console -StandardError = /dev/console +StandardIn = /dev/tty1 +StandardOut = /dev/tty1 +StandardError = /dev/tty1 # Seconds between respawns. Guards against a shell that dies instantly # defaults to 10. One is enough for a shell someone just exited on purpose. diff --git a/etc/schinit/tty2-shell.job b/etc/schinit/tty2-shell.job new file mode 100644 index 0000000..e7127a2 --- /dev/null +++ b/etc/schinit/tty2-shell.job @@ -0,0 +1,25 @@ +# A second interactive shell, on /dev/tty2. +# +# This is what virtual terminals are for. Alt+F2 shows it; Alt+F1 comes back. +# The two shells are unrelated processes with their own sessions, their own +# foreground process groups and their own line discipline -- which is exactly +# what /dev/tty1 and /dev/tty2 being different terminals means. + +Label = os.schos.tty2-shell +Program = /bin/zsh +Args = -zsh +RunAtLoad = yes +KeepAlive = yes + +Environment = TERM=linux +Environment = PATH=/bin:/usr/bin +Environment = HOME=/root +Environment = SHELL=/bin/zsh + +ControllingTerminal = yes + +StandardIn = /dev/tty2 +StandardOut = /dev/tty2 +StandardError = /dev/tty2 + +ThrottleInterval = 1 diff --git a/etc/zsh/zshrc b/etc/zsh/zshrc index 1ec8e7e..0c40795 100644 --- a/etc/zsh/zshrc +++ b/etc/zsh/zshrc @@ -109,5 +109,8 @@ zstyle ':completion:*:descriptions' format '%F{244}%d%f' # the only thing here that ever turns red -- same rule as the rest of the # system: normal states are calm, only failure is loud. setopt PROMPT_SUBST -PROMPT='%F{magenta}%B%n@schos%b%f %F{cyan}%~%f %(?.%F{green}❯.%F{red}❯)%f ' +# The chevron is », not ❯. The console font is CP437 and has a glyph for +# the first and none for the second, so ❯ renders as '?' -- correctly, since +# drawing a *different* character would be worse, but it still looks broken. +PROMPT='%F{magenta}%B%n@schos%b%f %F{cyan}%~%f %(?.%F{green}».%F{red}»)%f ' RPROMPT='' diff --git a/include/defs.h b/include/defs.h index 1ffc33c..3b86adc 100644 --- a/include/defs.h +++ b/include/defs.h @@ -35,13 +35,16 @@ int bflush(uint dev); // console.c struct termios; void consoleinit(void); -extern int tty_fg_pgrp; // The terminal's line-discipline settings. TCGETS/TCSETS in sys_fs.c are the // only callers; the console honours ICANON, ECHO, ECHOE, ECHONL, ISIG, // ICRNL, INLCR, IGNCR, ISTRIP and VMIN. -void cons_get_termios(struct termios *); -void cons_set_termios(const struct termios *); -void cons_flush_input(void); +void cons_get_termios(int minor, struct termios *); +void cons_set_termios(int minor, const struct termios *); +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); +struct vt *vt_for_minor(int minor); extern uint lapic_timer_hz; // measured, kernel/arch/x86_64/lapic.c extern uint64 tsc_hz; void cprintf(char*, ...); @@ -619,7 +622,7 @@ pml4e_t* copyuvm(pml4e_t*, uint64); void switchuvm(struct proc*); void switchkvm(void); int pipe_poll(struct pipe*, int readable, int writable); -int console_input_ready(void); +int console_input_ready(int minor); int copyin(pml4e_t*, addr_t, void*, uint64); int copyout(pml4e_t*, addr_t, void*, uint64); void vm_unmap_range(pml4e_t*, uint64, uint64); diff --git a/include/gfx.h b/include/gfx.h index 75c9ebb..c0f17cf 100644 --- a/include/gfx.h +++ b/include/gfx.h @@ -72,6 +72,11 @@ void gfx_set_bg(uint32 color); void gfx_screen_reset(void); int gfx_cols(void); int gfx_rows(void); + +// For the text layer in kernel/dev/vt.c: one cell with its own colours, and +// a screen scroll that leaves the cursor alone. +void gfx_draw_cell(int col, int row, uchar glyph, uint32 fg, uint32 bg); +void gfx_scroll_up(uint32 bg); void gfx_cursor_get(int *col, int *row); void gfx_cursor_set(int col, int row); void gfx_erase_display(int mode); diff --git a/include/kbd.h b/include/kbd.h index babbd6e..a8530f2 100644 --- a/include/kbd.h +++ b/include/kbd.h @@ -28,6 +28,15 @@ #define KEY_INS 0xE8 #define KEY_DEL 0xE9 +// Alt+F1..Alt+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 of those would be +// typed into the terminal instead of switching away from it. +#define KEY_ALT_F1 0x120 +#define SCAN_F1 0x3B // F1..F6 are 0x3B..0x40 + // C('A') == Control-A #define C(x) (x - '@') diff --git a/include/posix.h b/include/posix.h index 7a2be39..ace0af7 100644 --- a/include/posix.h +++ b/include/posix.h @@ -123,6 +123,21 @@ struct rlimit { #define TIOCGWINSZ 0x5413 #define TIOCSWINSZ 0x5414 #define TIOCSCTTY 0x540E + +// Virtual terminal control, Linux's numbers. The argument to VT_ACTIVATE and +// VT_WAITACTIVE is a 1-based terminal number passed by value, not a pointer. +#define VT_OPENQRY 0x5600 // first free terminal, out through a pointer +#define VT_GETMODE 0x5601 +#define VT_SETMODE 0x5602 +#define VT_GETSTATE 0x5603 // struct vt_stat +#define VT_ACTIVATE 0x5606 +#define VT_WAITACTIVE 0x5607 + +struct vt_stat { + uint16 v_active; // active terminal, 1-based + uint16 v_signal; // unused; Linux keeps it for VT_SETMODE + uint16 v_state; // bitmask of terminals in use, bit 1 = tty1 +}; #define TIOCGSERIAL 0x541E #define FIONREAD 0x541B // FICLONE: share another file's data blocks with this one. The argument is a diff --git a/include/proc.h b/include/proc.h index de4ef40..e26b5ee 100644 --- a/include/proc.h +++ b/include/proc.h @@ -245,6 +245,10 @@ struct proc { // which is why mksh's job control never made progress. int pgid; int sid; + // Controlling terminal, 1-based (1 = /dev/tty1), 0 = none. TIOCSCTTY sets + // it; /dev/tty resolves through it. Was impossible to have before there was + // more than one terminal to choose between. + int ctty; // Real/effective/saved user and group ids. Nothing enforces permissions with // them yet (the on-disk format has no owner field until SchFS), but they diff --git a/include/vt.h b/include/vt.h new file mode 100644 index 0000000..31ca3c6 --- /dev/null +++ b/include/vt.h @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Virtual terminals. +// +// Until this existed there was one console. `/dev/tty` was an alias for +// `/dev/console` with the same major and minor, `tty_fg_pgrp` was a single +// global int, and TIOCSCTTY was `return 0`. One screen, one foreground +// process group, one line discipline, no way to have a second shell. +// +// The thing that makes switching 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 ever wants the screen: there was +// nowhere to put terminal 2 while terminal 1 was visible, and nothing to +// repaint terminal 1 from when it came back. So each VT owns a grid of cells +// and the framebuffer shows whichever grid is active. +// +// A VT that is not active is not a special case. Writes to it land in its +// grid, its cursor moves, it scrolls; the only thing it does not do is draw. +// That is the whole switching mechanism: vt_switch repaints, and everything +// else is written as though it were always on screen. + +#ifndef VT_H +#define VT_H + +#include "types.h" +#include "param.h" +#include "posix.h" + +// Six, and Linux's numbering: /dev/tty1 through /dev/tty6, reached with +// Alt+F1..Alt+F6. Alt+F7 is deliberately left alone -- that is where a +// display server goes, and Schcom will want it. +#define NVT 6 + +// A cell. +// +// `ch` is the glyph the font draws, not a codepoint: the console maps UTF-8 +// to CP437 before it gets here, because that is what include/font.h contains. +// Storing the codepoint would mean re-running that mapping on every repaint +// and getting a different answer for anything the font cannot draw. +// +// The colours are palette indices rather than RGB. Sixteen ANSI colours plus +// the two theme defaults fit in a byte each and make a cell four bytes; the +// same cell holding two 32-bit colours would be twelve, and the grids are +// static. +struct vtcell { + uint8 ch; + uint8 fg; + uint8 bg; + uint8 attr; // reserved: bold/underline/reverse, which do not change +}; // the glyph and so can be added without touching a grid + +#define VT_FG_DEFAULT 16 // SCH_FG from include/theme.h +#define VT_BG_DEFAULT 17 // SCH_BG +#define VT_NCOLOUR 18 + +// Cell attributes that do not change which glyph is drawn, only how. +#define VT_ATTR_REVERSE 0x01 + +// The largest console a grid can hold, which is 1920x1200 with the 8x16 font. +// A framebuffer wider or taller than this still works -- vt_init clamps the +// grid and the rest of the screen stays background -- but it is worth raising +// rather than living with, because the clamp is invisible until someone +// notices the bottom of their screen never has text on it. +#define VT_MAXCOLS 240 +#define VT_MAXROWS 75 + +#define VT_INPUT_BUF 256 +#define VT_MAXPARM 8 + +// Escape parser states. E_OSC and E_OSC_ESC exist for hyperlinks: a terminal +// that does not follow OSC 8 must still consume it, or the URL is printed as +// text. See the comment in kernel/core/console.c that this came from. +enum { VE_NORMAL, VE_ESC, VE_CSI, VE_OSC, VE_OSC_ESC }; + +struct vt { + int index; // 0-based; /dev/tty + int cols, rows; + + struct vtcell *cells; // cols*rows, row-major, into vt_storage + + int cx, cy; // cursor, in cells + int saved_cx, saved_cy; // DECSC/DECRC + + uint8 fg, bg; // current SGR colours + int bold; + int attr; // VT_ATTR_* applied to new cells + + // Escape parser. + int esc_state; + int esc_parm[VT_MAXPARM]; + int esc_nparm; + int esc_private; + + // UTF-8 decoder. + uint32 u8_cp; + int u8_need; + + // Line discipline. One per terminal, because ICANON and ECHO are properties + // of a terminal and not of the machine. + struct termios tio; + + struct { + char buf[VT_INPUT_BUF]; + uint r, w, e; + uint abort_read; + uint eof; + } in; + + // Job control. This was one global for the whole system; a terminal each + // is the entire point of having more than one. + int fg_pgrp; + int sid; // session that owns this terminal, 0 = none + + int opened; // times it has been opened, for /dev/tty +}; + +// The active terminal, and the one the framebuffer is showing. +extern int vt_active; + +void vt_init(void); +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 + +// 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 +void vt_putc(struct vt *v, int glyph); // one glyph, already mapped +void vt_cursor_set(struct vt *v, int col, int row); +void vt_cursor_get(struct vt *v, int *col, int *row); +void vt_erase_display(struct vt *v, int mode); +void vt_erase_line(struct vt *v, int mode); +void vt_clear(struct vt *v); +void vt_scroll(struct vt *v); +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 + +#endif // VT_H diff --git a/kernel/core/console.c b/kernel/core/console.c index 100c48b..a0bfa69 100644 --- a/kernel/core/console.c +++ b/kernel/core/console.c @@ -1,6 +1,8 @@ #include #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); diff --git a/kernel/core/main.c b/kernel/core/main.c index 01ee806..70b87ba 100644 --- a/kernel/core/main.c +++ b/kernel/core/main.c @@ -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(); diff --git a/kernel/dev/cdev.c b/kernel/dev/cdev.c index 2071a91..dd5233d 100644 --- a/kernel/dev/cdev.c +++ b/kernel/dev/cdev.c @@ -64,6 +64,10 @@ memread(struct inode *ip, uint64 off, char *dst, int n) case 1: return consoleread(ip, off, dst, n); case 2: return nullread(ip, off, dst, n); case 3: return consoleread(ip, off, dst, n); + /* 10 = /dev/tty, 11..16 = /dev/tty1..tty6. consoleread resolves the + minor to a terminal; see vt_for_minor(). */ + case 10: case 11: case 12: case 13: case 14: case 15: case 16: + return consoleread(ip, off, dst, n); case 5: return zeroread(ip, off, dst, n); case 7: return zeroread(ip, off, dst, n); // /dev/full reads as zeros case 8: @@ -77,6 +81,8 @@ memwrite(struct inode *ip, uint64 off, char *dst, int n) { switch(ip->minor) { case 1: return consolewrite(ip, off, dst, n); + case 10: case 11: case 12: case 13: case 14: case 15: case 16: + return consolewrite(ip, off, dst, n); case 7: return fullwrite(ip, off, dst, n); // /dev/full: always ENOSPC case 2: case 5: diff --git a/kernel/dev/gfx.c b/kernel/dev/gfx.c index d42be91..71853f3 100644 --- a/kernel/dev/gfx.c +++ b/kernel/dev/gfx.c @@ -202,6 +202,55 @@ gfx_putc(int c, uint32 color) } } +// --- primitives for the text layer ---------------------------------------- +// +// kernel/dev/vt.c owns a grid of cells and needs exactly two things the +// console-cursor interface above cannot give it: draw one cell at a grid +// position with its *own* background, and scroll the whole screen without +// consulting a cursor. Everything else a terminal does is bookkeeping on the +// grid, which is not this file's business. + +// One cell, addressed in cells rather than pixels, with its own colours. +// gfx_draw_char paints its background from the file-static gfx_bg, which is +// right for a console that has one background and wrong for a terminal where +// every cell carries its own. +void +gfx_draw_cell(int col, int row, uchar glyph, uint32 fg, uint32 bg) +{ + if (!fb) + return; + + int x = col * CHAR_WIDTH, y = row * CHAR_HEIGHT; + if (x < 0 || y < 0 || + x + CHAR_WIDTH > (int)scanout.width || y + CHAR_HEIGHT > (int)scanout.height) + return; + + for (int r = 0; r < CHAR_HEIGHT; r++) { + uchar bits = font_data[glyph][r]; + for (int c = 0; c < CHAR_WIDTH; c++) + gfx_draw_pixel(x + c, y + r, (bits & (0x80 >> c)) ? fg : bg); + } +} + +// Move the whole screen up one text row and clear the row it exposes. The +// cursor is not touched: the caller tracks it in cells and knows where it +// wants to be, which is not necessarily where a pixel-scroll would leave it. +void +gfx_scroll_up(uint32 bg) +{ + if (!fb || scanout.height < CHAR_HEIGHT) + return; + + uint32 line_size = scanout.stride * CHAR_HEIGHT; + uint32 total_size = scanout.stride * scanout.height; + + memmove((void*)fb, (void*)(fb + line_size), total_size - line_size); + + for (int y = (int)scanout.height - CHAR_HEIGHT; y < (int)scanout.height; y++) + for (int x = 0; x < (int)scanout.width; x++) + gfx_draw_pixel(x, y, bg); +} + void gfx_screen_reset(void) { diff --git a/kernel/dev/kbd.c b/kernel/dev/kbd.c index 2808870..037709d 100644 --- a/kernel/dev/kbd.c +++ b/kernel/dev/kbd.c @@ -2,6 +2,7 @@ #include "x86.h" #include "defs.h" #include "kbd.h" +#include "vt.h" /* NVT -- how many terminals the chords reach */ #include "input.h" int @@ -58,6 +59,14 @@ kbdgetc(void) shift |= shiftcode[data]; shift ^= togglecode[data]; + + // Alt+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(). + if ((shift & ALT) && data >= SCAN_F1 && data < SCAN_F1 + NVT) + return KEY_ALT_F1 + (data - SCAN_F1); + c = charcode[shift & (CTL | SHIFT)][data]; if(shift & CAPSLOCK){ if('a' <= c && c <= 'z') diff --git a/kernel/dev/vt.c b/kernel/dev/vt.c new file mode 100644 index 0000000..7528c83 --- /dev/null +++ b/kernel/dev/vt.c @@ -0,0 +1,627 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Virtual terminals. See include/vt.h for why this exists at all; the short +// version is that switching needs a backing store, and the console drew +// straight into the framebuffer. +// +// The rule this file is built on: **a terminal does not know whether it is +// visible.** Writes land in the grid, the cursor moves, the grid scrolls -- +// all of it unconditionally. Drawing is a separate question asked once, at +// the bottom, and answered by comparing against vt_active. Every alternative +// spreads "am I on screen" through the escape parser, and then a terminal +// that was switched away mid-sequence comes back wrong. + +#include "types.h" +#include "defs.h" +#include "param.h" +#include "spinlock.h" +#include "sleeplock.h" +#include "posix.h" +#include "theme.h" +#include "gfx.h" +#include "vt.h" + +int vt_active = 0; + +// The grids. Static rather than allocated: there is no contiguous multi-page +// allocator, a grid is 72 KB, and six of them are a fixed cost the kernel can +// pay once at link time instead of failing to pay at run time. +static struct vtcell vt_storage[NVT][VT_MAXCOLS * VT_MAXROWS]; +static struct vt vts[NVT]; +static int vt_ready; + +// The sixteen ANSI colours, then the two theme defaults. Moved here from +// console.c, where they were the only palette because there was only one +// terminal to paint. +static const uint32 palette[VT_NCOLOUR] = { + 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, + SCH_FG, // VT_FG_DEFAULT + SCH_BG, // VT_BG_DEFAULT +}; + +uint32 +vt_rgb(uint8 idx) +{ + return palette[idx < VT_NCOLOUR ? idx : VT_FG_DEFAULT]; +} + +static struct vtcell * +cell_at(struct vt *v, int col, int row) +{ + return &v->cells[row * v->cols + col]; +} + +static int +is_active(struct vt *v) +{ + return vt_ready && v->index == vt_active; +} + +// Paint one cell from the grid. The single place that turns stored state into +// pixels, so anything that changes a cell can simply call this and not think +// about whether the terminal is visible. +static void +show(struct vt *v, int col, int row) +{ + if (!is_active(v)) + return; + struct vtcell *k = cell_at(v, col, row); + // Reverse video swaps the two colours at draw time rather than at store + // time, so turning it off restores what was underneath instead of leaving + // the swapped pair behind. + if (k->attr & VT_ATTR_REVERSE) + gfx_draw_cell(col, row, k->ch, vt_rgb(k->bg), vt_rgb(k->fg)); + else + gfx_draw_cell(col, row, k->ch, vt_rgb(k->fg), vt_rgb(k->bg)); +} + +static void +blank_row(struct vt *v, int row) +{ + for (int c = 0; c < v->cols; c++) { + struct vtcell *k = cell_at(v, c, row); + k->ch = ' '; + k->fg = v->fg; + k->bg = v->bg; + k->attr = 0; + } +} + +// --------------------------------------------------------------------------- +// Setup +// --------------------------------------------------------------------------- + +// The termios every terminal starts with. This was a single file-static in +// console.c called `tio`; it is per-terminal now because ICANON and ECHO +// describe a terminal, and a program that turns off echo on tty2 has said +// nothing whatever about tty1. +static void +termios_defaults(struct termios *t) +{ + memset(t, 0, sizeof(*t)); + t->c_iflag = ICRNL; + t->c_oflag = OPOST | ONLCR; + t->c_cflag = CS8 | CREAD; + t->c_lflag = ISIG | ICANON | ECHO | ECHOE; + t->c_cc[VINTR] = 'C' & 0x1f; + t->c_cc[VQUIT] = '\\' & 0x1f; + t->c_cc[VERASE] = '\x7f'; + t->c_cc[VKILL] = 'U' & 0x1f; + t->c_cc[VEOF] = 'D' & 0x1f; + t->c_cc[VSUSP] = 'Z' & 0x1f; + t->c_cc[VWERASE]= 'W' & 0x1f; + t->c_cc[VMIN] = 1; + t->c_ispeed = 15; + t->c_ospeed = 15; +} + +void +vt_init(void) +{ + int cols = gfx_cols(), rows = gfx_rows(); + + // No framebuffer (serial-only boot): 80x24 is a stated default rather than + // a measurement of nothing. The grids still work and still scroll; nobody + // is looking at them. + if (cols <= 0) cols = 80; + if (rows <= 0) rows = 24; + if (cols > VT_MAXCOLS) cols = VT_MAXCOLS; + if (rows > VT_MAXROWS) rows = VT_MAXROWS; + + for (int i = 0; i < NVT; i++) { + struct vt *v = &vts[i]; + memset(v, 0, sizeof(*v)); + v->index = i; + v->cols = cols; + v->rows = rows; + v->cells = vt_storage[i]; + v->fg = VT_FG_DEFAULT; + v->bg = VT_BG_DEFAULT; + v->esc_state = VE_NORMAL; + // pgid 1, which is init's, so the first shell on each terminal already + // sees itself as the foreground job rather than stopping with SIGTTIN. + v->fg_pgrp = 1; + termios_defaults(&v->tio); + for (int r = 0; r < rows; r++) + blank_row(v, r); + } + + vt_active = 0; + vt_ready = 1; +} + +struct vt * +vt_get(int i) +{ + if (i < 0) i = 0; + if (i >= NVT) i = NVT - 1; + return &vts[i]; +} + +struct vt * +vt_current(void) +{ + return &vts[vt_active]; +} + +// --------------------------------------------------------------------------- +// Output +// --------------------------------------------------------------------------- + +void +vt_repaint(struct vt *v) +{ + if (!is_active(v)) + return; + for (int r = 0; r < v->rows; r++) + for (int c = 0; c < v->cols; c++) { + struct vtcell *k = cell_at(v, c, r); + if (k->attr & VT_ATTR_REVERSE) + gfx_draw_cell(c, r, k->ch, vt_rgb(k->bg), vt_rgb(k->fg)); + else + gfx_draw_cell(c, r, k->ch, vt_rgb(k->fg), vt_rgb(k->bg)); + } +} + +void +vt_scroll(struct vt *v) +{ + // The grid moves whether or not anyone is watching. If they are, the + // framebuffer is scrolled with a memmove rather than repainted cell by + // cell -- a full repaint of a 160x50 screen is a million pixel writes, and + // a shell printing a long file would spend all its time in this function. + memmove(v->cells, cell_at(v, 0, 1), + (uint64)(v->rows - 1) * v->cols * sizeof(struct vtcell)); + blank_row(v, v->rows - 1); + + if (is_active(v)) { + gfx_scroll_up(vt_rgb(v->bg)); + // The exposed row was blanked in v->bg; gfx_scroll_up painted it the same + // colour, so there is nothing further to draw. + } + + if (v->cy > 0) + v->cy--; +} + +void +vt_cursor_set(struct vt *v, int col, int row) +{ + // Clamped rather than validated: an escape sequence is untrusted input from + // any program that can write to the terminal, and "CUP to row 9999" should + // land on the last row rather than off the end of the grid. + if (col < 0) col = 0; + if (row < 0) row = 0; + if (col >= v->cols) col = v->cols - 1; + if (row >= v->rows) row = v->rows - 1; + v->cx = col; + v->cy = row; +} + +void +vt_cursor_get(struct vt *v, int *col, int *row) +{ + if (col) *col = v->cx; + if (row) *row = v->cy; +} + +static void +put_blank_at(struct vt *v, int col, int row) +{ + struct vtcell *k = cell_at(v, col, row); + k->ch = ' '; + k->fg = v->fg; + k->bg = v->bg; + k->attr = 0; + show(v, col, row); +} + +void +vt_putc(struct vt *v, int glyph) +{ + switch (glyph) { + case '\n': + v->cx = 0; + v->cy++; + break; + + case '\r': + v->cx = 0; + break; + + case '\b': + case 0x7f: + if (v->cx > 0) { + v->cx--; + put_blank_at(v, v->cx, v->cy); + } + break; + + case '\t': + // To the next multiple of eight, blanking as it goes so a tab overwrites + // rather than skipping over stale characters. Without a case here '\t' + // reaches the font as glyph 9, which in CP437 is a hollow circle: pressing + // Tab printed a circle. + do { + put_blank_at(v, v->cx, v->cy); + v->cx++; + } while (v->cx % 8 != 0 && v->cx < v->cols); + break; + + default: { + // A C0 control character that got this far has no handled meaning, and a + // terminal never *draws* one. The font has a picture for every byte below + // 0x20 and none of those pictures is what the sender meant: zsh emits + // 0x0F to leave alt-charset mode, because that is what the `linux` + // terminfo says rmacs is, and CP437 glyph 0x0F is a sun. It appeared in + // the prompt on every line. + if (glyph < 0x20) + return; + if (glyph < 0 || glyph > 255) + glyph = '?'; + struct vtcell *k = cell_at(v, v->cx, v->cy); + k->ch = (uint8)glyph; + k->fg = v->fg; + k->bg = v->bg; + k->attr = (uint8)v->attr; + show(v, v->cx, v->cy); + v->cx++; + break; + } + } + + if (v->cx >= v->cols) { + v->cx = 0; + v->cy++; + } + while (v->cy >= v->rows) + vt_scroll(v); +} + +// Fill a run of cells with the current background, wrapping at the right edge. +static void +erase_run(struct vt *v, int col, int row, int count) +{ + while (count-- > 0 && row < v->rows) { + put_blank_at(v, col, row); + if (++col >= v->cols) { col = 0; row++; } + } +} + +// ED -- erase in display. 0: cursor to end, 1: start to cursor, 2: all. +void +vt_erase_display(struct vt *v, int mode) +{ + switch (mode) { + case 0: erase_run(v, v->cx, v->cy, (v->rows - v->cy) * v->cols - v->cx); break; + case 1: erase_run(v, 0, 0, v->cy * v->cols + v->cx + 1); break; + default: vt_clear(v); break; + } +} + +// EL -- erase in line. The same three modes, bounded to the current row. +void +vt_erase_line(struct vt *v, int mode) +{ + switch (mode) { + case 0: erase_run(v, v->cx, v->cy, v->cols - v->cx); break; + case 1: erase_run(v, 0, v->cy, v->cx + 1); break; + default: erase_run(v, 0, v->cy, v->cols); break; + } +} + +void +vt_clear(struct vt *v) +{ + for (int r = 0; r < v->rows; r++) + blank_row(v, r); + v->cx = v->cy = 0; + vt_repaint(v); +} + + +// --------------------------------------------------------------------------- +// The escape parser +// +// Moved here from console.c along with the state it needs. It was a set of +// file-statics there -- one parser for one screen -- and a parser is per +// terminal for the same reason a cursor is: a program halfway through a CSI +// sequence on tty2 has said nothing about tty1. +// +// The serial line does NOT go through any of this. On the other end of it is +// a real terminal emulator that understands ANSI far better than this ever +// will, so escapes are passed to it verbatim and interpreted only for the +// framebuffer. Two consumers, two right answers. +// --------------------------------------------------------------------------- + +// --------------------------------------------------------------------------- +// 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; +} + + +// SGR. Simpler than it was, because a cell stores a palette index rather than +// an RGB value: "bright" is +8 and "default" is a named index, where before +// both had to be recovered by searching the palette for a matching colour. +static void +vt_sgr(struct vt *v) +{ + // "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 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 = v->esc_nparm ? v->esc_nparm : 1; // esc_parm[0] is already 0 + + for (int i = 0; i < nparm; i++) { + int p = v->esc_parm[i]; + + if (p == 0) { + v->fg = VT_FG_DEFAULT; + v->bg = VT_BG_DEFAULT; + v->bold = 0; + v->attr = 0; + } else if (p == 1) { + v->bold = 1; + if (v->fg < 8) v->fg += 8; // brighten a colour already chosen + } else if (p == 7) { + v->attr |= VT_ATTR_REVERSE; + } else if (p == 27) { + v->attr &= ~VT_ATTR_REVERSE; + } else if (p == 22) { + v->bold = 0; + if (v->fg >= 8 && v->fg < 16) v->fg -= 8; + } else if (p >= 30 && p <= 37) { + v->fg = (uint8)((p - 30) + (v->bold ? 8 : 0)); + } else if (p == 39) { + v->fg = VT_FG_DEFAULT; + } else if (p >= 40 && p <= 47) { + v->bg = (uint8)(p - 40); + } else if (p == 49) { + v->bg = VT_BG_DEFAULT; + } else if (p >= 90 && p <= 97) { + v->fg = (uint8)((p - 90) + 8); + } else if (p >= 100 && p <= 107) { + v->bg = (uint8)((p - 100) + 8); + } + } +} + +// One final byte of a CSI sequence, with esc_parm[] already filled in. +static void +vt_csi(struct vt *v, int final) +{ + int col = v->cx, row = v->cy; + int n = v->esc_nparm > 0 ? v->esc_parm[0] : 0; + int n1 = n ? n : 1; // most sequences default to 1 + + switch (final) { + case 'A': vt_cursor_set(v, col, row - n1); break; + case 'B': vt_cursor_set(v, col, row + n1); break; + case 'C': vt_cursor_set(v, col + n1, row); break; + case 'D': vt_cursor_set(v, col - n1, row); break; + case 'E': vt_cursor_set(v, 0, row + n1); break; + case 'F': vt_cursor_set(v, 0, row - n1); break; + case 'G': vt_cursor_set(v, n1 - 1, row); break; + case 'd': vt_cursor_set(v, col, n1 - 1); break; + + case 'H': case 'f': { + int r = v->esc_nparm > 0 && v->esc_parm[0] ? v->esc_parm[0] : 1; + int c = v->esc_nparm > 1 && v->esc_parm[1] ? v->esc_parm[1] : 1; + vt_cursor_set(v, c - 1, r - 1); // ANSI counts from 1 + break; + } + + case 'J': vt_erase_display(v, n); break; + case 'K': vt_erase_line(v, n); break; + + case 'm': vt_sgr(v); break; + + case 's': v->saved_cx = col; v->saved_cy = row; break; + case 'u': vt_cursor_set(v, v->saved_cx, v->saved_cy); 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 correct, 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 + } +} + +// Feed one output byte to a terminal. The serial side has already had the +// raw byte; this is the framebuffer's interpretation of it. +void +vt_feed(struct vt *v, int c) +{ + switch (v->esc_state) { + case VE_ESC: + if (c == '[') { v->esc_state = VE_CSI; + v->esc_nparm = 0; v->esc_private = 0; + for (int i = 0; i < VT_MAXPARM; i++) v->esc_parm[i] = 0; + return; } + if (c == ']') { v->esc_state = VE_OSC; return; } + v->esc_state = VE_NORMAL; + return; + + case VE_OSC: + // Consume to BEL or to ESC \. A terminal that does not follow OSC 8 must + // still swallow it, or the URL is printed as text -- which is what + // "Disk (]8;;file:///\]8;;\): 4.6 MiB" was: fastfetch wrapping a path in + // a hyperlink, and this console spelling it out. + if (c == 0x07) { v->esc_state = VE_NORMAL; return; } + if (c == 0x1b) { v->esc_state = VE_OSC_ESC; return; } + return; + + case VE_OSC_ESC: + v->esc_state = (c == '\\') ? VE_NORMAL : VE_OSC; + return; + + case VE_CSI: + if (c == '?') { v->esc_private = 1; return; } + if (c >= '0' && c <= '9') { + if (v->esc_nparm == 0) v->esc_nparm = 1; + if (v->esc_nparm <= VT_MAXPARM) + v->esc_parm[v->esc_nparm - 1] = v->esc_parm[v->esc_nparm - 1] * 10 + (c - '0'); + return; + } + if (c == ';') { + if (v->esc_nparm < VT_MAXPARM) v->esc_nparm++; + return; + } + v->esc_state = VE_NORMAL; + if (!v->esc_private) + vt_csi(v, c); + return; + } + + // VE_NORMAL + if (c == 0x1b) { v->esc_state = VE_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 (v->u8_need > 0) { + if ((c & 0xC0) != 0x80) { // malformed: abandon the sequence + v->u8_need = 0; + vt_putc(v, '?'); + return; + } + v->u8_cp = (v->u8_cp << 6) | (c & 0x3F); + if (--v->u8_need > 0) + return; + int g = glyph_for(v->u8_cp); + vt_putc(v, g ? g : '?'); + return; + } + + if (c >= 0xF0) { v->u8_cp = c & 0x07; v->u8_need = 3; return; } + else if (c >= 0xE0) { v->u8_cp = c & 0x0F; v->u8_need = 2; return; } + else if (c >= 0xC0) { v->u8_cp = c & 0x1F; v->u8_need = 1; return; } + else if (c >= 0x80) { vt_putc(v, '?'); return; } // stray continuation + + vt_putc(v, c); +} + +// --------------------------------------------------------------------------- +// Switching +// --------------------------------------------------------------------------- + +int +vt_switch(int i) +{ + if (i < 0 || i >= NVT) + return -1; + if (!vt_ready) + return -1; + if (i == vt_active) + return 0; + + vt_active = i; + + // Repaint from the grid. Nothing was saved on the way out because nothing + // needed to be: the outgoing terminal's grid has been current all along, + // which is the whole reason the grid exists. + vt_repaint(&vts[i]); + return 0; +} + +int +vt_index(struct vt *v) +{ + return v ? v->index : 0; +} diff --git a/kernel/fs/devfs.c b/kernel/fs/devfs.c index 8ff2f8b..1b58e22 100644 --- a/kernel/fs/devfs.c +++ b/kernel/fs/devfs.c @@ -72,7 +72,17 @@ static const struct devnode devnodes[] = { { "full", 1, 7, D_ROOT }, { "random", 1, 8, D_ROOT }, { "urandom", 1, 9, D_ROOT }, - { "tty", 1, 1, D_ROOT }, /* alias for the console */ + /* /dev/tty is the caller's *controlling* terminal, which is a different + thing from the console and used to be the same thing only because there + was one terminal. Minor 10 resolves through proc->ctty; see + vt_for_minor() in kernel/core/console.c. */ + { "tty", 1, 10, D_ROOT }, + { "tty1", 1, 11, D_ROOT }, + { "tty2", 1, 12, D_ROOT }, + { "tty3", 1, 13, D_ROOT }, + { "tty4", 1, 14, D_ROOT }, + { "tty5", 1, 15, D_ROOT }, + { "tty6", 1, 16, D_ROOT }, { "stdin", 1, 1, D_ROOT }, { "stdout", 1, 1, D_ROOT }, { "stderr", 1, 1, D_ROOT }, diff --git a/kernel/fs/file.c b/kernel/fs/file.c index 0ce123a..42a0ce5 100644 --- a/kernel/fs/file.c +++ b/kernel/fs/file.c @@ -288,7 +288,7 @@ file_poll(struct file *f, int events) if(f->ip && f->ip->type == T_DEV && f->ip->major == CONSOLE){ int rev = 0; - if((events & POLLIN) && f->readable && console_input_ready()) + if((events & POLLIN) && f->readable && console_input_ready(f->ip ? f->ip->minor : 1)) rev |= POLLIN; if((events & POLLOUT) && f->writable) rev |= POLLOUT; diff --git a/kernel/proc/proc.c b/kernel/proc/proc.c index ba37749..e0a6cd7 100644 --- a/kernel/proc/proc.c +++ b/kernel/proc/proc.c @@ -618,7 +618,7 @@ wakeup(void *chan) // POSIX, and Linux in sig_task_ignored(): a signal whose disposition is the // default action is discarded for init rather than delivered. Without this the // system has a single keystroke that kills it -- Ctrl-C sends SIGINT to the -// foreground process group, init is pgid 1, and any moment where tty_fg_pgrp +// foreground process group, init is pgid 1, and any moment where a terminal's fg_pgrp // is 1 (before a shell claims the terminal, after one exits, or in the window // between the two while the key is held down) lands SIGINT on init. init has // no handler, so killed is set, exit() runs, and exit() panics outright on diff --git a/kernel/sys/sys_fs.c b/kernel/sys/sys_fs.c index 4adaf07..aad94d2 100644 --- a/kernel/sys/sys_fs.c +++ b/kernel/sys/sys_fs.c @@ -16,6 +16,7 @@ #include "errno.h" #include "schfs.h" /* SCHFS_BLKSIZE, SCHFS_NAME_MAX — statfs reports them */ #include "gfx.h" /* gfx_cols/gfx_rows — TIOCGWINSZ reports them */ +#include "vt.h" /* the terminals TIOC*/VT_* act on */ // ----------------------------------------------------------------------- // Linux-ABI-compatible struct stat (x86_64 "struct stat" from @@ -1048,13 +1049,13 @@ addr_t sys_ioctl(void) struct termios *tp = (struct termios*)user_ptr(argp, sizeof(*tp)); if (!tp) return -EFAULT; // The real thing, not a hardcoded snapshot: whatever was last set. - cons_get_termios(tp); + cons_get_termios(f->ip->minor, tp); return 0; } if (request == TCSETS || request == TCSETSW || request == TCSETSF) { struct termios *tp = (struct termios*)user_ptr(argp, sizeof(*tp)); if (!tp) return -EFAULT; - cons_set_termios(tp); + cons_set_termios(f->ip->minor, tp); return 0; } if (request == TIOCGWINSZ) { @@ -1092,7 +1093,7 @@ addr_t sys_ioctl(void) // during a command survives the flush and lands in the next prompt. if (request == TCFLSH) { if (argp == TCIFLUSH || argp == TCIOFLUSH) - cons_flush_input(); + cons_flush_input(f->ip->minor); return 0; } // Controlling-terminal foreground process group. @@ -1104,7 +1105,8 @@ addr_t sys_ioctl(void) // while (tcgetpgrp(tty) != getpgrp()) kill(0, SIGTTIN); // spun forever. Both ends now read the same variable. if (request == TIOCGPGRP) { - put_user(tty_fg_pgrp, argp); + int pg = cons_fg_pgrp(f->ip->minor); + put_user(pg, argp); return 0; } if (request == TIOCSPGRP) { @@ -1113,10 +1115,70 @@ addr_t sys_ioctl(void) return -EFAULT; if (pg <= 0) return -EINVAL; - tty_fg_pgrp = pg; + cons_set_fg_pgrp(f->ip->minor, pg); return 0; } - if (request == TIOCSCTTY) return 0; + // TIOCSCTTY was `return 0` -- it claimed to make this terminal the + // caller's controlling terminal and did nothing, because there was one + // terminal and nothing to choose. It records the choice now, which is what + // /dev/tty resolves through and what a session leader on tty3 needs so + // that its /dev/tty is tty3 and not whatever is on screen. + if (request == TIOCSCTTY) { + struct vt *v = vt_for_minor(f->ip->minor); + myproc()->ctty = vt_index(v) + 1; + v->sid = myproc()->sid; + return 0; + } + + // --- virtual terminal control --------------------------------------- + // + // The argument is the terminal number by value for ACTIVATE and + // WAITACTIVE, and a pointer for GETSTATE and OPENQRY. Passing argp to + // user_ptr for the first two would fault on a legitimate call, which is + // the same shape of bug TCFLSH had. + if (request == VT_ACTIVATE) + return vt_switch((int)argp - 1) == 0 ? 0 : -EINVAL; + + if (request == VT_WAITACTIVE) { + // Nothing to wait for: vt_switch repaints synchronously, so by the + // time VT_ACTIVATE has returned the terminal is already active. + // Answering success is honest here; blocking would be a lie about + // work that has already happened. + int want = (int)argp - 1; + if (want < 0 || want >= NVT) return -EINVAL; + return vt_active == want ? 0 : -EINVAL; + } + + if (request == VT_GETSTATE) { + struct vt_stat *st = (struct vt_stat*)user_ptr(argp, sizeof(*st)); + if (!st) return -EFAULT; + st->v_active = (uint16)(vt_active + 1); + st->v_signal = 0; + // Bit n set means terminal n is in use. Bit 0 is unused because + // terminals are numbered from 1. + st->v_state = 0; + for (int i = 0; i < NVT; i++) + if (vt_get(i)->opened > 0) + st->v_state |= (uint16)(1u << (i + 1)); + return 0; + } + + if (request == VT_OPENQRY) { + int *out = (int*)user_ptr(argp, sizeof(int)); + if (!out) return -EFAULT; + for (int i = 0; i < NVT; i++) + if (vt_get(i)->opened == 0) { *out = i + 1; return 0; } + *out = -1; + 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) + return 0; + return 0; }