From 94b549f93e8cc4372e5ab7dc9c832219f337819c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 5 Jun 2021 22:48:20 +0200 Subject: [PATCH 1/5] vt: emit a tab character if all cells between cursor and tab stop are empty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TAB (\t) move the cursor to the next tab stop. That’s it, according to the specification. However, many terminal emulators try to keep tabs in the grid, to be able to e.g. copy them. That is, copying a text chunk containing tabs should result in tabs being pasted, not spaces. In order to do that, we need to print a tab character to the grid. To improve text reflow of tabs, we also print spaces to the subsequent cells, up until (but not including) the next tab stop. However, we can only do this if all the cells between the cursor and the next tab stop are empty, since (obviously), we cannot overwrite pre-existing characters. Finally, while some fonts render tabs as spaces (i.e. an empty glyph), some use a glyph representing “unprintable” characters, or similar. Thus, we need to exclude cells with tab characters when rendering. --- render.c | 2 +- vt.c | 43 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/render.c b/render.c index 22deaded..68ddf5a6 100644 --- a/render.c +++ b/render.c @@ -620,7 +620,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, if (has_cursor && term->cursor_style == CURSOR_BLOCK && term->kbd_focus) draw_cursor(term, cell, font, pix, &fg, &bg, x, y, cell_cols); - if (cell->wc == 0 || cell->wc >= CELL_SPACER || + if (cell->wc == 0 || cell->wc >= CELL_SPACER || cell->wc == L'\t' || (unlikely(cell->attrs.conceal) && !is_selected)) { goto draw_cursor; diff --git a/vt.c b/vt.c index d0d215f1..fe7ec52a 100644 --- a/vt.c +++ b/vt.c @@ -162,20 +162,57 @@ action_execute(struct terminal *term, uint8_t c) case '\t': { /* HT - horizontal tab */ + int start_col = term->grid->cursor.point.col; int new_col = term->cols - 1; + tll_foreach(term->tab_stops, it) { - if (it->item > term->grid->cursor.point.col) { + if (it->item > start_col) { new_col = it->item; break; } } - xassert(new_col >= term->grid->cursor.point.col); + xassert(new_col >= start_col); + xassert(new_col < term->cols); + + + bool emit_tab_char = true; + struct row *row = term->grid->cur_row; + + /* Check if all cells from here until the next tab stop are empty */ + for (const struct cell *cell = &row->cells[start_col]; + cell < &row->cells[new_col]; + cell++) + { + if (!(cell->wc == 0 || cell->wc == L' ')) { + emit_tab_char = false; + break; + } + } + + /* + * Emit a tab in current cell, and write spaces to the + * subsequent cells, all the way until the next tab stop. + */ + if (emit_tab_char) { + row->dirty = true; + + row->cells[start_col].wc = '\t'; + row->cells[start_col].attrs.clean = 0; + + for (struct cell *cell = &row->cells[start_col + 1]; + cell < &row->cells[new_col]; + cell++) + { + cell->wc = L' '; + cell->attrs.clean = 0; + } + } /* According to the specification, HT _should_ cancel LCF. But * XTerm, and nearly all other emulators, don't. So we follow * suit */ bool lcf = term->grid->cursor.lcf; - term_cursor_right(term, new_col - term->grid->cursor.point.col); + term_cursor_right(term, new_col - start_col); term->grid->cursor.lcf = lcf; break; } From 4d56dd430b5a084b2589998a03d958324aa56af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 6 Jun 2021 12:06:32 +0200 Subject: [PATCH 2/5] extract: consume spaces following a tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit That is, we choose to copy the tab character, and not the spaces it represents. Most importantly, we don’t copy *both* the tab and the spaces. --- extract.c | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/extract.c b/extract.c index b9fd3331..07144597 100644 --- a/extract.c +++ b/extract.c @@ -9,6 +9,7 @@ struct extraction_context { wchar_t *buf; size_t size; size_t idx; + size_t tab_spaces_left; size_t empty_count; size_t newline_count; bool strip_trailing_empty; @@ -191,8 +192,17 @@ extract_one(const struct terminal *term, const struct row *row, } ctx->empty_count = 0; } + + ctx->tab_spaces_left = 0; } + if (cell->wc == L' ' && ctx->tab_spaces_left > 0) { + ctx->tab_spaces_left--; + return true; + } + + ctx->tab_spaces_left = 0; + if (cell->wc == 0) { ctx->empty_count++; ctx->last_row = row; @@ -231,6 +241,19 @@ extract_one(const struct terminal *term, const struct row *row, if (!ensure_size(ctx, 1)) goto err; ctx->buf[ctx->idx++] = cell->wc; + + if (cell->wc == L'\t') { + int next_tab_stop = term->cols - 1; + tll_foreach(term->tab_stops, it) { + if (it->item > col) { + next_tab_stop = it->item; + break; + } + } + + xassert(next_tab_stop >= col); + ctx->tab_spaces_left = next_tab_stop - col; + } } ctx->last_row = row; From 9d3351472d9d6ef9a4314906fe71bf5fa3ed77bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 7 Jun 2021 21:16:38 +0200 Subject: [PATCH 3/5] =?UTF-8?q?vt:=20TAB:=20don=E2=80=99t=20print=20a=20?= =?UTF-8?q?=E2=80=98\t=E2=80=99=20to=20the=20grid=20if=20the=20*current*?= =?UTF-8?q?=20cell=20isn=E2=80=99t=20empty?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If the cursor is already at the right edge, our logic that checked for non-empty cells failed; it didn’t check the current cell. Fix by initializing ‘emit_tab_char’ to true/false, depending on the contents of the current cell. --- vt.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/vt.c b/vt.c index fe7ec52a..7c884cca 100644 --- a/vt.c +++ b/vt.c @@ -174,12 +174,13 @@ action_execute(struct terminal *term, uint8_t c) xassert(new_col >= start_col); xassert(new_col < term->cols); - - bool emit_tab_char = true; struct row *row = term->grid->cur_row; + bool emit_tab_char = (row->cells[start_col].wc == 0 || + row->cells[start_col].wc == L' '); + /* Check if all cells from here until the next tab stop are empty */ - for (const struct cell *cell = &row->cells[start_col]; + for (const struct cell *cell = &row->cells[start_col + 1]; cell < &row->cells[new_col]; cell++) { From 9929a5ce0a38b6d0291f45e8b14e822ca5b84534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 7 Jun 2021 21:18:15 +0200 Subject: [PATCH 4/5] =?UTF-8?q?csi:=20CHT:=20don=E2=80=99t=20alter=20the?= =?UTF-8?q?=20LCF=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes us pass the vttest 11.5.4 (Test non-VT100 -> Test ISO-6429 cursor-movement -> CHT) test. --- csi.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/csi.c b/csi.c index 045a5b6c..2d497636 100644 --- a/csi.c +++ b/csi.c @@ -1120,7 +1120,10 @@ csi_dispatch(struct terminal *term, uint8_t final) } } xassert(new_col >= term->grid->cursor.point.col); + + bool lcf = term->grid->cursor.lcf; term_cursor_right(term, new_col - term->grid->cursor.point.col); + term->grid->cursor.lcf = lcf; } break; } From fff75e082f61ec9ada3724e507aef952c46ffc9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 9 Jun 2021 09:57:20 +0200 Subject: [PATCH 5/5] changelog: tabs are preserved --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b96667dd..d6cdfef1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,8 @@ * Reverse video (`\E[?5h`) now only swaps the default foreground and background colors. Cells with explicit foreground and/or background colors remain unchanged. +* Tabs (`\t`) are now preserved when the window is resized, and when + copying text (https://codeberg.org/dnkl/foot/issues/508). ### Deprecated