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

@ -25,7 +25,7 @@ KERN_CORE = $(addprefix $(BUILD)/kernel/core/, \
KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \ 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 \ 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 # Drivers for hardware that has to be *found* rather than assumed. Anything
# discovered by walking a bus belongs here; the fixed-address legacy devices # 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/. # Third-party software does NOT go here — see ports/.
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
ULIB = $(addprefix $(BUILD)/lib/, start.o glue.o usys.o) 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)) CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS))
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------

93
cmd/chvt/main.c Normal file
View file

@ -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();
}

View file

@ -29,6 +29,7 @@
["architecture/syscalls.md", "Syscalls"], ["architecture/syscalls.md", "Syscalls"],
["architecture/filesystem.md", "The VFS"], ["architecture/filesystem.md", "The VFS"],
["architecture/schfs.md", "SchFS"], ["architecture/schfs.md", "SchFS"],
["architecture/terminals.md", "Virtual terminals"],
["architecture/devices.md", "Device model"], ["architecture/devices.md", "Device model"],
["architecture/usb.md", "USB"], ["architecture/usb.md", "USB"],
["architecture/graphics.md", "Graphics"], ["architecture/graphics.md", "Graphics"],

View file

@ -18,6 +18,10 @@ its distribution.
| API | **SchAPI** | The native programming interface — *designed, not built* | | API | **SchAPI** | The native programming interface — *designed, not built* |
| Compositor | **Schcom** | Owns the display; v0 runs | | 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 ## The stack, bottom to top

View file

@ -0,0 +1,128 @@
<!-- SPDX-License-Identifier: Apache-2.0 -->
# 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.

View file

@ -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 Label = os.schos.console-shell
# zsh, not /bin/sh. mksh stays as /bin/sh and is what schinit falls back to # 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. # terminal input at all. Without this the shell echoes and executes nothing.
ControllingTerminal = yes ControllingTerminal = yes
StandardIn = /dev/console StandardIn = /dev/tty1
StandardOut = /dev/console StandardOut = /dev/tty1
StandardError = /dev/console StandardError = /dev/tty1
# Seconds between respawns. Guards against a shell that dies instantly # Seconds between respawns. Guards against a shell that dies instantly
# defaults to 10. One is enough for a shell someone just exited on purpose. # defaults to 10. One is enough for a shell someone just exited on purpose.

View file

@ -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

View file

@ -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 # the only thing here that ever turns red -- same rule as the rest of the
# system: normal states are calm, only failure is loud. # system: normal states are calm, only failure is loud.
setopt PROMPT_SUBST 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='' RPROMPT=''

View file

@ -35,13 +35,16 @@ int bflush(uint dev);
// console.c // console.c
struct termios; struct termios;
void consoleinit(void); void consoleinit(void);
extern int tty_fg_pgrp;
// The terminal's line-discipline settings. TCGETS/TCSETS in sys_fs.c are the // The terminal's line-discipline settings. TCGETS/TCSETS in sys_fs.c are the
// only callers; the console honours ICANON, ECHO, ECHOE, ECHONL, ISIG, // only callers; the console honours ICANON, ECHO, ECHOE, ECHONL, ISIG,
// ICRNL, INLCR, IGNCR, ISTRIP and VMIN. // ICRNL, INLCR, IGNCR, ISTRIP and VMIN.
void cons_get_termios(struct termios *); void cons_get_termios(int minor, struct termios *);
void cons_set_termios(const struct termios *); void cons_set_termios(int minor, const struct termios *);
void cons_flush_input(void); 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 uint lapic_timer_hz; // measured, kernel/arch/x86_64/lapic.c
extern uint64 tsc_hz; extern uint64 tsc_hz;
void cprintf(char*, ...); void cprintf(char*, ...);
@ -619,7 +622,7 @@ pml4e_t* copyuvm(pml4e_t*, uint64);
void switchuvm(struct proc*); void switchuvm(struct proc*);
void switchkvm(void); void switchkvm(void);
int pipe_poll(struct pipe*, int readable, int writable); 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 copyin(pml4e_t*, addr_t, void*, uint64);
int copyout(pml4e_t*, addr_t, void*, uint64); int copyout(pml4e_t*, addr_t, void*, uint64);
void vm_unmap_range(pml4e_t*, uint64, uint64); void vm_unmap_range(pml4e_t*, uint64, uint64);

