diff --git a/kitty/boss.py b/kitty/boss.py index 65f840482..539b08100 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -996,6 +996,23 @@ class Boss: if tm is not None: tm.move_tab(-1) + def disable_ligatures_in(self, where, strategy): + if isinstance(where, str): + windows = () + if where == 'active': + if self.active_window is not None: + windows = (self.active_window,) + elif where == 'all': + windows = self.all_windows + elif where == 'tab': + if self.active_tab is not None: + windows = tuple(self.active_tab) + else: + windows = where + for window in windows: + window.screen.disable_ligatures = strategy + window.refresh() + def patch_colors(self, spec, cursor_text_color, configured=False): if configured: for k, v in spec.items(): diff --git a/kitty/cmds.py b/kitty/cmds.py index c6a558e67..7ffea4c0e 100644 --- a/kitty/cmds.py +++ b/kitty/cmds.py @@ -78,6 +78,23 @@ for that window is used. ''' +def windows_for_payload(boss, window, payload): + if payload.get('all'): + windows = tuple(boss.all_windows) + else: + windows = (window or boss.active_window,) + if payload.get('match_window'): + windows = tuple(boss.match_windows(payload['match_window'])) + if not windows: + raise MatchError(payload['match_window']) + if payload.get('match_tab'): + tabs = tuple(boss.match_tabs(payload['match_tab'])) + if not tabs: + raise MatchError(payload['match_tab'], 'tabs') + for tab in tabs: + windows += tuple(tab) + + # ls {{{ @cmd( 'List all tabs/windows', @@ -711,20 +728,7 @@ def cmd_set_colors(global_opts, opts, args): def set_colors(boss, window, payload): from .rgb import color_as_int, Color - if payload['all']: - windows = tuple(boss.all_windows) - else: - windows = (window or boss.active_window,) - if payload['match_window']: - windows = tuple(boss.match_windows(payload['match_window'])) - if not windows: - raise MatchError(payload['match_window']) - if payload['match_tab']: - tabs = tuple(boss.match_tabs(payload['match_tab'])) - if not tabs: - raise MatchError(payload['match_tab'], 'tabs') - for tab in tabs: - windows += tuple(tab) + windows = windows_for_payload(boss, window, payload) if payload['reset']: payload['colors'] = {k: color_as_int(v) for k, v in boss.startup_colors.items()} payload['cursor_text_color'] = boss.startup_cursor_text_color @@ -815,6 +819,36 @@ def set_background_opacity(boss, window, payload): # }}} +# disable_ligatures {{{ +@cmd( + 'Control ligature rendering', + 'Control ligature rendering for the specified windows/tabs (defaults to active window). The STRATEGY' + ' can be one of: never, always, cursor', + options_spec='''\ +--all -a +type=bool-set +By default, ligatures are only affected in the active window. This option will +cause ligatures to be changed in all windows. + +''' + '\n\n' + MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t'), + argspec='STRATEGY' +) +def cmd_disable_ligatures(global_opts, opts, args): + strategy = args[0] + if strategy not in ('never', 'always', 'cursor'): + raise ValueError('{} is not a valid disable_ligatures strategy'.format('strategy')) + return { + 'strategy': strategy, 'match_window': opts.match, 'match_tab': opts.match_tab, + 'all': opts.all, + } + + +def disable_ligatures(boss, window, payload): + windows = windows_for_payload(boss, window, payload) + boss.disable_ligatures_in(windows, payload['strategy']) +# }}} + + # kitten {{{ @cmd( 'Run a kitten', diff --git a/kitty/config.py b/kitty/config.py index c7eba3766..d1c41fdfb 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -214,6 +214,20 @@ def nth_window(func, rest): return func, [num] +@func_with_args('disable_ligatures_in') +def disable_ligatures_in(func, rest): + parts = rest.split(maxsplit=1) + if len(parts) == 1: + where, strategy = 'active', parts[0] + else: + where, strategy = parts + if where not in ('active', 'all', 'tab'): + raise ValueError('{} is not a valid set of windows to disable ligatures in'.format(where)) + if strategy not in ('never', 'always', 'cursor'): + raise ValueError('{} is not a valid disable ligatures strategy'.format(strategy)) + return func, [where, strategy] + + def parse_key_action(action): parts = action.strip().split(maxsplit=1) func = parts[0] diff --git a/kitty/config_data.py b/kitty/config_data.py index 4e389c40a..264ee2a93 100644 --- a/kitty/config_data.py +++ b/kitty/config_data.py @@ -267,7 +267,13 @@ o('disable_ligatures', 'never', option_type=disable_ligatures, long_text=_(''' Choose how you want to handle multi-character ligatures. The default is to always render them. You can tell kitty to not render them when the cursor is over them by using :code:`cursor` to make editing easier, or have kitty never -render them at all by using :code:`never`, if you don't like them. +render them at all by using :code:`always`, if you don't like them. The ligature +strategy can be set per-window either using the kitty remote control facility +or by defining shortcuts for it in kitty.conf, for example:: + + map alt+1 disable_ligatures_in active always + map alt+2 disable_ligatures_in all never + map alt+3 disable_ligatures_in tab cursor ''')) diff --git a/kitty/data-types.h b/kitty/data-types.h index 78f94d942..b4ff02327 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -40,6 +40,7 @@ typedef uint16_t sprite_index; typedef uint16_t attrs_type; typedef uint8_t line_attrs_type; typedef enum CursorShapes { NO_CURSOR_SHAPE, CURSOR_BLOCK, CURSOR_BEAM, CURSOR_UNDERLINE, NUM_OF_CURSOR_SHAPES } CursorShape; +typedef enum { DISABLE_LIGATURES_NEVER, DISABLE_LIGATURES_CURSOR, DISABLE_LIGATURES_ALWAYS } DisableLigature; #define ERROR_PREFIX "[PARSE ERROR]" typedef enum MouseTrackingModes { NO_TRACKING, BUTTON_MODE, MOTION_MODE, ANY_MODE } MouseTrackingMode; diff --git a/kitty/fonts.c b/kitty/fonts.c index cb07fe78e..166a1acab 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -744,7 +744,7 @@ shape(CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_cells, hb group_state.last_gpu_cell = first_gpu_cell + (num_cells ? num_cells - 1 : 0); load_hb_buffer(first_cpu_cell, first_gpu_cell, num_cells); - if (disable_ligature || OPT(disable_ligatures) == DISABLE_LIGATURES_ALWAYS) { + if (disable_ligature) { hb_shape(font, harfbuzz_buffer, &no_calt_feature, 1); } else { hb_shape(font, harfbuzz_buffer, NULL, 0); @@ -1015,10 +1015,10 @@ test_shape(PyObject UNUSED *self, PyObject *args) { #undef G static inline void -render_run(FontGroup *fg, CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_cells, ssize_t font_idx, bool pua_space_ligature, bool center_glyph, int cursor_offset) { +render_run(FontGroup *fg, CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_cells, ssize_t font_idx, bool pua_space_ligature, bool center_glyph, int cursor_offset, DisableLigature disable_ligature_strategy) { switch(font_idx) { default: - shape_run(first_cpu_cell, first_gpu_cell, num_cells, &fg->fonts[font_idx], false); + shape_run(first_cpu_cell, first_gpu_cell, num_cells, &fg->fonts[font_idx], disable_ligature_strategy == DISABLE_LIGATURES_ALWAYS); if (pua_space_ligature) merge_groups_for_pua_space_ligature(); else if (cursor_offset > -1) { index_type left, right; @@ -1052,21 +1052,18 @@ render_run(FontGroup *fg, CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, inde } void -render_line(FONTS_DATA_HANDLE fg_, Line *line, index_type lnum, Cursor *cursor) { +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) { \ int cursor_offset = -1; \ - if (disable_ligature_in_line && first_cell_in_run <= cursor->x && cursor->x <= i) cursor_offset = cursor->x - first_cell_in_run; \ - render_run(fg, line->cpu_cells + first_cell_in_run, line->gpu_cells + first_cell_in_run, i - first_cell_in_run, run_font_idx, false, center_glyph, cursor_offset); \ + if (disable_ligature_at_cursor && first_cell_in_run <= cursor->x && cursor->x <= i) cursor_offset = cursor->x - first_cell_in_run; \ + render_run(fg, line->cpu_cells + first_cell_in_run, line->gpu_cells + first_cell_in_run, i - first_cell_in_run, run_font_idx, false, center_glyph, cursor_offset, disable_ligature_strategy); \ } FontGroup *fg = (FontGroup*)fg_; ssize_t run_font_idx = NO_FONT; bool center_glyph = false; - bool disable_ligature_in_line = false; + bool disable_ligature_at_cursor = cursor != NULL && disable_ligature_strategy == DISABLE_LIGATURES_CURSOR && lnum == cursor->y; index_type first_cell_in_run, i; attrs_type prev_width = 0; - if (cursor != NULL && OPT(disable_ligatures) == DISABLE_LIGATURES_CURSOR) { - if (lnum == cursor->y) disable_ligature_in_line = true; - } for (i=0, first_cell_in_run=0; i < line->xnum; i++) { if (prev_width == 2) { prev_width = 0; continue; } CPUCell *cpu_cell = line->cpu_cells + i; @@ -1104,7 +1101,7 @@ render_line(FONTS_DATA_HANDLE fg_, Line *line, index_type lnum, Cursor *cursor) center_glyph = true; RENDER center_glyph = false; - render_run(fg, line->cpu_cells + i, line->gpu_cells + i, num_spaces + 1, cell_font_idx, true, center_glyph, -1); + render_run(fg, line->cpu_cells + i, line->gpu_cells + i, num_spaces + 1, cell_font_idx, true, center_glyph, -1, disable_ligature_strategy); run_font_idx = NO_FONT; first_cell_in_run = i + num_spaces + 1; prev_width = line->gpu_cells[i+num_spaces].attrs & WIDTH_MASK; @@ -1296,7 +1293,7 @@ test_render_line(PyObject UNUSED *self, PyObject *args) { PyObject *line; if (!PyArg_ParseTuple(args, "O!", &Line_Type, &line)) return NULL; if (!num_font_groups) { PyErr_SetString(PyExc_RuntimeError, "must create font group first"); return NULL; } - render_line((FONTS_DATA_HANDLE)font_groups, (Line*)line, 0, NULL); + render_line((FONTS_DATA_HANDLE)font_groups, (Line*)line, 0, NULL, DISABLE_LIGATURES_NEVER); Py_RETURN_NONE; } diff --git a/kitty/fonts.h b/kitty/fonts.h index 842eec10f..0791c633b 100644 --- a/kitty/fonts.h +++ b/kitty/fonts.h @@ -34,7 +34,7 @@ PyObject* face_from_descriptor(PyObject*, FONTS_DATA_HANDLE); void sprite_tracker_current_layout(FONTS_DATA_HANDLE data, unsigned int *x, unsigned int *y, unsigned int *z); void render_alpha_mask(uint8_t *alpha_mask, pixel* dest, Region *src_rect, Region *dest_rect, size_t src_stride, size_t dest_stride); -void render_line(FONTS_DATA_HANDLE, Line *line, index_type lnum, Cursor *cursor); +void render_line(FONTS_DATA_HANDLE, Line *line, index_type lnum, Cursor *cursor, DisableLigature); void sprite_tracker_set_limits(size_t max_texture_size, size_t max_array_len); typedef void (*free_extra_data_func)(void*); StringCanvas render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline); diff --git a/kitty/screen.c b/kitty/screen.c index c37a1f5b0..c7a04296a 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -113,6 +113,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { self->alt_grman = grman_alloc(); self->grman = self->main_grman; self->pending_mode.wait_time = 2.0; + self->disable_ligatures = OPT(disable_ligatures); self->main_tabstops = PyMem_Calloc(2 * self->columns, sizeof(bool)); if (self->cursor == NULL || self->main_linebuf == NULL || self->alt_linebuf == NULL || self->main_tabstops == NULL || self->historybuf == NULL || self->main_grman == NULL || self->alt_grman == NULL || self->color_profile == NULL) { Py_CLEAR(self); return NULL; @@ -1479,7 +1480,7 @@ screen_update_cell_data(Screen *self, void *address, FONTS_DATA_HANDLE fonts_dat lnum = self->scrolled_by - 1 - y; historybuf_init_line(self->historybuf, lnum, self->historybuf->line); if (self->historybuf->line->has_dirty_text) { - render_line(fonts_data, self->historybuf->line, lnum, self->cursor); + render_line(fonts_data, self->historybuf->line, lnum, self->cursor, self->disable_ligatures); historybuf_mark_line_clean(self->historybuf, lnum); } update_line_data(self->historybuf->line, y, address); @@ -1489,7 +1490,7 @@ screen_update_cell_data(Screen *self, void *address, FONTS_DATA_HANDLE fonts_dat linebuf_init_line(self->linebuf, lnum); if (self->linebuf->line->has_dirty_text || (cursor_has_moved && (self->cursor->y == lnum || self->last_rendered_cursor_y == lnum))) { - render_line(fonts_data, self->linebuf->line, lnum, self->cursor); + render_line(fonts_data, self->linebuf->line, lnum, self->cursor, self->disable_ligatures); linebuf_mark_line_clean(self->linebuf, lnum); } update_line_data(self->linebuf->line, y, address); @@ -1888,6 +1889,37 @@ MODE_GETSET(auto_repeat_enabled, DECARM) MODE_GETSET(cursor_visible, DECTCEM) MODE_GETSET(cursor_key_mode, DECCKM) +static PyObject* disable_ligatures_get(Screen *self, void UNUSED *closure) { + const char *ans = NULL; + switch(self->disable_ligatures) { + case DISABLE_LIGATURES_NEVER: + ans = "never"; + break; + case DISABLE_LIGATURES_CURSOR: + ans = "cursor"; + break; + case DISABLE_LIGATURES_ALWAYS: + ans = "always"; + break; + } + return PyUnicode_FromString(ans); +} + +static int disable_ligatures_set(Screen *self, PyObject *val, void UNUSED *closure) { + if (val == NULL) { PyErr_SetString(PyExc_TypeError, "Cannot delete attribute"); return -1; } + if (!PyUnicode_Check(val)) { PyErr_SetString(PyExc_TypeError, "unicode string expected"); return -1; } + if (PyUnicode_READY(val) != 0) return -1; + const char *q = PyUnicode_AsUTF8(val); + DisableLigature dl = DISABLE_LIGATURES_NEVER; + if (strcmp(q, "always") == 0) dl = DISABLE_LIGATURES_ALWAYS; + else if (strcmp(q, "cursor") == 0) dl = DISABLE_LIGATURES_CURSOR; + if (dl != self->disable_ligatures) { + self->disable_ligatures = dl; + screen_dirty_sprite_positions(self); + } + return 0; +} + static PyObject* cursor_up(Screen *self, PyObject *args) { unsigned int count = 1; @@ -2238,6 +2270,7 @@ static PyGetSetDef getsetters[] = { GETSET(focus_tracking_enabled) GETSET(cursor_visible) GETSET(cursor_key_mode) + GETSET(disable_ligatures) {NULL} /* Sentinel */ }; diff --git a/kitty/screen.h b/kitty/screen.h index deef15c27..b134e3ab0 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -105,6 +105,7 @@ typedef struct { int state; uint8_t stop_buf[32]; } pending_mode; + DisableLigature disable_ligatures; } Screen; diff --git a/kitty/shaders.c b/kitty/shaders.c index 5948a7279..70ea8af1a 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -293,7 +293,7 @@ cell_prepare_to_render(ssize_t vao_idx, ssize_t gvao_idx, Screen *screen, GLfloa bool cursor_pos_changed = screen->cursor->x != screen->last_rendered_cursor_x || screen->cursor->y != screen->last_rendered_cursor_y; - bool disable_ligatures = OPT(disable_ligatures) == DISABLE_LIGATURES_CURSOR; + bool disable_ligatures = screen->disable_ligatures == DISABLE_LIGATURES_CURSOR; if (screen->scroll_changed || screen->is_dirty || (disable_ligatures && cursor_pos_changed)) { sz = sizeof(GPUCell) * screen->lines * screen->columns; diff --git a/kitty/state.h b/kitty/state.h index fbdac3383..75375e5a5 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -11,7 +11,6 @@ #define OPT(name) global_state.opts.name typedef enum { LEFT_EDGE, TOP_EDGE, RIGHT_EDGE, BOTTOM_EDGE } Edge; -typedef enum { DISABLE_LIGATURES_NEVER, DISABLE_LIGATURES_CURSOR, DISABLE_LIGATURES_ALWAYS } DisableLigature; typedef struct { double visual_bell_duration, cursor_blink_interval, cursor_stop_blinking_after, mouse_hide_wait, click_interval, wheel_scroll_multiplier, touch_scroll_multiplier;