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 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; } 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; 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..7c884cca 100644 --- a/vt.c +++ b/vt.c @@ -162,20 +162,58 @@ 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); + + 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 + 1]; + 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; }