Everything the kernel printed went to the serial port and nowhere else until the first program started and called console_takeover(). On a machine with a UART that is fine. On one without -- a laptop, a Chromebox, anything built this century -- the screen showed the loader's progress bar and then nothing at all, for the whole of boot, on exactly the machines where something was going wrong. A report of "it shows Loading SchOS and freezes" cannot be acted on. The same boot with a log on it says which line it stopped after, and that is usually the whole diagnosis. The loader has already put a framebuffer on the screen and described it in the boot block, so there was a perfectly good console sitting unused for the entire early boot. kmain attaches it on its first line now. Not gated on the F8 verbose menu, which was the first instinct and is wrong: F8 is no use to someone whose machine resets before they can read anything. This is what Linux does too -- the log is the default and `quiet splash` is the thing you ask for. The splash survives until the kernel's first message, and a boot that succeeds clears the lot when the shell starts, so the only people who ever see this are the people who need to. vt_init is idempotent now, because the early attach brings the terminals up before main() reaches its usual call and the second one must not wipe six screens of boot messages. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
814 lines
26 KiB
C
814 lines
26 KiB
C
// 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;
|
|
}
|
|
|
|
// The screen size, clamped to what a grid can hold.
|
|
//
|
|
// 80x24 when there is no framebuffer yet -- which is the usual case when
|
|
// vt_init runs, because the display driver is found on the PCI bus and the
|
|
// terminals are set up before the bus is walked. vt_resize() corrects it the
|
|
// moment a scanout arrives.
|
|
static void
|
|
measure(int *out_cols, int *out_rows)
|
|
{
|
|
int cols = gfx_cols(), rows = gfx_rows();
|
|
if (cols <= 0) cols = 80;
|
|
if (rows <= 0) rows = 24;
|
|
if (cols > VT_MAXCOLS) cols = VT_MAXCOLS;
|
|
if (rows > VT_MAXROWS) rows = VT_MAXROWS;
|
|
*out_cols = cols;
|
|
*out_rows = rows;
|
|
}
|
|
|
|
void
|
|
vt_init(void)
|
|
{
|
|
int cols, rows;
|
|
|
|
// Idempotent. A verbose boot brings the terminals up early, before the PCI
|
|
// bus is walked, so that the kernel log has somewhere to go; main() then
|
|
// calls this again in its usual place. The second call must not wipe six
|
|
// screens of boot messages.
|
|
if (vt_ready) {
|
|
vt_resize();
|
|
return;
|
|
}
|
|
measure(&cols, &rows);
|
|
|
|
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;
|
|
}
|
|
|
|
// The real screen has arrived. Re-measure and re-lay the grids.
|
|
//
|
|
// This exists because of the order the kernel comes up in: terminals are
|
|
// initialised before the PCI bus is walked, so the display driver attaches its
|
|
// scanout *after* vt_init has already sized every grid from a framebuffer that
|
|
// was not there yet. Without this the grids stayed 80x24 on a 160x50 screen --
|
|
// text stopped two thirds of the way down, `ls` formatted for a width the grid
|
|
// did not have and split words across the wrap, and zsh's cursor tracking was
|
|
// wrong on every line.
|
|
//
|
|
// Content is not preserved. Reflowing a grid into a different width is a real
|
|
// problem and there is nothing here worth reflowing: this runs during boot,
|
|
// before console_takeover(), when every grid is still blank.
|
|
void
|
|
vt_resize(void)
|
|
{
|
|
int cols, rows;
|
|
|
|
if (!vt_ready)
|
|
return; // vt_init has not run; it will measure itself
|
|
measure(&cols, &rows);
|
|
if (cols == vts[0].cols && rows == vts[0].rows)
|
|
return; // same screen, nothing to do
|
|
|
|
for (int i = 0; i < NVT; i++) {
|
|
struct vt *v = &vts[i];
|
|
v->cols = cols;
|
|
v->rows = rows;
|
|
if (v->cx >= cols) v->cx = cols - 1;
|
|
if (v->cy >= rows) v->cy = rows - 1;
|
|
for (int r = 0; r < rows; r++)
|
|
blank_row(v, r);
|
|
}
|
|
vt_repaint(vt_current());
|
|
}
|
|
|
|
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;
|
|
v->wrap_pending = 0; // an explicit move is not a wrap
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// Take the deferred wrap, if one is owed. Called before anything that needs
|
|
// the cursor to be at a real position.
|
|
static void
|
|
wrap_now(struct vt *v)
|
|
{
|
|
if (!v->wrap_pending)
|
|
return;
|
|
v->wrap_pending = 0;
|
|
v->cx = 0;
|
|
v->cy++;
|
|
while (v->cy >= v->rows)
|
|
vt_scroll(v);
|
|
}
|
|
|
|
void
|
|
vt_putc(struct vt *v, int glyph)
|
|
{
|
|
switch (glyph) {
|
|
case '\n':
|
|
v->wrap_pending = 0;
|
|
v->cx = 0;
|
|
v->cy++;
|
|
break;
|
|
|
|
case '\r':
|
|
v->wrap_pending = 0;
|
|
v->cx = 0;
|
|
break;
|
|
|
|
case '\b':
|
|
case 0x7f:
|
|
v->wrap_pending = 0;
|
|
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.
|
|
wrap_now(v);
|
|
do {
|
|
put_blank_at(v, v->cx, v->cy);
|
|
v->cx++;
|
|
} while (v->cx % 8 != 0 && v->cx < v->cols);
|
|
if (v->cx >= v->cols) { v->cx = v->cols - 1; v->wrap_pending = 1; }
|
|
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 = '?';
|
|
wrap_now(v); // the previous glyph filled the line
|
|
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);
|
|
// Filling the last column does not move to the next line. It owes a wrap,
|
|
// and pays it when the next glyph arrives -- which is what makes a line
|
|
// that is exactly as wide as the screen take one row instead of two, and
|
|
// what lets a program write to the bottom-right cell without scrolling.
|
|
if (v->cx + 1 >= v->cols)
|
|
v->wrap_pending = 1;
|
|
else
|
|
v->cx++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
while (v->cy >= v->rows)
|
|
vt_scroll(v);
|
|
}
|
|
|
|
// Fill a run of cells with the current background, wrapping at the right edge.
|
|
//
|
|
// The grid is updated cell by cell -- it has to be -- but the *drawing* is
|
|
// done as rectangles, one per row span. Blanking a cell through the glyph
|
|
// path writes 128 pixels through the generic pixel routine; blanking a whole
|
|
// row as one fill writes the same pixels with the bounds checks hoisted out,
|
|
// and the console erases to end of screen on every shell prompt. At 160x50
|
|
// that is the difference between a million checked pixel writes per prompt and
|
|
// fifty row fills.
|
|
static void
|
|
erase_run(struct vt *v, int col, int row, int count)
|
|
{
|
|
int active = is_active(v);
|
|
|
|
while (count > 0 && row < v->rows) {
|
|
int n = v->cols - col; // to the end of this row
|
|
if (n > count)
|
|
n = count;
|
|
|
|
// Only draw what actually changes.
|
|
//
|
|
// This is the whole reason a terminal keeps a grid. `ESC[J` -- erase to
|
|
// end of screen -- arrives after every shell prompt, and almost every
|
|
// time the screen below the cursor is *already* blank in exactly the
|
|
// colours it is being asked to blank to. Drawing it anyway means writing
|
|
// four megabytes to a framebuffer that runs at about 128 MB/s, which is
|
|
// 32 ms of nothing, on every prompt, and zsh redraws the prompt on every
|
|
// keystroke.
|
|
//
|
|
// The grid already knows. Comparing is free; writing is not.
|
|
int dirty = 0;
|
|
for (int i = 0; i < n; i++) {
|
|
struct vtcell *k = cell_at(v, col + i, row);
|
|
if (k->ch != ' ' || k->fg != v->fg || k->bg != v->bg || k->attr != 0)
|
|
dirty = 1;
|
|
k->ch = ' ';
|
|
k->fg = v->fg;
|
|
k->bg = v->bg;
|
|
k->attr = 0;
|
|
}
|
|
if (active && dirty)
|
|
gfx_fill_rect(col * gfx_char_w(), row * gfx_char_h(),
|
|
n * gfx_char_w(), gfx_char_h(), vt_rgb(v->bg));
|
|
|
|
count -= n;
|
|
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)
|
|
{
|
|
int dirty = 0;
|
|
|
|
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->ch != ' ' || k->fg != v->fg || k->bg != v->bg || k->attr != 0) {
|
|
dirty = 1;
|
|
break;
|
|
}
|
|
}
|
|
if (dirty)
|
|
break;
|
|
}
|
|
for (int r = 0; r < v->rows; r++)
|
|
blank_row(v, r);
|
|
v->cx = v->cy = 0;
|
|
v->wrap_pending = 0;
|
|
|
|
// A screen fill, not a repaint. vt_repaint draws every cell through the
|
|
// glyph path -- eight thousand of them -- to produce a screen that is
|
|
// entirely one colour.
|
|
if (is_active(v) && dirty)
|
|
gfx_clear(vt_rgb(v->bg));
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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;
|
|
}
|
|
|
|
// May the kernel act on the switch chord?
|
|
//
|
|
// Not if the terminal on screen is in VT_PROCESS: a program has said it will
|
|
// handle switching itself, and the whole point of saying so is that the chord
|
|
// belongs to it now. This is the documented Unix answer to "my application
|
|
// wants that key", and it is the one a display server will use -- Schcom, on
|
|
// the terminal it owns, so that Ctrl+Alt+Shift+F2 reaches its own compositor
|
|
// bindings instead of yanking the screen out from under it.
|
|
//
|
|
// What this does *not* do yet: deliver relsig/acqsig and wait for VT_RELDISP.
|
|
// Linux signals the owning process on every switch request and blocks until it
|
|
// answers, which is how a display server gets to release the GPU first. There
|
|
// is nothing here that needs to release anything, so the kernel simply stops
|
|
// switching rather than negotiating. When Schcom needs the handshake, this is
|
|
// where it goes, and struct vt_mode already carries the fields for it.
|
|
int
|
|
vt_kernel_switches(void)
|
|
{
|
|
return vt_current()->mode != VT_PROCESS;
|
|
}
|
|
|
|
// Step to the previous or next terminal that is actually in use.
|
|
//
|
|
// Skipping the idle ones matters: six terminals with two shells on them means
|
|
// four blank screens between the things you wanted, and cycling past them is
|
|
// worse than not having the key. "In use" is `opened`, the same count
|
|
// VT_GETSTATE reports, so what the key does and what `chvt` says are the same
|
|
// fact.
|
|
//
|
|
// If nothing else is open this stays put rather than wrapping to a blank one.
|
|
int
|
|
vt_cycle(int dir)
|
|
{
|
|
if (!vt_ready)
|
|
return -1;
|
|
|
|
for (int step = 1; step <= NVT; step++) {
|
|
int i = vt_active + dir * step;
|
|
while (i < 0) i += NVT;
|
|
while (i >= NVT) i -= NVT;
|
|
if (i == vt_active)
|
|
break;
|
|
if (vts[i].opened > 0)
|
|
return vt_switch(i);
|
|
}
|
|
return 0; // nowhere else to go; not an error
|
|
}
|