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:
parent
14f0e4d0e2
commit
1c1b0c9570
24 changed files with 1460 additions and 458 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
627
kernel/dev/vt.c
Normal file
627
kernel/dev/vt.c
Normal 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;
|
||||
}
|
||||
Loading…
Reference in a new issue