diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6510aa0e4..23274a325 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,10 @@ version 0.9.1 [future] - Unicode input: When searching by name search for prefix matches as well as whole word matches +- Dynamically allocate the memory used for the scrollback history buffer. + Reduces startup memory consumption when using very large scrollback + buffer sizes. + - Add an option to not request window attention on bell. - Remote control: Allow matching windows by number (visible position). @@ -28,26 +32,34 @@ version 0.9.1 [future] - hints kitten: Detect bracketed URLs and dont include the closing bracket in the URL. -- When calling pass_selection_to_program use the current directory of the child process as the cwd of the program. +- When calling pass_selection_to_program use the current directory of the child + process as the cwd of the program. - Add macos_hide_from_tasks option to hide kitty from the macOS task switcher -- macOS: When the macos_titlebar_color is set to background change the titlebar colors to match the current background color of the active kitty window +- macOS: When the macos_titlebar_color is set to background change the titlebar + colors to match the current background color of the active kitty window - Add a setting to clear all shortcuts defined up to that point in the config file(s) -- Add a setting (kitty_mod) to change the modifier used by all the default kitty shortcuts, globally +- Add a setting (kitty_mod) to change the modifier used by all the default + kitty shortcuts, globally - Fix Shift+function key not working - Support the F13 to F25 function keys -- Don't fail to start if the user deletes the hintstyle key from their fontconfig configuration. +- Don't fail to start if the user deletes the hintstyle key from their + fontconfig configuration. -- When rendering a private use unicode codepoint and a space as a two cell ligature, set the foreground colors of the space cell to match the colors of the first cell. Works around applications like powerline that use different colors for the two cells. +- When rendering a private use unicode codepoint and a space as a two cell + ligature, set the foreground colors of the space cell to match the colors of + the first cell. Works around applications like powerline that use different + colors for the two cells. -- Fix passing @text to other programs such as when viewing the scrollback buffer not working correctly if kitty is itself scrolled up. +- Fix passing @text to other programs such as when viewing the scrollback + buffer not working correctly if kitty is itself scrolled up. - Fix window focus gained/lost events not being reported to child programs when switching windows/tabs using the various keyboard shortcuts. diff --git a/kitty/data-types.h b/kitty/data-types.h index 76c1432a6..3fcf9d3e7 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -161,15 +161,18 @@ typedef struct { Line *line; } LineBuf; +typedef struct { + Cell *cells; + line_attrs_type *line_attrs; +} HistoryBufSegment; typedef struct { PyObject_HEAD - Cell *buf; - index_type xnum, ynum; + index_type xnum, ynum, num_segments; + HistoryBufSegment* segments; Line *line; index_type start_of_data, count; - line_attrs_type *line_attrs; } HistoryBuf; typedef struct { diff --git a/kitty/history.c b/kitty/history.c index ff4857e6b..508ae2fac 100644 --- a/kitty/history.c +++ b/kitty/history.c @@ -10,10 +10,42 @@ #include extern PyTypeObject Line_Type; +#define SEGMENT_SIZE 2048 + +static inline void +add_segment(HistoryBuf *self) { + self->num_segments += 1; + self->segments = PyMem_Realloc(self->segments, sizeof(HistoryBufSegment) * self->num_segments); + if (self->segments == NULL) fatal("Out of memory allocating new history buffer segment"); + HistoryBufSegment *s = self->segments + self->num_segments - 1; + s->cells = PyMem_Calloc(self->xnum * SEGMENT_SIZE, sizeof(Cell)); + s->line_attrs = PyMem_Calloc(SEGMENT_SIZE, sizeof(line_attrs_type)); + if (s->cells == NULL || s->line_attrs == NULL) fatal("Out of memory allocating new history buffer segment"); +} + +static inline index_type +segment_for(HistoryBuf *self, index_type y) { + index_type seg_num = y / SEGMENT_SIZE; + while (UNLIKELY(seg_num >= self->num_segments && SEGMENT_SIZE * self->num_segments < self->ynum)) add_segment(self); + if (UNLIKELY(seg_num >= self->num_segments)) fatal("Out of bounds access to history buffer line number: %u", y); + return seg_num; +} + +#define seg_ptr(which, stride) { \ + index_type seg_num = segment_for(self, y); \ + y -= seg_num * SEGMENT_SIZE; \ + return self->segments[seg_num].which + y * stride; \ +} static inline Cell* -lineptr(HistoryBuf *linebuf, index_type y) { - return linebuf->buf + y * linebuf->xnum; +lineptr(HistoryBuf *self, index_type y) { + seg_ptr(cells, self->xnum); +} + + +static inline line_attrs_type* +attrptr(HistoryBuf *self, index_type y) { + seg_ptr(line_attrs, 1); } static PyObject * @@ -32,18 +64,12 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { if (self != NULL) { self->xnum = xnum; self->ynum = ynum; - self->buf = PyMem_Calloc(xnum * ynum, sizeof(Cell)); - self->line_attrs = PyMem_Calloc(ynum, sizeof(line_attrs_type)); self->line = alloc_line(); - if (self->buf == NULL || self->line == NULL || self->line_attrs == NULL) { - PyErr_NoMemory(); - PyMem_Free(self->buf); Py_CLEAR(self->line); PyMem_Free(self->line_attrs); - Py_CLEAR(self); - } else { - self->line->xnum = xnum; - for(index_type y = 0; y < self->ynum; y++) { - clear_chars_in_line(lineptr(self, y), self->xnum, BLANK_CHAR); - } + self->num_segments = 0; + add_segment(self); + self->line->xnum = xnum; + for(index_type y = 0; y < self->ynum; y++) { + clear_chars_in_line(lineptr(self, y), self->xnum, BLANK_CHAR); } } @@ -53,8 +79,11 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { static void dealloc(HistoryBuf* self) { Py_CLEAR(self->line); - PyMem_Free(self->buf); - PyMem_Free(self->line_attrs); + for (size_t i = 0; i < self->num_segments; i++) { + PyMem_Free(self->segments[i].cells); + PyMem_Free(self->segments[i].line_attrs); + } + PyMem_Free(self->segments); Py_TYPE(self)->tp_free((PyObject*)self); } @@ -71,8 +100,8 @@ static inline void init_line(HistoryBuf *self, index_type num, Line *l) { // Initialize the line l, setting its pointer to the offsets for the line at index (buffer position) num l->cells = lineptr(self, num); - l->continued = self->line_attrs[num] & CONTINUED_MASK; - l->has_dirty_text = self->line_attrs[num] & TEXT_DIRTY_MASK ? true : false; + l->continued = *attrptr(self, num) & CONTINUED_MASK; + l->has_dirty_text = *attrptr(self, num) & TEXT_DIRTY_MASK ? true : false; } void @@ -82,12 +111,14 @@ historybuf_init_line(HistoryBuf *self, index_type lnum, Line *l) { void historybuf_mark_line_clean(HistoryBuf *self, index_type y) { - self->line_attrs[index_of(self, y)] &= ~TEXT_DIRTY_MASK; + line_attrs_type *p = attrptr(self, index_of(self, y)); + *p &= ~TEXT_DIRTY_MASK; } void historybuf_mark_line_dirty(HistoryBuf *self, index_type y) { - self->line_attrs[index_of(self, y)] |= TEXT_DIRTY_MASK; + line_attrs_type *p = attrptr(self, index_of(self, y)); + *p |= TEXT_DIRTY_MASK; } inline void @@ -105,43 +136,11 @@ historybuf_push(HistoryBuf *self) { return idx; } -bool -historybuf_resize(HistoryBuf *self, index_type lines) { - HistoryBuf t = {{0}}; - t.xnum=self->xnum; - t.ynum=lines; - if (t.ynum > 0 && t.ynum != self->ynum) { - t.buf = PyMem_Calloc(t.xnum * t.ynum, sizeof(Cell)); - if (t.buf == NULL) { PyErr_NoMemory(); return false; } - t.line_attrs = PyMem_Calloc(t.ynum, sizeof(line_attrs_type)); - if (t.line_attrs == NULL) { PyMem_Free(t.buf); PyErr_NoMemory(); return false; } - t.count = MIN(self->count, t.ynum); - for (index_type s=0; s < t.count; s++) { - index_type si = index_of(self, s), ti = index_of(&t, s); - copy_cells(lineptr(self, si), lineptr(&t, ti), t.xnum); - t.line_attrs[ti] = self->line_attrs[si]; - } - self->count = t.count; - self->start_of_data = t.start_of_data; - self->ynum = t.ynum; - PyMem_Free(self->buf); PyMem_Free(self->line_attrs); - self->buf = t.buf; self->line_attrs = t.line_attrs; - } - return true; -} - void historybuf_add_line(HistoryBuf *self, const Line *line) { index_type idx = historybuf_push(self); copy_line(line, self->line); - self->line_attrs[idx] = (line->continued & CONTINUED_MASK) | (line->has_dirty_text ? TEXT_DIRTY_MASK : 0); -} - -static PyObject* -change_num_of_lines(HistoryBuf *self, PyObject *val) { -#define change_num_of_lines_doc "Change the number of lines in this buffer" - if(!historybuf_resize(self, (index_type)PyLong_AsUnsignedLong(val))) return NULL; - Py_RETURN_NONE; + *attrptr(self, idx) = (line->continued & CONTINUED_MASK) | (line->has_dirty_text ? TEXT_DIRTY_MASK : 0); } static PyObject* @@ -188,7 +187,7 @@ as_ansi(HistoryBuf *self, PyObject *callback) { for(unsigned int i = 0; i < self->count; i++) { init_line(self, i, &l); if (i < self->count - 1) { - l.continued = self->line_attrs[index_of(self, i + 1)] & CONTINUED_MASK; + l.continued = *attrptr(self, index_of(self, i + 1)) & CONTINUED_MASK; } else l.continued = false; index_type num = line_as_ansi(&l, t, 5120); if (!(l.continued) && num < 5119) t[num++] = 10; // 10 = \n @@ -219,7 +218,7 @@ dirty_lines(HistoryBuf *self, PyObject *a UNUSED) { #define dirty_lines_doc "dirty_lines() -> Line numbers of all lines that have dirty text." PyObject *ans = PyList_New(0); for (index_type i = 0; i < self->ynum; i++) { - if (self->line_attrs[i] & TEXT_DIRTY_MASK) { + if (*attrptr(self, i) & TEXT_DIRTY_MASK) { PyList_Append(ans, PyLong_FromUnsignedLong(i)); } } @@ -232,7 +231,6 @@ static PyObject* rewrap(HistoryBuf *self, PyObject *args); #define rewrap_doc "" static PyMethodDef methods[] = { - METHOD(change_num_of_lines, METH_O) METHOD(line, METH_O) METHOD(as_ansi, METH_O) METHODB(as_text, METH_VARARGS), @@ -275,19 +273,22 @@ HistoryBuf *alloc_historybuf(unsigned int lines, unsigned int columns) { #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 ? (src->line_attrs[map_src_index(src_y + 1)] & CONTINUED_MASK) : false) +#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) dest->line_attrs[historybuf_push(dest)] = cont & CONTINUED_MASK; dest->line->continued = cont; +#define next_dest_line(cont) *attrptr(dest, historybuf_push(dest)) = 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) { - // Fast path + while(other->num_segments < self->num_segments) add_segment(other); if (other->xnum == self->xnum && other->ynum == self->ynum) { - memcpy(other->buf, self->buf, sizeof(Cell) * self->xnum * self->ynum); - memcpy(other->line_attrs, self->line_attrs, sizeof(line_attrs_type) * self->ynum); + // Fast path + for (index_type i = 0; i < self->num_segments; i++) { + memcpy(other->segments[i].cells, self->segments[i].cells, SEGMENT_SIZE * self->xnum * sizeof(Cell)); + memcpy(other->segments[i].line_attrs, self->segments[i].line_attrs, SEGMENT_SIZE * sizeof(line_attrs_type)); + } other->count = self->count; other->start_of_data = self->start_of_data; return; } @@ -295,7 +296,7 @@ void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other) { index_type x = 0, y = 0; if (self->count > 0) { rewrap_inner(self, other, self->count, NULL, &x, &y); - for (index_type i = 0; i < other->count; i++) other->line_attrs[(other->start_of_data + i) % other->ynum] |= TEXT_DIRTY_MASK; + for (index_type i = 0; i < other->count; i++) *attrptr(other, (other->start_of_data + i) % other->ynum) |= TEXT_DIRTY_MASK; } } diff --git a/kitty/lineops.h b/kitty/lineops.h index 11ae09171..ec93cc7a8 100644 --- a/kitty/lineops.h +++ b/kitty/lineops.h @@ -72,7 +72,6 @@ void linebuf_mark_line_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_refresh_sprite_positions(LineBuf *self); -bool historybuf_resize(HistoryBuf *self, index_type lines); void historybuf_add_line(HistoryBuf *self, const Line *line); void historybuf_rewrap(HistoryBuf *self, HistoryBuf *other); void historybuf_init_line(HistoryBuf *self, index_type num, Line *l); diff --git a/kitty/screen.c b/kitty/screen.c index f256ef6c7..f2e019364 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -212,12 +212,6 @@ screen_rescale_images(Screen *self, unsigned int old_cell_width, unsigned int ol } -static bool -screen_change_scrollback_size(Screen *self, unsigned int size) { - if (size != self->historybuf->ynum) return historybuf_resize(self->historybuf, size); - return true; -} - static PyObject* reset_callbacks(Screen *self, PyObject *a UNUSED) { Py_CLEAR(self->callbacks); @@ -1690,14 +1684,6 @@ start_selection(Screen *self, PyObject *args) { Py_RETURN_NONE; } -static PyObject* -change_scrollback_size(Screen *self, PyObject *args) { - unsigned int count = 1; - if (!PyArg_ParseTuple(args, "|I", &count)) return NULL; - if (!screen_change_scrollback_size(self, MAX(self->lines, count))) return NULL; - Py_RETURN_NONE; -} - static PyObject* text_for_selection(Screen *self, PyObject *a UNUSED) { FullSelectionBoundary start, end; @@ -1965,7 +1951,6 @@ static PyMethodDef methods[] = { MND(delete_lines, METH_VARARGS) MND(insert_characters, METH_VARARGS) MND(delete_characters, METH_VARARGS) - MND(change_scrollback_size, METH_VARARGS) MND(erase_characters, METH_VARARGS) MND(cursor_up, METH_VARARGS) MND(cursor_up1, METH_VARARGS) diff --git a/kitty_tests/datatypes.py b/kitty_tests/datatypes.py index 12a8420f2..fe27cb953 100644 --- a/kitty_tests/datatypes.py +++ b/kitty_tests/datatypes.py @@ -400,6 +400,15 @@ class TestDataTypes(BaseTest): hb.push(lb.line(2)) self.ae(str(hb.line(0)), '2' * hb.xnum) self.ae(str(hb.line(4)), '1' * hb.xnum) + hb = large_hb = HistoryBuf(3000, 5) + c = filled_cursor() + for i in range(3000): + line = lb.line(1) + t = str(i).ljust(5) + line.set_text(t, 0, 5, c) + hb.push(line) + for i in range(3000): + self.ae(str(hb.line(i)).rstrip(), str(3000 - 1 - i)) # rewrap hb = filled_history_buf(5, 5) @@ -427,6 +436,11 @@ class TestDataTypes(BaseTest): for i in range(hb.ynum): self.ae(hb.line(i), hb3.line(i)) + hb2 = HistoryBuf(hb.ynum, hb.xnum) + large_hb.rewrap(hb2) + hb2 = HistoryBuf(large_hb.ynum, large_hb.xnum) + large_hb.rewrap(hb2) + def test_ansi_repr(self): lb = filled_line_buf() l0 = lb.line(0)