From a214097b55dbbdb70945f3730066168515162058 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 23 Jan 2026 10:11:26 +0530 Subject: [PATCH] Close hyperlinks more eagerly when serializing to ANSI Fixes #9405 --- kitty/line.c | 7 ++++++- kitty_tests/screen.py | 36 ++++++++++++++++++++++++++---------- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/kitty/line.c b/kitty/line.c index 44aa36997..020cc504b 100644 --- a/kitty/line.c +++ b/kitty/line.c @@ -573,7 +573,10 @@ line_as_ansi(Line *self, ANSILineState *s, index_type start_at, index_type stop_ case OUTPUT_START: write_mark_to_ansi_buf(s, "C"); break; } } - if (s->limit <= start_at) return s->escape_code_written; + if (s->limit <= start_at) { + if (s->output_buf->active_hyperlink_id && !self->cpu_cells[0].hyperlink_id) write_hyperlink_to_ansi_buf(s, 0); + return s->escape_code_written; + } static const GPUCell blank_cell = { 0 }; GPUCell *cell; @@ -603,6 +606,8 @@ line_as_ansi(Line *self, ANSILineState *s, index_type start_at, index_type stop_ } } close_multicell(s); + if (s->output_buf->active_hyperlink_id && s->limit < self->xnum && !self->cpu_cells[s->limit].hyperlink_id) + write_hyperlink_to_ansi_buf(s, 0); return s->escape_code_written; #undef CMP_ATTRS #undef CMP diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index 0909d04cf..0783047be 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -805,12 +805,13 @@ class TestScreen(BaseTest): def test_serialize(self): from kitty.window import as_text + sgr0 = '\x1b[m' # ] s = self.create_screen() - parse_bytes(s, b'\x1b[1;91m') + parse_bytes(s, b'\x1b[1;91m') # ] s.draw('X') - parse_bytes(s, b'\x1b[0m\x1b[2m') + parse_bytes(s, b'\x1b[0m\x1b[2m') # ]] s.draw('Y') - self.ae(as_text(s, True), '\x1b[m\x1b[22;1;91mX\x1b[22;2;39mY\n\n\n\n') + self.ae(as_text(s, True), f'{sgr0}\x1b[22;1;91mX\x1b[22;2;39mY\n\n\n\n') # ]] s.reset() s.draw('ab' * s.columns) @@ -818,30 +819,45 @@ class TestScreen(BaseTest): s.draw('c') 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(s, True), f'{sgr0}ababa{sgr0}babab\n{sgr0}c\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') + self.ae(as_text(s, True, True), f'{sgr0}\x1b[31m11{sgr0}\x1b[32m22{sgr0}\x1b[33m33{sgr0}\x1b[34m44{sgr0}{sgr0}\x1b[35m55{sgr0}\x1b[36m66') + # ]]]]]]]]]]]]]]]]]]]]] + + def hl(url='', id=''): + return '\x1b]8;{};{}\x1b\\'.format(f'id={id}' if id else '', url or '') def set_link(url=None, id=None): - parse_bytes(s, '\x1b]8;id={};{}\x1b\\'.format(id or '', url or '').encode('utf-8')) + parse_bytes(s, hl(url, id).encode('utf-8')) s = self.create_screen() + set_link('moo') + s.draw('X') + set_link() + self.ae(as_text(s, True), f'{sgr0}{hl("moo")}X{hl()}\n\n\n\n') + s.reset() + set_link('moo') + s.draw('X'*s.columns) + set_link() + self.ae(as_text(s, True), f'{sgr0}{hl("moo")}XXXXX\n{sgr0}{hl()}\n\n\n') + + s.reset() s.draw('a') set_link('moo', 'foo') s.draw('bcdef') - self.ae(as_text(s, True), '\x1b[ma\x1b]8;id=foo;moo\x1b\\bcde\x1b[mf\n\n\n\x1b]8;;\x1b\\') + self.ae(as_text(s, True), f'{sgr0}a{hl("moo", "foo")}bcde{sgr0}f{hl()}\n\n\n') set_link() s.draw('gh') - self.ae(as_text(s, True), '\x1b[ma\x1b]8;id=foo;moo\x1b\\bcde\x1b[mf\x1b]8;;\x1b\\gh\n\n\n') - s = self.create_screen() + self.ae(as_text(s, True), f'{sgr0}a{hl("moo", "foo")}bcde{sgr0}f{hl()}gh\n\n\n') + s.reset() s.draw('a') set_link('moo') s.draw('bcdef') - self.ae(as_text(s, True), '\x1b[ma\x1b]8;;moo\x1b\\bcde\x1b[mf\n\n\n\x1b]8;;\x1b\\') + self.ae(as_text(s, True), f'{sgr0}a{hl("moo")}bcde{sgr0}f{hl()}\n\n\n') def test_wrapping_serialization(self): from kitty.window import as_text