Compare commits

...

7 Commits

Author SHA1 Message Date
Kovid Goyal
c1d507dbe8 Merge branch 'dependabot/github_actions/actions-77b068c286' of https://github.com/kovidgoyal/kitty 2026-06-29 11:51:19 +05:30
Kovid Goyal
2b38afabce Merge branch 'dependabot/go_modules/all-go-deps-9c5f66ad81' of https://github.com/kovidgoyal/kitty 2026-06-29 11:51:05 +05:30
dependabot[bot]
2ead860607 Bump the actions group with 4 updates
Bumps the actions group with 4 updates: [actions/checkout](https://github.com/actions/checkout), [actions/setup-python](https://github.com/actions/setup-python), [actions/setup-go](https://github.com/actions/setup-go) and [actions/cache](https://github.com/actions/cache).


Updates `actions/checkout` from 6.0.3 to 7.0.0
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6.0.3...v7.0.0)

Updates `actions/setup-python` from 6 to 6.2.0
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6...v6.2.0)

Updates `actions/setup-go` from 6 to 6.4.0
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v6...v6.4.0)

Updates `actions/cache` from 5 to 5.0.5
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v5...v5.0.5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/setup-python
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: actions/setup-go
  dependency-version: 6.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: actions/cache
  dependency-version: 5.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-29 03:44:56 +00:00
