"Ą Ę Ś Ł Ż" drew as "?????" and the obvious diagnosis -- the font has no glyphs for them -- was wrong twice over. `struct vtcell.ch` was **one byte** holding a CP437 index, and the console mapped UTF-8 down to it on the way in. U+0104 does not fit in a byte. No font could have fixed that; the cell had to get wider first. So: `ch` is a uint32 codepoint, the mapping on the way in is gone, and the cell draws through kernel/dev/ttf.c against JetBrains Mono linked into the image. Cell size comes from the font's measured advance instead of a hardcoded 8x16. The grid costs 864 KB of BSS instead of 432 KB, which is the honest price of holding a codepoint per cell for six terminals at 240x75. The rasteriser needed three fixes before it could be trusted with this. **21 KB of locals on a 4 KB kernel stack.** `ttf_render` held an outline (512 points as three arrays) and a raster (1024 edges) as automatics. KSTACKSIZE is 4096. They walked off the bottom of the stack into whatever was underneath, which surfaced as user processes faulting on addresses they never touched and then a panic inside freevm() on an overwritten page directory. Nothing pointed anywhere near a font. **And then again, recursively.** load_outline() held a 4736-byte outline as a local *and recursed* for composite glyphs. Composite glyphs are exactly Ą, Ę, Ś, Ł, Ż -- so the second crash arrived the moment the first fix let Unicode reach the screen, looking like a fresh bug. Now a static outline per recursion level with a depth cap of 4 (the font's deepest real nesting is 2), and the leaf parse split into load_simple() so its 512-byte flag array is not multiplied by the depth. Worst chain measured from the object file: ~2.3 KB. Neither showed in tools/ttftest, which has an 8 MB stack. That is the standing hazard of one source file built for host and kernel: the host is most forgiving about exactly the resource the kernel is strictest about. **Component transforms were being skipped**, on the stated theory that a wrong transform is worse than none. It is not. `‘` in this font is the comma glyph with a scale of -1 -- a 180-degree rotation -- and the offset that ships with it only lands correctly after the rotation. Without it the mark went twelve pixels into a ten-pixel cell and was clipped away entirely, so the character read as missing from the font rather than as misplaced. F2Dot14 2x2 now, with SCALED_COMPONENT_OFFSET honoured. The glyph cache was ASCII-only, so every accented letter re-rasterised on every repaint: a screenful of Polish did hundreds of bezier flattenings per scroll where the same screenful of English did none. 95 reserved ASCII slots plus 224 shared slots keyed by codepoint, 64 KB pool. Verified 13680 cached renders byte-identical to direct ones across collisions and a pool flush. tools/ttftest speaks UTF-8 now. Iterating bytes meant it could only ever ask for ASCII, and ASCII is entirely simple glyphs -- it could not reach the code path that mattered. Also: resolvconf's success line moves behind /proc/bootmode, same rule as vgpud. A quiet boot opened with "resolvconf: nameserver 10.0.2.3" where the one line was supposed to be "Welcome to SchOS!". Failures stay loud. Verified on screen, not just on the host: Ą Ę Ś Ł Ż ć ń ó ź, Ω λ π Œ Ð Æ ß, ∞ ‘x’ → ≤ all render, panic=0 across four boots. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
900 lines
30 KiB
C
900 lines
30 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;
|
|
|
|
// Backspace moves the cursor. It does **not** erase.
|
|
//
|
|
// This blanked the cell it moved onto, which is what a line-discipline erase
|
|
// does, not what the BS control character does. A program that wants to rub
|
|
// a character out sends `\b \b` -- backspace, space, backspace -- and the
|
|
// separation matters because programs also use plain `\b` simply to go back.
|
|
//
|
|
// zsh does, on every edit of a line you have already typed. Inserting a
|
|
// character in the middle, it sends:
|
|
//
|
|
// echo alpha \b\b zha \b\b
|
|
//
|
|
// -- move back two, rewrite the tail, then move back two again to leave the
|
|
// cursor where the user expects it. Against a destructive backspace that
|
|
// last pair deleted the `ha` it had just drawn, so the line appeared to lose
|
|
// its tail while the buffer was perfectly correct. Which is exactly how it
|
|
// looked: type in the middle of a line and the end of it vanishes.
|
|
case '\b':
|
|
v->wrap_pending = 0;
|
|
if (v->cx > 0)
|
|
v->cx--;
|
|
break;
|
|
|
|
// DEL is ignored, as it is on a real terminal. It is not a movement and it
|
|
// is not an erase; it exists to be discarded on a paper tape.
|
|
case 0x7f:
|
|
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;
|
|
// No upper bound any more. The cell holds a codepoint, so a character the
|
|
// font may not have is stored as itself and the font decides -- which is
|
|
// the difference between a terminal that cannot show Ą and one whose font
|
|
// happens not to. It used to be clamped to 255 and replaced with '?' here,
|
|
// before any font was ever consulted.
|
|
if (glyph < 0)
|
|
return;
|
|
wrap_now(v); // the previous glyph filled the line
|
|
struct vtcell *k = cell_at(v, v->cx, v->cy);
|
|
k->ch = (uint32)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;
|
|
}
|
|
}
|
|
|
|
// Insert and delete characters within a line: ESC[n@ and ESC[nP.
|
|
//
|
|
// These were missing, and what was missing with them was **editing a line you
|
|
// have already typed**. `TERM=linux` declares `ich1=\E[@` and `dch1=\E[P`, so a
|
|
// line editor that inserts a character in the middle sends one of these and
|
|
// then writes the character -- and against a terminal that silently drops the
|
|
// escape, the text after the cursor is overwritten instead of shifted. What
|
|
// that looks like is the end of the line disappearing as you type into it,
|
|
// with the *buffer* perfectly correct, which is a maddening pair of symptoms
|
|
// because the program is doing nothing wrong.
|
|
//
|
|
// The row is shifted in the grid and then repainted from the cursor to the end
|
|
// of the line. No attempt at a hardware scroll: a line is at most a couple of
|
|
// hundred cells and this happens once per keystroke.
|
|
static void
|
|
vt_shift_line(struct vt *v, int n, int insert)
|
|
{
|
|
int row = v->cy, start = v->cx, width = v->cols;
|
|
int i;
|
|
|
|
if (n < 1)
|
|
n = 1;
|
|
if (n > width - start)
|
|
n = width - start;
|
|
|
|
if (insert) {
|
|
for (i = width - 1; i >= start + n; i--)
|
|
*cell_at(v, i, row) = *cell_at(v, i - n, row);
|
|
for (i = start; i < start + n; i++) {
|
|
struct vtcell *k = cell_at(v, i, row);
|
|
k->ch = ' '; k->fg = v->fg; k->bg = v->bg; k->attr = 0;
|
|
}
|
|
} else {
|
|
for (i = start; i < width - n; i++)
|
|
*cell_at(v, i, row) = *cell_at(v, i + n, row);
|
|
for (i = width - n; i < width; i++) {
|
|
struct vtcell *k = cell_at(v, i, row);
|
|
k->ch = ' '; k->fg = v->fg; k->bg = v->bg; k->attr = 0;
|
|
}
|
|
}
|
|
|
|
for (i = start; i < width; i++)
|
|
show(v, i, row);
|
|
}
|
|
|
|
// ESC[nX -- erase n characters where they stand, without moving anything.
|
|
static void
|
|
vt_erase_chars(struct vt *v, int n)
|
|
{
|
|
if (n < 1)
|
|
n = 1;
|
|
if (n > v->cols - v->cx)
|
|
n = v->cols - v->cx;
|
|
erase_run(v, v->cx, v->cy, n);
|
|
}
|
|
|
|
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 '@': vt_shift_line(v, n1, 1); break; // ICH -- insert characters
|
|
case 'P': vt_shift_line(v, n1, 0); break; // DCH -- delete characters
|
|
case 'X': vt_erase_chars(v, n1); break; // ECH -- erase characters
|
|
|
|
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;
|
|
// The codepoint itself, not a CP437 approximation of it. glyph_for() is
|
|
// still here for the bitmap fallback, but the grid no longer needs it:
|
|
// what reaches the font is what the program sent.
|
|
vt_putc(v, (int)v->u8_cp);
|
|
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
|
|
}
|