diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index beb7a03a5..19f6af7ea 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1235,6 +1235,7 @@ class Screen: def test_commit_write_buffer(self, inp: memoryview, output: memoryview) -> int: ... def test_parse_written_data(self, dump_callback: None = None) -> None: ... def hyperlink_for_id(self, hyperlink_id: int) -> str: ... + def erase_last_command(self, include_prompt: bool = True) -> bool: ... def cursor_at_prompt(self) -> bool: pass diff --git a/kitty/history.c b/kitty/history.c index fbe9e93ca..a63ded7fb 100644 --- a/kitty/history.c +++ b/kitty/history.c @@ -327,6 +327,30 @@ historybuf_pop_line(HistoryBuf *self, Line *line) { return true; } +void +historybuf_delete_newest_lines(HistoryBuf *self, index_type count) { + if (!count) return; + count = MIN(self->count, count); + self->count -= count - 1; + // now nuke multi cell chars that overlap onto the last line + index_type idx = (self->start_of_data + self->count - 1) % self->ynum; + init_line(self, idx, self->line); + CPUCell *cells = self->line->cpu_cells; + self->count--; + for (index_type x = 0; x < self->line->xnum; x++) { + CPUCell *c = cells + x; + if (c->is_multicell && c->y) { + for (index_type i = 0, pos = self->count; i < c->y && pos; i++, pos--) { + index_type idx = (self->start_of_data + pos - 1) % self->ynum; + init_line(self, idx, self->line); + CPUCell *m = self->line->cpu_cells + x; + cell_set_char(m, 0); m->is_multicell = false; + clear_sprite_position(self->line->gpu_cells[x]); + } + } + } +} + 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" diff --git a/kitty/history.h b/kitty/history.h index 6fe83a18a..0e34a8dba 100644 --- a/kitty/history.h +++ b/kitty/history.h @@ -41,3 +41,4 @@ void historybuf_finish_rewrap(HistoryBuf *dest, HistoryBuf *src); void historybuf_fast_rewrap(HistoryBuf *dest, HistoryBuf *src); index_type historybuf_next_dest_line(HistoryBuf *self, ANSIBuf *as_ansi_buf, Line *src_line, index_type dest_y, Line *dest_line, bool continued); bool historybuf_is_line_continued(HistoryBuf *self, index_type lnum); +void historybuf_delete_newest_lines(HistoryBuf *self, index_type count); diff --git a/kitty/screen.c b/kitty/screen.c index 04d7612d2..30c03d5ae 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -2550,20 +2550,25 @@ screen_scroll_until_cursor_prompt(Screen *self, bool add_to_scrollback) { screen_ensure_bounds(self, false, in_margins); } +static void +screen_delete_lines_impl(Screen *self, index_type start, index_type count, index_type top, index_type bottom) { + index_type y = start; + nuke_multiline_char_intersecting_with(self, 0, self->columns, y, y + 1, false); + y += count; + y = MIN(bottom, y); + nuke_multiline_char_intersecting_with(self, 0, self->columns, y, y + 1, false); + screen_dirty_line_graphics(self, top, bottom, self->linebuf == self->main_linebuf); + linebuf_delete_lines(self->linebuf, count, start, bottom); + self->is_dirty = true; + clear_all_selections(self); +} + void screen_delete_lines(Screen *self, unsigned int count) { unsigned int top = self->margin_top, bottom = self->margin_bottom; if (count == 0) count = 1; if (top <= self->cursor->y && self->cursor->y <= bottom) { - index_type y = self->cursor->y; - nuke_multiline_char_intersecting_with(self, 0, self->columns, y, y + 1, false); - y += count; - y = MIN(bottom, y); - nuke_multiline_char_intersecting_with(self, 0, self->columns, y, y + 1, false); - screen_dirty_line_graphics(self, top, bottom, self->linebuf == self->main_linebuf); - linebuf_delete_lines(self->linebuf, count, self->cursor->y, bottom); - self->is_dirty = true; - clear_all_selections(self); + screen_delete_lines_impl(self, self->cursor->y, count, self->margin_bottom, self->margin_bottom); screen_carriage_return(self); } } @@ -4223,6 +4228,32 @@ find_cmd_output(Screen *self, OutputOffset *oo, index_type start_screen_y, unsig return oo->num_lines > 0; } +static PyObject* +erase_last_command(Screen *self, PyObject *args) { + int include_prompt = 1; + if (!PyArg_ParseTuple(args, "|p", &include_prompt)) return NULL; + OutputOffset oo = {.screen=self}; + if (self->linebuf != self->main_linebuf || !find_cmd_output(self, &oo, self->cursor->y + self->scrolled_by, self->scrolled_by, -1, false)) Py_RETURN_FALSE; + if (include_prompt) { + int y = oo.start - 1; Line *line; + while ((line = checked_range_line(self, y))) { + oo.start--; oo.num_lines++; y--; + if (line->attrs.prompt_kind == PROMPT_START) break; + } + } + index_type num_lines_to_erase_in_screen = oo.start >= 0 ? oo.num_lines : oo.num_lines + oo.start; + num_lines_to_erase_in_screen = MIN(self->cursor->y, num_lines_to_erase_in_screen); + if (num_lines_to_erase_in_screen) { + screen_delete_lines_impl(self, self->cursor->y - num_lines_to_erase_in_screen, num_lines_to_erase_in_screen, 0, self->lines - 1); + self->cursor->y -= num_lines_to_erase_in_screen; + } + if (oo.num_lines > num_lines_to_erase_in_screen) { + index_type num_of_lines_to_erase_from_history = oo.num_lines - num_lines_to_erase_in_screen; + historybuf_delete_newest_lines(self->historybuf, num_of_lines_to_erase_from_history); + } + Py_RETURN_TRUE; +} + static PyObject* cmd_output(Screen *self, PyObject *args) { unsigned int which = 0; @@ -5555,6 +5586,7 @@ static PyMethodDef methods[] = { MND(draw, METH_O) MND(apply_sgr, METH_O) MND(cursor_position, METH_VARARGS) + MND(erase_last_command, METH_VARARGS) MND(set_window_char, METH_VARARGS) MND(set_mode, METH_VARARGS) MND(reset_mode, METH_VARARGS) diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index 41b81f431..aa8c61add 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -1227,6 +1227,10 @@ class TestScreen(BaseTest): for i in range(n): s.draw(f'{i}{x}'), s.index(), s.carriage_return() + from kitty.window import as_text + def at(): + return as_text(s, add_history=True) + s = self.create_screen(cols=5, lines=5, scrollback=15) draw_output(3, 'oo') draw_prompt('pp') @@ -1481,6 +1485,34 @@ class TestScreen(BaseTest): draw_prompt('p1') self.ae(lco(which=3), '0a\n1a') + # erase last command + s = self.create_screen(cols=10, lines=10, scrollback=15) + s.draw('before\r\n') + draw_prompt('p1'), draw_output(2), mark_prompt(), s.draw('partial') + self.ae('before\n$ p1\n0\n1\npartial', at().rstrip()) + self.ae(lco(), '0\n1') + x = s.cursor.x + s.erase_last_command() + self.ae('before\npartial', at().rstrip()) + self.ae((x, 1), (s.cursor.x, s.cursor.y)) + s.reset() + s.draw('before\r\n') + draw_prompt('p1'), draw_output(2), mark_prompt(), s.draw('partial') + x = s.cursor.x + s.erase_last_command(False) + self.ae('before\n$ p1\npartial', at().rstrip()) + for scroll in (8, 9, 10): + s.reset() + s.draw('before'), s.carriage_return(), s.linefeed() + draw_prompt('p1'), draw_output(scroll), mark_prompt(), s.draw('partial') + s.erase_last_command() + self.ae('before\npartial', at().rstrip()) + s.reset() + draw_multicell(s, 'A', scale=2), s.draw('a'), draw_multicell(s, 'B', scale=2), s.draw('b\r\n') + draw_prompt('p1'), draw_output(9), mark_prompt(), s.draw('partial') + s.erase_last_command() + self.ae(at().rstrip(), ' a b\npartial') + def test_pointer_shapes(self): from kitty.window import set_pointer_shape s = self.create_screen()