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:
Nexoniarz 2026-10-25 02:11:10 +01:00
commit fd9d392416
28 changed files with 1577 additions and 47 deletions

View file

@ -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)