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
|
|
@ -35,13 +35,16 @@ int bflush(uint dev);
|
|||
// console.c
|
||||
struct termios;
|
||||
void consoleinit(void);
|
||||
extern int tty_fg_pgrp;
|
||||
// The terminal's line-discipline settings. TCGETS/TCSETS in sys_fs.c are the
|
||||
// only callers; the console honours ICANON, ECHO, ECHOE, ECHONL, ISIG,
|
||||
// ICRNL, INLCR, IGNCR, ISTRIP and VMIN.
|
||||
void cons_get_termios(struct termios *);
|
||||
void cons_set_termios(const struct termios *);
|
||||
void cons_flush_input(void);
|
||||
void cons_get_termios(int minor, struct termios *);
|
||||
void cons_set_termios(int minor, const struct termios *);
|
||||
void cons_flush_input(int minor);
|
||||
int cons_fg_pgrp(int minor);
|
||||
void cons_set_fg_pgrp(int minor, int pg);
|
||||
int console_vt_hotkey(int key);
|
||||
struct vt *vt_for_minor(int minor);
|
||||
extern uint lapic_timer_hz; // measured, kernel/arch/x86_64/lapic.c
|
||||
extern uint64 tsc_hz;
|
||||
void cprintf(char*, ...);
|
||||
|
|
@ -619,7 +622,7 @@ pml4e_t* copyuvm(pml4e_t*, uint64);
|
|||
void switchuvm(struct proc*);
|
||||
void switchkvm(void);
|
||||
int pipe_poll(struct pipe*, int readable, int writable);
|
||||
int console_input_ready(void);
|
||||
int console_input_ready(int minor);
|
||||
int copyin(pml4e_t*, addr_t, void*, uint64);
|
||||
int copyout(pml4e_t*, addr_t, void*, uint64);
|
||||
void vm_unmap_range(pml4e_t*, uint64, uint64);
|
||||
|
|
|
|||
|
|
@ -72,6 +72,11 @@ void gfx_set_bg(uint32 color);
|
|||
void gfx_screen_reset(void);
|
||||
int gfx_cols(void);
|
||||
int gfx_rows(void);
|
||||
|
||||
// For the text layer in kernel/dev/vt.c: one cell with its own colours, and
|
||||
// a screen scroll that leaves the cursor alone.
|
||||
void gfx_draw_cell(int col, int row, uchar glyph, uint32 fg, uint32 bg);
|
||||
void gfx_scroll_up(uint32 bg);
|
||||
void gfx_cursor_get(int *col, int *row);
|
||||
void gfx_cursor_set(int col, int row);
|
||||
void gfx_erase_display(int mode);
|
||||
|
|
|
|||
|
|
@ -28,6 +28,15 @@
|
|||
#define KEY_INS 0xE8
|
||||
#define KEY_DEL 0xE9
|
||||
|
||||
// Alt+F1..Alt+F6 -- switch virtual terminal.
|
||||
//
|
||||
// Above 0xFF deliberately. kbdgetc returns an int and consoleintr takes one,
|
||||
// and every value that fits in a byte is already either a character or a
|
||||
// UTF-8 continuation byte. A chord that collided with one of those would be
|
||||
// typed into the terminal instead of switching away from it.
|
||||
#define KEY_ALT_F1 0x120
|
||||
#define SCAN_F1 0x3B // F1..F6 are 0x3B..0x40
|
||||
|
||||
// C('A') == Control-A
|
||||
#define C(x) (x - '@')
|
||||
|
||||
|
|
|
|||
|
|
@ -123,6 +123,21 @@ struct rlimit {
|
|||
#define TIOCGWINSZ 0x5413
|
||||
#define TIOCSWINSZ 0x5414
|
||||
#define TIOCSCTTY 0x540E
|
||||
|
||||
// Virtual terminal control, Linux's numbers. The argument to VT_ACTIVATE and
|
||||
// VT_WAITACTIVE is a 1-based terminal number passed by value, not a pointer.
|
||||
#define VT_OPENQRY 0x5600 // first free terminal, out through a pointer
|
||||
#define VT_GETMODE 0x5601
|
||||
#define VT_SETMODE 0x5602
|
||||
#define VT_GETSTATE 0x5603 // struct vt_stat
|
||||
#define VT_ACTIVATE 0x5606
|
||||
#define VT_WAITACTIVE 0x5607
|
||||
|
||||
struct vt_stat {
|
||||
uint16 v_active; // active terminal, 1-based
|
||||
uint16 v_signal; // unused; Linux keeps it for VT_SETMODE
|
||||
uint16 v_state; // bitmask of terminals in use, bit 1 = tty1
|
||||
};
|
||||
#define TIOCGSERIAL 0x541E
|
||||
#define FIONREAD 0x541B
|
||||
// FICLONE: share another file's data blocks with this one. The argument is a
|
||||
|
|
|
|||
|
|
@ -245,6 +245,10 @@ struct proc {
|
|||
// which is why mksh's job control never made progress.
|
||||
int pgid;
|
||||
int sid;
|
||||
// Controlling terminal, 1-based (1 = /dev/tty1), 0 = none. TIOCSCTTY sets
|
||||
// it; /dev/tty resolves through it. Was impossible to have before there was
|
||||
// more than one terminal to choose between.
|
||||
int ctty;
|
||||
|
||||
// Real/effective/saved user and group ids. Nothing enforces permissions with
|
||||
// them yet (the on-disk format has no owner field until SchFS), but they
|
||||
|
|
|
|||
139
include/vt.h
Normal file
139
include/vt.h
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Virtual terminals.
|
||||
//
|
||||
// Until this existed there was one console. `/dev/tty` was an alias for
|
||||
// `/dev/console` with the same major and minor, `tty_fg_pgrp` was a single
|
||||
// global int, and TIOCSCTTY was `return 0`. One screen, one foreground
|
||||
// process group, one line discipline, no way to have a second shell.
|
||||
//
|
||||
// The thing that makes switching possible is a backing store. The console
|
||||
// drew straight into the framebuffer and scrolled by moving pixels, which
|
||||
// works exactly as long as nothing else ever wants the screen: there was
|
||||
// nowhere to put terminal 2 while terminal 1 was visible, and nothing to
|
||||
// repaint terminal 1 from when it came back. So each VT owns a grid of cells
|
||||
// and the framebuffer shows whichever grid is active.
|
||||
//
|
||||
// A VT that is not active is not a special case. Writes to it land in its
|
||||
// grid, its cursor moves, it scrolls; the only thing it does not do is draw.
|
||||
// That is the whole switching mechanism: vt_switch repaints, and everything
|
||||
// else is written as though it were always on screen.
|
||||
|
||||
#ifndef VT_H
|
||||
#define VT_H
|
||||
|
||||
#include "types.h"
|
||||
#include "param.h"
|
||||
#include "posix.h"
|
||||
|
||||
// Six, and Linux's numbering: /dev/tty1 through /dev/tty6, reached with
|
||||
// Alt+F1..Alt+F6. Alt+F7 is deliberately left alone -- that is where a
|
||||
// display server goes, and Schcom will want it.
|
||||
#define NVT 6
|
||||
|
||||
// A cell.
|
||||
//
|
||||
// `ch` is the glyph the font draws, not a codepoint: the console maps UTF-8
|
||||
// to CP437 before it gets here, because that is what include/font.h contains.
|
||||
// Storing the codepoint would mean re-running that mapping on every repaint
|
||||
// and getting a different answer for anything the font cannot draw.
|
||||
//
|
||||
// The colours are palette indices rather than RGB. Sixteen ANSI colours plus
|
||||
// the two theme defaults fit in a byte each and make a cell four bytes; the
|
||||
// same cell holding two 32-bit colours would be twelve, and the grids are
|
||||
// static.
|
||||
struct vtcell {
|
||||
uint8 ch;
|
||||
uint8 fg;
|
||||
uint8 bg;
|
||||
uint8 attr; // reserved: bold/underline/reverse, which do not change
|
||||
}; // the glyph and so can be added without touching a grid
|
||||
|
||||
#define VT_FG_DEFAULT 16 // SCH_FG from include/theme.h
|
||||
#define VT_BG_DEFAULT 17 // SCH_BG
|
||||
#define VT_NCOLOUR 18
|
||||
|
||||
// Cell attributes that do not change which glyph is drawn, only how.
|
||||
#define VT_ATTR_REVERSE 0x01
|
||||
|
||||
// The largest console a grid can hold, which is 1920x1200 with the 8x16 font.
|
||||
// A framebuffer wider or taller than this still works -- vt_init clamps the
|
||||
// grid and the rest of the screen stays background -- but it is worth raising
|
||||
// rather than living with, because the clamp is invisible until someone
|
||||
// notices the bottom of their screen never has text on it.
|
||||
#define VT_MAXCOLS 240
|
||||
#define VT_MAXROWS 75
|
||||
|
||||
#define VT_INPUT_BUF 256
|
||||
#define VT_MAXPARM 8
|
||||
|
||||
// Escape parser states. E_OSC and E_OSC_ESC exist for hyperlinks: a terminal
|
||||
// that does not follow OSC 8 must still consume it, or the URL is printed as
|
||||
// text. See the comment in kernel/core/console.c that this came from.
|
||||
enum { VE_NORMAL, VE_ESC, VE_CSI, VE_OSC, VE_OSC_ESC };
|
||||
|
||||
struct vt {
|
||||
int index; // 0-based; /dev/tty<index+1>
|
||||
int cols, rows;
|
||||
|
||||
struct vtcell *cells; // cols*rows, row-major, into vt_storage
|
||||
|
||||
int cx, cy; // cursor, in cells
|
||||
int saved_cx, saved_cy; // DECSC/DECRC
|
||||
|
||||
uint8 fg, bg; // current SGR colours
|
||||
int bold;
|
||||
int attr; // VT_ATTR_* applied to new cells
|
||||
|
||||
// Escape parser.
|
||||
int esc_state;
|
||||
int esc_parm[VT_MAXPARM];
|
||||
int esc_nparm;
|
||||
int esc_private;
|
||||
|
||||
// UTF-8 decoder.
|
||||
uint32 u8_cp;
|
||||
int u8_need;
|
||||
|
||||
// Line discipline. One per terminal, because ICANON and ECHO are properties
|
||||
// of a terminal and not of the machine.
|
||||
struct termios tio;
|
||||
|
||||
struct {
|
||||
char buf[VT_INPUT_BUF];
|
||||
uint r, w, e;
|
||||
uint abort_read;
|
||||
uint eof;
|
||||
} in;
|
||||
|
||||
// Job control. This was one global for the whole system; a terminal each
|
||||
// is the entire point of having more than one.
|
||||
int fg_pgrp;
|
||||
int sid; // session that owns this terminal, 0 = none
|
||||
|
||||
int opened; // times it has been opened, for /dev/tty
|
||||
};
|
||||
|
||||
// The active terminal, and the one the framebuffer is showing.
|
||||
extern int vt_active;
|
||||
|
||||
void vt_init(void);
|
||||
struct vt *vt_get(int i); // 0-based, clamped, never null
|
||||
struct vt *vt_current(void);
|
||||
int vt_switch(int i); // 0-based; repaints. -1 if out of range
|
||||
|
||||
// Output, per terminal. Drawing happens only if `v` is the active one.
|
||||
void vt_feed(struct vt *v, int c); // one output byte, through the parser
|
||||
void vt_putc(struct vt *v, int glyph); // one glyph, already mapped
|
||||
void vt_cursor_set(struct vt *v, int col, int row);
|
||||
void vt_cursor_get(struct vt *v, int *col, int *row);
|
||||
void vt_erase_display(struct vt *v, int mode);
|
||||
void vt_erase_line(struct vt *v, int mode);
|
||||
void vt_clear(struct vt *v);
|
||||
void vt_scroll(struct vt *v);
|
||||
void vt_repaint(struct vt *v); // grid -> framebuffer, if active
|
||||
|
||||
uint32 vt_rgb(uint8 idx); // palette index -> RGB
|
||||
int vt_index(struct vt *v); // 0-based number of a terminal
|
||||
|
||||
#endif // VT_H
|
||||
Loading…
Reference in a new issue