dependabot[bot]
691a971a70 Bump github.com/alecthomas/chroma/v2 in the all-go-deps group
Bumps the all-go-deps group with 1 update: [github.com/alecthomas/chroma/v2](https://github.com/alecthomas/chroma).


Updates `github.com/alecthomas/chroma/v2` from 2.26.1 to 2.27.0
- [Release notes](https://github.com/alecthomas/chroma/releases)
- [Commits](https://github.com/alecthomas/chroma/compare/v2.26.1...v2.27.0)

---
updated-dependencies:
- dependency-name: github.com/alecthomas/chroma/v2
  dependency-version: 2.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-go-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-29 03:42:55 +00:00
Kovid Goyal
9cd2c60064 Clean previous PR 2026-06-29 06:17:08 +05:30
Kovid Goyal
4e0f7faef8 Merge branch 'fix-erase-last-command-multiline-prompt' of https://github.com/slayerlab/kitty 2026-06-29 06:13:36 +05:30
amoena
207b22aa80 fix(erase_last_command): erase the most recent command even empty output
erase_last_command selected the region to erase via find_cmd_output(..., -1),
which anchors on OUTPUT_START (OSC, 133;C). Commands that produce no output
(an empty Enter, a comment, cd, export, etc. -- never emit 133;C, so they were
skipped and an older command-with-output was erased instead. "Erase the last
command" therefore did not erase the last command whenever the most recent ones
has no output.

Select the region by prompt marks instead: erase the prompt block immediately
above the current (live) prompt, whatever it contains. Every submittd command
is now one unit, removed newest-first, one prompt block per invocation.

This also fixes two latent defects in the previous implementation:

* The on-screen deletion was anchored at `cursor->y - count`, which
  assumes the region ends exactly one row above the cursor.
  Multi-line prompts and skipped rows broke that assumption and left
  residual lines. Anchor at the top of the region instead.

* When part of the erased region was in the scrollback, the lines
  were removed from the history buffer but no redraw was signalled,
  so the deletion of the off-screen lines only became visible after
  the next scroll event recomputed the history viewport. Clamp
  scrolled_by to the new history length and call dirty_scroll()
  after shrinking the buffer.

include_prompt is retained for API compatibility but is now a no-op: the
unit erased is always the whole prompt block.
2026-06-28 11:17:01 -03:00
9 changed files with 93 additions and 44 deletions

View File

@@ -45,18 +45,18 @@ jobs:
steps:
- name: Checkout source code
uses: actions/checkout@v6.0.3
uses: actions/checkout@v7.0.0
with:
fetch-depth: 10
persist-credentials: false
- name: Set up Python ${{ matrix.pyver }}
uses: actions/setup-python@v6
uses: actions/setup-python@v6.2.0
with:
python-version: ${{ matrix.pyver }}
- name: Install Go
uses: actions/setup-go@v6
uses: actions/setup-go@v6.4.0
with:
go-version-file: go.mod
@@ -73,7 +73,7 @@ jobs:
CFLAGS: -funsigned-char
steps:
- name: Checkout source code
uses: actions/checkout@v6.0.3
uses: actions/checkout@v7.0.0
with:
fetch-depth: 0 # needed for :commit: docs role
persist-credentials: false
@@ -85,18 +85,18 @@ jobs:
run: if grep -Inr ':code:`\s' kitty kitty_tests kittens docs *.py *.asciidoc *.rst *.go .gitattributes .gitignore; then echo Space at code block start found, aborting.; exit 1; fi
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v6.2.0
with:
python-version: "3.14"
- name: Install Go
uses: actions/setup-go@v6
uses: actions/setup-go@v6.4.0
with:
go-version-file: go.mod
cache: false
- name: Cache Go build artifacts separately
uses: actions/cache@v5
uses: actions/cache@v5.0.5
with:
path: |
~/.cache/go-build
@@ -143,13 +143,13 @@ jobs:
KITTY_SANITIZE: 1
steps:
- name: Checkout source code
uses: actions/checkout@v6.0.3
uses: actions/checkout@v7.0.0
with:
fetch-depth: 10
persist-credentials: false
- name: Install Go
uses: actions/setup-go@v6
uses: actions/setup-go@v6.4.0
with:
go-version-file: go.mod
@@ -164,18 +164,18 @@ jobs:
runs-on: macos-latest
steps:
- name: Checkout source code
uses: actions/checkout@v6.0.3
uses: actions/checkout@v7.0.0
with:
fetch-depth: 0 # needed for :commit: docs role
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v6.2.0
with:
python-version: "3.14"
- name: Install Go
uses: actions/setup-go@v6
uses: actions/setup-go@v6.4.0
with:
go-version-file: go.mod
@@ -202,7 +202,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v6.0.3
uses: actions/checkout@v7.0.0
with:
fetch-depth: 10
persist-credentials: false
@@ -211,7 +211,7 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y curl xz-utils build-essential git pkg-config libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl1-mesa-dev libxkbcommon-x11-dev libfontconfig-dev libx11-xcb-dev libdbus-1-dev
- name: Install Go
uses: actions/setup-go@v6
uses: actions/setup-go@v6.4.0
with:
go-version-file: go.mod

View File

@@ -39,7 +39,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6.0.3
uses: actions/checkout@v7.0.0
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -48,7 +48,7 @@ jobs:
- name: Install Go
if: matrix.language == 'c' || matrix.language == 'go'
uses: actions/setup-go@v6
uses: actions/setup-go@v6.4.0
with:
go-version-file: go.mod

View File

@@ -22,13 +22,13 @@ jobs:
KITTY_BUNDLE: 1
steps:
- name: Checkout source code
uses: actions/checkout@v6.0.3
uses: actions/checkout@v7.0.0
with:
fetch-depth: 10
persist-credentials: false
- name: Checkout bypy
uses: actions/checkout@v6.0.3
uses: actions/checkout@v7.0.0
with:
fetch-depth: 1
persist-credentials: false

View File

@@ -196,6 +196,8 @@ Detailed list of changes
- ``edit-in-kitty``: Return exit code from underlying editor process on exit (:iss:`10198`)
- Make erasing last command robust against commands with no output and commands in the scrollback (:pull:`10201`)
0.47.4 [2026-06-15]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

4
go.mod
View File

@@ -6,7 +6,7 @@ toolchain go1.26.4
require (
github.com/ALTree/bigfloat v0.2.0
github.com/alecthomas/chroma/v2 v2.26.1
github.com/alecthomas/chroma/v2 v2.27.0
github.com/bmatcuk/doublestar/v4 v4.10.0
github.com/dlclark/regexp2 v1.12.0
github.com/ebitengine/purego v0.10.1
@@ -39,7 +39,7 @@ require (
// replace github.com/kovidgoyal/imaging => ../imaging
require (
github.com/dlclark/regexp2/v2 v2.1.1 // indirect
github.com/dlclark/regexp2/v2 v2.2.1 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect

8
go.sum
View File

@@ -2,8 +2,8 @@ github.com/ALTree/bigfloat v0.2.0 h1:AwNzawrpFuw55/YDVlcPw0F0cmmXrmngBHhVrvdXPvM
github.com/ALTree/bigfloat v0.2.0/go.mod h1:+NaH2gLeY6RPBPPQf4aRotPPStg+eXc8f9ZaE4vRfD4=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.26.1 h1:2X21EdxGZNv5GF9mG5u+uzc02GCFyGxbcBm3Grd9A78=
github.com/alecthomas/chroma/v2 v2.26.1/go.mod h1:lxhRRa9H4hPmRLOOdYga4zkQIQjq3dtrrdwQeCfu78Y=
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
@@ -12,8 +12,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2/v2 v2.1.1 h1:LCUGyd9Wf+r+VVOl8Ny38JTpWJcAsdVnCIuhhtthmKw=
github.com/dlclark/regexp2/v2 v2.1.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/emmansun/base64 v0.9.0 h1:92dLrE7iro6g/yWuPsd7M9TzJpe9fEeqKH0H7MApDtE=

View File

@@ -1261,7 +1261,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 erase_last_command(self) -> bool: ...
def set_progress(self, state: int, percent: int) -> None: ...
def mark_potential_url_drag(self) -> bool: ...

View File

@@ -4599,27 +4599,47 @@ find_cmd_output(Screen *self, OutputOffset *oo, index_type start_screen_y, unsig
}
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;
}
erase_last_command(Screen *self, PyObject *args UNUSED) {
if (self->linebuf != self->main_linebuf) Py_RETURN_FALSE;
Line *line;
// Erase the most recent command: the prompt block immediately above the
// current (live) prompt, regardless of whether that command produced any
// output. Commands without output (an empty Enter, a comment, cd, export,
// ...) emit no OSC 133;C, so a search anchored on OUTPUT_START would skip
// them and erase an older command instead. Selecting by prompt marks makes
// every submitted command one unit, removed newest-first.
int y = self->cursor->y + self->scrolled_by;
bool found = false;
for (; (line = checked_range_line(self, y)); y--) {
if (line->attrs.prompt_kind == PROMPT_START) { found = true; break; }
}
index_type num_lines_to_erase_in_screen = oo.start >= 0 ? oo.num_lines : oo.num_lines + oo.start;
if (!found) Py_RETURN_FALSE;
const int live_prompt_start = y;
found = false;
for (y = live_prompt_start - 1; (line = checked_range_line(self, y)); y--) {
if (line->attrs.prompt_kind == PROMPT_START) { found = true; break; }
}
if (!found) Py_RETURN_FALSE; // nothing above the current prompt to erase
const int start = y;
const index_type num_lines = (index_type)(live_prompt_start - start);
index_type num_lines_to_erase_in_screen = start >= 0 ? num_lines : num_lines + 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);
// Anchor the on-screen deletion at the top of the region, not at
// cursor->y - count: a multi-line prompt or rows skipped above would
// otherwise shift the deletion and leave residual lines on screen.
index_type screen_erase_start = start >= 0 ? (index_type)start : 0;
screen_delete_lines_impl(self, screen_erase_start, 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);
if (num_lines > num_lines_to_erase_in_screen) {
historybuf_delete_newest_lines(self->historybuf, num_lines - num_lines_to_erase_in_screen);
// The scrollback shrank: clamp the scroll position and signal a redraw,
// otherwise the deletion of off-screen lines is not reflected until the
// next scroll event forces the history viewport to be recomputed.
self->scrolled_by = MIN(self->scrolled_by, self->historybuf->count);
self->is_dirty = true;
dirty_scroll(self);
}
Py_RETURN_TRUE;
}
@@ -6221,7 +6241,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(erase_last_command, METH_NOARGS)
MND(set_window_char, METH_VARARGS)
MND(set_progress, METH_VARARGS)
MND(set_mode, METH_VARARGS)

View File

@@ -1627,8 +1627,8 @@ class TestScreen(BaseTest):
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())
s.erase_last_command()
self.ae('before\npartial', at().rstrip())
for scroll in (8, 9, 10):
s.reset()
s.draw('before'), s.carriage_return(), s.linefeed()
@@ -1641,6 +1641,33 @@ class TestScreen(BaseTest):
s.erase_last_command()
self.ae(at().rstrip(), ' a b\npartial')
# the most recent command is erased even when it produced no output (an
# empty Enter, a comment, cd, ...): such commands emit no OSC 133;C and
# must not be skipped in favour of an older command-with-output.
s = self.create_screen(cols=10, lines=12, scrollback=30)
s.draw('before\r\n')
draw_prompt('p1'), draw_output(2), draw_prompt('# note'), mark_prompt(), s.draw('partial')
s.erase_last_command()
self.ae('before\n$ p1\n0\n1\npartial', at().rstrip()) # the output-less command goes first
s.erase_last_command()
self.ae('before\npartial', at().rstrip()) # then the command with output
# consecutive output-less commands are removed newest-first, one per call
s.reset()
s.draw('before\r\n')
draw_prompt('p1'), draw_output(1), draw_prompt('e1'), draw_prompt('e2'), mark_prompt(), s.draw('partial')
s.erase_last_command()
self.ae('before\n$ p1\n0\n$ e1\npartial', at().rstrip())
s.erase_last_command()
self.ae('before\n$ p1\n0\npartial', at().rstrip())
s.erase_last_command()
self.ae('before\npartial', at().rstrip())
# multi-line live prompt: the command region is erased with no residual
s.reset()
s.draw('before\r\n')
draw_prompt('p1'), draw_output(9), mark_prompt(), s.draw('l1'), s.carriage_return(), s.index(), s.draw('partial')
s.erase_last_command()
self.ae('before\nl1\npartial', at().rstrip())
def test_pointer_shapes(self):
from kitty.window import set_pointer_shape
s = self.create_screen()