mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-16 13:37:52 +02:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdfcfc4fc2 | ||
|
|
1dc3730782 | ||
|
|
65e6a127ff | ||
|
|
3c648e42f6 | ||
|
|
9b218001a1 | ||
|
|
b43ca8fe31 | ||
|
|
a685af8c35 | ||
|
|
ba6b652850 | ||
|
|
fc23ef57ec | ||
|
|
d4106ef2db | ||
|
|
9393e29793 | ||
|
|
e4bce6485b | ||
|
|
fbd4e5ad1f | ||
|
|
48ee061454 | ||
|
|
aa4e94cef5 | ||
|
|
8a39929c15 | ||
|
|
8be2a10b29 | ||
|
|
779a49acde |
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6.0.2
|
||||
uses: actions/checkout@v6.0.3
|
||||
with:
|
||||
fetch-depth: 10
|
||||
persist-credentials: false
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
CFLAGS: -funsigned-char
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6.0.2
|
||||
uses: actions/checkout@v6.0.3
|
||||
with:
|
||||
fetch-depth: 0 # needed for :commit: docs role
|
||||
persist-credentials: false
|
||||
@@ -145,7 +145,7 @@ jobs:
|
||||
KITTY_BUNDLE: 1
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6.0.2
|
||||
uses: actions/checkout@v6.0.3
|
||||
with:
|
||||
fetch-depth: 10
|
||||
persist-credentials: false
|
||||
@@ -166,7 +166,7 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6.0.2
|
||||
uses: actions/checkout@v6.0.3
|
||||
with:
|
||||
fetch-depth: 0 # needed for :commit: docs role
|
||||
persist-credentials: false
|
||||
@@ -204,7 +204,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6.0.2
|
||||
uses: actions/checkout@v6.0.3
|
||||
with:
|
||||
fetch-depth: 10
|
||||
persist-credentials: false
|
||||
|
||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6.0.2
|
||||
uses: actions/checkout@v6.0.3
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4.36.0
|
||||
uses: github/codeql-action/init@v4.36.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
trap-caching: false
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
run: python3 .github/workflows/ci.py build
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4.36.0
|
||||
uses: github/codeql-action/analyze@v4.36.2
|
||||
|
||||
- name: Run govulncheck
|
||||
if: matrix.language == 'go'
|
||||
|
||||
4
.github/workflows/depscan.yml
vendored
4
.github/workflows/depscan.yml
vendored
@@ -22,13 +22,13 @@ jobs:
|
||||
KITTY_BUNDLE: 1
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v6.0.2
|
||||
uses: actions/checkout@v6.0.3
|
||||
with:
|
||||
fetch-depth: 10
|
||||
persist-credentials: false
|
||||
|
||||
- name: Checkout bypy
|
||||
uses: actions/checkout@v6.0.2
|
||||
uses: actions/checkout@v6.0.3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
@@ -78,11 +78,11 @@
|
||||
},
|
||||
|
||||
{
|
||||
"name": "sqlite 3.53.0",
|
||||
"name": "sqlite 3.53.2",
|
||||
"unix": {
|
||||
"file_extension": "tar.gz",
|
||||
"hash": "sha256:851e9b38192fe2ceaa65e0baa665e7fa06230c3d9bd1a6a9662d02380d73365a",
|
||||
"urls": ["https://www.sqlite.org/2026/{name}-autoconf-3530000.{file_extension}"]
|
||||
"hash": "sha256:588ad51949419a56ebe81fe56193d510c559eb94c9a57748387860b5d3069316",
|
||||
"urls": ["https://www.sqlite.org/2026/{name}-autoconf-3530200.{file_extension}"]
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -173,6 +173,13 @@ consumption to do the same tasks.
|
||||
Detailed list of changes
|
||||
-------------------------------------
|
||||
|
||||
0.47.4 [2026-06-15]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Linux: Fix a regression in the previous release that broke rendering of bitmap color fonts (:pull:`10145`)
|
||||
|
||||
- Linux: Allow fake italics defined via a matrix in fontconfig settings to work for fonts like Fira Code that do not ship with an italic face (:pull:`10120`)
|
||||
|
||||
0.47.3 [2026-06-12]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -96,7 +96,8 @@ polymorphic_string_as_utf8(id string) {
|
||||
characters = [string string];
|
||||
else
|
||||
characters = (NSString*) string;
|
||||
return [characters UTF8String];
|
||||
const char* ans = [characters UTF8String];
|
||||
return ans ? ans : "(nil)";
|
||||
}
|
||||
|
||||
static bool
|
||||
@@ -2023,6 +2024,10 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) {
|
||||
|
||||
- (void)insertText:(id)string replacementRange:(NSRange)replacementRange
|
||||
{
|
||||
if (!string) {
|
||||
debug_input("\n\tinsertText: nil replacementRange: (%lu, %lu)\n", replacementRange.location, replacementRange.length);
|
||||
return;
|
||||
}
|
||||
const char *utf8 = polymorphic_string_as_utf8(string);
|
||||
debug_input("\n\tinsertText: %s replacementRange: (%lu, %lu)\n", utf8, replacementRange.location, replacementRange.length);
|
||||
if ([self hasMarkedText] && !is_ascii_control_char(utf8[0])) {
|
||||
|
||||
2
go.mod
2
go.mod
@@ -27,7 +27,7 @@ require (
|
||||
github.com/zeebo/xxh3 v1.1.0
|
||||
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b
|
||||
golang.org/x/image v0.42.0
|
||||
golang.org/x/sys v0.45.0
|
||||
golang.org/x/sys v0.46.0
|
||||
golang.org/x/text v0.38.0
|
||||
howett.net/plist v1.0.1
|
||||
)
|
||||
|
||||
4
go.sum
4
go.sum
@@ -80,8 +80,8 @@ golang.org/x/image v0.42.0 h1:1gSs6ehNWXLbkHBIPcWztk3D/6aIA/8hauiAYtlodVY=
|
||||
golang.org/x/image v0.42.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -22,7 +22,7 @@ class Version(NamedTuple):
|
||||
|
||||
appname: str = 'kitty'
|
||||
kitty_face = '🐱'
|
||||
version: Version = Version(0, 47, 3)
|
||||
version: Version = Version(0, 47, 4)
|
||||
str_version: str = '.'.join(map(str, version))
|
||||
_plat = sys.platform.lower()
|
||||
is_macos: bool = 'darwin' in _plat
|
||||
|
||||
@@ -458,6 +458,10 @@ specialize_font_descriptor(PyObject *base_descriptor, double font_sz_in_pts, dou
|
||||
if (axes) {
|
||||
if (PyDict_SetItemString(ans, "axes", axes) != 0) return NULL;
|
||||
}
|
||||
PyObject *matrix = PyDict_GetItemString(base_descriptor, "matrix");
|
||||
if (matrix) {
|
||||
if (PyDict_SetItemString(ans, "matrix", matrix) != 0) return NULL;
|
||||
}
|
||||
PyObject *ff = PyDict_GetItemString(ans, "fontfeatures");
|
||||
if (ff && PyList_GET_SIZE(ff)) {
|
||||
for (Py_ssize_t i = 0; i < PyList_GET_SIZE(ff); i++) {
|
||||
|
||||
@@ -401,7 +401,38 @@ def get_font_files(opts: Options) -> FontFiles:
|
||||
font = medium_font
|
||||
key = kd[(bold, italic)]
|
||||
ans[key] = font
|
||||
return {'medium': ans['medium'], 'bold': ans['bold'], 'italic': ans['italic'], 'bi': ans['bi']}
|
||||
|
||||
def apply_synthetic_matrix(font: Descriptor, bold: bool, italic: bool) -> Descriptor:
|
||||
# fontconfig's FcFontList (used by find_best_match) omits FC_MATRIX from
|
||||
# its object set, so a roman font found there carries no synthetic-italic
|
||||
# shear and its "italic" renders upright. Fira Code is the case (it ships
|
||||
# no italic), in both its static and variable builds. The italic intent
|
||||
# exists only here at selection finalize, not at face construction, so
|
||||
# recover the matrix now: for an italic slot whose chosen face is upright
|
||||
# (no slant) and has no matrix yet, ask fc_match what fontconfig would do.
|
||||
# fc_match returns a synthetic matrix only when there is no real italic to
|
||||
# use (no italic face and no slanted named instance or variable slant
|
||||
# axis); when a real italic exists it returns no matrix, so a font that is
|
||||
# already italic, static or variable, is never double-slanted. Face construction applies the matrix
|
||||
# via FT_Set_Transform; specialize_font_descriptor preserves it when the
|
||||
# descriptor is sized for rendering. Only the matrix is taken, so
|
||||
# selection is unchanged. Covers the four configured faces; fc_match
|
||||
# re-matches by family name (see commit message).
|
||||
if (italic and font['descriptor_type'] == 'fontconfig'
|
||||
and not font.get('matrix') and not font.get('slant')):
|
||||
from kitty.fast_data_types import FC_MONO
|
||||
from kitty.fonts.fontconfig import fc_match
|
||||
mtx = fc_match(font['family'], bold, italic, FC_MONO if is_monospace(font) else -1).get('matrix')
|
||||
if mtx:
|
||||
new_font = font.copy()
|
||||
new_font['matrix'] = mtx
|
||||
return new_font
|
||||
return font
|
||||
return {
|
||||
'medium': ans['medium'], 'bold': ans['bold'],
|
||||
'italic': apply_synthetic_matrix(ans['italic'], False, True),
|
||||
'bi': apply_synthetic_matrix(ans['bi'], True, True),
|
||||
}
|
||||
|
||||
|
||||
def axis_values_are_equal(defaults: dict[str, float], a: dict[str, float], b: dict[str, float]) -> bool:
|
||||
|
||||
@@ -878,18 +878,26 @@ apply_cairo_font_size(Face *self, unsigned sz_px) {
|
||||
// on self->face in face_from_descriptor. cairo owns FT_Set_Transform on
|
||||
// its face and derives it from the font matrix on every render
|
||||
// (_cairo_ft_unscaled_font_set_scale in cairo-ft-font.c), so the only
|
||||
// channel that reaches glyph rasterization is the cairo font matrix
|
||||
// itself. Encode FC_MATRIX there.
|
||||
// FT_Matrix is xx,xy,yx,yy (row-major); cairo_matrix_init takes
|
||||
// xx,yx,xy,yy. Same matrix, transposed argument order.
|
||||
if (!self->has_matrix) { cairo_set_font_size(self->cairo.cr, sz_px); return; }
|
||||
// channel that reaches glyph rasterization is the cairo font matrix.
|
||||
//
|
||||
// FC_MATRIX is overloaded. Besides synthetic slant, fontconfig also encodes
|
||||
// the pixel size fixup of fixed-size faces here, as a pure diagonal scale
|
||||
// (Noto Color Emoji is matched with [0.1147 0; 0 0.1147]). The color path
|
||||
// already sizes glyphs with cairo_set_font_size() + fit_cairo_glyph(), and
|
||||
// fit_cairo_glyph() only shrinks, so feeding the fixup scale in here shrinks
|
||||
// color emoji with no way to recover (#10144). Honor only the shear that
|
||||
// carries synthetic slant and leave the size to cairo_set_font_size().
|
||||
if (!self->has_matrix || self->matrix.xx == 0 || self->matrix.yy == 0) {
|
||||
cairo_set_font_size(self->cairo.cr, sz_px); return;
|
||||
}
|
||||
double shear_xy = (double)self->matrix.xy / (double)self->matrix.xx;
|
||||
double shear_yx = (double)self->matrix.yx / (double)self->matrix.yy;
|
||||
if (shear_xy == 0 && shear_yx == 0) { cairo_set_font_size(self->cairo.cr, sz_px); return; }
|
||||
// FT_Matrix is xx,xy,yx,yy (row-major); cairo_matrix_init takes xx,yx,xy,yy.
|
||||
// The diagonal is unit scale (size is handled above); apply only the shear.
|
||||
double s = (double)sz_px;
|
||||
double xx = self->matrix.xx / 65536.0;
|
||||
double xy = self->matrix.xy / 65536.0;
|
||||
double yx = self->matrix.yx / 65536.0;
|
||||
double yy = self->matrix.yy / 65536.0;
|
||||
cairo_matrix_t m;
|
||||
cairo_matrix_init(&m, xx * s, yx * s, xy * s, yy * s, 0, 0);
|
||||
cairo_matrix_init(&m, s, shear_yx * s, shear_xy * s, s, 0, 0);
|
||||
cairo_set_font_matrix(self->cairo.cr, &m);
|
||||
}
|
||||
|
||||
|
||||
@@ -173,6 +173,41 @@ class Selection(BaseTest):
|
||||
self.ae(face_from_descriptor(ff['medium']).applied_features(), {'dlig': 'dlig', 'test': 'test=3'})
|
||||
self.ae(face_from_descriptor(ff['bold']).applied_features(), {'dlig': 'dlig', 'test': 'test=3'})
|
||||
|
||||
def test_synthetic_italic_matrix(self):
|
||||
# A roman-only font that find_best_match finds (e.g. Fira Code, which ships
|
||||
# no italic face) must get fontconfig's synthetic-italic FC_MATRIX
|
||||
# (90-synthetic.conf) attached, so its italic renders slanted rather than
|
||||
# upright; real-italic faces must not. The shear value is fontconfig's, not
|
||||
# ours, so assert the invariant (a non-identity matrix is present), not the
|
||||
# exact tuple, for cross-config stability.
|
||||
if is_macos:
|
||||
self.skipTest('synthetic-italic FC_MATRIX is a fontconfig feature')
|
||||
from kitty.fonts.fontconfig import FC_MONO, fc_match
|
||||
names = set(all_fonts_map(True)['family_map']) | set(all_fonts_map(True)['variable_map'])
|
||||
if family_name_to_key('fira code') not in names:
|
||||
self.skipTest('Fira Code not installed')
|
||||
# Probe fc_match directly so we can tell "environment lacks the rule" (skip)
|
||||
# from "code did not attach the matrix" (fail).
|
||||
if fc_match('Fira Code', False, True, FC_MONO).get('matrix') is None:
|
||||
self.skipTest('fontconfig 90-synthetic.conf not active; no synthetic-italic matrix')
|
||||
opts = Options()
|
||||
opts.font_family = parse_font_spec('Fira Code')
|
||||
ff = get_font_files(opts)
|
||||
self.assertIsNone(ff['medium'].get('matrix')) # upright stays upright
|
||||
mi = ff['italic'].get('matrix')
|
||||
self.assertIsNotNone(mi) # roman, no italic -> sheared
|
||||
self.assertNotEqual(mi[1], 0.0) # actually slanted, not identity
|
||||
# Faces are built from a size-specialized descriptor at render time; the
|
||||
# matrix must survive specialize_font_descriptor or the glyphs render
|
||||
# upright despite the descriptor above being correct.
|
||||
from kitty.fast_data_types import specialize_font_descriptor
|
||||
sd = specialize_font_descriptor(dict(ff['italic']), 12.0, 96.0, 96.0)
|
||||
self.ae(sd.get('matrix'), mi)
|
||||
if family_name_to_key('liberation mono') in names: # real-italic control
|
||||
opts.font_family = parse_font_spec('Liberation Mono')
|
||||
self.assertIsNone(get_font_files(opts)['italic'].get('matrix'))
|
||||
|
||||
|
||||
def block_helpers(s, sprites, cell_width, cell_height):
|
||||
mr = {}
|
||||
actual = b''
|
||||
@@ -355,6 +390,30 @@ class Rendering(FontBaseTest):
|
||||
self.assertGreater(w, 64)
|
||||
self.assertGreater(h, 64)
|
||||
|
||||
def test_color_emoji_not_shrunk(self):
|
||||
# Regression test for https://github.com/kovidgoyal/kitty/issues/10144.
|
||||
# fontconfig gives fixed-size color faces (e.g. Noto Color Emoji) a
|
||||
# pixel-size fixup encoded as FC_MATRIX. That scale must not reach the
|
||||
# cairo font matrix used for color glyphs; applying it shrinks color emoji
|
||||
# to a dot (ee937bdd1b). Render the same font two ways at the same size:
|
||||
# from its fontconfig descriptor, which carries the fixup matrix, and from
|
||||
# its file path, which does not. A correct build renders both at the same
|
||||
# size; the bug shrinks the descriptor one. Comparing the two is
|
||||
# environment-independent since only the matrix differs.
|
||||
if is_macos:
|
||||
self.skipTest('FC_MATRIX is a fontconfig feature, not used on macOS')
|
||||
from kitty.fonts.fontconfig import fc_match
|
||||
desc = dict(fc_match('emoji', False, False, 0))
|
||||
if not (desc.get('color') and desc.get('matrix')):
|
||||
self.skipTest('no fixed-size color emoji font with a fontconfig fixup matrix')
|
||||
with_matrix = face_from_descriptor(desc)
|
||||
with_matrix.set_size(64, 96, 96)
|
||||
without_matrix = create_face(desc['path'])
|
||||
without_matrix.set_size(64, 96, 96)
|
||||
_, mw, mh = with_matrix.render_codepoint(0x1F40D)
|
||||
_, rw, rh = without_matrix.render_codepoint(0x1F40D)
|
||||
self.assertGreater(mh, 0.5 * rh, f'color emoji shrunk by FC_MATRIX: {mh}px vs {rh}px (#10144)')
|
||||
|
||||
def test_shaping(self):
|
||||
|
||||
def ss(text, font=None):
|
||||
|
||||
Reference in New Issue
Block a user