Allow controlling the ligature strategy dynamically, per window
Fixes #1574
This commit is contained in:
@@ -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():
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
'''))
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
};
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@ typedef struct {
|
||||
int state;
|
||||
uint8_t stop_buf[32];
|
||||
} pending_mode;
|
||||
DisableLigature disable_ligatures;
|
||||
|
||||
} Screen;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user