diff --git a/docs/changelog.rst b/docs/changelog.rst index 87f2564e0..c1445ec07 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -88,6 +88,8 @@ Detailed list of changes - 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`) - A new action to clear the screen up to the line containing the cursor, see diff --git a/docs/faq.rst b/docs/faq.rst index 009646dc1..77aa968f6 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -6,21 +6,22 @@ Frequently Asked Questions Some special symbols are rendered small/truncated in kitty? ----------------------------------------------------------- -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 +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 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 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 neighboring cell is empty, but looks terrible if it is not. -Some programs, like powerline, vim with fancy gutter symbols/status-bar, etc. -misuse unicode characters from the private use area to represent symbols. Often -these symbols are square and should be rendered in two cells. However, since -private use area symbols all have their width set to one in the unicode +Some programs, like Powerline, vim with fancy gutter symbols/status-bar, etc. +use Unicode characters from the private use area to represent symbols. Often +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 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 -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? diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index c6e48b885..c305d1569 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -967,7 +967,8 @@ def set_font_data( descriptor_for_idx: Callable[[int], Tuple[FontObject, bool, bool]], bold: int, italic: int, bold_italic: int, num_symbol_fonts: int, 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: pass diff --git a/kitty/fonts.c b/kitty/fonts.c index 3207e891f..083b5aaac 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -50,8 +50,8 @@ typedef struct { size_t font_idx; } SymbolMap; -static SymbolMap *symbol_maps = NULL; -static size_t num_symbol_maps = 0; +static SymbolMap *symbol_maps = NULL, *narrow_symbols = NULL; +static size_t num_symbol_maps = 0, num_narrow_symbols = 0; 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; } +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 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) { \ @@ -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); 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; while ( @@ -1303,6 +1315,7 @@ render_simple_text(FONTS_DATA_HANDLE fg_, const char *text) { static void clear_symbol_maps(void) { 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 { @@ -1311,26 +1324,33 @@ typedef struct { DescriptorIndices descriptor_indices = {0}; -static PyObject* -set_font_data(PyObject UNUSED *m, PyObject *args) { - PyObject *sm; - Py_CLEAR(box_drawing_function); Py_CLEAR(prerender_function); Py_CLEAR(descriptor_for_idx); Py_CLEAR(font_feature_settings); - if (!PyArg_ParseTuple(args, "OOOIIIIO!dO", - &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)) 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++) { +static bool +set_symbol_maps(SymbolMap **symbol_maps, size_t *num_symbol_maps, const PyObject *sm) { + *num_symbol_maps = PyTuple_GET_SIZE(sm); + *symbol_maps = calloc(*num_symbol_maps, sizeof(SymbolMap)); + if (*symbol_maps == NULL) { PyErr_NoMemory(); return false; } + for (size_t s = 0; s < *num_symbol_maps; s++) { 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; 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; } diff --git a/kitty/fonts/render.py b/kitty/fonts/render.py index 217187c32..817b855a5 100644 --- a/kitty/fonts/render.py +++ b/kitty/fonts/render.py @@ -21,6 +21,7 @@ from kitty.fonts.box_drawing import ( ) from kitty.options.types import Options, defaults from kitty.typing import CoreTextFont, FontConfigPattern +from kitty.types import _T from kitty.utils import log_error if is_macos: @@ -50,10 +51,9 @@ def font_for_family(family: str) -> Tuple[FontObject, bool, bool]: return font_for_family_fontconfig(family) -Range = Tuple[Tuple[int, int], str] - - -def merge_ranges(a: Range, b: Range, priority_map: Dict[Tuple[int, int], int]) -> Generator[Range, None, None]: +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]: a_start, a_end = a[0] b_start, b_end = b[0] 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_prio = a_prio # check if the before, mid and after ranges can be coalesced - ranges: List[Range] = [] + ranges: List[Tuple[Tuple[int, int], _T]] = [] priorities: List[int] = [] for rq, prio in ((before_range, before_range_prio), (mid_range, mid_range_prio), (after_range, after_range_prio)): 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 -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: return maps 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 +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]: 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)) before = len(current_faces) sm = create_symbol_map(opts) + ns = create_narrow_symbols(opts) num_symbol_fonts = len(current_faces) - before font_features = {} for face, _, _ in current_faces: @@ -203,7 +208,7 @@ def set_font_family(opts: Optional[Options] = None, override_font_size: Optional set_font_data( render_box_drawing, prerender_function, descriptor_for_idx, indices['bold'], indices['italic'], indices['bi'], num_symbol_fonts, - sm, sz, font_features + sm, sz, font_features, ns ) diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 6be39b577..cb537a942 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -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', option_type='disable_ligatures', ctype='int', long_text=''' diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 9f291ff0f..effa9b7cf 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -11,12 +11,12 @@ from kitty.options.utils import ( 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_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, - resize_draw_strategy, scrollback_lines, scrollback_pager_history_size, shell_integration, - store_multiple, symbol_map, tab_activity_symbol, tab_bar_edge, tab_bar_margin_height, - tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator, tab_title_template, titlebar_color, - to_cursor_shape, to_font_size, to_layout_names, to_modifiers, url_prefixes, url_style, - visual_window_select_characters, window_border_width, window_size + macos_option_as_alt, macos_titlebar_color, narrow_symbols, optional_edge_width, parse_map, + parse_mouse_map, resize_draw_strategy, scrollback_lines, scrollback_pager_history_size, + shell_integration, store_multiple, symbol_map, tab_activity_symbol, tab_bar_edge, + tab_bar_margin_height, tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator, + tab_title_template, titlebar_color, to_cursor_shape, to_font_size, to_layout_names, to_modifiers, + 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: 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: ans['open_url_with'] = to_cmdline(val) @@ -1331,6 +1335,7 @@ def create_result_dict() -> typing.Dict[str, typing.Any]: 'exe_search_path': {}, 'font_features': {}, 'kitten_alias': {}, + 'narrow_symbols': {}, 'symbol_map': {}, 'watcher': {}, 'map': [], diff --git a/kitty/options/types.py b/kitty/options/types.py index d9a5e005e..3cbf5c7c7 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -387,6 +387,7 @@ option_names = ( # {{{ 'mark3_foreground', 'mouse_hide_wait', 'mouse_map', + 'narrow_symbols', 'open_url_with', 'placement_strategy', 'pointer_shape_when_dragging', @@ -594,6 +595,7 @@ class Options: exe_search_path: typing.Dict[str, str] = {} font_features: typing.Dict[str, typing.Tuple[kitty.fonts.FontFeature, ...]] = {} kitten_alias: typing.Dict[str, str] = {} + narrow_symbols: typing.Dict[typing.Tuple[int, int], int] = {} symbol_map: typing.Dict[typing.Tuple[int, int], str] = {} watcher: typing.Dict[str, str] = {} map: typing.List[kitty.options.utils.KeyDefinition] = [] @@ -712,6 +714,7 @@ defaults.env = {} defaults.exe_search_path = {} defaults.font_features = {} defaults.kitten_alias = {} +defaults.narrow_symbols = {} defaults.symbol_map = {} defaults.watcher = {} defaults.map = [ diff --git a/kitty/options/utils.py b/kitty/options/utils.py index 1dfe7df8c..f6be89de7 100644 --- a/kitty/options/utils.py +++ b/kitty/options/utils.py @@ -818,19 +818,19 @@ def action_alias(val: str) -> Iterable[Tuple[str, str]]: 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() def abort() -> None: log_error(f'Symbol map: {val} is invalid, ignoring') - if len(parts) < 2: + if len(parts) < min_size: return abort() family = ' '.join(parts[1:]) def to_chr(x: str) -> int: 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) 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 +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: parts = action.strip().split(maxsplit=1) func = parts[0]