tty: the grid was 80x24 on a 160x50 screen, and wrapping was eager
Three symptoms, two causes, both in the terminal layer.
**The grid and TIOCGWINSZ disagreed about the size.** vt_init() sizes every
grid from gfx_cols()/gfx_rows(), and it runs at main.c:75 -- while the display
driver attaches its scanout from the PCI bus walk at main.c:99. So the grids
were the 80x24 fallback, and TIOCGWINSZ, which computed its own answer live,
said 160x50.
That one disagreement produced all three things visible in the report: text
stopped two thirds of the way down the screen, `ls` laid out 160 columns into
an 80-column grid so words split across the wrap ("vers" / "ion"), and zsh's
cursor tracking was wrong on every line.
Fixed in two places, because either alone would leave the trap set:
gfx_attach_scanout() now calls vt_resize(), so the grids learn the real screen
the moment there is one; and TIOCGWINSZ reports v->cols/v->rows rather than
measuring again. One number, from the terminal being asked.
**Wrapping was eager.** A glyph printed in the last column moved the cursor to
the next line immediately. Real terminals set a pending-wrap flag and wrap when
the *next* glyph arrives -- which is why a line exactly as wide as the screen
takes one row and not two, and why a program can write the bottom-right cell
without scrolling.
zsh's partial-line marker depends on that rule directly: it writes exactly
COLUMNS-1 spaces and expects to still be on the line. With eager wrap it left a
reverse-video '#' above every prompt, which is the hashtag in the report. Both
are gone.
Verified by screendump: `ls /proc` now fits one line with "version" intact, no
markers, and output fills the screen to the last row. schfs-check passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
parent
1c1b0c9570
commit
a5b0d5a0ef
5 changed files with 137 additions and 25 deletions
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue