diff --git a/kitty/data-types.h b/kitty/data-types.h index 28cd38bf5..dca3d441c 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -200,6 +200,11 @@ typedef struct { bool rewrap_needed; } PagerHistoryBuf; +typedef struct { + Py_UCS4 *buf; + size_t len, capacity; +} ANSIBuf; + typedef struct { PyObject_HEAD diff --git a/kitty/history.c b/kitty/history.c index 20935cc8f..5dccc1512 100644 --- a/kitty/history.c +++ b/kitty/history.c @@ -93,19 +93,13 @@ pagerhist_clear(HistoryBuf *self) { self->pagerhist = alloc_pagerhist(pagerhist_sz); } -static PyObject * -new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { - HistoryBuf *self; - unsigned int xnum = 1, ynum = 1, pagerhist_sz = 0; - - if (!PyArg_ParseTuple(args, "II|I", &ynum, &xnum, &pagerhist_sz)) return NULL; - +static HistoryBuf* +create_historybuf(PyTypeObject *type, unsigned int xnum, unsigned int ynum, unsigned int pagerhist_sz) { if (xnum == 0 || ynum == 0) { PyErr_SetString(PyExc_ValueError, "Cannot create an empty history buffer"); return NULL; } - - self = (HistoryBuf *)type->tp_alloc(type, 0); + HistoryBuf *self = (HistoryBuf *)type->tp_alloc(type, 0); if (self != NULL) { self->xnum = xnum; self->ynum = ynum; @@ -115,8 +109,15 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { self->line->xnum = xnum; self->pagerhist = alloc_pagerhist(pagerhist_sz); } + return self; +} - return (PyObject*)self; +static PyObject * +new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { + unsigned int xnum = 1, ynum = 1, pagerhist_sz = 0; + if (!PyArg_ParseTuple(args, "II|I", &ynum, &xnum, &pagerhist_sz)) return NULL; + HistoryBuf *ans = create_historybuf(type, xnum, ynum, pagerhist_sz); + return (PyObject*)ans; } static void @@ -179,57 +180,49 @@ historybuf_clear(HistoryBuf *self) { self->start_of_data = 0; } +static inline bool +pagerhist_write(PagerHistoryBuf *ph, const Py_UCS4 *buf, size_t sz) { + if (sz > ph->bufsize) return false; + if (!sz) return true; + if (ph->bufsize - ph->end < sz && !pagerhist_extend(ph, sz)) { + ph->bufend = ph->end; ph->end = 0; + } + memcpy(ph->buffer + ph->end, buf, sz * sizeof(Py_UCS4)); + ph->end += sz; + if (ph->bufend) { + ph->start = ph->end + 1 < ph->bufend ? ph->end + 1 : 0; + } + return true; +} + static inline void -pagerhist_push(HistoryBuf *self) { +pagerhist_push(HistoryBuf *self, ANSIBuf *as_ansi_buf) { PagerHistoryBuf *ph = self->pagerhist; if (!ph) return; - bool truncated; const GPUCell *prev_cell = NULL; Line l = {.xnum=self->xnum}; init_line(self, self->start_of_data, &l); -#define EXPAND_IF_FULL(sz) { \ - if (ph->bufsize - ph->end < sz && !pagerhist_extend(ph, sz)) { \ - ph->bufend = ph->end; ph->end = 0; \ - } \ -} - size_t sz = MAX(1024u, ph->bufsize - ph->end); - sz = MAX(sz, self->xnum + self->xnum); - EXPAND_IF_FULL(sz); - if (ph->start != ph->end && !l.continued) { - ph->buffer[ph->end++] = '\n'; - } - while(sz < ph->maxsz - 2) { - size_t num = line_as_ansi(&l, ph->buffer + ph->end, ph->bufsize - ph->end - 2, &truncated, &prev_cell); - if (!truncated) { - ph->end += num; - ph->buffer[ph->end++] = '\r'; - if (ph->bufend) { - ph->start = ph->end + 1 < ph->bufend ? ph->end + 1 : 0; - } - break; - } - // check if sz is too large too fit in buffer - if (ph->bufsize > ph->maxsz && !ph->end) break; - sz *= 2; - EXPAND_IF_FULL(sz); - } -#undef EXPAND_IF_FULL + line_as_ansi(&l, as_ansi_buf, &prev_cell); + static const Py_UCS4 nl[1] = {'\n'}, cr[1] = {'\r'}, sgr_rest[3] = {0x1b, '[', 'm'}; + if (ph->start != ph->end && !l.continued) pagerhist_write(ph, nl, arraysz(nl)); + pagerhist_write(ph, sgr_rest, arraysz(sgr_rest)); + if (pagerhist_write(ph, as_ansi_buf->buf, as_ansi_buf->len)) pagerhist_write(ph, cr, arraysz(cr)); } static inline index_type -historybuf_push(HistoryBuf *self) { +historybuf_push(HistoryBuf *self, ANSIBuf *as_ansi_buf) { index_type idx = (self->start_of_data + self->count) % self->ynum; init_line(self, idx, self->line); if (self->count == self->ynum) { - pagerhist_push(self); + pagerhist_push(self, as_ansi_buf); self->start_of_data = (self->start_of_data + 1) % self->ynum; } else self->count++; return idx; } void -historybuf_add_line(HistoryBuf *self, const Line *line) { - index_type idx = historybuf_push(self); +historybuf_add_line(HistoryBuf *self, const Line *line, ANSIBuf *as_ansi_buf) { + index_type idx = historybuf_push(self, as_ansi_buf); copy_line(line, self->line); *attrptr(self, idx) = (line->continued & CONTINUED_MASK) | (line->has_dirty_text ? TEXT_DIRTY_MASK : 0); } @@ -266,31 +259,38 @@ push(HistoryBuf *self, PyObject *args) { #define push_doc "Push a line into this buffer, removing the oldest line, if necessary" Line *line; if (!PyArg_ParseTuple(args, "O!", &Line_Type, &line)) return NULL; - historybuf_add_line(self, line); + ANSIBuf as_ansi_buf = {0}; + historybuf_add_line(self, line, &as_ansi_buf); + free(as_ansi_buf.buf); Py_RETURN_NONE; } static PyObject* as_ansi(HistoryBuf *self, PyObject *callback) { #define as_ansi_doc "as_ansi(callback) -> The contents of this buffer as ANSI escaped text. callback is called with each successive line." - static Py_UCS4 t[5120]; Line l = {.xnum=self->xnum}; - bool truncated; const GPUCell *prev_cell = NULL; + ANSIBuf output = {0}; for(unsigned int i = 0; i < self->count; i++) { init_line(self, i, &l); if (i < self->count - 1) { l.continued = *attrptr(self, index_of(self, i + 1)) & CONTINUED_MASK; } else l.continued = false; - index_type num = line_as_ansi(&l, t, 5120, &truncated, &prev_cell); - if (!(l.continued) && num < 5119) t[num++] = 10; // 10 = \n - PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, t, num); - if (ans == NULL) return PyErr_NoMemory(); + line_as_ansi(&l, &output, &prev_cell); + if (!l.continued) { + ensure_space_for(&output, buf, Py_UCS4, output.len + 1, capacity, 2048, false); + output.buf[output.len++] = 10; // 10 = \n + } + PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output.buf, output.len); + if (ans == NULL) { PyErr_NoMemory(); goto end; } PyObject *ret = PyObject_CallFunctionObjArgs(callback, ans, NULL); Py_CLEAR(ans); - if (ret == NULL) return NULL; + if (ret == NULL) goto end; Py_CLEAR(ret); } +end: + free(output.buf); + if (PyErr_Occurred()) return NULL; Py_RETURN_NONE; } @@ -400,7 +400,10 @@ static PyObject* as_text(HistoryBuf *self, PyObject *args) { GetLineWrapper glw = {.self=self}; glw.line.xnum = self->xnum; - return as_text_generic(args, &glw, get_line_wrapper, self->count, self->xnum); + ANSIBuf output = {0}; + PyObject *ans = as_text_generic(args, &glw, get_line_wrapper, self->count, self->xnum, &output); + free(output.buf); + return ans; } @@ -455,7 +458,7 @@ PyTypeObject HistoryBuf_Type = { INIT_TYPE(HistoryBuf) HistoryBuf *alloc_historybuf(unsigned int lines, unsigned int columns, unsigned int pagerhist_sz) { - return (HistoryBuf*)new(&HistoryBuf_Type, Py_BuildValue("III", lines, columns, pagerhist_sz), NULL); + return create_historybuf(&HistoryBuf_Type, columns, lines, pagerhist_sz); } // }}} @@ -467,13 +470,13 @@ HistoryBuf *alloc_historybuf(unsigned int lines, unsigned int columns, unsigned #define is_src_line_continued(src_y) (map_src_index(src_y) < src->ynum - 1 ? (*attrptr(src, map_src_index(src_y + 1)) & CONTINUED_MASK) : false) -#define next_dest_line(cont) *attrptr(dest, historybuf_push(dest)) = cont & CONTINUED_MASK; dest->line->continued = cont; +#define next_dest_line(cont) *attrptr(dest, historybuf_push(dest, as_ansi_buf)) = cont & CONTINUED_MASK; dest->line->continued = cont; #define first_dest_line next_dest_line(false); #include "rewrap.h" -void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other) { +void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other, ANSIBuf *as_ansi_buf) { while(other->num_segments < self->num_segments) add_segment(other); if (other->xnum == self->xnum && other->ynum == self->ynum) { // Fast path @@ -490,7 +493,7 @@ void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other) { other->count = 0; other->start_of_data = 0; index_type x = 0, y = 0; if (self->count > 0) { - rewrap_inner(self, other, self->count, NULL, &x, &y); + rewrap_inner(self, other, self->count, NULL, &x, &y, as_ansi_buf); for (index_type i = 0; i < other->count; i++) *attrptr(other, (other->start_of_data + i) % other->ynum) |= TEXT_DIRTY_MASK; } } @@ -499,6 +502,8 @@ static PyObject* rewrap(HistoryBuf *self, PyObject *args) { HistoryBuf *other; if (!PyArg_ParseTuple(args, "O!", &HistoryBuf_Type, &other)) return NULL; - historybuf_rewrap(self, other); + ANSIBuf as_ansi_buf = {0}; + historybuf_rewrap(self, other, &as_ansi_buf); + free(as_ansi_buf.buf); Py_RETURN_NONE; } diff --git a/kitty/line-buf.c b/kitty/line-buf.c index 58d6839ce..a5f46ff77 100644 --- a/kitty/line-buf.c +++ b/kitty/line-buf.c @@ -399,30 +399,36 @@ delete_lines(LineBuf *self, PyObject *args) { static PyObject* as_ansi(LineBuf *self, PyObject *callback) { #define as_ansi_doc "as_ansi(callback) -> The contents of this buffer as ANSI escaped text. callback is called with each successive line." - static Py_UCS4 t[5120]; Line l = {.xnum=self->xnum}; // remove trailing empty lines index_type ylimit = self->ynum - 1; - bool truncated; const GPUCell *prev_cell = NULL; + ANSIBuf output = {0}; do { init_line(self, (&l), self->line_map[ylimit]); - if (line_as_ansi(&l, t, 5120, &truncated, &prev_cell) != 0) break; + line_as_ansi(&l, &output, &prev_cell); + if (output.len) break; ylimit--; } while(ylimit > 0); for(index_type i = 0; i <= ylimit; i++) { l.continued = ((i < self->ynum - 1) ? self->line_attrs[i+1] : self->line_attrs[i]) & CONTINUED_MASK; init_line(self, (&l), self->line_map[i]); - index_type num = line_as_ansi(&l, t, 5120, &truncated, &prev_cell); - if (!(l.continued) && num < 5119) t[num++] = 10; // 10 = \n - PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, t, num); - if (ans == NULL) return PyErr_NoMemory(); + line_as_ansi(&l, &output, &prev_cell); + if (!l.continued) { + ensure_space_for(&output, buf, Py_UCS4, output.len + 1, capacity, 2048, false); + output.buf[output.len++] = 10; // 10 = \n + } + PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output.buf, output.len); + if (ans == NULL) { PyErr_NoMemory(); goto end; } PyObject *ret = PyObject_CallFunctionObjArgs(callback, ans, NULL); Py_CLEAR(ans); - if (ret == NULL) return NULL; + if (ret == NULL) goto end; Py_CLEAR(ret); } +end: + free(output.buf); + if (PyErr_Occurred()) return NULL; Py_RETURN_NONE; } @@ -435,7 +441,10 @@ get_line(void *x, int y) { static PyObject* as_text(LineBuf *self, PyObject *args) { - return as_text_generic(args, self, get_line, self->ynum, self->xnum); + ANSIBuf output = {0}; + PyObject* ans = as_text_generic(args, self, get_line, self->ynum, self->xnum, &output); + free(output.buf); + return ans; } @@ -528,7 +537,7 @@ copy_old(LineBuf *self, PyObject *y) { #include "rewrap.h" void -linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *num_content_lines_before, index_type *num_content_lines_after, HistoryBuf *historybuf, index_type *track_x, index_type *track_y) { +linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *num_content_lines_before, index_type *num_content_lines_after, HistoryBuf *historybuf, index_type *track_x, index_type *track_y, ANSIBuf *as_ansi_buf) { index_type first, i; bool is_empty = true; @@ -558,7 +567,7 @@ linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *num_content_lines_befo return; } - rewrap_inner(self, other, first + 1, historybuf, track_x, track_y); + rewrap_inner(self, other, first + 1, historybuf, track_x, track_y, as_ansi_buf); *num_content_lines_after = other->line->ynum + 1; for (i = 0; i < *num_content_lines_after; i++) other->line_attrs[i] |= TEXT_DIRTY_MASK; *num_content_lines_before = first + 1; @@ -572,7 +581,9 @@ rewrap(LineBuf *self, PyObject *args) { if (!PyArg_ParseTuple(args, "O!O!", &LineBuf_Type, &other, &HistoryBuf_Type, &historybuf)) return NULL; index_type x = 0, y = 0; - linebuf_rewrap(self, other, &nclb, &ncla, historybuf, &x, &y); + ANSIBuf as_ansi_buf = {0}; + linebuf_rewrap(self, other, &nclb, &ncla, historybuf, &x, &y, &as_ansi_buf); + free(as_ansi_buf.buf); return Py_BuildValue("II", nclb, ncla); } diff --git a/kitty/line.c b/kitty/line.c index 127cf5fc1..bf9ddf8eb 100644 --- a/kitty/line.c +++ b/kitty/line.c @@ -268,23 +268,23 @@ sprite_at(Line* self, PyObject *x) { return Py_BuildValue("HHH", c->sprite_x, c->sprite_y, c->sprite_z); } -static inline bool -write_sgr(const char *val, Py_UCS4 *buf, index_type buflen, index_type *i) { - static char s[128]; - unsigned int num = snprintf(s, sizeof(s), "\x1b[%sm", val); - if (buflen - (*i) < num + 3) return false; - for(unsigned int si=0; si < num; si++) buf[(*i)++] = s[si]; - return true; +static inline void +write_sgr(const char *val, ANSIBuf *output) { +#define W(c) output->buf[output->len++] = c + W(0x1b); W('['); + for (size_t i = 0; val[i] != 0 && i < 122; i++) W(val[i]); + W('m'); +#undef W } -index_type -line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen, bool *truncated, const GPUCell** prev_cell) { -#define WRITE_SGR(val) { if (!write_sgr(val, buf, buflen, &i)) { *truncated = true; return i; } } -#define WRITE_CH(val) if (i > buflen - 1) { *truncated = true; return i; } buf[i++] = val; - - index_type limit = xlimit_for_line(self), i=0; - *truncated = false; - if (limit == 0) return 0; +void +line_as_ansi(Line *self, ANSIBuf *output, const GPUCell** prev_cell) { +#define ENSURE_SPACE(extra) ensure_space_for(output, buf, Py_UCS4, output->len + extra, capacity, 2048, false); +#define WRITE_SGR(val) { ENSURE_SPACE(128); write_sgr(val, output); } +#define WRITE_CH(val) { ENSURE_SPACE(1); output->buf[output->len++] = val; } + output->len = 0; + index_type limit = xlimit_for_line(self); + if (limit == 0) return; char_type previous_width = 0; static const GPUCell blank_cell = { 0 }; @@ -320,21 +320,21 @@ line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen, bool *truncated, const } previous_width = cell->attrs & WIDTH_MASK; } - return i; #undef CMP_ATTRS #undef CMP #undef WRITE_SGR #undef WRITE_CH +#undef ENSURE_SPACE } static PyObject* as_ansi(Line* self, PyObject *a UNUSED) { #define as_ansi_doc "Return the line's contents with ANSI (SGR) escape codes for formatting" - static Py_UCS4 t[5120] = {0}; - bool truncated; const GPUCell *prev_cell = NULL; - index_type num = line_as_ansi(self, t, 5120, &truncated, &prev_cell); - PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, t, num); + ANSIBuf output = {0}; + line_as_ansi(self, &output, &prev_cell); + PyObject *ans = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output.buf, output.len); + free(output.buf); return ans; } @@ -762,7 +762,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, index_type columns) { +as_text_generic(PyObject *args, void *container, get_line_func get_line, index_type lines, index_type columns, ANSIBuf *ansibuf) { PyObject *callback; int as_ansi = 0, insert_wrap_markers = 0; if (!PyArg_ParseTuple(args, "O|pp", &callback, &as_ansi, &insert_wrap_markers)) return NULL; @@ -785,16 +785,15 @@ as_text_generic(PyObject *args, void *container, get_line_func get_line, index_t Py_CLEAR(ret); } if (as_ansi) { - bool truncated; // 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 // pretty sad performance wise, but I guess it will remain till I // get around to writing a nice pager kitten. // see https://github.com/kovidgoyal/kitty/issues/2381 prev_cell = NULL; - index_type num = line_as_ansi(line, buf, columns * 100 - 2, &truncated, &prev_cell); - t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, buf, num); - if (t && num > 0) { + line_as_ansi(line, ansibuf, &prev_cell); + t = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, ansibuf->buf, ansibuf->len); + if (t && ansibuf->len > 0) { ret = PyObject_CallFunctionObjArgs(callback, sgr_reset, NULL); if (ret == NULL) goto end; Py_CLEAR(ret); diff --git a/kitty/lineops.h b/kitty/lineops.h index ee42c7a62..3cd4b9cfc 100644 --- a/kitty/lineops.h +++ b/kitty/lineops.h @@ -77,7 +77,7 @@ void line_add_combining_char(Line *, uint32_t , unsigned int ); index_type line_url_start_at(Line *self, index_type x); index_type line_url_end_at(Line *self, index_type x, bool, char_type, bool); bool line_startswith_url_chars(Line*); -index_type line_as_ansi(Line *self, Py_UCS4 *buf, index_type buflen, bool*, const GPUCell**) __attribute__((nonnull)); +void line_as_ansi(Line *self, ANSIBuf *output, const GPUCell**) __attribute__((nonnull)); unsigned int line_length(Line *self); 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); @@ -94,14 +94,14 @@ void linebuf_clear_line(LineBuf *self, index_type y); void linebuf_insert_lines(LineBuf *self, unsigned int num, unsigned int y, unsigned int bottom); void linebuf_delete_lines(LineBuf *self, index_type num, index_type y, index_type bottom); void linebuf_set_attribute(LineBuf *, unsigned int , unsigned int ); -void linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *, index_type *, HistoryBuf *, index_type *, index_type *); +void linebuf_rewrap(LineBuf *self, LineBuf *other, index_type *, index_type *, HistoryBuf *, index_type *, index_type *, ANSIBuf*); void linebuf_mark_line_dirty(LineBuf *self, index_type y); void linebuf_mark_line_clean(LineBuf *self, index_type y); void linebuf_mark_line_as_not_continued(LineBuf *self, index_type y); unsigned int linebuf_char_width_at(LineBuf *self, index_type x, index_type y); void linebuf_refresh_sprite_positions(LineBuf *self); -void historybuf_add_line(HistoryBuf *self, const Line *line); -void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other); +void historybuf_add_line(HistoryBuf *self, const Line *line, ANSIBuf*); +void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other, ANSIBuf*); void historybuf_init_line(HistoryBuf *self, index_type num, Line *l); CPUCell* historybuf_cpu_cells(HistoryBuf *self, index_type num); void historybuf_mark_line_clean(HistoryBuf *self, index_type y); @@ -110,4 +110,4 @@ 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 *, attrs_type mark); -PyObject* as_text_generic(PyObject *args, void *container, get_line_func get_line, index_type lines, index_type columns); +PyObject* as_text_generic(PyObject *args, void *container, get_line_func get_line, index_type lines, index_type columns, ANSIBuf *ansibuf); diff --git a/kitty/rewrap.h b/kitty/rewrap.h index 7859362c0..af2954449 100644 --- a/kitty/rewrap.h +++ b/kitty/rewrap.h @@ -30,7 +30,7 @@ if (historybuf != NULL) { \ init_dest_line(dest->ynum - 1); \ dest->line->has_dirty_text = true; \ - historybuf_add_line(historybuf, dest->line); \ + historybuf_add_line(historybuf, dest->line, as_ansi_buf); \ }\ linebuf_clear_line(dest, dest->ynum - 1); \ } else dest_y++; \ @@ -50,7 +50,7 @@ copy_range(Line *src, index_type src_at, Line* dest, index_type dest_at, index_t static void -rewrap_inner(BufType *src, BufType *dest, const index_type src_limit, HistoryBuf UNUSED *historybuf, index_type *track_x, index_type *track_y) { +rewrap_inner(BufType *src, BufType *dest, const index_type src_limit, HistoryBuf UNUSED *historybuf, index_type *track_x, index_type *track_y, ANSIBuf *as_ansi_buf) { bool src_line_is_continued = false; index_type src_y = 0, src_x = 0, dest_x = 0, dest_y = 0, num = 0, src_x_limit = 0; diff --git a/kitty/screen.c b/kitty/screen.c index d9bc47631..c578bc9f7 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -187,19 +187,19 @@ screen_dirty_sprite_positions(Screen *self) { } static inline HistoryBuf* -realloc_hb(HistoryBuf *old, unsigned int lines, unsigned int columns) { +realloc_hb(HistoryBuf *old, unsigned int lines, unsigned int columns, ANSIBuf *as_ansi_buf) { HistoryBuf *ans = alloc_historybuf(lines, columns, 0); if (ans == NULL) { PyErr_NoMemory(); return NULL; } ans->pagerhist = old->pagerhist; old->pagerhist = NULL; - historybuf_rewrap(old, ans); + historybuf_rewrap(old, ans, as_ansi_buf); return ans; } static inline LineBuf* -realloc_lb(LineBuf *old, unsigned int lines, unsigned int columns, index_type *nclb, index_type *ncla, HistoryBuf *hb, index_type *x, index_type *y) { +realloc_lb(LineBuf *old, unsigned int lines, unsigned int columns, index_type *nclb, index_type *ncla, HistoryBuf *hb, index_type *x, index_type *y, ANSIBuf *as_ansi_buf) { LineBuf *ans = alloc_linebuf(lines, columns); if (ans == NULL) { PyErr_NoMemory(); return NULL; } - linebuf_rewrap(old, ans, nclb, ncla, hb, x, y); + linebuf_rewrap(old, ans, nclb, ncla, hb, x, y, as_ansi_buf); return ans; } @@ -221,11 +221,11 @@ screen_resize(Screen *self, unsigned int lines, unsigned int columns) { if (!init_overlay_line(self, columns)) return false; // Resize main linebuf - HistoryBuf *nh = realloc_hb(self->historybuf, self->historybuf->ynum, columns); + HistoryBuf *nh = realloc_hb(self->historybuf, self->historybuf->ynum, columns, &self->as_ansi_buf); if (nh == NULL) return false; Py_CLEAR(self->historybuf); self->historybuf = nh; index_type x = self->cursor->x, y = self->cursor->y; - LineBuf *n = realloc_lb(self->main_linebuf, lines, columns, &num_content_lines_before, &num_content_lines_after, self->historybuf, &x, &y); + LineBuf *n = realloc_lb(self->main_linebuf, lines, columns, &num_content_lines_before, &num_content_lines_after, self->historybuf, &x, &y, &self->as_ansi_buf); if (n == NULL) return false; Py_CLEAR(self->main_linebuf); self->main_linebuf = n; if (is_main) setup_cursor(); @@ -233,7 +233,7 @@ screen_resize(Screen *self, unsigned int lines, unsigned int columns) { // Resize alt linebuf x = self->cursor->x, y = self->cursor->y; - n = realloc_lb(self->alt_linebuf, lines, columns, &num_content_lines_before, &num_content_lines_after, NULL, &x, &y); + n = realloc_lb(self->alt_linebuf, lines, columns, &num_content_lines_before, &num_content_lines_after, NULL, &x, &y, &self->as_ansi_buf); if (n == NULL) return false; Py_CLEAR(self->alt_linebuf); self->alt_linebuf = n; if (!is_main) setup_cursor(); @@ -302,6 +302,7 @@ dealloc(Screen* self) { free(self->selections.items); free(self->url_ranges.items); free_hyperlink_pool(self->hyperlink_pool); + free(self->as_ansi_buf.buf); Py_TYPE(self)->tp_free((PyObject*)self); } // }}} @@ -998,7 +999,7 @@ index_selection(const Screen *self, Selections *selections, bool up) { if (self->linebuf == self->main_linebuf && bottom == self->lines - 1) { \ /* Only add to history when no page margins have been set */ \ linebuf_init_line(self->linebuf, bottom); \ - historybuf_add_line(self->historybuf, self->linebuf->line); \ + historybuf_add_line(self->historybuf, self->linebuf->line, &self->as_ansi_buf); \ self->history_line_added_count++; \ } \ linebuf_clear_line(self->linebuf, bottom); \ @@ -2029,17 +2030,17 @@ 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->columns); + return as_text_generic(args, self, get_visual_line, self->lines, self->columns, &self->as_ansi_buf); } static PyObject* as_text_non_visual(Screen *self, PyObject *args) { - return as_text_generic(args, self, get_range_line, self->lines, self->columns); + return as_text_generic(args, self, get_range_line, self->lines, self->columns, &self->as_ansi_buf); } static inline 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->columns); + return as_text_generic(args, self, get_line, self->lines, self->columns, &self->as_ansi_buf); } static PyObject* diff --git a/kitty/screen.h b/kitty/screen.h index 63bc97d8e..6546ae244 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -132,6 +132,7 @@ typedef struct { bool has_activity_since_last_focus; hyperlink_id_type active_hyperlink_id; HYPERLINK_POOL_HANDLE hyperlink_pool; + ANSIBuf as_ansi_buf; } Screen; diff --git a/kitty/window.py b/kitty/window.py index ddd14679d..7bcfb602c 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -142,6 +142,36 @@ def calculate_gl_geometry(window_geometry: WindowGeometry, viewport_width: int, return ScreenGeometry(xstart, ystart, window_geometry.xnum, window_geometry.ynum, dx, dy) +def as_text( + screen: Screen, + as_ansi: bool = False, + add_history: bool = False, + add_wrap_markers: bool = False, + alternate_screen: bool = False +) -> str: + lines: List[str] = [] + add_history = add_history and not (screen.is_using_alternate_linebuf() ^ alternate_screen) + if alternate_screen: + f = screen.as_text_alternate + else: + f = screen.as_text_non_visual if add_history else screen.as_text + f(lines.append, as_ansi, add_wrap_markers) + if add_history: + h: List[str] = [] + screen.historybuf.pagerhist_as_text(h.append) + if h and (not as_ansi or not add_wrap_markers): + sanitizer = text_sanitizer(as_ansi, add_wrap_markers) + h = list(map(sanitizer, h)) + screen.historybuf.as_text(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' + return ''.join(chain(h, lines)) + return ''.join(lines) + + class LoadShaderPrograms: use_selection_fg = True @@ -772,27 +802,7 @@ class Window: add_wrap_markers: bool = False, alternate_screen: bool = False ) -> str: - lines: List[str] = [] - add_history = add_history and not (self.screen.is_using_alternate_linebuf() ^ alternate_screen) - if alternate_screen: - f = self.screen.as_text_alternate - else: - f = self.screen.as_text_non_visual if add_history else self.screen.as_text - f(lines.append, as_ansi, add_wrap_markers) - if add_history: - h: List[str] = [] - self.screen.historybuf.pagerhist_as_text(h.append) - if h and (not as_ansi or not add_wrap_markers): - sanitizer = text_sanitizer(as_ansi, add_wrap_markers) - h = list(map(sanitizer, h)) - self.screen.historybuf.as_text(h.append, as_ansi, add_wrap_markers) - if h: - if not self.screen.linebuf.is_continued(0): - h[-1] += '\n' - if as_ansi: - h[-1] += '\x1b[m' - return ''.join(chain(h, lines)) - return ''.join(lines) + return as_text(self.screen, as_ansi, add_history, add_wrap_markers, alternate_screen) @property def cwd_of_child(self) -> Optional[str]: diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index 9a58d9e9b..e833845d2 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -252,8 +252,7 @@ class TestScreen(BaseTest): self.ae(str(s.line(2)), '4'*5) s.resize(5, 1) self.ae(str(s.line(0)), '4') - hb = s.historybuf - self.ae(str(hb), '3\n3\n3\n3\n3\n2') + self.ae(str(s.historybuf), '3\n3\n3\n3\n3\n2') s = self.create_screen(scrollback=20) s.draw(''.join(str(i) * s.columns for i in range(s.lines*2))) self.ae(str(s.linebuf), '55555\n66666\n77777\n88888\n99999') @@ -453,18 +452,20 @@ class TestScreen(BaseTest): self.ae(s.cursor.x, 2) def test_serialize(self): + from kitty.window import as_text s = self.create_screen() s.draw('ab' * s.columns) s.carriage_return(), s.linefeed() s.draw('c') - def as_text(as_ansi=False): - d = [] - s.as_text(d.append, as_ansi) - return ''.join(d) + self.ae(as_text(s), 'ababababab\nc\n\n') + self.ae(as_text(s, True), '\x1b[mababa\x1b[mbabab\n\x1b[mc\n\n') - self.ae(as_text(), 'ababababab\nc\n\n') - self.ae(as_text(True), '\x1b[mababa\x1b[mbabab\n\x1b[mc\n\n') + s = self.create_screen(cols=2, lines=2, scrollback=2) + for i in range(1, 7): + s.select_graphic_rendition(30 + i) + s.draw(f'{i}' * s.columns) + self.ae(as_text(s, True, True), '\x1b[m\x1b[31m11\x1b[m\x1b[32m22\x1b[m\x1b[33m33\x1b[m\x1b[34m44\x1b[m\x1b[m\x1b[35m55\x1b[m\x1b[36m66') def test_user_marking(self):