diff --git a/docs/architecture/terminals.md b/docs/architecture/terminals.md index f0ab541..924a7e9 100644 --- a/docs/architecture/terminals.md +++ b/docs/architecture/terminals.md @@ -109,6 +109,30 @@ keyboard driver names it and stops. Input goes to whichever terminal is on screen. That is what makes a terminal a terminal: the keyboard follows the eyes. +## Two rules that are easy to get wrong + +**The grid is the only source of truth for the size.** `TIOCGWINSZ` reports +`v->cols`/`v->rows`, not a fresh `gfx_cols()`/`gfx_rows()`. When it computed +its own answer there were two, and they disagreed: the grids are sized at +`vt_init()`, which runs before the display driver is found on the PCI bus, so +they were 80x24 while `TIOCGWINSZ` said 160x50. Text stopped two thirds of the +way down the screen, `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 -- +three symptoms, one disagreement. `vt_resize()` now corrects the grids when a +scanout arrives, and the ioctl reads them. + +**A glyph in the last column does not wrap.** It sets `wrap_pending`, and the +wrap happens when the *next* glyph arrives. Every real terminal behaves this +way and none of them document it clearly. Wrapping immediately makes a line +exactly as wide as the screen take two rows instead of one, and makes it +impossible to write the bottom-right cell without scrolling -- and zsh's +partial-line marker depends on the rule directly: it writes exactly +`COLUMNS - 1` spaces and expects the cursor to still be on that line. With +immediate wrap, a reverse-video `#` appeared above every prompt. + +Anything that moves the cursor deliberately -- CR, LF, backspace, an escape +sequence -- clears the debt. + ## What it does not do - **No scrollback.** The grid is exactly the visible screen. Shift+PageUp does diff --git a/include/vt.h b/include/vt.h index 31ca3c6..b37ebad 100644 --- a/include/vt.h +++ b/include/vt.h @@ -79,6 +79,12 @@ struct vt { struct vtcell *cells; // cols*rows, row-major, into vt_storage int cx, cy; // cursor, in cells + // Deferred wrap, the rule every real terminal follows and none of them + // document well: a glyph printed in the last column does NOT move the + // cursor to the next line. It sets this instead, and the wrap happens when + // the *next* glyph arrives. Anything that moves the cursor deliberately -- + // CR, LF, backspace, an escape sequence -- clears it. + int wrap_pending; int saved_cx, saved_cy; // DECSC/DECRC uint8 fg, bg; // current SGR colours @@ -118,6 +124,7 @@ struct vt { extern int vt_active; void vt_init(void); +void vt_resize(void); // a scanout arrived; re-measure 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 diff --git a/kernel/dev/gfx.c b/kernel/dev/gfx.c index 71853f3..e1821de 100644 --- a/kernel/dev/gfx.c +++ b/kernel/dev/gfx.c @@ -21,6 +21,7 @@ #include "device.h" #include "display.h" #include "gfx.h" +#include "vt.h" #define BACKSPACE 0x100 #define CHAR_HEIGHT 16 @@ -47,6 +48,12 @@ gfx_attach_scanout(const struct fb_info *info) { scanout = *info; fb = info->ptr; + + // Tell the terminals how big the screen actually is. They were sized before + // this driver existed -- the PCI bus is walked long after the terminals are + // set up -- so until this call every grid is the 80x24 fallback. See + // vt_resize(). + vt_resize(); } const struct fb_info * diff --git a/kernel/dev/vt.c b/kernel/dev/vt.c index 7528c83..f8c9734 100644 --- a/kernel/dev/vt.c +++ b/kernel/dev/vt.c @@ -118,18 +118,29 @@ termios_defaults(struct termios *t) t->c_ospeed = 15; } -void -vt_init(void) +// 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(); - - // No framebuffer (serial-only boot): 80x24 is a stated default rather than - // a measurement of nothing. The grids still work and still scroll; nobody - // is looking at them. 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]; @@ -153,6 +164,42 @@ vt_init(void) 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) { @@ -219,6 +266,7 @@ vt_cursor_set(struct vt *v, int col, int row) 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 @@ -239,21 +287,38 @@ put_blank_at(struct vt *v, int col, int row) 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); @@ -265,10 +330,12 @@ vt_putc(struct vt *v, int glyph) // 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: { @@ -282,21 +349,25 @@ vt_putc(struct vt *v, int glyph) 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); - v->cx++; + // 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; } } - if (v->cx >= v->cols) { - v->cx = 0; - v->cy++; - } while (v->cy >= v->rows) vt_scroll(v); } @@ -339,6 +410,7 @@ 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); } diff --git a/kernel/sys/sys_fs.c b/kernel/sys/sys_fs.c index aad94d2..969054f 100644 --- a/kernel/sys/sys_fs.c +++ b/kernel/sys/sys_fs.c @@ -1061,22 +1061,24 @@ addr_t sys_ioctl(void) if (request == TIOCGWINSZ) { struct winsize *ws = (struct winsize*)user_ptr(argp, sizeof(*ws)); if (!ws) return -EFAULT; - // The console's real geometry, not a VT100's. + // The terminal's own grid, and nothing else. // // This used to answer 24x80 unconditionally -- the size of a terminal - // SchOS has never drawn. Every program that lays out a screen asks - // this one question first: a line editor to decide where to wrap, `ls` - // to pick a column count, fastfetch to place its logo beside the text. - // At 1024x768 with the 8x16 font the console is 128x48, so all of them - // were formatting for a screen a third the area of the real one, and - // each looked like a separate bug in a separate program. - int cols = gfx_cols(), rows = gfx_rows(); - // Serial-only boot, with no framebuffer attached: 80x24 is then a - // stated default rather than a wrong measurement. - ws->ws_col = cols > 0 ? (uint16)cols : 80; - ws->ws_row = rows > 0 ? (uint16)rows : 24; - ws->ws_xpixel = (uint16)gfx_width(); - ws->ws_ypixel = (uint16)gfx_height(); + // SchOS has never drawn -- and was then fixed to compute the answer + // live from gfx_cols()/gfx_rows(). That was a second source of truth, + // and it disagreed with the first: the grids are sized once at + // vt_init(), which runs before the display driver is found on the PCI + // bus, so they were 80x24 while this reported 160x50. Programs + // formatted for a screen the grid did not have -- `ls` split words + // across the wrap, text stopped two thirds of the way down, and zsh's + // cursor tracking was wrong on every line. + // + // One number, from the terminal being asked. They cannot disagree. + struct vt *v = vt_for_minor(f->ip->minor); + ws->ws_col = (uint16)v->cols; + ws->ws_row = (uint16)v->rows; + ws->ws_xpixel = (uint16)(v->cols * gfx_char_w()); + ws->ws_ypixel = (uint16)(v->rows * gfx_char_h()); return 0; } // The console's size is a property of the framebuffer, so a request to