Pseudo-terminals, and what tmux and htop needed around them
- /dev/ptmx and /dev/pts/N: Unix98 pairs with a full line discipline, window sizes, job control and hangup (kernel/dev/pty.c). - The console speaks scroll regions, insert/delete line, SU/SD, RI/IND/NEL, DECSC/DECRC; swallows charset designations and private CSI sequences; and LF only returns the carriage under ONLCR. - setsid() drops the controlling terminal, so a pane's pty becomes its ctty. - pipe2() honours O_NONBLOCK and O_CLOEXEC (libevent's signal pipe). - File modes are stored and enforced: chmod, fchmod(at), umask, creation modes; /tmp is 1777. - /proc/stat has real user/system/idle; /proc/<pid>/task; kthread flag. - SO_PEERCRED, IOV_MAX 1024. - /proc/strace: name a program and its syscalls go to the kernel log. - World gains htop, btop, tmux, mc, ncdu, fastfetch, games, X toys, NetSurf. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
parent
c65bfa3fa3
commit
fd9d392416
28 changed files with 1577 additions and 47 deletions
177
kernel/dev/vt.c
177
kernel/dev/vt.c
|
|
@ -286,6 +286,99 @@ vt_scroll(struct vt *v)
|
|||
v->cy--;
|
||||
}
|
||||
|
||||
// --- the scrolling region ----------------------------------------------------
|
||||
//
|
||||
// tmux, htop, less and every curses program on TERM=linux scroll part of the
|
||||
// screen: a status line stays put while the rest moves. Without a region the
|
||||
// whole screen scrolled instead, and a pane's output smeared into its
|
||||
// neighbours and over the status bar.
|
||||
|
||||
static int
|
||||
region_bot(struct vt *v)
|
||||
{
|
||||
if (v->scroll_bot > v->scroll_top && v->scroll_bot < v->rows)
|
||||
return v->scroll_bot;
|
||||
return v->rows - 1;
|
||||
}
|
||||
|
||||
static int
|
||||
region_top(struct vt *v)
|
||||
{
|
||||
return v->scroll_top < region_bot(v) ? v->scroll_top : 0;
|
||||
}
|
||||
|
||||
static void
|
||||
draw_rows(struct vt *v, int r0, int r1)
|
||||
{
|
||||
if (!is_active(v))
|
||||
return;
|
||||
for (int r = r0; r <= r1; r++)
|
||||
for (int c = 0; c < v->cols; c++)
|
||||
show(v, c, r);
|
||||
}
|
||||
|
||||
// Rows top..bot move up n; the n exposed at the bottom are blank.
|
||||
static void
|
||||
region_up(struct vt *v, int top, int bot, int n)
|
||||
{
|
||||
if (n <= 0 || top > bot)
|
||||
return;
|
||||
if (n > bot - top + 1)
|
||||
n = bot - top + 1;
|
||||
if (top == 0 && bot == v->rows - 1) {
|
||||
// The whole screen: the framebuffer can be moved instead of redrawn.
|
||||
while (n-- > 0) {
|
||||
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));
|
||||
}
|
||||
return;
|
||||
}
|
||||
memmove(cell_at(v, 0, top), cell_at(v, 0, top + n),
|
||||
(uint64)(bot - top + 1 - n) * v->cols * sizeof(struct vtcell));
|
||||
for (int r = bot - n + 1; r <= bot; r++)
|
||||
blank_row(v, r);
|
||||
draw_rows(v, top, bot);
|
||||
}
|
||||
|
||||
// Rows top..bot move down n; the n exposed at the top are blank.
|
||||
static void
|
||||
region_down(struct vt *v, int top, int bot, int n)
|
||||
{
|
||||
if (n <= 0 || top > bot)
|
||||
return;
|
||||
if (n > bot - top + 1)
|
||||
n = bot - top + 1;
|
||||
memmove(cell_at(v, 0, top + n), cell_at(v, 0, top),
|
||||
(uint64)(bot - top + 1 - n) * v->cols * sizeof(struct vtcell));
|
||||
for (int r = top; r < top + n; r++)
|
||||
blank_row(v, r);
|
||||
draw_rows(v, top, bot);
|
||||
}
|
||||
|
||||
// Down a line, scrolling the region when the cursor is on its last row. Below
|
||||
// the region the cursor only moves, and stops at the bottom of the screen.
|
||||
static void
|
||||
linefeed(struct vt *v)
|
||||
{
|
||||
if (v->cy == region_bot(v))
|
||||
region_up(v, region_top(v), region_bot(v), 1);
|
||||
else if (v->cy < v->rows - 1)
|
||||
v->cy++;
|
||||
}
|
||||
|
||||
// Up a line, the reverse: the region scrolls down from its first row.
|
||||
static void
|
||||
reverse_index(struct vt *v)
|
||||
{
|
||||
if (v->cy == region_top(v))
|
||||
region_down(v, region_top(v), region_bot(v), 1);
|
||||
else if (v->cy > 0)
|
||||
v->cy--;
|
||||
}
|
||||
|
||||
void
|
||||
vt_cursor_set(struct vt *v, int col, int row)
|
||||
{
|
||||
|
|
@ -328,19 +421,23 @@ wrap_now(struct vt *v)
|
|||
return;
|
||||
v->wrap_pending = 0;
|
||||
v->cx = 0;
|
||||
v->cy++;
|
||||
while (v->cy >= v->rows)
|
||||
vt_scroll(v);
|
||||
linefeed(v);
|
||||
}
|
||||
|
||||
void
|
||||
vt_putc(struct vt *v, int glyph)
|
||||
{
|
||||
switch (glyph) {
|
||||
// LF is a line feed: down one row, same column. The carriage return a
|
||||
// shell expects with it is ONLCR's, so it happens only while the terminal
|
||||
// asks for it. Doing it always broke every program that turns OPOST off and
|
||||
// moves down with a bare LF -- tmux does, row after row, and each row of a
|
||||
// right-hand pane landed in column 0, where its ESC [ K erased the left one.
|
||||
case '\n':
|
||||
v->wrap_pending = 0;
|
||||
v->cx = 0;
|
||||
v->cy++;
|
||||
if ((v->tio.c_oflag & (OPOST | ONLCR)) == (OPOST | ONLCR))
|
||||
v->cx = 0;
|
||||
linefeed(v);
|
||||
break;
|
||||
|
||||
case '\r':
|
||||
|
|
@ -759,6 +856,38 @@ vt_csi(struct vt *v, int final)
|
|||
case 's': v->saved_cx = col; v->saved_cy = row; break;
|
||||
case 'u': vt_cursor_set(v, v->saved_cx, v->saved_cy); break;
|
||||
|
||||
// DECSTBM: the scrolling region, 1-based and inclusive, and the cursor home.
|
||||
case 'r': {
|
||||
int t = v->esc_nparm > 0 && v->esc_parm[0] ? v->esc_parm[0] : 1;
|
||||
int b = v->esc_nparm > 1 && v->esc_parm[1] ? v->esc_parm[1] : v->rows;
|
||||
if (b > v->rows) b = v->rows;
|
||||
if (t < b) {
|
||||
v->scroll_top = t - 1;
|
||||
v->scroll_bot = b - 1;
|
||||
} else {
|
||||
v->scroll_top = v->scroll_bot = 0;
|
||||
}
|
||||
vt_cursor_set(v, 0, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
// Insert and delete lines at the cursor, within the region: what a curses
|
||||
// program sends to open or close a gap rather than repaint everything below.
|
||||
case 'L':
|
||||
if (row >= region_top(v) && row <= region_bot(v))
|
||||
region_down(v, row, region_bot(v), n1);
|
||||
v->wrap_pending = 0;
|
||||
break;
|
||||
case 'M':
|
||||
if (row >= region_top(v) && row <= region_bot(v))
|
||||
region_up(v, row, region_bot(v), n1);
|
||||
v->wrap_pending = 0;
|
||||
break;
|
||||
|
||||
// Scroll the region without moving the cursor.
|
||||
case 'S': region_up(v, region_top(v), region_bot(v), n1); break;
|
||||
case 'T': region_down(v, region_top(v), region_bot(v), n1); 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
|
||||
|
|
@ -781,6 +910,37 @@ vt_feed_1(struct vt *v, int c)
|
|||
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;
|
||||
switch (c) {
|
||||
case '7': // DECSC
|
||||
v->saved_cx = v->cx;
|
||||
v->saved_cy = v->cy;
|
||||
break;
|
||||
case '8': // DECRC
|
||||
vt_cursor_set(v, v->saved_cx, v->saved_cy);
|
||||
break;
|
||||
case 'M': // RI: up, scrolling back
|
||||
v->wrap_pending = 0;
|
||||
reverse_index(v);
|
||||
break;
|
||||
case 'D': // IND: down, scrolling
|
||||
v->wrap_pending = 0;
|
||||
linefeed(v);
|
||||
break;
|
||||
case 'E': // NEL: CR and IND
|
||||
v->wrap_pending = 0;
|
||||
v->cx = 0;
|
||||
linefeed(v);
|
||||
break;
|
||||
case '(': case ')': case '*': case '+':
|
||||
// A character set designation. The set itself is not offered, but the
|
||||
// byte naming it must be swallowed: ESC ( B printed a stray "B".
|
||||
v->esc_state = VE_CHARSET;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
|
||||
case VE_CHARSET:
|
||||
v->esc_state = VE_NORMAL;
|
||||
return;
|
||||
|
||||
|
|
@ -798,7 +958,12 @@ vt_feed_1(struct vt *v, int c)
|
|||
return;
|
||||
|
||||
case VE_CSI:
|
||||
if (c == '?') { v->esc_private = 1; return; }
|
||||
// Private markers (< = > ?) and intermediates (space to /): sequences
|
||||
// this terminal does not speak, but must still consume to the final byte.
|
||||
// Ending at the marker instead printed the rest -- tmux asking for
|
||||
// extended keys as ESC [ > 4 ; 1 m left "4;1m" on the screen.
|
||||
if (c >= '<' && c <= '?') { v->esc_private = 1; return; }
|
||||
if (c >= 0x20 && c <= 0x2F) { 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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue