osc: wip: kitty text size protocol

This brings initial support for the new kitty text-sizing
protocol. Note hat only the width-parameter ('w') is supported. That
is, no font scaling, and no multi-line cells.

For now, only explicit widths are supported. That is, w=0 does not yet
work.

There are a couple of changes to the renderer, to handle e.g.

    OSC 66 ; w=6 ; foobar ST

There are two ways this can get rendered, depending on whether
grapheme shaping has been enabled. We either shape it, and get an
array of glyphs back that we render. Or, we rasterize each codepoint
ourselves, and render each resulting glyph.

The two cases ends up in two different renderer loops, that worked
somewhat different. In particular, the first case has probably never
been tested/used at all...

With this patch, both are changed, and now uses some heuristic to
differentiate between multi-cell text strings (like in the example
above), or single-cell combining characters. The difference is mainly
in which offset to use for the secondary glyphs.

In a multi-cell string, each glyph is mapped to its own cell, while in
the combining case, we try to map all glyphs to the same cell.
This commit is contained in:
Daniel Eklöf 2025-01-25 14:09:35 +01:00
parent 1111f7e918
commit 7a8d2b5e01
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
2 changed files with 104 additions and 14 deletions

81
osc.c
View file

@ -610,7 +610,6 @@ verify_kitty_id_is_valid(const char *id)
}
UNIGNORE_WARNINGS
static void
kitty_notification(struct terminal *term, char *string)
{
@ -1135,6 +1134,82 @@ out:
free(sound_name);
}
static void
kitty_text_size(struct terminal *term, char *string)
{
char *text = strchr(string, ';');
if (text == NULL)
return;
char *parameters = string;
*text = '\0';
text++;
char32_t *wchars = ambstoc32(text);
if (wchars == NULL)
return;
int width = 0;
char *ctx = NULL;
for (char *param = strtok_r(parameters, ":", &ctx);
param != NULL;
param = strtok_r(NULL, ":", &ctx))
{
/* All parameters are on the form X=value, where X is always
exactly one character */
if (param[0] == '\0' || param[1] != '=')
continue;
char *value = &param[2];
switch (param[0]) {
case 'w': {
errno = 0;
char *end = NULL;
unsigned long w = strtoul(value, &end, 10);
if (*end == '\0' && errno == 0 && w <= 7) {
width = (int)w;
break;
} else
LOG_ERR("OSC-66: invalid 'w' value, ignoring");
break;
}
case 's':
case 'n':
case 'd':
case 'v':
LOG_WARN("OSC-66: unsupported: '%c' parameter, ignoring", param[0]);
break;
}
}
const size_t len = c32len(wchars);
uint32_t key = composed_key_from_chars(wchars, len);
const struct composed *composed = composed_lookup_without_collision(
term->composed, &key, wchars, len - 1, wchars[len - 1], width);
if (composed == NULL) {
struct composed *new_cc = xmalloc(sizeof(*new_cc));
new_cc->chars = wchars;
new_cc->count = len;
new_cc->key = key;
new_cc->width = width;
new_cc->forced_width = width;
term->composed_count++;
composed_insert(&term->composed, new_cc);
composed = new_cc;
} else if (composed->width == width) {
free(wchars);
}
term_print(term, CELL_COMB_CHARS_LO + composed->key, composed->forced_width > 0 ? composed->forced_width : composed->width);
}
void
osc_dispatch(struct terminal *term)
{
@ -1371,6 +1446,10 @@ osc_dispatch(struct terminal *term)
osc_selection(term, string);
break;
case 66: /* text-size protocol (kitty) */
kitty_text_size(term, string);
break;
case 99: /* Kitty notifications */
kitty_notification(term, string);
break;

View file

@ -869,11 +869,16 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag
}
if (grapheme != NULL) {
cell_cols = composed->width;
const int forced_width = composed->forced_width;
cell_cols = forced_width > 0 ? forced_width : composed->width;
composed = NULL;
glyphs = grapheme->glyphs;
glyph_count = grapheme->count;
if (forced_width > 0)
glyph_count = min(glyph_count, forced_width);
}
}
@ -890,7 +895,9 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag
} else {
glyph_count = 1;
glyphs = &single;
cell_cols = single->cols;
const size_t forced_width = composed != NULL ? composed->forced_width : 0;
cell_cols = forced_width > 0 ? forced_width : single->cols;
}
}
}
@ -972,7 +979,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag
int g_x = glyph->x;
int g_y = glyph->y;
if (i > 0 && glyph->x >= 0)
if (i > 0 && glyph->x >= 0 && cell_cols == 1)
g_x -= term->cell_width;
if (unlikely(pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8)) {
@ -993,9 +1000,9 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag
if (composed != NULL) {
assert(glyph_count == 1);
for (size_t i = 1; i < composed->count; i++) {
for (size_t j = 1; j < composed->count; j++) {
const struct fcft_glyph *g = fcft_rasterize_char_utf32(
font, composed->chars[i], term->font_subpixel);
font, composed->chars[j], term->font_subpixel);
if (g == NULL)
continue;
@ -1017,22 +1024,26 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag
* somewhat deal with double-width glyphs we use
* an offset of *one* cell.
*/
int x_ofs = g->x < 0
? cell_cols * term->cell_width
: (cell_cols - 1) * term->cell_width;
int x_ofs = cell_cols == 1
? g->x < 0
? cell_cols * term->cell_width
: (cell_cols - 1) * term->cell_width
: 0;
if (cell_cols > 1)
pen_x += term->cell_width;
pixman_image_composite32(
PIXMAN_OP_OVER, clr_pix, g->pix, pix, 0, 0, 0, 0,
/* Some fonts use a negative offset, while others use a
* "normal" offset */
pen_x + x_ofs + g->x,
y + term->font_baseline - g->y,
g->width, g->height);
pen_x + letter_x_ofs + x_ofs + g->x,
y + term->font_baseline - g->y, g->width, g->height);
}
}
}
pen_x += glyph->advance.x;
pen_x += cell_cols > 1 ? term->cell_width : glyph->advance.x;
}
pixman_image_unref(clr_pix);
@ -4398,7 +4409,7 @@ render_resize(struct terminal *term, int width, int height, uint8_t opts)
}
/* Don't shrink grid too much */
const int min_cols = 2;
const int min_cols = 7;
const int min_rows = 1;
/* Minimum window size (must be divisible by the scaling factor)*/