diff --git a/kitty/data-types.h b/kitty/data-types.h index 79fd5e058..def0627ea 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -158,6 +158,7 @@ typedef union CellAttrs { uint16_t strike : 1; uint16_t dim : 1; uint16_t mark : 2; + uint16_t next_char_was_wrapped : 1; }; uint16_t val; } CellAttrs; @@ -165,7 +166,7 @@ typedef union CellAttrs { #define WIDTH_MASK (3u) #define DECORATION_MASK (7u) #define NUM_UNDERLINE_STYLES (5u) -#define SGR_MASK (~(((CellAttrs){.width=WIDTH_MASK, .mark=MARK_MASK}).val)) +#define SGR_MASK (~(((CellAttrs){.width=WIDTH_MASK, .mark=MARK_MASK, .next_char_was_wrapped=1}).val)) typedef struct { color_type fg, bg, decoration_fg; @@ -184,7 +185,7 @@ static_assert(sizeof(CPUCell) == 12, "Fix the ordering of CPUCell"); typedef enum { UNKNOWN_PROMPT_KIND = 0, PROMPT_START = 1, SECONDARY_PROMPT = 2, OUTPUT_START = 3 } PromptKind; typedef union LineAttrs { struct { - uint8_t continued : 1; + uint8_t is_continued : 1; uint8_t has_dirty_text : 1; PromptKind prompt_kind : 2; }; diff --git a/kitty/history.c b/kitty/history.c index bc06d525c..028334ee3 100644 --- a/kitty/history.c +++ b/kitty/history.c @@ -164,6 +164,16 @@ init_line(HistoryBuf *self, index_type num, Line *l) { l->cpu_cells = cpu_lineptr(self, num); l->gpu_cells = gpu_lineptr(self, num); l->attrs = *attrptr(self, num); + if (num > 0) { + l->attrs.is_continued = gpu_lineptr(self, num - 1)[self->xnum-1].attrs.next_char_was_wrapped; + } else { + l->attrs.is_continued = false; + size_t sz; + if (self->pagerhist && self->pagerhist->ringbuf && (sz = ringbuf_bytes_used(self->pagerhist->ringbuf)) > 0) { + size_t pos = ringbuf_findchr(self->pagerhist->ringbuf, '\n', sz - 1); + if (pos >= sz) l->attrs.is_continued = true; // ringbuf does not end with a newline + } + } } void @@ -171,6 +181,11 @@ historybuf_init_line(HistoryBuf *self, index_type lnum, Line *l) { init_line(self, index_of(self, lnum), l); } +bool +history_buf_endswith_wrap(HistoryBuf *self) { + return gpu_lineptr(self, index_of(self, 0))[self->xnum-1].attrs.next_char_was_wrapped; +} + CPUCell* historybuf_cpu_cells(HistoryBuf *self, index_type lnum) { return cpu_lineptr(self, index_of(self, lnum)); @@ -243,9 +258,13 @@ pagerhist_push(HistoryBuf *self, ANSIBuf *as_ansi_buf) { Line l = {.xnum=self->xnum}; init_line(self, self->start_of_data, &l); line_as_ansi(&l, as_ansi_buf, &prev_cell, 0, l.xnum, 0); - if (ringbuf_bytes_used(ph->ringbuf) && !l.attrs.continued) pagerhist_write_bytes(ph, (const uint8_t*)"\n", 1); pagerhist_write_bytes(ph, (const uint8_t*)"\x1b[m", 3); - if (pagerhist_write_ucs4(ph, as_ansi_buf->buf, as_ansi_buf->len)) pagerhist_write_bytes(ph, (const uint8_t*)"\r", 1); + if (pagerhist_write_ucs4(ph, as_ansi_buf->buf, as_ansi_buf->len)) { + char line_end[2]; size_t num = 0; + line_end[num++] = '\r'; + if (!l.gpu_cells[l.xnum - 1].attrs.next_char_was_wrapped) line_end[num++] = '\n'; + pagerhist_write_bytes(ph, (const uint8_t*)line_end, num); + } } static index_type @@ -275,6 +294,13 @@ historybuf_pop_line(HistoryBuf *self, Line *line) { return true; } +static void +history_buf_set_last_char_as_continuation(HistoryBuf *self, index_type y, bool wrapped) { + if (self->count > 0) { + gpu_lineptr(self, index_of(self, y))[self->xnum-1].attrs.next_char_was_wrapped = wrapped; + } +} + static PyObject* line(HistoryBuf *self, PyObject *val) { #define line_doc "Return the line with line number val. This buffer grows upwards, i.e. 0 is the most recently added line" @@ -321,13 +347,10 @@ as_ansi(HistoryBuf *self, PyObject *callback) { ANSIBuf output = {0}; for(unsigned int i = 0; i < self->count; i++) { init_line(self, i, &l); - if (i < self->count - 1) { - l.attrs.continued = attrptr(self, index_of(self, i + 1))->continued; - } else l.attrs.continued = false; line_as_ansi(&l, &output, &prev_cell, 0, l.xnum, 0); - if (!l.attrs.continued) { + if (!l.gpu_cells[l.xnum - 1].attrs.next_char_was_wrapped) { ensure_space_for(&output, buf, Py_UCS4, output.len + 1, capacity, 2048, false); - output.buf[output.len++] = 10; // 10 = \n + output.buf[output.len++] = '\n'; } PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output.buf, output.len); if (ans == NULL) { PyErr_NoMemory(); goto end; } @@ -438,14 +461,11 @@ pagerhist_as_bytes(HistoryBuf *self, PyObject *args) { pagerhist_ensure_start_is_valid_utf8(ph); if (ph->rewrap_needed) pagerhist_rewrap_to(self, self->xnum); - Line l = {.xnum=self->xnum}; get_line(self, 0, &l); size_t sz = ringbuf_bytes_used(ph->ringbuf); - if (!l.attrs.continued) sz += 1; PyObject *ans = PyBytes_FromStringAndSize(NULL, sz); if (!ans) return NULL; uint8_t *buf = (uint8_t*)PyBytes_AS_STRING(ans); ringbuf_memcpy_from(buf, ph->ringbuf, sz); - if (!l.attrs.continued) buf[sz-1] = '\n'; if (upto_output_start) { const uint8_t *p = reverse_find(buf, sz, (const uint8_t*)"\x1b]133;C\x1b\\"); if (p) { @@ -484,7 +504,7 @@ PyObject* as_text_history_buf(HistoryBuf *self, PyObject *args, ANSIBuf *output) { GetLineWrapper glw = {.self=self}; glw.line.xnum = self->xnum; - PyObject *ans = as_text_generic(args, &glw, get_line_wrapper, self->count, output); + PyObject *ans = as_text_generic(args, &glw, get_line_wrapper, self->count, output, true); return ans; } @@ -560,9 +580,7 @@ HistoryBuf *alloc_historybuf(unsigned int lines, unsigned int columns, unsigned #define init_src_line(src_y) init_line(src, map_src_index(src_y), src->line); -#define is_src_line_continued(src_y) (map_src_index(src_y) < src->ynum - 1 ? (attrptr(src, map_src_index(src_y + 1))->continued) : false) - -#define next_dest_line(cont) { LineAttrs *lap = attrptr(dest, historybuf_push(dest, as_ansi_buf)); *lap = src->line->attrs; if (cont) lap->continued = true; dest->line->attrs.continued = cont; } +#define next_dest_line(cont) { history_buf_set_last_char_as_continuation(dest, 0, cont); LineAttrs *lap = attrptr(dest, historybuf_push(dest, as_ansi_buf)); *lap = src->line->attrs; } #define first_dest_line next_dest_line(false); diff --git a/kitty/line-buf.c b/kitty/line-buf.c index 5149fc8b0..5cbb82c8e 100644 --- a/kitty/line-buf.c +++ b/kitty/line-buf.c @@ -130,6 +130,7 @@ linebuf_init_line(LineBuf *self, index_type idx) { self->line->ynum = idx; self->line->xnum = self->xnum; self->line->attrs = self->line_attrs[idx]; + self->line->attrs.is_continued = idx > 0 ? gpu_lineptr(self, self->line_map[idx - 1])[self->xnum - 1].attrs.next_char_was_wrapped : false; init_line(self, self->line, self->line_map[idx]); } @@ -151,6 +152,19 @@ linebuf_char_width_at(LineBuf *self, index_type x, index_type y) { return gpu_lineptr(self, self->line_map[y])[x].attrs.width; } +bool +linebuf_line_ends_with_continuation(LineBuf *self, index_type y) { + return y < self->ynum ? gpu_lineptr(self, self->line_map[y])[self->xnum - 1].attrs.next_char_was_wrapped : false; +} + +void +linebuf_set_last_char_as_continuation(LineBuf *self, index_type y, bool continued) { + if (y < self->ynum) { + gpu_lineptr(self, self->line_map[y])[self->xnum - 1].attrs.next_char_was_wrapped = continued; + } +} + + static PyObject* set_attribute(LineBuf *self, PyObject *args) { #define set_attribute_doc "set_attribute(which, val) -> Set the attribute on all cells in the line." @@ -172,8 +186,8 @@ set_continued(LineBuf *self, PyObject *args) { unsigned int y; int val; if (!PyArg_ParseTuple(args, "Ip", &y, &val)) return NULL; - if (y >= self->ynum) { PyErr_SetString(PyExc_ValueError, "Out of bounds."); return NULL; } - self->line_attrs[y].continued = val; + if (y > self->ynum || y < 1) { PyErr_SetString(PyExc_ValueError, "Out of bounds."); return NULL; } + linebuf_set_last_char_as_continuation(self, y-1, val); Py_RETURN_NONE; } @@ -316,7 +330,7 @@ is_continued(LineBuf *self, PyObject *val) { #define is_continued_doc "is_continued(y) -> Whether the line y is continued or not" unsigned long y = PyLong_AsUnsignedLong(val); if (y >= self->ynum) { PyErr_SetString(PyExc_ValueError, "Out of bounds."); return NULL; } - if (self->line_attrs[y].continued) { Py_RETURN_TRUE; } + if (y > 0 && linebuf_line_ends_with_continuation(self, y-1)) { Py_RETURN_TRUE; } Py_RETURN_FALSE; } @@ -334,7 +348,6 @@ linebuf_insert_lines(LineBuf *self, unsigned int num, unsigned int y, unsigned i self->line_map[i] = self->line_map[i - num]; self->line_attrs[i] = self->line_attrs[i - num]; } - if (y + num < self->ynum) self->line_attrs[y + num].continued = false; for (i = 0; i < num; i++) { self->line_map[y + i] = self->scratch[ylimit - num + i]; } @@ -369,7 +382,6 @@ linebuf_delete_lines(LineBuf *self, index_type num, index_type y, index_type bot self->line_map[i] = self->line_map[i + num]; self->line_attrs[i] = self->line_attrs[i + num]; } - self->line_attrs[y].continued = false; for (i = 0; i < num; i++) { self->line_map[ylimit - num + i] = self->scratch[y + i]; } @@ -414,10 +426,10 @@ as_ansi(LineBuf *self, PyObject *callback) { } while(ylimit > 0); for(index_type i = 0; i <= ylimit; i++) { - l.attrs.continued = self->line_attrs[(i + 1 < self->ynum) ? i+1 : i].continued; + bool output_newline = !linebuf_line_ends_with_continuation(self, i); init_line(self, (&l), self->line_map[i]); line_as_ansi(&l, &output, &prev_cell, 0, l.xnum, 0); - if (!l.attrs.continued) { + if (output_newline) { ensure_space_for(&output, buf, Py_UCS4, output.len + 1, capacity, 2048, false); output.buf[output.len++] = 10; // 10 = \n } @@ -444,7 +456,7 @@ get_line(void *x, int y) { static PyObject* as_text(LineBuf *self, PyObject *args) { ANSIBuf output = {0}; - PyObject* ans = as_text_generic(args, self, get_line, self->ynum, &output); + PyObject* ans = as_text_generic(args, self, get_line, self->ynum, &output, false); free(output.buf); return ans; } diff --git a/kitty/line.c b/kitty/line.c index daa4726cf..bb9cba7a3 100644 --- a/kitty/line.c +++ b/kitty/line.c @@ -229,10 +229,9 @@ cell_as_utf8_for_fallback(CPUCell *cell, char *buf) { PyObject* -unicode_in_range(const Line *self, const index_type start, const index_type limit, const bool include_cc, const char leading_char, const bool skip_zero_cells) { +unicode_in_range(const Line *self, const index_type start, const index_type limit, const bool include_cc, const bool add_trailing_newline, const bool skip_zero_cells) { size_t n = 0; static Py_UCS4 buf[4096]; - if (leading_char) buf[n++] = leading_char; char_type previous_width = 0; for(index_type i = start; i < limit && n < arraysz(buf) - 2 - arraysz(self->cpu_cells->cc_idx); i++) { char_type ch = self->cpu_cells[i].ch; @@ -252,12 +251,15 @@ unicode_in_range(const Line *self, const index_type start, const index_type limi } previous_width = self->gpu_cells[i].attrs.width; } + if (add_trailing_newline && !self->gpu_cells[self->xnum-1].attrs.next_char_was_wrapped && n < arraysz(buf)) { + buf[n++] = '\n'; + } return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, buf, n); } PyObject * line_as_unicode(Line* self, bool skip_zero_cells) { - return unicode_in_range(self, 0, xlimit_for_line(self), true, 0, skip_zero_cells); + return unicode_in_range(self, 0, xlimit_for_line(self), true, false, skip_zero_cells); } static PyObject* @@ -401,11 +403,10 @@ as_ansi(Line* self, PyObject *a UNUSED) { } static PyObject* -is_continued(Line* self, PyObject *a UNUSED) { -#define is_continued_doc "Return the line's continued flag" - PyObject *ans = self->attrs.continued ? Py_True : Py_False; - Py_INCREF(ans); - return ans; +last_char_has_wrapped_flag(Line* self, PyObject *a UNUSED) { +#define last_char_has_wrapped_flag_doc "Return True if the last cell of this line has the wrapped flags set" + if (self->gpu_cells[self->xnum - 1].attrs.next_char_was_wrapped) { Py_RETURN_TRUE; } + Py_RETURN_FALSE; } static PyObject* @@ -839,7 +840,7 @@ mark_text_in_line(PyObject *marker, Line *line) { } PyObject* -as_text_generic(PyObject *args, void *container, get_line_func get_line, index_type lines, ANSIBuf *ansibuf) { +as_text_generic(PyObject *args, void *container, get_line_func get_line, index_type lines, ANSIBuf *ansibuf, bool add_trailing_newline) { #define APPEND(x) { PyObject* retval = PyObject_CallFunctionObjArgs(callback, x, NULL); if (!retval) return NULL; Py_DECREF(retval); } #define APPEND_AND_DECREF(x) { if (x == NULL) { if (PyErr_Occurred()) return NULL; Py_RETURN_NONE; } PyObject* retval = PyObject_CallFunctionObjArgs(callback, x, NULL); Py_CLEAR(x); if (!retval) return NULL; Py_DECREF(retval); } PyObject *callback; @@ -852,10 +853,11 @@ as_text_generic(PyObject *args, void *container, get_line_func get_line, index_t if (nl == NULL || cr == NULL || sgr_reset == NULL) return NULL; const GPUCell *prev_cell = NULL; ansibuf->active_hyperlink_id = 0; + bool need_newline = false; for (index_type y = 0; y < lines; y++) { Line *line = get_line(container, y); if (!line) { if (PyErr_Occurred()) return NULL; break; } - if (!line->attrs.continued && y > 0) APPEND(nl); + if (need_newline) APPEND(nl); if (as_ansi) { // less has a bug where it resets colors when it sees a \r, so work // around it by resetting SGR at the start of every line. This is @@ -871,7 +873,9 @@ as_text_generic(PyObject *args, void *container, get_line_func get_line, index_t } APPEND_AND_DECREF(t); if (insert_wrap_markers) APPEND(cr); + need_newline = !line->gpu_cells[line->xnum-1].attrs.next_char_was_wrapped; } + if (need_newline && add_trailing_newline) APPEND(nl); if (ansibuf->active_hyperlink_id) { ansibuf->active_hyperlink_id = 0; t = PyUnicode_FromString("\x1b]8;;\x1b\\"); @@ -919,7 +923,7 @@ static PyMethodDef methods[] = { METHOD(set_char, METH_VARARGS) METHOD(set_attribute, METH_VARARGS) METHOD(as_ansi, METH_NOARGS) - METHOD(is_continued, METH_NOARGS) + METHOD(last_char_has_wrapped_flag, METH_NOARGS) METHOD(hyperlink_ids, METH_NOARGS) METHOD(width, METH_O) METHOD(url_start_at, METH_O) diff --git a/kitty/lineops.h b/kitty/lineops.h index b7eaaec4f..fc7d058e2 100644 --- a/kitty/lineops.h +++ b/kitty/lineops.h @@ -97,7 +97,7 @@ size_t cell_as_unicode(CPUCell *cell, bool include_cc, Py_UCS4 *buf, char_type); size_t cell_as_unicode_for_fallback(CPUCell *cell, Py_UCS4 *buf); size_t cell_as_utf8(CPUCell *cell, bool include_cc, char *buf, char_type); size_t cell_as_utf8_for_fallback(CPUCell *cell, char *buf); -PyObject* unicode_in_range(const Line *self, const index_type start, const index_type limit, const bool include_cc, const char leading_char, const bool skip_zero_cells); +PyObject* unicode_in_range(const Line *self, const index_type start, const index_type limit, const bool include_cc, const bool add_trailing_newline, const bool skip_zero_cells); PyObject* line_as_unicode(Line *, bool); void linebuf_init_line(LineBuf *, index_type); @@ -113,11 +113,14 @@ void linebuf_mark_line_dirty(LineBuf *self, index_type y); void linebuf_clear_attrs_and_dirty(LineBuf *self, index_type y); void linebuf_mark_line_clean(LineBuf *self, index_type y); unsigned int linebuf_char_width_at(LineBuf *self, index_type x, index_type y); +void linebuf_set_last_char_as_continuation(LineBuf *self, index_type y, bool continued); +bool linebuf_line_ends_with_continuation(LineBuf *self, index_type y); void linebuf_refresh_sprite_positions(LineBuf *self); void historybuf_add_line(HistoryBuf *self, const Line *line, ANSIBuf*); bool historybuf_pop_line(HistoryBuf *, Line *); void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other, ANSIBuf*); void historybuf_init_line(HistoryBuf *self, index_type num, Line *l); +bool history_buf_endswith_wrap(HistoryBuf *self); CPUCell* historybuf_cpu_cells(HistoryBuf *self, index_type num); void historybuf_mark_line_clean(HistoryBuf *self, index_type y); void historybuf_mark_line_dirty(HistoryBuf *self, index_type y); @@ -125,5 +128,5 @@ void historybuf_refresh_sprite_positions(HistoryBuf *self); void historybuf_clear(HistoryBuf *self); void mark_text_in_line(PyObject *marker, Line *line); bool line_has_mark(Line *, uint16_t mark); -PyObject* as_text_generic(PyObject *args, void *container, get_line_func get_line, index_type lines, ANSIBuf *ansibuf); +PyObject* as_text_generic(PyObject *args, void *container, get_line_func get_line, index_type lines, ANSIBuf *ansibuf, bool add_trailing_newline); bool colors_for_cell(Line *self, ColorProfile *cp, index_type *x, color_type *fg, color_type *bg); diff --git a/kitty/rewrap.h b/kitty/rewrap.h index a92fd34cd..696ea6619 100644 --- a/kitty/rewrap.h +++ b/kitty/rewrap.h @@ -15,14 +15,15 @@ #define init_src_line(src_y) linebuf_init_line(src, src_y); #endif -#define set_dest_line_attrs(dest_y, continued_) dest->line_attrs[dest_y] = src->line->attrs; if (continued_) dest->line_attrs[dest_y].continued = true; src->line->attrs.prompt_kind = UNKNOWN_PROMPT_KIND; +#define set_dest_line_attrs(dest_y) dest->line_attrs[dest_y] = src->line->attrs; src->line->attrs.prompt_kind = UNKNOWN_PROMPT_KIND; #ifndef first_dest_line -#define first_dest_line linebuf_init_line(dest, 0); set_dest_line_attrs(0, false) +#define first_dest_line linebuf_init_line(dest, 0); set_dest_line_attrs(0) #endif #ifndef next_dest_line #define next_dest_line(continued) \ + linebuf_set_last_char_as_continuation(dest, dest_y, continued); \ if (dest_y >= dest->ynum - 1) { \ linebuf_index(dest, 0, dest->ynum - 1); \ if (historybuf != NULL) { \ @@ -33,11 +34,11 @@ linebuf_clear_line(dest, dest->ynum - 1, true); \ } else dest_y++; \ linebuf_init_line(dest, dest_y); \ - set_dest_line_attrs(dest_y, continued); + set_dest_line_attrs(dest_y); #endif #ifndef is_src_line_continued -#define is_src_line_continued(src_y) (src_y + 1 < src->ynum ? (src->line_attrs[src_y + 1].continued) : false) +#define is_src_line_continued() (src->line->gpu_cells[src->xnum-1].attrs.next_char_was_wrapped) #endif static inline void @@ -54,7 +55,7 @@ typedef struct TrackCursor { static void rewrap_inner(BufType *src, BufType *dest, const index_type src_limit, HistoryBuf UNUSED *historybuf, TrackCursor *track, ANSIBuf *as_ansi_buf) { - bool src_line_is_continued = false, is_first_line = true; + bool is_first_line = true; index_type src_y = 0, src_x = 0, dest_x = 0, dest_y = 0, num = 0, src_x_limit = 0; TrackCursor tc_end = {.is_sentinel = true }; if (!track) track = &tc_end; @@ -62,11 +63,13 @@ rewrap_inner(BufType *src, BufType *dest, const index_type src_limit, HistoryBuf do { for (TrackCursor *t = track; !t->is_sentinel; t++) t->is_tracked_line = src_y == t->y; init_src_line(src_y); - src_line_is_continued = is_src_line_continued(src_y); + const bool src_line_is_continued = is_src_line_continued(); src_x_limit = src->xnum; if (!src_line_is_continued) { // Trim trailing blanks since there is a hard line break at the end of this line while(src_x_limit && (src->line->cpu_cells[src_x_limit - 1].ch) == BLANK_CHAR) src_x_limit--; + } else { + src->line->gpu_cells[src->xnum-1].attrs.next_char_was_wrapped = false; } for (TrackCursor *t = track; !t->is_sentinel; t++) { if (t->is_tracked_line && t->x >= src_x_limit) t->x = MAX(1u, src_x_limit) - 1; diff --git a/kitty/screen.c b/kitty/screen.c index 6ba03ab93..aad13644f 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -317,7 +317,6 @@ found: // so when resizing, simply blank all lines after the current // prompt and trust the shell to redraw them. for (; y < (int)self->main_linebuf->ynum; y++) { - self->main_linebuf->line_attrs[y].continued = false; linebuf_clear_line(self->main_linebuf, y, false); linebuf_init_line(self->main_linebuf, y); if (y <= (int)self->cursor->y) { @@ -506,9 +505,9 @@ move_widened_char(Screen *self, CPUCell* cpu_cell, GPUCell *gpu_cell, index_type line_clear_text(self->linebuf->line, xpos, 1, BLANK_CHAR); if (self->modes.mDECAWM) { // overflow goes onto next line + linebuf_set_last_char_as_continuation(self->linebuf, self->cursor->y, true); screen_carriage_return(self); screen_linefeed(self); - self->linebuf->line_attrs[self->cursor->y].continued = true; linebuf_init_line(self->linebuf, self->cursor->y); dest_cpu = self->linebuf->line->cpu_cells; dest_gpu = self->linebuf->line->gpu_cells; @@ -680,9 +679,9 @@ draw_codepoint(Screen *self, char_type och, bool from_input_stream) { if (from_input_stream) self->last_graphic_char = ch; if (UNLIKELY(self->columns - self->cursor->x < (unsigned int)char_width)) { if (self->modes.mDECAWM) { + linebuf_set_last_char_as_continuation(self->linebuf, self->cursor->y, true); screen_carriage_return(self); screen_linefeed(self); - self->linebuf->line_attrs[self->cursor->y].continued = true; } else { self->cursor->x = self->columns - char_width; } @@ -740,7 +739,7 @@ get_overlay_text(Screen *self) { if (ol.ynum >= self->lines || ol.xnum >= self->columns || !ol.xnum) return NULL; Line *line = range_line_(self, ol.ynum); if (!line) return NULL; - return unicode_in_range(line, ol.xstart, ol.xstart + ol.xnum, true, 0, true); + return unicode_in_range(line, ol.xstart, ol.xstart + ol.xnum, true, false, true); #undef ol } @@ -1368,7 +1367,6 @@ screen_linefeed(Screen *self) { bool in_margins = cursor_within_margins(self); screen_index(self); if (self->modes.mLNM) screen_carriage_return(self); - if (self->cursor->y < self->lines) self->linebuf->line_attrs[self->cursor->y].continued = false; screen_ensure_bounds(self, false, in_margins); } @@ -1674,6 +1672,7 @@ screen_erase_in_display(Screen *self, unsigned int how, bool private) { linebuf_init_line(self->linebuf, i); if (private) { line_clear_text(self->linebuf->line, 0, self->columns, BLANK_CHAR); + linebuf_set_last_char_as_continuation(self->linebuf, i, false); } else { line_apply_cursor(self->linebuf->line, self->cursor, 0, self->columns, true); } @@ -2312,6 +2311,15 @@ num_lines_between_selection_boundaries(const SelectionBoundary *a, const Selecti typedef Line*(linefunc_t)(Screen*, int); +static Line* +init_line(Screen *self, index_type y) { + linebuf_init_line(self->linebuf, y); + if (y == 0 && self->linebuf == self->main_linebuf) { + if (history_buf_endswith_wrap(self->historybuf)) self->linebuf->line->attrs.is_continued = true; + } + return self->linebuf->line; +} + static Line* visual_line_(Screen *self, int y_) { index_type y = MAX(0, y_); @@ -2322,8 +2330,7 @@ visual_line_(Screen *self, int y_) { } y -= self->scrolled_by; } - linebuf_init_line(self->linebuf, y); - return self->linebuf->line; + return init_line(self, y); } static Line* @@ -2332,8 +2339,7 @@ range_line_(Screen *self, int y) { historybuf_init_line(self->historybuf, -(y + 1), self->historybuf->line); return self->historybuf->line; } - linebuf_init_line(self->linebuf, y); - return self->linebuf->line; + return init_line(self, y); } static Line* @@ -2512,7 +2518,6 @@ text_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool st for (int i = 0, y = idata.y; y < limit; y++, i++) { Line *line = range_line_(self, y); XRange xr = xrange_for_iteration(&idata, y, line); - char leading_char = (i > 0 && insert_newlines && !line->attrs.continued) ? '\n' : 0; index_type x_limit = xr.x_limit; if (strip_trailing_whitespace) { index_type new_limit = limit_without_trailing_whitespace(line, x_limit); @@ -2526,7 +2531,7 @@ text_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool st } } } - PyObject *text = unicode_in_range(line, xr.x, x_limit, true, leading_char, false); + PyObject *text = unicode_in_range(line, xr.x, x_limit, true, insert_newlines && y != limit-1, false); if (text == NULL) { Py_DECREF(ans); return PyErr_NoMemory(); } PyTuple_SET_ITEM(ans, i, text); } @@ -2544,12 +2549,12 @@ ansi_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool st ANSIBuf output = {0}; const GPUCell *prev_cell = NULL; bool has_escape_codes = false; + bool need_newline = false; for (int i = 0, y = idata.y; y < limit; y++, i++) { Line *line = range_line_(self, y); XRange xr = xrange_for_iteration(&idata, y, line); output.len = 0; - char_type prefix_char = 0; - if (i > 0 && insert_newlines && !line->attrs.continued) prefix_char = '\n'; + char_type prefix_char = need_newline ? '\n' : 0; index_type x_limit = xr.x_limit; if (strip_trailing_whitespace) { index_type new_limit = limit_without_trailing_whitespace(line, x_limit); @@ -2562,6 +2567,7 @@ ansi_for_range(Screen *self, const Selection *sel, bool insert_newlines, bool st } } if (line_as_ansi(line, &output, &prev_cell, xr.x, x_limit, prefix_char)) has_escape_codes = true; + need_newline = insert_newlines && !line->gpu_cells[line->xnum-1].attrs.next_char_was_wrapped; PyObject *t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output.buf, output.len); if (!t) return NULL; PyTuple_SET_ITEM(ans, i, t); @@ -2792,12 +2798,12 @@ static Line* get_range_line(void *x, int y) { return range_line_(x, y); } static PyObject* as_text(Screen *self, PyObject *args) { - return as_text_generic(args, self, get_visual_line, self->lines, &self->as_ansi_buf); + return as_text_generic(args, self, get_visual_line, self->lines, &self->as_ansi_buf, false); } static PyObject* as_text_non_visual(Screen *self, PyObject *args) { - return as_text_generic(args, self, get_range_line, self->lines, &self->as_ansi_buf); + return as_text_generic(args, self, get_range_line, self->lines, &self->as_ansi_buf, false); } static PyObject* @@ -2807,7 +2813,7 @@ as_text_for_history_buf(Screen *self, PyObject *args) { static PyObject* as_text_generic_wrapper(Screen *self, PyObject *args, get_line_func get_line) { - return as_text_generic(args, self, get_line, self->lines, &self->as_ansi_buf); + return as_text_generic(args, self, get_line, self->lines, &self->as_ansi_buf, false); } static PyObject* @@ -2849,7 +2855,7 @@ find_cmd_output(Screen *self, OutputOffset *oo, index_type start_screen_y, unsig found_prompt = true; // change direction to downwards to find command output direction = 1; - } else if (line && line->attrs.prompt_kind == OUTPUT_START && !line->attrs.continued) { + } else if (line && line->attrs.prompt_kind == OUTPUT_START && !line->attrs.is_continued) { found_output = true; start = y1; found_prompt = true; // keep finding the first output start upwards @@ -2863,14 +2869,14 @@ find_cmd_output(Screen *self, OutputOffset *oo, index_type start_screen_y, unsig // find upwards: find prompt after the output, and the first output while (y1 >= upward_limit) { line = checked_range_line(self, y1); - if (line && line->attrs.prompt_kind == PROMPT_START && !line->attrs.continued) { + if (line && line->attrs.prompt_kind == PROMPT_START && !line->attrs.is_continued) { if (direction == 0) { // find around: stop at prompt start start = y1 + 1; break; } found_next_prompt = true; end = y1; - } else if (line && line->attrs.prompt_kind == OUTPUT_START && !line->attrs.continued) { + } else if (line && line->attrs.prompt_kind == OUTPUT_START && !line->attrs.is_continued) { start = y1; break; } @@ -2941,7 +2947,7 @@ cmd_output(Screen *self, PyObject *args) { bool reached_upper_limit = false; while (!found && !reached_upper_limit) { line = checked_range_line(self, y); - if (!line || (line->attrs.prompt_kind == OUTPUT_START && !line->attrs.continued)) { + if (!line || (line->attrs.prompt_kind == OUTPUT_START && !line->attrs.is_continued)) { int start = line ? y : y + 1; reached_upper_limit = !line; int y2 = start; unsigned int num_lines = 0; bool found_content = false; @@ -2964,7 +2970,7 @@ cmd_output(Screen *self, PyObject *args) { return NULL; } if (found) { - DECREF_AFTER_FUNCTION PyObject *ret = as_text_generic(as_text_args, &oo, get_line_from_offset, oo.num_lines, &self->as_ansi_buf); + DECREF_AFTER_FUNCTION PyObject *ret = as_text_generic(as_text_args, &oo, get_line_from_offset, oo.num_lines, &self->as_ansi_buf, false); if (!ret) return NULL; } if (oo.reached_upper_limit && self->linebuf == self->main_linebuf && OPT(scrollback_pager_history_size) > 0) Py_RETURN_TRUE; @@ -3364,7 +3370,7 @@ screen_selection_range_for_word(Screen *self, const index_type x, const index_ty start = x; end = x; while(true) { while(start > 0 && is_ok(start - 1, false)) start--; - if (start > 0 || !line->attrs.continued || *y1 == 0) break; + if (start > 0 || !line->attrs.is_continued || *y1 == 0) break; line = visual_line_(self, *y1 - 1); if (!is_ok(self->columns - 1, false)) break; (*y1)--; start = self->columns - 1; @@ -3374,7 +3380,7 @@ screen_selection_range_for_word(Screen *self, const index_type x, const index_ty while(end < self->columns - 1 && is_ok(end + 1, true)) end++; if (end < self->columns - 1 || *y2 >= self->lines - 1) break; line = visual_line_(self, *y2 + 1); - if (!line->attrs.continued || !is_ok(0, true)) break; + if (!line->attrs.is_continued || !is_ok(0, true)) break; (*y2)++; end = 0; } *s = start; *e = end; @@ -3542,7 +3548,7 @@ screen_mark_hyperlink(Screen *self, index_type x, index_type y) { static index_type continue_line_upwards(Screen *self, index_type top_line, SelectionBoundary *start, SelectionBoundary *end) { - while (top_line > 0 && visual_line_(self, top_line)->attrs.continued) { + while (top_line > 0 && visual_line_(self, top_line)->attrs.is_continued) { if (!screen_selection_range_for_line(self, top_line - 1, &start->x, &end->x)) break; top_line--; } @@ -3551,7 +3557,7 @@ continue_line_upwards(Screen *self, index_type top_line, SelectionBoundary *star static index_type continue_line_downwards(Screen *self, index_type bottom_line, SelectionBoundary *start, SelectionBoundary *end) { - while (bottom_line < self->lines - 1 && visual_line_(self, bottom_line + 1)->attrs.continued) { + while (bottom_line < self->lines - 1 && visual_line_(self, bottom_line + 1)->attrs.is_continued) { if (!screen_selection_range_for_line(self, bottom_line + 1, &start->x, &end->x)) break; bottom_line++; } @@ -3971,7 +3977,7 @@ dump_lines_with_attrs(Screen *self, PyObject *accum) { PyObject_CallFunction(accum, "s", "\x1b[33moutput \x1b[39m"); break; } - if (line->attrs.continued) PyObject_CallFunction(accum, "s", "continued "); + if (line->attrs.is_continued) PyObject_CallFunction(accum, "s", "continued "); if (line->attrs.has_dirty_text) PyObject_CallFunction(accum, "s", "dirty "); PyObject_CallFunction(accum, "s", "\n"); t = line_as_unicode(line, false); diff --git a/kitty/window.py b/kitty/window.py index 129e8b75c..47e3b7b22 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -278,8 +278,6 @@ def as_text( h: List[str] = [pht] if pht else [] screen.as_text_for_history_buf(h.append, as_ansi, add_wrap_markers) if h: - if not screen.linebuf.is_continued(0): - h[-1] += '\n' if as_ansi: h[-1] += '\x1b[m' ans = ''.join(chain(h, lines)) diff --git a/kitty_tests/datatypes.py b/kitty_tests/datatypes.py index bf4b24588..3061883c4 100644 --- a/kitty_tests/datatypes.py +++ b/kitty_tests/datatypes.py @@ -20,11 +20,10 @@ from . import BaseTest, filled_cursor, filled_history_buf, filled_line_buf def create_lbuf(*lines): maxw = max(map(len, lines)) ans = LineBuf(len(lines), maxw) - prev_full_length = False for i, l0 in enumerate(lines): ans.line(i).set_text(l0, 0, len(l0), C()) - ans.set_continued(i, prev_full_length) - prev_full_length = len(l0) == maxw + if i > 0: + ans.set_continued(i, len(lines[i-1]) == maxw) return ans @@ -73,9 +72,9 @@ class TestDataTypes(BaseTest): self.assertFalse(c.reverse) self.assertTrue(c.bold) self.assertFalse(old.is_continued(0)) - old.set_continued(0, True) - self.assertTrue(old.is_continued(0)) - self.assertFalse(old.is_continued(1)) + old.set_continued(1, True) + self.assertTrue(old.is_continued(1)) + self.assertFalse(old.is_continued(0)) lb = filled_line_buf(5, 5, filled_cursor()) lb2 = LineBuf(5, 5) @@ -518,7 +517,7 @@ class TestDataTypes(BaseTest): self.ae(l2.as_ansi(), '\x1b[1;2;3;7;9;34;48:2:1:2:3;58:5:5m' '1' '\x1b[22;23;27;29;39;49;59m' '0000') lb = filled_line_buf() - for i in range(lb.ynum): + for i in range(1, lb.ynum + 1): lb.set_continued(i, True) a = [] lb.as_ansi(a.append) diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index 9ada46448..1de9fe08b 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -210,22 +210,23 @@ class TestScreen(BaseTest): s.reset_dirty() s.cursor.x, s.cursor.y = 2, 1 s.cursor.bold = True + self.ae(continuations(s), (True, True, True, True, False)) def all_lines(s): return tuple(str(s.line(i)) for i in range(s.lines)) def continuations(s): - return tuple(s.line(i).is_continued() for i in range(s.lines)) + return tuple(s.line(i).last_char_has_wrapped_flag() for i in range(s.lines)) init() s.erase_in_display(0) self.ae(all_lines(s), ('12345', '12', '', '', '')) - self.ae(continuations(s), (False, True, False, False, False)) + self.ae(continuations(s), (True, False, False, False, False)) init() s.erase_in_display(1) self.ae(all_lines(s), ('', ' 45', '12345', '12345', '12345')) - self.ae(continuations(s), (False, False, True, True, True)) + self.ae(continuations(s), (False, True, True, True, False)) init() s.erase_in_display(2) @@ -547,16 +548,20 @@ class TestScreen(BaseTest): s.draw(str(i) * s.columns) s.start_selection(0, 0) s.update_selection(4, 4) - expected = ('55555', '\n66666', '\n77777', '\n88888', '\n99999') - self.ae(s.text_for_selection(), expected) + + def ts(*args): + return ''.join(s.text_for_selection(*args)) + + expected = ''.join(('55555', '\n66666', '\n77777', '\n88888', '\n99999')) + self.ae(ts(), expected) s.scroll(2, True) - self.ae(s.text_for_selection(), expected) + self.ae(ts(), expected) s.reset() s.draw('ab cd') s.start_selection(0, 0) s.update_selection(1, 3) - self.ae(s.text_for_selection(), ('ab ', 'cd')) - self.ae(s.text_for_selection(False, True), ('ab', 'cd')) + self.ae(ts(), ''.join(('ab ', 'cd'))) + self.ae(ts(False, True), ''.join(('ab', 'cd'))) s.reset() s.draw('ab cd') s.start_selection(0, 0) @@ -630,6 +635,22 @@ class TestScreen(BaseTest): s.draw('bcdef') self.ae(as_text(s, True), '\x1b[ma\x1b]8;;moo\x1b\\bcde\x1b[mf\n\n\n\x1b]8;;\x1b\\') + def test_wrapping_serialization(self): + from kitty.window import as_text + s = self.create_screen(cols=2, lines=2, scrollback=2, options={'scrollback_pager_history_size': 128}) + s.draw('aabbccddeeff') + self.ae(as_text(s, add_history=True), 'aabbccddeeff') + self.assertNotIn('\n', as_text(s, add_history=True, as_ansi=True)) + s = self.create_screen(cols=2, lines=2, scrollback=2, options={'scrollback_pager_history_size': 128}) + s.draw('1'), s.carriage_return(), s.linefeed() + s.draw('2'), s.carriage_return(), s.linefeed() + s.draw('3'), s.carriage_return(), s.linefeed() + s.draw('4'), s.carriage_return(), s.linefeed() + s.draw('5'), s.carriage_return(), s.linefeed() + s.draw('6'), s.carriage_return(), s.linefeed() + s.draw('7') + self.ae(as_text(s, add_history=True), '1\n2\n3\n4\n5\n6\n7') + def test_pagerhist(self): hsz = 8 s = self.create_screen(cols=2, lines=2, scrollback=2, options={'scrollback_pager_history_size': hsz}) @@ -666,17 +687,17 @@ class TestScreen(BaseTest): s = self.create_screen(options={'scrollback_pager_history_size': 2048}) text = '\x1b[msoft\r\x1b[mbreak\nnext😼cat' w(text) - self.ae(contents(), text + '\n') + self.ae(contents(), text) s.historybuf.pagerhist_rewrap(2) - self.ae(contents(), '\x1b[mso\rft\x1b[m\rbr\rea\rk\nne\rxt\r😼\rca\rt\n') + self.ae(contents(), '\x1b[mso\rft\x1b[m\rbr\rea\rk\nne\rxt\r😼\rca\rt') s = self.create_screen(options={'scrollback_pager_history_size': 8}) w('😼') - self.ae(contents(), '😼\n') + self.ae(contents(), '😼') w('abcd') - self.ae(contents(), '😼abcd\n') + self.ae(contents(), '😼abcd') w('e') - self.ae(contents(), 'abcde\n') + self.ae(contents(), 'abcde') def test_user_marking(self):