From d3fd0646fbedd02f0f5c7d2be9c31d8a9e186f1b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 2 Dec 2016 14:23:38 +0530 Subject: [PATCH] Implement the mouse tracking protocol --- kitty/char_grid.py | 98 ++++++++++++++++++++++++---------------------- kitty/data-types.c | 8 +++- kitty/data-types.h | 11 +++++- kitty/modes.h | 2 + kitty/mouse.py | 62 +++++++++++++++++++++++++++++ kitty/screen.c | 45 +++++++++++++++------ kitty/window.py | 84 +++++++++++++++++++++++++++++---------- 7 files changed, 227 insertions(+), 83 deletions(-) create mode 100644 kitty/mouse.py diff --git a/kitty/char_grid.py b/kitty/char_grid.py index 2583a3052..a2d63293a 100644 --- a/kitty/char_grid.py +++ b/kitty/char_grid.py @@ -263,11 +263,14 @@ class CharGrid: self.current_cursor = Cursor(c.x, c.y, c.hidden, c.shape, c.color, c.blink) def cell_for_pos(self, x, y): - return int(x // cell_size.width), int(y // cell_size.height) + x, y = int(x // cell_size.width), int(y // cell_size.height) + if 0 <= x < self.screen.columns and 0 <= y < self.screen.lines: + return x, y + return None, None def update_drag(self, is_press, x, y): x, y = self.cell_for_pos(x, y) - if 0 <= x < self.screen.columns and 0 <= y < self.screen.lines: + if x is not None: ps = None with self.buffer_lock: if is_press: @@ -287,24 +290,26 @@ class CharGrid: def has_url_at(self, x, y): x, y = self.cell_for_pos(x, y) - l = self.screen_line(y) - if l is not None: - text = l.as_base_text() - for m in self.url_pat.finditer(text): - if m.start() <= x < m.end(): - return True + if x is not None: + l = self.screen_line(y) + if l is not None: + text = l.as_base_text() + for m in self.url_pat.finditer(text): + if m.start() <= x < m.end(): + return True return False def click_url(self, x, y): x, y = self.cell_for_pos(x, y) - l = self.screen_line(y) - if l is not None: - text = l.as_base_text() - for m in self.url_pat.finditer(text): - if m.start() <= x < m.end(): - url = ''.join(l[i] for i in range(*m.span())).rstrip('.') - if url: - open_url(url, self.opts.open_url_with) + if x is not None: + l = self.screen_line(y) + if l is not None: + text = l.as_base_text() + for m in self.url_pat.finditer(text): + if m.start() <= x < m.end(): + url = ''.join(l[i] for i in range(*m.span())).rstrip('.') + if url: + open_url(url, self.opts.open_url_with) def screen_line(self, y): ' Return the Line object corresponding to the yth line on the rendered screen ' @@ -318,36 +323,37 @@ class CharGrid: def multi_click(self, count, x, y): x, y = self.cell_for_pos(x, y) - line = self.screen_line(y) - if line is not None and 0 <= x < self.screen.columns and count in (2, 3): - s = self.current_selection - s.start_scrolled_by = s.end_scrolled_by = self.scrolled_by - s.start_y = s.end_y = y - s.in_progress = False - if count == 3: - for i in range(self.screen.columns): - if line[i] != ' ': - s.start_x = i - break - else: - s.start_x = 0 - for i in range(self.screen.columns): - c = self.screen.columns - 1 - i - if line[c] != ' ': - s.end_x = c - break - else: - s.end_x = self.screen.columns - 1 - elif count == 2: - i = x - pat = re.compile(r'\w') - while i >= 0 and pat.match(line[i]) is not None: - i -= 1 - s.start_x = i if i == x else i + 1 - i = x - while i < self.screen.columns and pat.match(line[i]) is not None: - i += 1 - s.end_x = i if i == x else i - 1 + if x is not None: + line = self.screen_line(y) + if line is not None and count in (2, 3): + s = self.current_selection + s.start_scrolled_by = s.end_scrolled_by = self.scrolled_by + s.start_y = s.end_y = y + s.in_progress = False + if count == 3: + for i in range(self.screen.columns): + if line[i] != ' ': + s.start_x = i + break + else: + s.start_x = 0 + for i in range(self.screen.columns): + c = self.screen.columns - 1 - i + if line[c] != ' ': + s.end_x = c + break + else: + s.end_x = self.screen.columns - 1 + elif count == 2: + i = x + pat = re.compile(r'\w') + while i >= 0 and pat.match(line[i]) is not None: + i -= 1 + s.start_x = i if i == x else i + 1 + i = x + while i < self.screen.columns and pat.match(line[i]) is not None: + i += 1 + s.end_x = i if i == x else i - 1 def text_for_selection(self, sel=None): start, end = (sel or self.current_selection).limits(self.scrolled_by) diff --git a/kitty/data-types.c b/kitty/data-types.c index 8d7d2e8c3..872683226 100644 --- a/kitty/data-types.c +++ b/kitty/data-types.c @@ -73,7 +73,13 @@ PyInit_fast_data_types(void) { PyModule_AddIntMacro(m, DECOM); PyModule_AddIntMacro(m, IRM); PyModule_AddIntMacro(m, DATA_CELL_SIZE); - + PyModule_AddIntMacro(m, ANY_MODE); + PyModule_AddIntMacro(m, MOTION_MODE); + PyModule_AddIntMacro(m, BUTTON_MODE); + PyModule_AddIntMacro(m, SGR_PROTOCOL); + PyModule_AddIntMacro(m, NORMAL_PROTOCOL); + PyModule_AddIntMacro(m, URXVT_PROTOCOL); + PyModule_AddIntMacro(m, UTF8_PROTOCOL); } return m; diff --git a/kitty/data-types.h b/kitty/data-types.h index dc30d15cb..d1034a573 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -23,6 +23,13 @@ typedef uint32_t combining_type; typedef unsigned int index_type; #define ERROR_PREFIX "[PARSE ERROR]" +#define ANY_MODE 3 +#define MOTION_MODE 2 +#define BUTTON_MODE 1 +#define NORMAL_PROTOCOL 0 +#define UTF8_PROTOCOL 1 +#define SGR_PROTOCOL 2 +#define URXVT_PROTOCOL 3 #define CELL_SIZE (sizeof(char_type) + sizeof(color_type) + sizeof(decoration_type) + sizeof(combining_type)) // The data cell size must be a multiple of 3 @@ -227,8 +234,8 @@ PyTypeObject ChangeTracker_Type; typedef struct { bool mLNM, mIRM, mDECTCEM, mDECSCNM, mDECOM, mDECAWM, mDECCOLM, mDECARM, - mBRACKETED_PASTE, mFOCUS_TRACKING, mMOUSE_BUTTON_TRACKING, - mMOUSE_MOTION_TRACKING, mMOUSE_SGR_MODE, mMOUSE_MOVE_TRACKING; + mBRACKETED_PASTE, mFOCUS_TRACKING; + unsigned long mouse_tracking_mode, mouse_tracking_protocol; } ScreenModes; PyTypeObject ScreenModes_Type; diff --git a/kitty/modes.h b/kitty/modes.h index 19d4de4e0..0131f38ca 100644 --- a/kitty/modes.h +++ b/kitty/modes.h @@ -63,7 +63,9 @@ #define MOUSE_MOTION_TRACKING (1002 << 5) #define MOUSE_MOVE_TRACKING (1003 << 5) #define FOCUS_TRACKING (1004 << 5) +#define MOUSE_UTF8_MODE (1005 << 5) #define MOUSE_SGR_MODE (1006 << 5) +#define MOUSE_URXVT_MODE (1015 << 5) // Alternate screen buffer #define ALTERNATE_SCREEN (1049 << 5) diff --git a/kitty/mouse.py b/kitty/mouse.py new file mode 100644 index 000000000..26274bc41 --- /dev/null +++ b/kitty/mouse.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2016, Kovid Goyal + +from .fast_data_types import ( + GLFW_MOUSE_BUTTON_2, GLFW_MOUSE_BUTTON_3, GLFW_MOD_ALT, GLFW_MOD_CONTROL, + GLFW_MOD_SHIFT, GLFW_MOUSE_BUTTON_4, GLFW_MOUSE_BUTTON_5, SGR_PROTOCOL, + GLFW_MOUSE_BUTTON_1, URXVT_PROTOCOL, UTF8_PROTOCOL +) + +PRESS, RELEASE, DRAG, MOVE = range(4) +SHIFT_INDICATOR = 1 << 2 +ALT_INDICATOR = 1 << 3 +CONTROL_INDICATOR = 1 << 4 +MOTION_INDICATOR = 1 << 5 +EXTRA_BUTTON_INDICATOR = 1 << 6 + +cb_map = { + GLFW_MOUSE_BUTTON_1: 0, + GLFW_MOUSE_BUTTON_2: 0b1, + GLFW_MOUSE_BUTTON_3: 0b10, + GLFW_MOUSE_BUTTON_4: EXTRA_BUTTON_INDICATOR, + GLFW_MOUSE_BUTTON_5: EXTRA_BUTTON_INDICATOR | 0b1 +} + + +def encode_mouse_event(tracking_mode, tracking_protocol, button, action, mods, x, y): + x, y = x + 1, y + 1 # One based indexing + cb = 0 + if action is MOVE: + if tracking_protocol != SGR_PROTOCOL: + cb = 0b11 + else: + cb = cb_map.get(button) + if cb is None: + return + if action in (DRAG, MOVE): + cb |= MOTION_INDICATOR + elif action is RELEASE: + if tracking_protocol != SGR_PROTOCOL: + cb = 0b11 + if mods & GLFW_MOD_SHIFT: + cb |= SHIFT_INDICATOR + if mods & GLFW_MOD_ALT: + cb |= ALT_INDICATOR + if mods & GLFW_MOD_CONTROL: + cb |= CONTROL_INDICATOR + ans = None + if tracking_protocol == SGR_PROTOCOL: + ans = '\033[<%d;%d;%d%s' % (cb, x, y, 'm' if action is RELEASE else 'M') + ans = ans.encode('ascii') + elif tracking_protocol == URXVT_PROTOCOL: + ans = '\033[%d;%d;%dM' % (cb + 32, x, y) + ans = ans.encode('ascii') + elif tracking_protocol == UTF8_PROTOCOL: + ans = bytearray([0o33, ord('['), cb + 32]) + ans.extend(chr(x + 32).encode('utf-8') + chr(y + 32).encode('utf-8')) + ans = bytes(ans) + else: + if x <= 223 and y <= 223: + ans = bytearray([0o33, ord('['), cb + 32, x + 32, y + 32]) + return ans diff --git a/kitty/screen.c b/kitty/screen.c index 75162d85b..db449331c 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -344,17 +344,23 @@ set_mode_from_const(Screen *self, unsigned int mode, bool val) { case name: \ self->modes.m##name = val; break; +#define MOUSE_MODE(name, attr, value) \ + case name: \ + self->modes.attr = val ? value : 0; break; + bool private; switch(mode) { SIMPLE_MODE(LNM) SIMPLE_MODE(IRM) SIMPLE_MODE(DECARM) SIMPLE_MODE(BRACKETED_PASTE) - SIMPLE_MODE(MOUSE_BUTTON_TRACKING) - SIMPLE_MODE(MOUSE_MOVE_TRACKING) - SIMPLE_MODE(MOUSE_MOTION_TRACKING) - SIMPLE_MODE(MOUSE_SGR_MODE) SIMPLE_MODE(FOCUS_TRACKING) + MOUSE_MODE(MOUSE_BUTTON_TRACKING, mouse_tracking_mode, BUTTON_MODE) + MOUSE_MODE(MOUSE_MOTION_TRACKING, mouse_tracking_mode, MOTION_MODE) + MOUSE_MODE(MOUSE_MOVE_TRACKING, mouse_tracking_mode, ANY_MODE) + MOUSE_MODE(MOUSE_UTF8_MODE, mouse_tracking_protocol, UTF8_PROTOCOL) + MOUSE_MODE(MOUSE_SGR_MODE, mouse_tracking_protocol, SGR_PROTOCOL) + MOUSE_MODE(MOUSE_URXVT_MODE, mouse_tracking_protocol, URXVT_PROTOCOL) case DECCKM: case DECSCLM: @@ -403,6 +409,7 @@ set_mode_from_const(Screen *self, unsigned int mode, bool val) { fprintf(stderr, "%s %s %u %s\n", ERROR_PREFIX, "Unsupported screen mode: ", mode, private ? "(private)" : ""); } #undef SIMPLE_MODE +#undef MOUSE_MODE } void screen_set_mode(Screen *self, unsigned int mode) { @@ -993,12 +1000,17 @@ WRAP1B(erase_in_display, 0) MODE_GETTER(in_bracketed_paste_mode, BRACKETED_PASTE) MODE_GETTER(focus_tracking_enabled, FOCUS_TRACKING) -MODE_GETTER(mouse_button_tracking_enabled, MOUSE_BUTTON_TRACKING) -MODE_GETTER(mouse_motion_tracking_enabled, MOUSE_MOTION_TRACKING) -MODE_GETTER(mouse_move_tracking_enabled, MOUSE_MOVE_TRACKING) -MODE_GETTER(mouse_in_sgr_mode, MOUSE_SGR_MODE) MODE_GETTER(auto_repeat_enabled, DECARM) +static PyObject* +mouse_tracking_mode(Screen *self) { + return PyLong_FromUnsignedLong(self->modes.mouse_tracking_mode); +} + +static PyObject* +mouse_tracking_protocol(Screen *self) { + return PyLong_FromUnsignedLong(self->modes.mouse_tracking_protocol); +} static PyObject* cursor_up(Screen *self, PyObject *args) { @@ -1100,7 +1112,8 @@ static PyObject* mark_as_dirty(Screen *self) { Py_RETURN_NONE; } -static PyObject* current_char_width(Screen *self) { +static PyObject* +current_char_width(Screen *self) { #define current_char_width_doc "The width of the character under the cursor" unsigned long ans = 1; if (self->cursor->x < self->columns - 1 && self->cursor->y < self->lines) { @@ -1109,6 +1122,13 @@ static PyObject* current_char_width(Screen *self) { return PyLong_FromUnsignedLong(ans); } +static PyObject* +is_main_linebuf(Screen *self) { + PyObject *ans = (self->linebuf == self->main_linebuf) ? Py_True : Py_False; + Py_INCREF(ans); + return ans; +} + WRAP2(cursor_position, 1, 1) #define COUNT_WRAP(name) WRAP1(name, 1) @@ -1132,6 +1152,7 @@ static PyMethodDef methods[] = { MND(reset_mode, METH_VARARGS) MND(reset, METH_NOARGS) MND(reset_dirty, METH_NOARGS) + MND(is_main_linebuf, METH_NOARGS) MND(consolidate_changes, METH_NOARGS) MND(cursor_back, METH_VARARGS) MND(erase_in_line, METH_VARARGS) @@ -1144,6 +1165,8 @@ static PyMethodDef methods[] = { MND(change_scrollback_size, METH_VARARGS) MND(erase_characters, METH_VARARGS) MND(cursor_up, METH_VARARGS) + MND(mouse_tracking_mode, METH_NOARGS) + MND(mouse_tracking_protocol, METH_NOARGS) MND(cursor_up1, METH_VARARGS) MND(cursor_down, METH_VARARGS) MND(cursor_down1, METH_VARARGS) @@ -1165,10 +1188,6 @@ static PyMethodDef methods[] = { MND(in_bracketed_paste_mode, METH_NOARGS) MND(auto_repeat_enabled, METH_NOARGS) MND(focus_tracking_enabled, METH_NOARGS) - MND(mouse_button_tracking_enabled, METH_NOARGS) - MND(mouse_motion_tracking_enabled, METH_NOARGS) - MND(mouse_move_tracking_enabled, METH_NOARGS) - MND(mouse_in_sgr_mode, METH_NOARGS) {"update_cell_data", (PyCFunction)screen_update_cell_data, METH_VARARGS, ""}, {"select_graphic_rendition", (PyCFunction)_select_graphic_rendition, METH_VARARGS, ""}, diff --git a/kitty/window.py b/kitty/window.py index 68b4fd52e..c54f5a785 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -4,7 +4,7 @@ import os import weakref -from collections import deque +from collections import deque, defaultdict from functools import partial from time import monotonic @@ -13,9 +13,12 @@ from .constants import wakeup, tab_manager, appname, WindowGeometry from .fast_data_types import ( BRACKETED_PASTE_START, BRACKETED_PASTE_END, Screen, read_bytes_dump, read_bytes, GLFW_MOD_SHIFT, GLFW_MOUSE_BUTTON_1, GLFW_PRESS, - GLFW_MOUSE_BUTTON_MIDDLE, GLFW_RELEASE, GLFW_KEY_LEFT_SHIFT, - GLFW_KEY_RIGHT_SHIFT, glfw_post_empty_event + GLFW_MOUSE_BUTTON_MIDDLE, GLFW_RELEASE, glfw_post_empty_event, + GLFW_MOUSE_BUTTON_5, ANY_MODE, MOTION_MODE, GLFW_KEY_LEFT_SHIFT, + GLFW_KEY_RIGHT_SHIFT, GLFW_KEY_UP, GLFW_KEY_DOWN, GLFW_MOUSE_BUTTON_4 ) +from .keys import key_map +from .mouse import encode_mouse_event, PRESS, RELEASE, MOVE, DRAG from .terminfo import get_capabilities from .utils import sanitize_title, get_primary_selection, parse_color_set @@ -24,6 +27,7 @@ class Window: def __init__(self, tab, child, opts, args): self.tabref = weakref.ref(tab) + self.mouse_button_pressed = defaultdict(lambda: False) self.destroyed = False self.click_queue = deque(maxlen=3) self.geometry = WindowGeometry(0, 0, 0, 0, 0, 0) @@ -149,11 +153,13 @@ class Window: glfw_post_empty_event() def on_mouse_button(self, window, button, action, mods): - handle_event = mods == GLFW_MOD_SHIFT or not self.screen.mouse_button_tracking_enabled() - if handle_event: + self.mouse_button_pressed[button] = action == GLFW_PRESS + mode = self.screen.mouse_tracking_mode() + send_event = mods != GLFW_MOD_SHIFT and mode > 0 + x, y = window.get_cursor_pos() + x, y = max(0, x - self.geometry.left), max(0, y - self.geometry.top) + if not send_event: if button == GLFW_MOUSE_BUTTON_1: - x, y = window.get_cursor_pos() - x, y = max(0, x - self.geometry.left), max(0, y - self.geometry.top) self.char_grid.update_drag(action == GLFW_PRESS, x, y) if action == GLFW_RELEASE: if mods == self.char_grid.opts.open_url_modifiers: @@ -164,27 +170,62 @@ class Window: if action == GLFW_RELEASE: self.paste_from_selection() else: - x, y = window.get_cursor_pos() - x, y = max(0, x - self.geometry.left), max(0, y - self.geometry.top) + if action == GLFW_RELEASE and button == GLFW_MOUSE_BUTTON_1 and mods == self.char_grid.opts.open_url_modifiers: + self.char_grid.click_url(x, y) x, y = self.char_grid.cell_for_pos(x, y) + if x is not None: + ev = encode_mouse_event(mode, self.screen.mouse_tracking_protocol(), + button, PRESS if action == GLFW_PRESS else RELEASE, mods, x, y) + if ev: + self.write_to_child(ev) def on_mouse_move(self, window, x, y): + button = None + for b in range(0, GLFW_MOUSE_BUTTON_5 + 1): + if self.mouse_button_pressed[b]: + button = b + break + action = MOVE if button is None else DRAG + mode = self.screen.mouse_tracking_mode() + send_event = (mode == ANY_MODE or (mode == MOTION_MODE and button is not None)) and not ( + window.is_key_pressed(GLFW_KEY_LEFT_SHIFT) or window.is_key_pressed(GLFW_KEY_RIGHT_SHIFT)) x, y = max(0, x - self.geometry.left), max(0, y - self.geometry.top) - if self.char_grid.current_selection.in_progress: - self.char_grid.update_drag(None, x, y) tm = tab_manager() tm.queue_ui_action(tab_manager().change_mouse_cursor, self.char_grid.has_url_at(x, y)) + if send_event: + x, y = self.char_grid.cell_for_pos(x, y) + if x is not None: + ev = encode_mouse_event(mode, self.screen.mouse_tracking_protocol(), + button, action, 0, x, y) + if ev: + self.write_to_child(ev) + else: + if self.char_grid.current_selection.in_progress: + self.char_grid.update_drag(None, x, y) def on_mouse_scroll(self, window, x, y): - handle_event = ( - window.is_key_pressed(GLFW_KEY_LEFT_SHIFT) or - window.is_key_pressed(GLFW_KEY_RIGHT_SHIFT) or - not self.screen.mouse_button_tracking_enabled()) - if handle_event: - s = int(round(y * self.opts.wheel_scroll_multiplier)) - if abs(s) > 0: - self.char_grid.scroll(abs(s), s > 0) - glfw_post_empty_event() + s = int(round(y * self.opts.wheel_scroll_multiplier)) + if abs(s) < 0: + return + upwards = s > 0 + if self.screen.is_main_linebuf(): + self.char_grid.scroll(abs(s), upwards) + glfw_post_empty_event() + else: + mode = self.screen.mouse_tracking_mode() + send_event = mode > 0 + if send_event: + x, y = window.get_cursor_pos() + x, y = max(0, x - self.geometry.left), max(0, y - self.geometry.top) + x, y = self.char_grid.cell_for_pos(x, y) + if x is not None: + ev = encode_mouse_event(mode, self.screen.mouse_tracking_protocol(), + GLFW_MOUSE_BUTTON_4 if upwards else GLFW_MOUSE_BUTTON_5, PRESS, 0, x, y) + if ev: + self.write_to_child(ev) + else: + k = key_map[GLFW_KEY_UP if upwards else GLFW_KEY_DOWN] + self.write_to_child(k * abs(s)) # actions {{{ @@ -239,7 +280,7 @@ class Window: glfw_post_empty_event() # }}} - def dump_commands(self, *a): + def dump_commands(self, *a): # {{{ if a: if a[0] == 'draw': if a[1] is None: @@ -253,3 +294,4 @@ class Window: print('draw', ''.join(self.draw_dump_buf)) self.draw_dump_buf = [] print(*a) + # }}}