View file

@ -72,6 +72,11 @@ void gfx_set_bg(uint32 color);
void gfx_screen_reset(void); void gfx_screen_reset(void);
int gfx_cols(void); int gfx_cols(void);
int gfx_rows(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_get(int *col, int *row);
void gfx_cursor_set(int col, int row); void gfx_cursor_set(int col, int row);
void gfx_erase_display(int mode); void gfx_erase_display(int mode);

View file

@ -28,6 +28,15 @@
#define KEY_INS 0xE8 #define KEY_INS 0xE8
#define KEY_DEL 0xE9 #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 // C('A') == Control-A
#define C(x) (x - '@') #define C(x) (x - '@')

View file

@ -123,6 +123,21 @@ struct rlimit {
#define TIOCGWINSZ 0x5413 #define TIOCGWINSZ 0x5413
#define TIOCSWINSZ 0x5414 #define TIOCSWINSZ 0x5414
#define TIOCSCTTY 0x540E #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 TIOCGSERIAL 0x541E
#define FIONREAD 0x541B #define FIONREAD 0x541B
// FICLONE: share another file's data blocks with this one. The argument is a // FICLONE: share another file's data blocks with this one. The argument is a

View file

@ -245,6 +245,10 @@ struct proc {
// which is why mksh's job control never made progress. // which is why mksh's job control never made progress.
int pgid; int pgid;
int sid; 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 // 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 // them yet (the on-disk format has no owner field until SchFS), but they

139
include/vt.h Normal file
View file

@ -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<index+1>
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

View file

@ -1,6 +1,8 @@
#include <stdarg.h> #include <stdarg.h>
#include "types.h" #include "types.h"
#include "defs.h" #include "defs.h"
#include "vt.h"
#include "kbd.h" /* KEY_ALT_F1 -- the terminal-switch chords */
#include "gfx.h" #include "gfx.h"
#include "theme.h" #include "theme.h"
#include "irq.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 // 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 // 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. // blue" -- it looked black against the cream background purely by accident.
static uint32 cons_fg = SCH_FG;
static struct { static struct {
struct spinlock lock; struct spinlock lock;
@ -170,12 +171,6 @@ console_takeover(void)
gfx_screen_reset(); 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 #define BACKSPACE 0x100
@ -193,296 +188,9 @@ int tty_fg_pgrp = 1;
// only for the framebuffer. Two consumers, two right answers. // only for the framebuffer. Two consumers, two right answers.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// The sixteen ANSI colours as RGB, for a linear framebuffer. // 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
// consoleinit() used to program these into the VGA DAC with set_vga_palette(), // them is per-terminal and there used to be exactly one of each.
// 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);
}
void void
consputc(int c) consputc(int c)
@ -495,76 +203,92 @@ consputc(int c)
if (c == BACKSPACE) { if (c == BACKSPACE) {
uartputc('\b'); uartputc(' '); uartputc('\b'); uartputc('\b'); uartputc(' '); uartputc('\b');
if (!cons_serial_only) if (!cons_serial_only)
gfx_putc(c, cons_fg); vt_putc(vt_current(), '\b');
return; return;
} }
uartputc(c); uartputc(c);
if (!cons_serial_only) 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)-'@') #define C(x) ((x)-'@')
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// The line discipline // Terminals as devices
// //
// TCGETS used to return a hardcoded set of flags and TCSETS threw its argument // The line discipline below used to operate on one `struct input` and one
// away, so the terminal was permanently in one mode: line-at-a-time, always // `struct termios`, because there was one terminal. Both live in struct vt
// echoing. Anything wanting a keypress at a time -- a pager, an editor, a // now (kernel/dev/vt.c) and everything here takes the terminal it is acting
// shell doing its own line editing, which is most of what zsh is -- had no way // on as an argument.
// to ask for one. This is the real thing: a struct programs can write, that //
// changes what the kernel actually does. // 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 = { static struct spinlock input_lock;
.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,
};
void // Which terminal a device minor names.
cons_get_termios(struct termios *out) //
// 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); if (minor >= 11 && minor < 11 + NVT)
*out = tio; return vt_get(minor - 11);
release(&input.lock); 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 void
cons_set_termios(const struct termios *in) cons_get_termios(int minor, struct termios *out)
{ {
acquire(&input.lock); struct vt *v = vt_for_minor(minor);
tio = *in; 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 // 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 // but not yet submitted, or a shell that turns off ICANON mid-line silently
// eats the characters already in the edit buffer. // eats the characters already in the edit buffer.
if (!(tio.c_lflag & ICANON)) if (!(v->tio.c_lflag & ICANON))
input.w = input.e; v->in.w = v->in.e;
release(&input.lock); release(&input_lock);
wakeup(&input.r); wakeup(&v->in.r);
} }
// Discard everything typed but not yet consumed: completed lines waiting in // 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 // 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. // leaving it set would end the *next* read instead of the one it was typed at.
void void
cons_flush_input(void) cons_flush_input(int minor)
{ {
acquire(&input.lock); struct vt *v = vt_for_minor(minor);
input.e = input.w = input.r; acquire(&input_lock);
input.eof = 0; v->in.e = v->in.w = v->in.r;
release(&input.lock); 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 static void
echo(int c) echo(struct vt *v, int c)
{ {
if (c == '\n' || c == '\r') { if (c == '\n' || c == '\r') {
if ((tio.c_lflag & ECHO) || (tio.c_lflag & ECHONL)) if ((v->tio.c_lflag & ECHO) || (v->tio.c_lflag & ECHONL))
consputc('\n'); vt_out(v, '\n');
return; return;
} }
if (!(tio.c_lflag & ECHO)) if (!(v->tio.c_lflag & ECHO))
return; return;
// Control characters echo as ^X, the way every other terminal shows them. // Control characters echo as ^X, the way every other terminal shows them.
if (c < 32 && c != '\t') { if (c < 32 && c != '\t') {
consputc('^'); vt_out(v, '^');
consputc(c + '@'); vt_out(v, c + '@');
} else { } else {
consputc(c); vt_out(v, c);
} }
} }
// Rub out the last character of the current line. // Rub out the last character of the current line.
static void static void
erase_one(void) erase_one(struct vt *v)
{ {
if (input.e == input.w) if (v->in.e == v->in.w)
return; return;
input.e--; v->in.e--;
if (tio.c_lflag & ECHOE) if (v->tio.c_lflag & ECHOE) {
consputc(BACKSPACE); 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 void
@ -619,17 +378,30 @@ consoleintr(int (*getc)(void))
{ {
int c; int c;
acquire(&input.lock); acquire(&input_lock);
while((c = getc()) >= 0){ while((c = getc()) >= 0){
if (c == 0) if (c == 0)
continue; 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. // 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 (c == '\r') {
if (tio.c_iflag & IGNCR) continue; if (v->tio.c_iflag & IGNCR) continue;
if (tio.c_iflag & ICRNL) c = '\n'; if (v->tio.c_iflag & ICRNL) c = '\n';
} else if (c == '\n' && (tio.c_iflag & INLCR)) { } else if (c == '\n' && (v->tio.c_iflag & INLCR)) {
c = '\r'; c = '\r';
} }
@ -639,22 +411,22 @@ consoleintr(int (*getc)(void))
// handler is whatever happened to be running on this CPU when the key was // 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 // 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 // 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; // catch it and the shell never learned the job had died. Both wrong; both
// both fixed by sending a real signal to the foreground process group, // fixed by sending a real signal to the foreground process group -- which
// which the kernel has been tracking in tty_fg_pgrp all along. // is now this terminal's group rather than the machine's only one.
if (tio.c_lflag & ISIG) { if (v->tio.c_lflag & ISIG) {
int sig = 0; int sig = 0;
if (c == tio.c_cc[VINTR]) sig = SIGINT; if (c == v->tio.c_cc[VINTR]) sig = SIGINT;
else if (c == tio.c_cc[VQUIT]) sig = SIGQUIT; else if (c == v->tio.c_cc[VQUIT]) sig = SIGQUIT;
else if (c == tio.c_cc[VSUSP]) sig = SIGTSTP; else if (c == v->tio.c_cc[VSUSP]) sig = SIGTSTP;
if (sig) { if (sig) {
echo(c); echo(v, c);
input.e = input.w = input.r; // discard the partial line v->in.e = v->in.w = v->in.r; // discard the partial line
input.abort_read = 1; v->in.abort_read = 1;
release(&input.lock); release(&input_lock);
sigsend_pgrp(tty_fg_pgrp, sig); sigsend_pgrp(v->fg_pgrp, sig);
acquire(&input.lock); acquire(&input_lock);
wakeup(&input.r); wakeup(&v->in.r);
continue; continue;
} }
} }
@ -666,53 +438,53 @@ consoleintr(int (*getc)(void))
continue; continue;
} }
if (tio.c_lflag & ICANON) { if (v->tio.c_lflag & ICANON) {
if (c == tio.c_cc[VERASE] || c == C('H')) { if (c == v->tio.c_cc[VERASE] || c == C('H')) {
erase_one(); erase_one(v);
continue; continue;
} }
if (c == tio.c_cc[VKILL]) { if (c == v->tio.c_cc[VKILL]) {
while (input.e != input.w && while (v->in.e != v->in.w &&
input.buf[(input.e-1) % INPUT_BUF] != '\n') v->in.buf[(v->in.e-1) % VT_INPUT_BUF] != '\n')
erase_one(); erase_one(v);
continue; continue;
} }
if (c == tio.c_cc[VWERASE]) { if (c == v->tio.c_cc[VWERASE]) {
while (input.e != input.w && while (v->in.e != v->in.w &&
input.buf[(input.e-1) % INPUT_BUF] == ' ') v->in.buf[(v->in.e-1) % VT_INPUT_BUF] == ' ')
erase_one(); erase_one(v);
while (input.e != input.w && while (v->in.e != v->in.w &&
input.buf[(input.e-1) % INPUT_BUF] != ' ') v->in.buf[(v->in.e-1) % VT_INPUT_BUF] != ' ')
erase_one(); erase_one(v);
continue; continue;
} }
if (c == tio.c_cc[VEOF]) { if (c == v->tio.c_cc[VEOF]) {
if (input.e == input.w) if (v->in.e == v->in.w)
input.eof = 1; // empty line: end of input v->in.eof = 1; // empty line: end of input
input.w = input.e; // otherwise just submit what is there v->in.w = v->in.e; // otherwise submit what is there
wakeup(&input.r); wakeup(&v->in.r);
continue; continue;
} }
} }
if (input.e - input.r >= INPUT_BUF) if (v->in.e - v->in.r >= VT_INPUT_BUF)
continue; // buffer full: drop it continue; // buffer full: drop it
// Everything else goes in, including bytes above 127 -- those are UTF-8 // Everything else goes in, including bytes above 127 -- those are UTF-8
// continuation bytes, and the old `c >= 32 && c <= 126` test silently // continuation bytes, and the old `c >= 32 && c <= 126` test silently
// refused every non-ASCII character anyone tried to type. // refused every non-ASCII character anyone tried to type.
input.buf[input.e++ % INPUT_BUF] = c; v->in.buf[v->in.e++ % VT_INPUT_BUF] = c;
echo(c); echo(v, c);
// Canonical mode hands over a line at a time; raw mode hands over // Canonical mode hands over a line at a time; raw mode hands over
// everything as it arrives. // everything as it arrives.
if (!(tio.c_lflag & ICANON) || if (!(v->tio.c_lflag & ICANON) ||
c == '\n' || input.e == input.r + INPUT_BUF) { c == '\n' || v->in.e == v->in.r + VT_INPUT_BUF) {
input.w = input.e; v->in.w = v->in.e;
wakeup(&input.r); wakeup(&v->in.r);
} }
} }
release(&input.lock); release(&input_lock);
} }
int int
@ -720,11 +492,10 @@ consoleread(struct inode *ip, uint64 off, char *dst, int n)
{ {
uint target; uint target;
int c; int c;
static uint32 prng_seed = 0x9A7B3C1D; static uint32 prng_seed = 0x9A7B3C1D;
if (ip->minor == 2) { if (ip->minor == 2)
return 0; return 0;
}
if (ip->minor == 4) { if (ip->minor == 4) {
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
@ -736,56 +507,59 @@ consoleread(struct inode *ip, uint64 off, char *dst, int n)
return n; return n;
} }
struct vt *v = vt_for_minor(ip->minor);
iunlock(ip); iunlock(ip);
target = n; target = n;
acquire(&input.lock); acquire(&input_lock);
while(n > 0){ while(n > 0){
while(input.r == input.w){ while(v->in.r == v->in.w){
if (input.eof) { if (v->in.eof) {
input.eof = 0; v->in.eof = 0;
release(&input.lock); release(&input_lock);
ilock(ip); ilock(ip);
return 0; // end of input return 0; // end of input
} }
if (myproc()->killed || input.abort_read) { if (myproc()->killed || v->in.abort_read) {
input.abort_read = 0; v->in.abort_read = 0;
release(&input.lock); release(&input_lock);
ilock(ip); ilock(ip);
return -1; 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; *dst++ = c;
--n; --n;
// Canonical mode returns a line at a time. Raw mode returns whatever has // 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 // arrived: a program that asked for a keypress must not be made to wait
// for a newline that is never coming. // for a newline that is never coming.
if (tio.c_lflag & ICANON) { if (v->tio.c_lflag & ICANON) {
if (c == '\n') if (c == '\n')
break; break;
} else { } 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; break;
} }
} }
release(&input.lock); release(&input_lock);
ilock(ip); ilock(ip);
return target - n; return target - n;
} }
// Nonzero if a completed input line is waiting — poll(2)'s POLLIN answer // Nonzero if a completed input line is waiting -- poll(2)'s POLLIN answer for
// for the console tty. Mirrors the r/w cursors consoleread() blocks on. // a terminal. Mirrors the r/w cursors consoleread() blocks on.
int int
console_input_ready(void) console_input_ready(int minor)
{ {
struct vt *v = vt_for_minor(minor);
int ready; int ready;
acquire(&input.lock); acquire(&input_lock);
ready = input.r != input.w; ready = v->in.r != v->in.w;
release(&input.lock); release(&input_lock);
return ready; return ready;
} }
@ -794,20 +568,50 @@ consolewrite(struct inode *ip, uint64 off, char *buf, int n)
{ {
int i; int i;
if (ip->minor == 2 || ip->minor == 3 || ip->minor == 4) { if (ip->minor == 2 || ip->minor == 3 || ip->minor == 4)
return n; return n;
}
struct vt *v = vt_for_minor(ip->minor);
iunlock(ip); iunlock(ip);
acquire(&cons.lock); acquire(&cons.lock);
if (panicked) {
release(&cons.lock);
ilock(ip);
cli();
for(;;) hlt();
}
for(i = 0; i < n; i++) for(i = 0; i < n; i++)
consputc(buf[i] & 0xff); vt_out(v, buf[i] & 0xff);
release(&cons.lock); release(&cons.lock);
ilock(ip); ilock(ip);
return n; 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 // 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. // keyboard as an input source, so this is where the line gets claimed.
static void static void
@ -821,12 +625,12 @@ void
consoleinit(void) consoleinit(void)
{ {
initlock(&cons.lock, "console"); initlock(&cons.lock, "console");
initlock(&input.lock, "input"); initlock(&input_lock, "input");
memset(&input, 0, sizeof(input));
devsw[CONSOLE].write = consolewrite; devsw[CONSOLE].write = consolewrite;
devsw[CONSOLE].read = consoleread; devsw[CONSOLE].read = consoleread;
devsw[CONSOLE].open = consoleopen;
devsw[CONSOLE].close = consoleclose;
cons.locking = 1; cons.locking = 1;
irq_register(IRQ_KBD, kbd_irq, 0, "ps2-kbd", 0); irq_register(IRQ_KBD, kbd_irq, 0, "ps2-kbd", 0);

View file

@ -1,5 +1,6 @@
#include "types.h" #include "types.h"
#include "defs.h" #include "defs.h"
#include "vt.h"
#include "param.h" #include "param.h"
#include "memlayout.h" #include "memlayout.h"
#include "mmu.h" #include "mmu.h"
@ -68,6 +69,11 @@ kmain(uint32 fb_addr)
// their handlers are registered. // their handlers are registered.
inputinit(); inputinit();
if (boot_verbose) cprintf("verbose boot: inputinit done\n"); 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(); consoleinit();
if (boot_verbose) cprintf("verbose boot: consoleinit done\n"); if (boot_verbose) cprintf("verbose boot: consoleinit done\n");
uartinit(); uartinit();

View file

@ -64,6 +64,10 @@ memread(struct inode *ip, uint64 off, char *dst, int n)
case 1: return consoleread(ip, off, dst, n); case 1: return consoleread(ip, off, dst, n);
case 2: return nullread(ip, off, dst, n); case 2: return nullread(ip, off, dst, n);
case 3: return consoleread(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 5: return zeroread(ip, off, dst, n);
case 7: return zeroread(ip, off, dst, n); // /dev/full reads as zeros case 7: return zeroread(ip, off, dst, n); // /dev/full reads as zeros
case 8: case 8:
@ -77,6 +81,8 @@ memwrite(struct inode *ip, uint64 off, char *dst, int n)
{ {
switch(ip->minor) { switch(ip->minor) {
case 1: return consolewrite(ip, off, dst, n); 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 7: return fullwrite(ip, off, dst, n); // /dev/full: always ENOSPC
case 2: case 2:
case 5: case 5:

View file

@ -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 void
gfx_screen_reset(void) gfx_screen_reset(void)
{ {

View file

@ -2,6 +2,7 @@
#include "x86.h" #include "x86.h"
#include "defs.h" #include "defs.h"
#include "kbd.h" #include "kbd.h"
#include "vt.h" /* NVT -- how many terminals the chords reach */
#include "input.h" #include "input.h"
int int
@ -58,6 +59,14 @@ kbdgetc(void)
shift |= shiftcode[data]; shift |= shiftcode[data];
shift ^= togglecode[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]; c = charcode[shift & (CTL | SHIFT)][data];
if(shift & CAPSLOCK){ if(shift & CAPSLOCK){
if('a' <= c && c <= 'z') if('a' <= c && c <= 'z')

627
kernel/dev/vt.c Normal file
View file

@ -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;
}

View file

@ -72,7 +72,17 @@ static const struct devnode devnodes[] = {
{ "full", 1, 7, D_ROOT }, { "full", 1, 7, D_ROOT },
{ "random", 1, 8, D_ROOT }, { "random", 1, 8, D_ROOT },
{ "urandom", 1, 9, 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 }, { "stdin", 1, 1, D_ROOT },
{ "stdout", 1, 1, D_ROOT }, { "stdout", 1, 1, D_ROOT },
{ "stderr", 1, 1, D_ROOT }, { "stderr", 1, 1, D_ROOT },

View file

@ -288,7 +288,7 @@ file_poll(struct file *f, int events)
if(f->ip && f->ip->type == T_DEV && f->ip->major == CONSOLE){ if(f->ip && f->ip->type == T_DEV && f->ip->major == CONSOLE){
int rev = 0; 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; rev |= POLLIN;
if((events & POLLOUT) && f->writable) if((events & POLLOUT) && f->writable)
rev |= POLLOUT; rev |= POLLOUT;

View file

@ -618,7 +618,7 @@ wakeup(void *chan)
// POSIX, and Linux in sig_task_ignored(): a signal whose disposition is the // 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 // 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 // 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 // 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 // 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 // no handler, so killed is set, exit() runs, and exit() panics outright on

View file

@ -16,6 +16,7 @@
#include "errno.h" #include "errno.h"
#include "schfs.h" /* SCHFS_BLKSIZE, SCHFS_NAME_MAX — statfs reports them */ #include "schfs.h" /* SCHFS_BLKSIZE, SCHFS_NAME_MAX — statfs reports them */
#include "gfx.h" /* gfx_cols/gfx_rows — TIOCGWINSZ 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 // 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)); struct termios *tp = (struct termios*)user_ptr(argp, sizeof(*tp));
if (!tp) return -EFAULT; if (!tp) return -EFAULT;
// The real thing, not a hardcoded snapshot: whatever was last set. // The real thing, not a hardcoded snapshot: whatever was last set.
cons_get_termios(tp); cons_get_termios(f->ip->minor, tp);
return 0; return 0;
} }
if (request == TCSETS || request == TCSETSW || request == TCSETSF) { if (request == TCSETS || request == TCSETSW || request == TCSETSF) {
struct termios *tp = (struct termios*)user_ptr(argp, sizeof(*tp)); struct termios *tp = (struct termios*)user_ptr(argp, sizeof(*tp));
if (!tp) return -EFAULT; if (!tp) return -EFAULT;
cons_set_termios(tp); cons_set_termios(f->ip->minor, tp);
return 0; return 0;
} }
if (request == TIOCGWINSZ) { if (request == TIOCGWINSZ) {
@ -1092,7 +1093,7 @@ addr_t sys_ioctl(void)
// during a command survives the flush and lands in the next prompt. // during a command survives the flush and lands in the next prompt.
if (request == TCFLSH) { if (request == TCFLSH) {
if (argp == TCIFLUSH || argp == TCIOFLUSH) if (argp == TCIFLUSH || argp == TCIOFLUSH)
cons_flush_input(); cons_flush_input(f->ip->minor);
return 0; return 0;
} }
// Controlling-terminal foreground process group. // Controlling-terminal foreground process group.
@ -1104,7 +1105,8 @@ addr_t sys_ioctl(void)
// while (tcgetpgrp(tty) != getpgrp()) kill(0, SIGTTIN); // while (tcgetpgrp(tty) != getpgrp()) kill(0, SIGTTIN);
// spun forever. Both ends now read the same variable. // spun forever. Both ends now read the same variable.
if (request == TIOCGPGRP) { if (request == TIOCGPGRP) {
put_user(tty_fg_pgrp, argp); int pg = cons_fg_pgrp(f->ip->minor);
put_user(pg, argp);
return 0; return 0;
} }
if (request == TIOCSPGRP) { if (request == TIOCSPGRP) {
@ -1113,10 +1115,70 @@ addr_t sys_ioctl(void)
return -EFAULT; return -EFAULT;
if (pg <= 0) if (pg <= 0)
return -EINVAL; return -EINVAL;
tty_fg_pgrp = pg; cons_set_fg_pgrp(f->ip->minor, pg);
return 0; 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; return 0;
} }