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
- 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

View File

@ -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?

View File

@ -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

View File

@ -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;
}

View File

@ -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
)

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',
option_type='disable_ligatures', ctype='int',
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,
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': [],

View File

@ -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 = [

View File

@ -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]