mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-09-04 08:48:21 +00:00
455 lines
15 KiB
Python
455 lines
15 KiB
Python
#!/usr/bin/env python
|
|
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
|
|
|
from functools import partial
|
|
|
|
from kitty.fast_data_types import (
|
|
GLFW_MOD_ALT,
|
|
GLFW_MOD_CONTROL,
|
|
GLFW_MOUSE_BUTTON_LEFT,
|
|
GLFW_MOUSE_BUTTON_RIGHT,
|
|
MOUSE_SELECTION_WORD_AND_LINE_FROM_POINT,
|
|
create_mock_window,
|
|
mock_mouse_selection,
|
|
send_mock_mouse_event_to_window,
|
|
)
|
|
|
|
from .base import BaseTest
|
|
|
|
|
|
def send_mouse_event(
|
|
window,
|
|
button=-1,
|
|
modifiers=0,
|
|
is_release=False,
|
|
x=0.0,
|
|
y=0,
|
|
clear_click_queue=False,
|
|
):
|
|
ix = int(x)
|
|
in_left_half_of_cell = x - ix < 0.5
|
|
send_mock_mouse_event_to_window(window, button, modifiers, is_release, ix, y, clear_click_queue, in_left_half_of_cell)
|
|
|
|
|
|
class TestMouse(BaseTest):
|
|
def test_mouse_selection(self):
|
|
s = self.create_screen(options=dict(rectangle_select_modifiers=GLFW_MOD_ALT | GLFW_MOD_CONTROL))
|
|
w = create_mock_window(s)
|
|
ev = partial(send_mouse_event, w)
|
|
|
|
def mouse_selection(code: int) -> None:
|
|
mock_mouse_selection(w, s.callbacks.current_mouse_button, code)
|
|
|
|
s.callbacks.mouse_selection = mouse_selection
|
|
|
|
def sel():
|
|
return ''.join(s.text_for_selection())
|
|
|
|
def init():
|
|
s.reset()
|
|
s.draw('pqrst')
|
|
s.draw('uvwxy')
|
|
s.draw('ABCDE')
|
|
s.draw('FGHIJ')
|
|
s.draw('KLMNO')
|
|
s.draw('12345')
|
|
s.draw('67890')
|
|
s.draw('abcde')
|
|
s.draw('fghij')
|
|
s.draw('klmno')
|
|
|
|
def press(x=0, y=0, modifiers=0, button=GLFW_MOUSE_BUTTON_LEFT):
|
|
ev(button, x=x, y=y, modifiers=modifiers)
|
|
|
|
def release(x=0, y=0, button=GLFW_MOUSE_BUTTON_LEFT):
|
|
ev(button, x=x, y=y, is_release=True, clear_click_queue=True)
|
|
|
|
def move(x=0, y=0, button=-1, q=None):
|
|
ev(x=x, y=y, button=button)
|
|
if q is not None:
|
|
sl = sel()
|
|
from kitty.window import as_text
|
|
|
|
self.ae(sl, q, f'{sl!r} != {q!r} after movement to x={x} y={y}. Screen contents: {as_text(s)!r}')
|
|
|
|
def multi_click(x=0, y=0, count=2, modifiers=0):
|
|
clear_click_queue = True
|
|
while count > 0:
|
|
count -= 1
|
|
ev(GLFW_MOUSE_BUTTON_LEFT, x=x, y=y, modifiers=modifiers, clear_click_queue=clear_click_queue)
|
|
clear_click_queue = False
|
|
|
|
def scroll(x=0, y=0, up=True):
|
|
move(x=x, y=y, button=-2 if up else -3)
|
|
|
|
# Single line click, move, release test
|
|
init()
|
|
press()
|
|
move(x=3.6, q='1234')
|
|
release(x=3.6)
|
|
self.ae(sel(), '1234')
|
|
press(x=4), release(x=0.6)
|
|
self.ae(sel(), '234')
|
|
|
|
# multi line movement
|
|
init()
|
|
press(x=2, y=2)
|
|
move(x=2, y=1, q='890ab')
|
|
move(x=2.6, y=1, q='90ab')
|
|
move(y=1, q='67890ab')
|
|
move(x=4, y=1, q='0ab')
|
|
move(x=4.6, y=1, q='ab')
|
|
move(q='1234567890ab')
|
|
move(x=2, y=3, q='cdefg')
|
|
move(y=3, q='cde')
|
|
move(x=0.6, y=3, q='cdef')
|
|
move(x=2.6, y=3, q='cdefgh')
|
|
move(x=4.6, y=3, q='cdefghij')
|
|
|
|
# Single cell select
|
|
init()
|
|
press(), release(1)
|
|
self.ae(sel(), '1')
|
|
press(3), release(2)
|
|
self.ae(sel(), '3')
|
|
|
|
# Multi-line click release
|
|
init()
|
|
press(1, 1), release(3.6, 2)
|
|
self.ae(sel(), '7890abcd')
|
|
press(1.6, 1), release(3, 2)
|
|
self.ae(sel(), '890abc')
|
|
press(3.6, 4), release(2, 2)
|
|
self.ae(sel(), 'cdefghijklmn')
|
|
press(3, 4), release(2.6, 2)
|
|
self.ae(sel(), 'defghijklm')
|
|
|
|
# Word select with drag
|
|
s.reset()
|
|
s.draw('ab cd')
|
|
s.draw(' f gh')
|
|
s.draw(' stuv')
|
|
s.draw('X Y')
|
|
multi_click(x=1.4)
|
|
self.ae(sel(), 'ab')
|
|
move(2.6)
|
|
self.ae(sel(), 'ab ')
|
|
move(3.6)
|
|
self.ae(sel(), 'ab cd')
|
|
move(2.6)
|
|
self.ae(sel(), 'ab ')
|
|
release(3.6, 1)
|
|
self.ae(sel(), 'ab cd f gh')
|
|
multi_click(x=1, y=2)
|
|
self.ae(sel(), 'stuvX')
|
|
release()
|
|
multi_click(x=3.6)
|
|
self.ae(sel(), 'cd')
|
|
move(0.2)
|
|
release()
|
|
self.ae(sel(), 'ab cd')
|
|
multi_click(x=4.4)
|
|
self.ae(sel(), 'cd')
|
|
move(x=4.4, y=1)
|
|
self.ae(sel(), 'cd f gh')
|
|
move(x=4.4, y=0)
|
|
self.ae(sel(), 'cd')
|
|
release()
|
|
multi_click(x=4.4, y=1)
|
|
self.ae(sel(), 'gh')
|
|
move(x=4.4, y=0)
|
|
self.ae(sel(), 'cd f gh')
|
|
move(x=4.4, y=1)
|
|
self.ae(sel(), 'gh')
|
|
release()
|
|
multi_click(x=4.4)
|
|
self.ae(sel(), 'cd')
|
|
move()
|
|
self.ae(sel(), 'ab cd')
|
|
move(x=1, y=1)
|
|
self.ae(sel(), 'ab cd f')
|
|
move()
|
|
self.ae(sel(), 'ab cd')
|
|
release()
|
|
multi_click(x=1.4)
|
|
self.ae(sel(), 'ab')
|
|
move(x=4.4)
|
|
self.ae(sel(), 'ab cd')
|
|
move(x=4.4, y=1)
|
|
self.ae(sel(), 'ab cd f gh')
|
|
move(x=4.4)
|
|
self.ae(sel(), 'ab cd')
|
|
|
|
# Line select with drag
|
|
s.reset()
|
|
s.draw('1 2 3')
|
|
s.linefeed(), s.carriage_return()
|
|
s.draw('4 5 6')
|
|
s.linefeed(), s.carriage_return()
|
|
s.draw('7 8 9X')
|
|
multi_click(x=1, count=3)
|
|
self.ae(sel(), str(s.line(0)))
|
|
move(y=1)
|
|
self.ae(sel(), '1 2 3\n4 5 6')
|
|
move(y=2)
|
|
self.ae(sel(), '1 2 3\n4 5 6\n7 8 9X')
|
|
move(y=1)
|
|
self.ae(sel(), '1 2 3\n4 5 6')
|
|
move()
|
|
self.ae(sel(), str(s.line(0)))
|
|
release()
|
|
multi_click(y=1, count=3)
|
|
self.ae(sel(), '4 5 6')
|
|
move(y=0)
|
|
self.ae(sel(), '1 2 3\n4 5 6')
|
|
move(y=1)
|
|
self.ae(sel(), '4 5 6')
|
|
move(y=2)
|
|
self.ae(sel(), '4 5 6\n7 8 9X')
|
|
release()
|
|
s.reset()
|
|
s.draw(' 123')
|
|
s.linefeed(), s.carriage_return()
|
|
s.draw(' 456')
|
|
s.linefeed(), s.carriage_return()
|
|
multi_click(x=1, count=3)
|
|
self.ae(sel(), '123')
|
|
move(x=2, y=1)
|
|
self.ae(sel(), '123\n 456')
|
|
release()
|
|
press(x=2, y=1, button=GLFW_MOUSE_BUTTON_RIGHT)
|
|
release(x=2, y=1, button=GLFW_MOUSE_BUTTON_RIGHT)
|
|
self.ae(sel(), '123\n 456')
|
|
press(button=GLFW_MOUSE_BUTTON_RIGHT)
|
|
self.ae(sel(), ' 123\n 456')
|
|
release(button=GLFW_MOUSE_BUTTON_RIGHT)
|
|
|
|
# line select for wrapped lines in scrollback
|
|
s.reset()
|
|
s.draw('ABCDE12345')
|
|
s.linefeed(), s.carriage_return()
|
|
s.draw(('X' * s.columns) * (s.lines - 1))
|
|
multi_click(x=1, count=3)
|
|
self.ae(sel(), 'ABCDE12345')
|
|
s.reset()
|
|
s.draw('ABCDE12345')
|
|
s.linefeed(), s.carriage_return()
|
|
s.draw('678')
|
|
s.linefeed(), s.carriage_return()
|
|
s.draw(('X' * s.columns) * (s.lines - 2))
|
|
multi_click(x=1, y=1, count=3)
|
|
self.ae(sel(), '678')
|
|
press(x=2, button=GLFW_MOUSE_BUTTON_RIGHT)
|
|
release(x=2, button=GLFW_MOUSE_BUTTON_RIGHT)
|
|
self.ae(sel(), 'ABCDE12345\n678')
|
|
|
|
# line select for wrapped lines below viewport
|
|
s.reset()
|
|
s.scroll(100, False)
|
|
# draw enough content to enable scrolling: fill lines-1 visual lines, then draw a long wrapped line
|
|
s.draw(('X' * s.columns) * (s.lines - 1))
|
|
s.linefeed(), s.carriage_return()
|
|
s.draw('ABCDE12345') # wraps to 2 visual lines, second one pushed below viewport
|
|
# scroll up by 1 so top visual line of the wrapped text is visible but bottom wraps off-screen
|
|
s.scroll(1, True)
|
|
multi_click(x=1, y=s.lines - 1, count=3)
|
|
self.ae(sel(), 'ABCDE12345')
|
|
# extending selection to a line that wraps below viewport
|
|
s.reset()
|
|
s.scroll(100, False)
|
|
s.draw(('X' * s.columns) * (s.lines - 2))
|
|
s.linefeed(), s.carriage_return()
|
|
s.draw('678')
|
|
s.linefeed(), s.carriage_return()
|
|
s.draw('ABCDE12345')
|
|
s.scroll(1, True)
|
|
multi_click(x=1, y=3, count=3)
|
|
self.ae(sel(), '678')
|
|
press(x=2, y=s.lines - 1, button=GLFW_MOUSE_BUTTON_RIGHT)
|
|
release(x=2, y=s.lines - 1, button=GLFW_MOUSE_BUTTON_RIGHT)
|
|
self.ae(sel(), '678\nABCDE12345')
|
|
s.scroll(100, False)
|
|
|
|
# word select for wrapped words in scrollback
|
|
s.reset()
|
|
s.draw('abcde12345')
|
|
s.linefeed(), s.carriage_return()
|
|
s.draw(('X' * s.columns) * (s.lines - 1))
|
|
multi_click(x=1)
|
|
self.ae(sel(), 'abcde12345')
|
|
|
|
# word select for wrapped words below viewport
|
|
s.reset()
|
|
s.scroll(100, False)
|
|
s.draw(('X' * s.columns) * (s.lines - 1))
|
|
s.linefeed(), s.carriage_return()
|
|
s.draw('abcde12345')
|
|
s.scroll(1, True)
|
|
multi_click(x=1, y=s.lines - 1)
|
|
self.ae(sel(), 'abcde12345')
|
|
s.scroll(100, False)
|
|
|
|
# Rectangle select
|
|
init()
|
|
press(x=1, y=1, modifiers=GLFW_MOD_ALT | GLFW_MOD_CONTROL)
|
|
move(x=3.6, y=3)
|
|
self.ae(sel(), '789bcdghi')
|
|
release(x=3, y=3)
|
|
self.ae(sel(), '78bcgh')
|
|
press(x=3.6, y=1, modifiers=GLFW_MOD_ALT | GLFW_MOD_CONTROL)
|
|
self.ae(sel(), '')
|
|
move(x=1, y=3)
|
|
self.ae(sel(), '789bcdghi')
|
|
release(x=1.6)
|
|
self.ae(sel(), '3489')
|
|
|
|
# scrolling
|
|
init()
|
|
press(x=1.6)
|
|
scroll(x=1)
|
|
self.ae(sel(), 'LMNO12')
|
|
scroll(x=1)
|
|
self.ae(sel(), 'GHIJKLMNO12')
|
|
scroll(x=1, up=False)
|
|
self.ae(sel(), 'LMNO12')
|
|
scroll(x=2.6, up=False)
|
|
self.ae(sel(), '3')
|
|
release()
|
|
# fractional scrolling
|
|
init()
|
|
s.fractional_scroll(-0.5)
|
|
press()
|
|
move(x=3.6, q='1234')
|
|
release(x=3.6)
|
|
self.ae(sel(), '1234')
|
|
|
|
# extending selections
|
|
init()
|
|
press()
|
|
move(x=3.6, q='1234')
|
|
release(x=3.6)
|
|
self.ae(sel(), '1234')
|
|
press(x=1, y=1, button=GLFW_MOUSE_BUTTON_RIGHT)
|
|
self.ae(sel(), '123456')
|
|
move(x=2, y=1, q='1234567')
|
|
release(x=3, y=1, button=GLFW_MOUSE_BUTTON_RIGHT)
|
|
self.ae(sel(), '12345678')
|
|
init()
|
|
press(y=2)
|
|
move(x=3.6, y=2, q='abcd')
|
|
press(x=3, y=0, button=GLFW_MOUSE_BUTTON_RIGHT)
|
|
self.ae(sel(), '4567890abcd')
|
|
|
|
# blank line select
|
|
s.reset()
|
|
s.draw('abcde')
|
|
s.linefeed(), s.carriage_return()
|
|
s.linefeed(), s.carriage_return()
|
|
s.draw('12345')
|
|
press(x=0, y=0)
|
|
move(x=2, y=2, q='abcde\n\n12')
|
|
|
|
# Line from begin select (alt+triple click): selects from column 0
|
|
# (including any leading whitespace), unlike normal triple click which strips it
|
|
s.reset()
|
|
s.draw(' 123')
|
|
s.linefeed(), s.carriage_return()
|
|
s.draw(' 456')
|
|
s.linefeed(), s.carriage_return()
|
|
multi_click(x=1, count=3, modifiers=GLFW_MOD_ALT)
|
|
self.ae(sel(), ' 123')
|
|
multi_click(x=1, count=3)
|
|
self.ae(sel(), '123')
|
|
multi_click(x=1, count=3, modifiers=GLFW_MOD_ALT)
|
|
move(y=1)
|
|
self.ae(sel(), ' 123\n 456')
|
|
release()
|
|
|
|
# Line from point select (ctrl+alt+triple click): selects from click
|
|
# position to end of line; cannot start before the first non-blank char
|
|
s.reset()
|
|
s.draw(' 123')
|
|
s.linefeed(), s.carriage_return()
|
|
s.draw(' 456')
|
|
s.linefeed(), s.carriage_return()
|
|
multi_click(x=2, count=3, modifiers=GLFW_MOD_ALT | GLFW_MOD_CONTROL)
|
|
self.ae(sel(), '23')
|
|
# click before first non-blank: selection starts at first non-blank
|
|
multi_click(x=0, count=3, modifiers=GLFW_MOD_ALT | GLFW_MOD_CONTROL)
|
|
self.ae(sel(), '123')
|
|
# click past last non-blank char: no selection created
|
|
multi_click(x=4, count=3, modifiers=GLFW_MOD_ALT | GLFW_MOD_CONTROL)
|
|
self.ae(sel(), '')
|
|
# drag to next line
|
|
multi_click(x=2, count=3, modifiers=GLFW_MOD_ALT | GLFW_MOD_CONTROL)
|
|
move(y=1)
|
|
self.ae(sel(), '23\n 456')
|
|
release()
|
|
|
|
# Word and line from point: selects the word at the click point and
|
|
# extends the selection to the end of the line
|
|
sel_word_and_line = MOUSE_SELECTION_WORD_AND_LINE_FROM_POINT
|
|
s.reset()
|
|
s.draw('ab cd')
|
|
press(x=0)
|
|
mock_mouse_selection(w, GLFW_MOUSE_BUTTON_LEFT, sel_word_and_line)
|
|
self.ae(sel(), 'ab cd')
|
|
press(x=3)
|
|
mock_mouse_selection(w, GLFW_MOUSE_BUTTON_LEFT, sel_word_and_line)
|
|
self.ae(sel(), 'cd')
|
|
# click on space: no word, selects from space to end of line
|
|
press(x=2)
|
|
mock_mouse_selection(w, GLFW_MOUSE_BUTTON_LEFT, sel_word_and_line)
|
|
self.ae(sel(), ' cd')
|
|
# click past last non-blank char: no selection created
|
|
press(x=4)
|
|
mock_mouse_selection(w, GLFW_MOUSE_BUTTON_LEFT, sel_word_and_line)
|
|
self.ae(sel(), '')
|
|
|
|
# Double-click on whitespace: no word found, no selection created
|
|
s.reset()
|
|
s.draw('ab cd')
|
|
multi_click(x=2)
|
|
self.ae(sel(), '')
|
|
multi_click(x=2.4)
|
|
self.ae(sel(), '')
|
|
|
|
def test_v120_scroll_direction_reversal(self):
|
|
# A high resolution wheel delivers one physical detent as several
|
|
# VALUE120 events, and the fractional remainder is carried between them.
|
|
# Detents do not reliably total 120 units, so that carry is what lets an
|
|
# undersized detent still scroll a line. But when the direction changes
|
|
# the residual has the wrong sign, and the first detent of the new
|
|
# direction gets spent cancelling it instead of scrolling.
|
|
# The fragment sequences below were captured from a Logitech MX Master 3.
|
|
from kitty.fast_data_types import test_scale_scroll
|
|
|
|
self.set_options() # scale_scroll reads wheel_scroll_multiplier
|
|
cell = 40
|
|
|
|
def run(*v120):
|
|
# scale_scroll is exercised in mouse tracking mode, which reduces
|
|
# wheel_scroll_multiplier to its sign, so the result does not depend
|
|
# on the configured multiplier.
|
|
return test_scale_scroll([float(v) for v in v120], cell)
|
|
|
|
down = (-48, -32, -24, -24) # 128 units
|
|
up = (48, 32, 24, 24)
|
|
|
|
# Five detents down, then one up. Every detent must scroll, including
|
|
# the one that reverses direction. Before the residual was discarded on
|
|
# a reversal, that last detent scrolled nothing.
|
|
res = run(*(down * 5), *up)
|
|
self.ae(-5, sum(res[:20]), f'detents before the reversal: {res}')
|
|
self.ae(1, sum(res[20:]), f'detent that reversed direction: {res}')
|
|
|
|
# Repeated reversals keep working.
|
|
res = run(*(down * 2), *(up * 2), *(down * 2))
|
|
self.ae([-2, 2, -2], [sum(res[i : i + 8]) for i in range(0, 24, 8)])
|
|
|
|
# Scrolling in one direction is unaffected: the carry is retained, so a
|
|
# run of undersized detents still scrolls about one line each.
|
|
self.ae(-9, sum(run(*((-48, -24, -24, -16) * 10))))
|
|
|
|
# A fragment too small to be a detent does not scroll.
|
|
self.ae(0, sum(run(8)))
|