Close hyperlinks more eagerly when serializing to ANSI

Fixes #9405
This commit is contained in:
Kovid Goyal
2026-01-23 10:11:26 +05:30
parent 6f268bc04b
commit a214097b55
2 changed files with 32 additions and 11 deletions

View File

@@ -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

View File

@@ -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