A new option narrow_symbols to turn off opportunistic wide rendering of private use codepoints

This commit is contained in:
Kovid Goyal 2022-02-11 13:04:44 +05:30
parent b2317e0f12
commit 01b4654461
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
9 changed files with 100 additions and 42 deletions

View File

@ -88,6 +88,8 @@ Detailed list of changes
- Improve CWD detection when there are multiple foreground processes in the TTY process group - Improve CWD detection when there are multiple foreground processes in the TTY process group
- A new option :opt:`narrow_symbols` to turn off opportunistic wide rendering of private use codepoints
- ssh kitten: Fix location of generated terminfo files on NetBSD (:iss:`4622`) - ssh kitten: Fix location of generated terminfo files on NetBSD (:iss:`4622`)
- A new action to clear the screen up to the line containing the cursor, see - A new action to clear the screen up to the line containing the cursor, see

View File

@ -6,21 +6,22 @@ Frequently Asked Questions
Some special symbols are rendered small/truncated in kitty? Some special symbols are rendered small/truncated in kitty?
----------------------------------------------------------- -----------------------------------------------------------
The number of cells a unicode character takes up are controlled by the unicode The number of cells a Unicode character takes up are controlled by the Unicode
standard. All characters are rendered in a single cell unless the unicode standard. All characters are rendered in a single cell unless the Unicode
standard says they should be rendered in two cells. When a symbol does not fit, standard says they should be rendered in two cells. When a symbol does not fit,
it will either be rescaled to be smaller or truncated (depending on how much it will either be rescaled to be smaller or truncated (depending on how much
extra space it needs). This is often different from other terminals which just extra space it needs). This is often different from other terminals which just
let the character overflow into neighboring cells, which is fine if the let the character overflow into neighboring cells, which is fine if the
neighboring cell is empty, but looks terrible if it is not. neighboring cell is empty, but looks terrible if it is not.
Some programs, like powerline, vim with fancy gutter symbols/status-bar, etc. Some programs, like Powerline, vim with fancy gutter symbols/status-bar, etc.
misuse unicode characters from the private use area to represent symbols. Often use Unicode characters from the private use area to represent symbols. Often
these symbols are square and should be rendered in two cells. However, since these symbols are wide and should be rendered in two cells. However, since
private use area symbols all have their width set to one in the unicode private use area symbols all have their width set to one in the Unicode
standard, |kitty| renders them either smaller or truncated. The exception is if standard, |kitty| renders them either smaller or truncated. The exception is if
these characters are followed by a space or empty cell in which case kitty these characters are followed by a space or empty cell in which case kitty
makes use of the extra cell to render them in two cells. makes use of the extra cell to render them in two cells. This behavior can be
turned off for specific symbols using :opt:`narrow_symbols`.
Using a color theme with a background color does not work well in vim? Using a color theme with a background color does not work well in vim?

View File

@ -967,7 +967,8 @@ def set_font_data(
descriptor_for_idx: Callable[[int], Tuple[FontObject, bool, bool]], descriptor_for_idx: Callable[[int], Tuple[FontObject, bool, bool]],
bold: int, italic: int, bold_italic: int, num_symbol_fonts: int, bold: int, italic: int, bold_italic: int, num_symbol_fonts: int,
symbol_maps: Tuple[Tuple[int, int, int], ...], font_sz_in_pts: float, symbol_maps: Tuple[Tuple[int, int, int], ...], font_sz_in_pts: float,
font_feature_settings: Dict[str, Tuple[FontFeature, ...]] font_feature_settings: Dict[str, Tuple[FontFeature, ...]],
narrow_symbols: Tuple[Tuple[int, int, int], ...],
) -> None: ) -> None:
pass pass

View File

