Add a regression test that exercises the code path which crashed in
v0.46.2 (#10017): when paused_rendering is active and a selection
extends into the scrollback, the inner loop of apply_selection iterates
with a negative y. Without the recently-added paused_y translation and
the paused_y < 0 guard, the call to linebuf_init_line treats the
negative y as a huge unsigned index_type and reads ~4GB out of bounds
in line_attrs[idx], crashing with SIGBUS.
The test reproduces the trigger deterministically via the Screen Python
bindings and asserts that current_selections() returns the expected
buffer instead of crashing.
Color glyphs (COLR/CBDT/SVG) go through cairo on a second FT_Face,
self->face_for_cairo, opened in ensure_cairo_resources. That face
never receives the FT_Set_Transform installed on self->face in
face_from_descriptor (#9990): cairo owns FT_Set_Transform on its own
face and derives it from the font matrix on every render
(_cairo_ft_unscaled_font_set_scale in cairo-ft-font.c). The only
channel that reaches color-glyph rasterization is the cairo font
matrix, not the FT face transform, so FC_MATRIX is silently dropped on
that path. Stock fontconfig rules do not apply FC_MATRIX to color
fonts, so this is a hand-built config edge case.
Factor the two cairo_set_font_size() call sites in set_cairo_font_size
and fit_cairo_glyph into apply_cairo_font_size(), which calls
cairo_set_font_matrix() with size * FC_MATRIX when has_matrix is set,
and falls through to cairo_set_font_size() otherwise. The non-matrix
path is bit-identical to before. For pure shears (xx=1, yy=1)
cairo_scaled_font_glyph_extents reads the post-shear bbox so the
shrink loop bounds the destination correctly; the user-visible effect
is that a sheared color glyph reports a wider bbox and shrinks more
aggressively to fit a cell than its upright sibling. Acceptable for
the hand-built edge case this PR scopes to.
FT_Matrix stores xx,xy,yx,yy in row-major order;
cairo_matrix_init takes xx,yx,xy,yy. Same matrix, transposed argument
order - pinned with a comment because it is easy to flip.
Refs: #9990
In highlight_mark(), mark_text was sliced using byte-based indexing
(mark_text[:len(hint)] and mark_text[len(hint):]). Since len(hint)
equals the rune count (hint is always ASCII), but len(mark_text) is a
byte count, this could slice in the middle of a multi-byte UTF-8
sequence (e.g. CJK characters), producing an invalid byte sequence
rendered as the Unicode replacement character (�).
Fix by converting mark_text to []rune first, then slicing at rune
boundaries. The hint is ASCII so len(hint) == rune count, requiring
no conversion on the hint side.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>