SchOS/kernel/dev/vt.c
Nexoniarz 65906639da tty: AltGr+F1..F6 by default, and AltGr+-/+ to step
The sweet spot was available the whole time and I ruled it out by writing
"AltGr+anything" when the true statement is "AltGr+*letter*".

A Polish layout wants AltGr with a letter -- ą ć ę ł ń ó ś ź ż. AltGr with a
function key produces nothing on any layout, no window manager binds it, and
no application asks for it. It is the one combination the host, applications
and the keyboard layout all leave alone, and the previous three commits talked
themselves out of it on a technicality that was mine, not the hardware's.

Verified rather than argued, with QEMU sendkey and screendump hashes:

    AltGr+F2   -> tty2
    AltGr+F1   -> back, pixel-identical to the start
    AltGr+a    -> reaches zsh, so ą still types

That distinction is only expressible because Right Alt became its own modifier
bit two commits ago. Sharing ALT is harmless for typing -- neither is used to
look up a character -- and made this impossible to say.

Also added, since it was asked for in the same breath: minus and equals step
to the previous and next terminal, on whatever the chord's modifiers are. They
skip terminals that are not in use -- the same `opened` count VT_GETSTATE
reports -- because six terminals with two shells on them means four blank
screens in between, and cycling past those is worse than not having the key.

    AltGr+=  -> tty2, again -> wraps to tty1   (only two in use)
    AltGr+-  -> tty2

chvt -c still moves all of it, and `chvt` reports both halves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-13 13:22:01 +02:00

747 lines
24 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;
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.
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;
v->wrap_pending = 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;
}
// 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
}