@ -50,8 +50,8 @@ typedef struct {
size_t font_idx; size_t font_idx;
} SymbolMap; } SymbolMap;
static SymbolMap *symbol_maps = NULL; static SymbolMap *symbol_maps = NULL, *narrow_symbols = NULL;
static size_t num_symbol_maps = 0; static size_t num_symbol_maps = 0, num_narrow_symbols = 0;
typedef enum { SPACER_STRATEGY_UNKNOWN, SPACERS_BEFORE, SPACERS_AFTER, SPACERS_IOSEVKA } SpacerStrategy; typedef enum { SPACER_STRATEGY_UNKNOWN, SPACERS_BEFORE, SPACERS_AFTER, SPACERS_IOSEVKA } SpacerStrategy;
@ -1219,6 +1219,17 @@ is_non_emoji_dingbat(char_type ch) {
return false; return false;
} }
static unsigned int
cell_cap_for_codepoint(const char_type cp) {
unsigned int ans = UINT_MAX;
for (size_t i = 0; i < num_narrow_symbols; i++) {
SymbolMap *sm = narrow_symbols + i;
if (sm->left <= cp && cp <= sm->right) ans = sm->font_idx;
}
return ans;
}
void void
render_line(FONTS_DATA_HANDLE fg_, Line *line, index_type lnum, Cursor *cursor, DisableLigature disable_ligature_strategy) { render_line(FONTS_DATA_HANDLE fg_, Line *line, index_type lnum, Cursor *cursor, DisableLigature disable_ligature_strategy) {
#define RENDER if (run_font_idx != NO_FONT && i > first_cell_in_run) { \ #define RENDER if (run_font_idx != NO_FONT && i > first_cell_in_run) { \
@ -1251,6 +1262,7 @@ render_line(FONTS_DATA_HANDLE fg_, Line *line, index_type lnum, Cursor *cursor,
int width = get_glyph_width(font->face, glyph_id); int width = get_glyph_width(font->face, glyph_id);
desired_cells = (unsigned int)ceilf((float)width / fg->cell_width); desired_cells = (unsigned int)ceilf((float)width / fg->cell_width);
} }
desired_cells = MIN(desired_cells, cell_cap_for_codepoint(cpu_cell->ch));
unsigned int num_spaces = 0; unsigned int num_spaces = 0;
while ( while (
@ -1303,6 +1315,7 @@ render_simple_text(FONTS_DATA_HANDLE fg_, const char *text) {
static void static void
clear_symbol_maps(void) { clear_symbol_maps(void) {
if (symbol_maps) { free(symbol_maps); symbol_maps = NULL; num_symbol_maps = 0; } if (symbol_maps) { free(symbol_maps); symbol_maps = NULL; num_symbol_maps = 0; }
if (narrow_symbols) { free(narrow_symbols); narrow_symbols = NULL; num_narrow_symbols = 0; }
} }
typedef struct { typedef struct {
@ -1311,26 +1324,33 @@ typedef struct {
DescriptorIndices descriptor_indices = {0}; DescriptorIndices descriptor_indices = {0};
static PyObject* static bool
set_font_data(PyObject UNUSED *m, PyObject *args) { set_symbol_maps(SymbolMap **symbol_maps, size_t *num_symbol_maps, const PyObject *sm) {
PyObject *sm; *num_symbol_maps = PyTuple_GET_SIZE(sm);
Py_CLEAR(box_drawing_function); Py_CLEAR(prerender_function); Py_CLEAR(descriptor_for_idx); Py_CLEAR(font_feature_settings); *symbol_maps = calloc(*num_symbol_maps, sizeof(SymbolMap));
if (!PyArg_ParseTuple(args, "OOOIIIIO!dO", if (*symbol_maps == NULL) { PyErr_NoMemory(); return false; }
&box_drawing_function, &prerender_function, &descriptor_for_idx, for (size_t s = 0; s < *num_symbol_maps; s++) {
&descriptor_indices.bold, &descriptor_indices.italic, &descriptor_indices.bi, &descriptor_indices.num_symbol_fonts,
&PyTuple_Type, &sm, &OPT(font_size), &font_feature_settings)) return NULL;
Py_INCREF(box_drawing_function); Py_INCREF(prerender_function); Py_INCREF(descriptor_for_idx); Py_INCREF(font_feature_settings);
free_font_groups();
clear_symbol_maps();
num_symbol_maps = PyTuple_GET_SIZE(sm);
symbol_maps = calloc(num_symbol_maps, sizeof(SymbolMap));
if (symbol_maps == NULL) return PyErr_NoMemory();
for (size_t s = 0; s < num_symbol_maps; s++) {
unsigned int left, right, font_idx; unsigned int left, right, font_idx;
SymbolMap *x = symbol_maps + s; SymbolMap *x = *symbol_maps + s;
if (!PyArg_ParseTuple(PyTuple_GET_ITEM(sm, s), "III", &left, &right, &font_idx)) return NULL; if (!PyArg_ParseTuple(PyTuple_GET_ITEM(sm, s), "III", &left, &right, &font_idx)) return NULL;
x->left = left; x->right = right; x->font_idx = font_idx; x->left = left; x->right = right; x->font_idx = font_idx;
} }
return true;
}
static PyObject*
set_font_data(PyObject UNUSED *m, PyObject *args) {
PyObject *sm, *ns;
Py_CLEAR(box_drawing_function); Py_CLEAR(prerender_function); Py_CLEAR(descriptor_for_idx); Py_CLEAR(font_feature_settings);
if (!PyArg_ParseTuple(args, "OOOIIIIO!dOO!",
&box_drawing_function, &prerender_function, &descriptor_for_idx,
&descriptor_indices.bold, &descriptor_indices.italic, &descriptor_indices.bi, &descriptor_indices.num_symbol_fonts,
&PyTuple_Type, &sm, &OPT(font_size), &font_feature_settings, &PyTuple_Type, &ns)) return NULL;
Py_INCREF(box_drawing_function); Py_INCREF(prerender_function); Py_INCREF(descriptor_for_idx); Py_INCREF(font_feature_settings);
free_font_groups();
clear_symbol_maps();
set_symbol_maps(&symbol_maps, &num_symbol_maps, sm);
set_symbol_maps(&narrow_symbols, &num_narrow_symbols, ns);
Py_RETURN_NONE; Py_RETURN_NONE;
} }

View File

@ -21,6 +21,7 @@ from kitty.fonts.box_drawing import (
) )
from kitty.options.types import Options, defaults from kitty.options.types import Options, defaults
from kitty.typing import CoreTextFont, FontConfigPattern from kitty.typing import CoreTextFont, FontConfigPattern
from kitty.types import _T
from kitty.utils import log_error from kitty.utils import log_error
if is_macos: if is_macos:
@ -50,10 +51,9 @@ def font_for_family(family: str) -> Tuple[FontObject, bool, bool]:
return font_for_family_fontconfig(family) return font_for_family_fontconfig(family)
Range = Tuple[Tuple[int, int], str] def merge_ranges(
a: Tuple[Tuple[int, int], _T], b: Tuple[Tuple[int, int], _T], priority_map: Dict[Tuple[int, int], int]
) -> Generator[Tuple[Tuple[int, int], _T], None, None]:
def merge_ranges(a: Range, b: Range, priority_map: Dict[Tuple[int, int], int]) -> Generator[Range, None, None]:
a_start, a_end = a[0] a_start, a_end = a[0]
b_start, b_end = b[0] b_start, b_end = b[0]
a_val, b_val = a[1], b[1] a_val, b_val = a[1], b[1]
@ -95,7 +95,7 @@ def merge_ranges(a: Range, b: Range, priority_map: Dict[Tuple[int, int], int]) -
after_range = ((b_end + 1, a_end), a_val) after_range = ((b_end + 1, a_end), a_val)
after_range_prio = a_prio after_range_prio = a_prio
# check if the before, mid and after ranges can be coalesced # check if the before, mid and after ranges can be coalesced
ranges: List[Range] = [] ranges: List[Tuple[Tuple[int, int], _T]] = []
priorities: List[int] = [] priorities: List[int] = []
for rq, prio in ((before_range, before_range_prio), (mid_range, mid_range_prio), (after_range, after_range_prio)): for rq, prio in ((before_range, before_range_prio), (mid_range, mid_range_prio), (after_range, after_range_prio)):
if rq is None: if rq is None:
@ -117,7 +117,7 @@ def merge_ranges(a: Range, b: Range, priority_map: Dict[Tuple[int, int], int]) -
yield from ranges yield from ranges
def coalesce_symbol_maps(maps: Dict[Tuple[int, int], str]) -> Dict[Tuple[int, int], str]: def coalesce_symbol_maps(maps: Dict[Tuple[int, int], _T]) -> Dict[Tuple[int, int], _T]:
if not maps: if not maps:
return maps return maps
priority_map = {r: i for i, r in enumerate(maps.keys())} priority_map = {r: i for i, r in enumerate(maps.keys())}
@ -155,6 +155,10 @@ def create_symbol_map(opts: Options) -> Tuple[Tuple[int, int, int], ...]:
return sm return sm
def create_narrow_symbols(opts: Options) -> Tuple[Tuple[int, int, int], ...]:
return tuple((a, b, v) for (a, b), v in coalesce_symbol_maps(opts.narrow_symbols).items())
def descriptor_for_idx(idx: int) -> Tuple[FontObject, bool, bool]: def descriptor_for_idx(idx: int) -> Tuple[FontObject, bool, bool]:
return current_faces[idx] return current_faces[idx]
@ -193,6 +197,7 @@ def set_font_family(opts: Optional[Options] = None, override_font_size: Optional
current_faces.append((font_map[k], 'b' in k, 'i' in k)) current_faces.append((font_map[k], 'b' in k, 'i' in k))
before = len(current_faces) before = len(current_faces)
sm = create_symbol_map(opts) sm = create_symbol_map(opts)
ns = create_narrow_symbols(opts)
num_symbol_fonts = len(current_faces) - before num_symbol_fonts = len(current_faces) - before
font_features = {} font_features = {}
for face, _, _ in current_faces: for face, _, _ in current_faces:
@ -203,7 +208,7 @@ def set_font_family(opts: Optional[Options] = None, override_font_size: Optional
set_font_data( set_font_data(
render_box_drawing, prerender_function, descriptor_for_idx, render_box_drawing, prerender_function, descriptor_for_idx,
indices['bold'], indices['italic'], indices['bi'], num_symbol_fonts, indices['bold'], indices['italic'], indices['bi'], num_symbol_fonts,
sm, sz, font_features sm, sz, font_features, ns
) )

View File

@ -124,6 +124,22 @@ Syntax is::
''' '''
) )
opt('+narrow_symbols', 'U+E0A0-U+E0A3,U+E0C0-U+E0C7 1',
option_type='narrow_symbols',
add_to_default=False,
long_text='''
Usually, for Private Use Unicode characters and some symbol/dingbat characters,
if the character is followed by one or more spaces, kitty will use those extra cells
to render the character larger, if the character in the font has a wide aspect ratio.
Using this setting you can force kitty to restrict the specified code points to render in
the specified number of cells (defaulting to one cell).
Syntax is::
narrow_symbols codepoints Optionally the number of cells
'''
)
opt('disable_ligatures', 'never', opt('disable_ligatures', 'never',
option_type='disable_ligatures', ctype='int', option_type='disable_ligatures', ctype='int',
long_text=''' long_text='''

17
kitty/options/parse.py generated
View File

@ -11,12 +11,12 @@ from kitty.options.utils import (
clipboard_control, config_or_absolute_path, copy_on_select, cursor_text_color, clipboard_control, config_or_absolute_path, copy_on_select, cursor_text_color,
deprecated_hide_window_decorations_aliases, deprecated_macos_show_window_title_in_menubar_alias, deprecated_hide_window_decorations_aliases, deprecated_macos_show_window_title_in_menubar_alias,
deprecated_send_text, disable_ligatures, edge_width, env, font_features, hide_window_decorations, deprecated_send_text, disable_ligatures, edge_width, env, font_features, hide_window_decorations,
macos_option_as_alt, macos_titlebar_color, optional_edge_width, parse_map, parse_mouse_map, macos_option_as_alt, macos_titlebar_color, narrow_symbols, optional_edge_width, parse_map,
resize_draw_strategy, scrollback_lines, scrollback_pager_history_size, shell_integration, parse_mouse_map, resize_draw_strategy, scrollback_lines, scrollback_pager_history_size,
store_multiple, symbol_map, tab_activity_symbol, tab_bar_edge, tab_bar_margin_height, shell_integration, store_multiple, symbol_map, tab_activity_symbol, tab_bar_edge,
tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator, tab_title_template, titlebar_color, tab_bar_margin_height, tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator,
to_cursor_shape, to_font_size, to_layout_names, to_modifiers, url_prefixes, url_style, tab_title_template, titlebar_color, to_cursor_shape, to_font_size, to_layout_names, to_modifiers,
visual_window_select_characters, window_border_width, window_size url_prefixes, url_style, visual_window_select_characters, window_border_width, window_size
) )
@ -1079,6 +1079,10 @@ class Parser:
def mouse_hide_wait(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: def mouse_hide_wait(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['mouse_hide_wait'] = float(val) ans['mouse_hide_wait'] = float(val)
def narrow_symbols(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
for k, v in narrow_symbols(val):
ans["narrow_symbols"][k] = v
def open_url_with(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: def open_url_with(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['open_url_with'] = to_cmdline(val) ans['open_url_with'] = to_cmdline(val)
@ -1331,6 +1335,7 @@ def create_result_dict() -> typing.Dict[str, typing.Any]:
'exe_search_path': {}, 'exe_search_path': {},
'font_features': {}, 'font_features': {},
'kitten_alias': {}, 'kitten_alias': {},
'narrow_symbols': {},
'symbol_map': {}, 'symbol_map': {},
'watcher': {}, 'watcher': {},
'map': [], 'map': [],

View File

@ -387,6 +387,7 @@ option_names = ( # {{{
'mark3_foreground', 'mark3_foreground',
'mouse_hide_wait', 'mouse_hide_wait',
'mouse_map', 'mouse_map',
'narrow_symbols',
'open_url_with', 'open_url_with',
'placement_strategy', 'placement_strategy',
'pointer_shape_when_dragging', 'pointer_shape_when_dragging',
@ -594,6 +595,7 @@ class Options:
exe_search_path: typing.Dict[str, str] = {} exe_search_path: typing.Dict[str, str] = {}
font_features: typing.Dict[str, typing.Tuple[kitty.fonts.FontFeature, ...]] = {} font_features: typing.Dict[str, typing.Tuple[kitty.fonts.FontFeature, ...]] = {}
kitten_alias: typing.Dict[str, str] = {} kitten_alias: typing.Dict[str, str] = {}
narrow_symbols: typing.Dict[typing.Tuple[int, int], int] = {}
symbol_map: typing.Dict[typing.Tuple[int, int], str] = {} symbol_map: typing.Dict[typing.Tuple[int, int], str] = {}
watcher: typing.Dict[str, str] = {} watcher: typing.Dict[str, str] = {}
map: typing.List[kitty.options.utils.KeyDefinition] = [] map: typing.List[kitty.options.utils.KeyDefinition] = []
@ -712,6 +714,7 @@ defaults.env = {}
defaults.exe_search_path = {} defaults.exe_search_path = {}
defaults.font_features = {} defaults.font_features = {}
defaults.kitten_alias = {} defaults.kitten_alias = {}
defaults.narrow_symbols = {}
defaults.symbol_map = {} defaults.symbol_map = {}
defaults.watcher = {} defaults.watcher = {}
defaults.map = [ defaults.map = [

View File

@ -818,19 +818,19 @@ def action_alias(val: str) -> Iterable[Tuple[str, str]]:
kitten_alias = action_alias kitten_alias = action_alias
def symbol_map(val: str) -> Iterable[Tuple[Tuple[int, int], str]]: def symbol_map(val: str, min_size: int = 2) -> Iterable[Tuple[Tuple[int, int], str]]:
parts = val.split() parts = val.split()
def abort() -> None: def abort() -> None:
log_error(f'Symbol map: {val} is invalid, ignoring') log_error(f'Symbol map: {val} is invalid, ignoring')
if len(parts) < 2: if len(parts) < min_size:
return abort() return abort()
family = ' '.join(parts[1:]) family = ' '.join(parts[1:])
def to_chr(x: str) -> int: def to_chr(x: str) -> int:
if not x.startswith('U+'): if not x.startswith('U+'):
raise ValueError() raise ValueError(f'{x} is not a unicode codepoint of the form U+number')
return int(x[2:], 16) return int(x[2:], 16)
for x in parts[0].split(','): for x in parts[0].split(','):
@ -845,6 +845,11 @@ def symbol_map(val: str) -> Iterable[Tuple[Tuple[int, int], str]]:
yield (a, b), family yield (a, b), family
def narrow_symbols(val: str) -> Iterable[Tuple[Tuple[int, int], int]]:
for x, y in symbol_map(val, min_size=1):
yield x, int(y or 1)
def parse_key_action(action: str, action_type: str = 'map') -> KeyAction: def parse_key_action(action: str, action_type: str = 'map') -> KeyAction:
parts = action.strip().split(maxsplit=1) parts = action.strip().split(maxsplit=1)
func = parts[0] func = parts[0]