2019-05-20 13:38:06 +03:00
|
|
|
#!/usr/bin/env python3
|
2016-10-16 18:06:27 +03:00
|
|
|
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
|
|
|
|
2022-02-21 11:38:10 +03:00
|
|
|
import fcntl
|
|
|
|
import io
|
2021-02-05 07:12:01 +03:00
|
|
|
import os
|
2022-02-21 11:38:10 +03:00
|
|
|
import select
|
|
|
|
import shlex
|
|
|
|
import struct
|
2022-02-22 09:17:44 +03:00
|
|
|
import sys
|
2022-02-21 11:38:10 +03:00
|
|
|
import termios
|
2022-02-21 15:27:25 +03:00
|
|
|
import time
|
2022-02-21 11:38:10 +03:00
|
|
|
from pty import CHILD, fork
|
2016-10-16 18:06:27 +03:00
|
|
|
from unittest import TestCase
|
|
|
|
|
2021-12-23 07:43:16 +03:00
|
|
|
from kitty.config import finalize_keys, finalize_mouse_mappings
|
2021-05-11 09:19:44 +03:00
|
|
|
from kitty.fast_data_types import (
|
2022-02-21 11:38:10 +03:00
|
|
|
Cursor, HistoryBuf, LineBuf, Screen, get_options, parse_bytes, set_options
|
2021-05-11 09:19:44 +03:00
|
|
|
)
|
2021-05-30 10:46:18 +03:00
|
|
|
from kitty.options.parse import merge_result_dicts
|
2021-12-23 07:43:16 +03:00
|
|
|
from kitty.options.types import Options, defaults
|
2021-05-11 09:19:44 +03:00
|
|
|
from kitty.types import MouseEvent
|
2022-02-24 07:35:13 +03:00
|
|
|
from kitty.utils import read_screen_size
|
2022-02-27 07:50:19 +03:00
|
|
|
from kitty.window import process_remote_print, process_title_from_child
|
2016-11-04 12:14:46 +03:00
|
|
|
|
|
|
|
|
2016-11-24 13:58:08 +03:00
|
|
|
class Callbacks:
|
|
|
|
|
2022-02-23 13:04:19 +03:00
|
|
|
def __init__(self, pty=None) -> None:
|
2016-11-24 13:58:08 +03:00
|
|
|
self.clear()
|
2022-02-23 13:04:19 +03:00
|
|
|
self.pty = pty
|
2016-11-24 13:58:08 +03:00
|
|
|
|
2021-02-19 12:49:04 +03:00
|
|
|
def write(self, data) -> None:
|
2016-11-24 13:58:08 +03:00
|
|
|
self.wtcbuf += data
|
|
|
|
|
2022-02-27 07:50:19 +03:00
|
|
|
def title_changed(self, data, is_base64=False) -> None:
|
|
|
|
self.titlebuf.append(process_title_from_child(data, is_base64))
|
2016-11-24 13:58:08 +03:00
|
|
|
|
2021-02-19 12:49:04 +03:00
|
|
|
def icon_changed(self, data) -> None:
|
2016-11-24 13:58:08 +03:00
|
|
|
self.iconbuf += data
|
|
|
|
|
2021-02-19 12:49:04 +03:00
|
|
|
def set_dynamic_color(self, code, data) -> None:
|
2016-11-24 13:58:08 +03:00
|
|
|
self.colorbuf += data or ''
|
|
|
|
|
2021-02-19 12:49:04 +03:00
|
|
|
def set_color_table_color(self, code, data) -> None:
|
2016-11-24 13:58:08 +03:00
|
|
|
self.ctbuf += ''
|
|
|
|
|
2021-02-19 12:49:04 +03:00
|
|
|
def request_capabilities(self, q) -> None:
|
2017-12-02 12:05:06 +03:00
|
|
|
from kitty.terminfo import get_capabilities
|
2020-10-07 11:05:16 +03:00
|
|
|
for c in get_capabilities(q, None):
|
|
|
|
self.write(c.encode('ascii'))
|
2016-11-24 13:58:08 +03:00
|
|
|
|
2021-02-19 12:49:04 +03:00
|
|
|
def use_utf8(self, on) -> None:
|
2017-04-28 07:11:47 +03:00
|
|
|
self.iutf8 = on
|
|
|
|
|
2020-08-21 17:50:02 +03:00
|
|
|
def desktop_notify(self, osc_code: int, raw_data: str) -> None:
|
|
|
|
self.notifications.append((osc_code, raw_data))
|
|
|
|
|
2020-09-03 19:25:02 +03:00
|
|
|
def open_url(self, url: str, hyperlink_id: int) -> None:
|
|
|
|
self.open_urls.append((url, hyperlink_id))
|
|
|
|
|
2021-07-23 19:42:04 +03:00
|
|
|
def clipboard_control(self, data: str, is_partial: bool = False) -> None:
|
|
|
|
self.cc_buf.append((data, is_partial))
|
|
|
|
|
2021-02-19 12:49:04 +03:00
|
|
|
def clear(self) -> None:
|
2016-11-24 13:58:08 +03:00
|
|
|
self.wtcbuf = b''
|
2022-02-21 18:14:10 +03:00
|
|
|
self.iconbuf = self.colorbuf = self.ctbuf = ''
|
|
|
|
self.titlebuf = []
|
2017-04-28 07:11:47 +03:00
|
|
|
self.iutf8 = True
|
2020-08-21 17:50:02 +03:00
|
|
|
self.notifications = []
|
2020-09-03 19:25:02 +03:00
|
|
|
self.open_urls = []
|
2021-07-23 19:42:04 +03:00
|
|
|
self.cc_buf = []
|
|
|
|
self.bell_count = 0
|
|
|
|
|
|
|
|
def on_bell(self) -> None:
|
|
|
|
self.bell_count += 1
|
2016-11-24 13:58:08 +03:00
|
|
|
|
2021-05-05 11:53:27 +03:00
|
|
|
def on_activity_since_last_focus(self) -> None:
|
|
|
|
pass
|
|
|
|
|
2021-05-11 09:19:44 +03:00
|
|
|
def on_mouse_event(self, event):
|
|
|
|
ev = MouseEvent(**event)
|
2022-02-21 11:38:10 +03:00
|
|
|
opts = get_options()
|
|
|
|
action_def = opts.mousemap.get(ev)
|
2021-11-29 19:21:42 +03:00
|
|
|
if not action_def:
|
2021-05-11 09:19:44 +03:00
|
|
|
return False
|
|
|
|
self.current_mouse_button = ev.button
|
2022-02-21 11:38:10 +03:00
|
|
|
for action in opts.alias_map.resolve_aliases(action_def, 'mouse_map'):
|
2021-11-22 17:22:43 +03:00
|
|
|
getattr(self, action.func)(*action.args)
|
2021-05-11 09:19:44 +03:00
|
|
|
self.current_mouse_button = 0
|
|
|
|
return True
|
|
|
|
|
2022-02-22 09:17:44 +03:00
|
|
|
def handle_remote_print(self, msg):
|
|
|
|
text = process_remote_print(msg)
|
|
|
|
print(text, file=sys.__stderr__)
|
|
|
|
|
2022-02-23 13:04:19 +03:00
|
|
|
def handle_remote_ssh(self, msg):
|
|
|
|
from kittens.ssh.main import get_ssh_data
|
2022-02-23 21:30:45 +03:00
|
|
|
if self.pty:
|
|
|
|
for line in get_ssh_data(msg):
|
|
|
|
self.pty.write_to_child(line)
|
2022-02-23 13:04:19 +03:00
|
|
|
|
|
|
|
def handle_remote_echo(self, msg):
|
|
|
|
from base64 import standard_b64decode
|
2022-02-23 21:30:45 +03:00
|
|
|
if self.pty:
|
|
|
|
data = standard_b64decode(msg)
|
|
|
|
self.pty.write_to_child(data)
|
2022-02-23 13:04:19 +03:00
|
|
|
|
2016-11-24 13:58:08 +03:00
|
|
|
|
2016-11-04 12:14:46 +03:00
|
|
|
def filled_line_buf(ynum=5, xnum=5, cursor=Cursor()):
|
|
|
|
ans = LineBuf(ynum, xnum)
|
|
|
|
cursor.x = 0
|
|
|
|
for i in range(ynum):
|
2021-10-21 10:13:55 +03:00
|
|
|
t = (f'{i}') * xnum
|
2016-11-04 12:14:46 +03:00
|
|
|
ans.line(i).set_text(t, 0, xnum, cursor)
|
|
|
|
return ans
|
2016-10-18 08:34:30 +03:00
|
|
|
|
2016-10-16 18:06:27 +03:00
|
|
|
|
2016-11-04 12:26:31 +03:00
|
|
|
def filled_cursor():
|
|
|
|
ans = Cursor()
|
2018-05-20 08:13:26 +03:00
|
|
|
ans.bold = ans.italic = ans.reverse = ans.strikethrough = ans.dim = True
|
2016-11-04 12:26:31 +03:00
|
|
|
ans.fg = 0x101
|
|
|
|
ans.bg = 0x201
|
|
|
|
ans.decoration_fg = 0x301
|
|
|
|
return ans
|
|
|
|
|
|
|
|
|
2016-11-20 18:05:30 +03:00
|
|
|
def filled_history_buf(ynum=5, xnum=5, cursor=Cursor()):
|
|
|
|
lb = filled_line_buf(ynum, xnum, cursor)
|
|
|
|
ans = HistoryBuf(ynum, xnum)
|
|
|
|
for i in range(ynum):
|
|
|
|
ans.push(lb.line(i))
|
|
|
|
return ans
|
|
|
|
|
|
|
|
|
2016-10-16 18:06:27 +03:00
|
|
|
class BaseTest(TestCase):
|
|
|
|
|
|
|
|
ae = TestCase.assertEqual
|
2021-09-19 17:55:08 +03:00
|
|
|
maxDiff = 2048
|
2021-02-05 07:12:01 +03:00
|
|
|
is_ci = os.environ.get('CI') == 'true'
|
2016-10-18 08:34:30 +03:00
|
|
|
|
2020-03-08 10:46:34 +03:00
|
|
|
def set_options(self, options=None):
|
2020-02-13 06:03:57 +03:00
|
|
|
final_options = {'scrollback_pager_history_size': 1024, 'click_interval': 0.5}
|
|
|
|
if options:
|
|
|
|
final_options.update(options)
|
2021-05-30 10:46:18 +03:00
|
|
|
options = Options(merge_result_dicts(defaults._asdict(), final_options))
|
2021-11-22 17:22:43 +03:00
|
|
|
finalize_keys(options, {})
|
|
|
|
finalize_mouse_mappings(options, {})
|
2018-09-15 19:40:13 +03:00
|
|
|
set_options(options)
|
2021-05-11 09:19:44 +03:00
|
|
|
return options
|
2020-03-08 10:46:34 +03:00
|
|
|
|
2022-02-21 11:54:08 +03:00
|
|
|
def cmd_to_run_python_code(self, code):
|
2022-02-21 12:15:12 +03:00
|
|
|
from kitty.constants import kitty_exe
|
|
|
|
return [kitty_exe(), '+runpy', code]
|
2022-02-21 11:54:08 +03:00
|
|
|
|
2021-03-17 19:29:11 +03:00
|
|
|
def create_screen(self, cols=5, lines=5, scrollback=5, cell_width=10, cell_height=20, options=None):
|
2022-02-21 11:38:10 +03:00
|
|
|
self.set_options(options)
|
|
|
|
c = Callbacks()
|
2021-03-09 07:51:27 +03:00
|
|
|
s = Screen(c, lines, cols, scrollback, cell_width, cell_height, 0, c)
|
|
|
|
return s
|
2016-10-18 08:34:30 +03:00
|
|
|
|
2022-02-21 15:27:25 +03:00
|
|
|
def create_pty(self, argv, cols=80, lines=25, scrollback=100, cell_width=10, cell_height=20, options=None, cwd=None, env=None):
|
2022-02-21 11:38:10 +03:00
|
|
|
self.set_options(options)
|
2022-02-21 15:27:25 +03:00
|
|
|
return PTY(argv, lines, cols, scrollback, cell_width, cell_height, cwd, env)
|
2022-02-21 11:38:10 +03:00
|
|
|
|
2016-11-02 08:14:35 +03:00
|
|
|
def assertEqualAttributes(self, c1, c2):
|
|
|
|
x1, y1, c1.x, c1.y = c1.x, c1.y, 0, 0
|
|
|
|
x2, y2, c2.x, c2.y = c2.x, c2.y, 0, 0
|
|
|
|
try:
|
|
|
|
self.assertEqual(c1, c2)
|
|
|
|
finally:
|
|
|
|
c1.x, c1.y, c2.x, c2.y = x1, y1, x2, y2
|
2022-02-21 11:38:10 +03:00
|
|
|
|
|
|
|
|
|
|
|
class PTY:
|
|
|
|
|
2022-02-21 15:27:25 +03:00
|
|
|
def __init__(self, argv, rows=25, columns=80, scrollback=100, cell_width=10, cell_height=20, cwd=None, env=None):
|
2022-02-22 12:22:54 +03:00
|
|
|
if isinstance(argv, str):
|
|
|
|
argv = shlex.split(argv)
|
2022-02-21 11:38:10 +03:00
|
|
|
pid, self.master_fd = fork()
|
|
|
|
self.is_child = pid == CHILD
|
2022-02-24 07:35:13 +03:00
|
|
|
self.write_buf = b''
|
2022-02-21 11:38:10 +03:00
|
|
|
if self.is_child:
|
2022-02-21 15:27:25 +03:00
|
|
|
while read_screen_size().width != columns * cell_width:
|
|
|
|
time.sleep(0.01)
|
2022-02-21 11:38:10 +03:00
|
|
|
if cwd:
|
|
|
|
os.chdir(cwd)
|
2022-02-22 12:22:54 +03:00
|
|
|
os.execvpe(argv[0], argv, env or os.environ)
|
2022-02-21 11:38:10 +03:00
|
|
|
os.set_blocking(self.master_fd, False)
|
2022-02-21 15:27:25 +03:00
|
|
|
self.cell_width = cell_width
|
|
|
|
self.cell_height = cell_height
|
2022-02-21 11:38:10 +03:00
|
|
|
self.set_window_size(rows=rows, columns=columns)
|
2022-02-23 13:04:19 +03:00
|
|
|
self.callbacks = Callbacks(self)
|
2022-02-21 11:38:10 +03:00
|
|
|
self.screen = Screen(self.callbacks, rows, columns, scrollback, cell_width, cell_height, 0, self.callbacks)
|
2022-02-22 09:17:44 +03:00
|
|
|
self.received_bytes = b''
|
2022-02-21 11:38:10 +03:00
|
|
|
|
|
|
|
def __del__(self):
|
|
|
|
if not self.is_child:
|
2022-02-23 21:57:09 +03:00
|
|
|
fd = self.master_fd
|
2022-02-21 11:38:10 +03:00
|
|
|
del self.master_fd
|
2022-02-23 21:57:09 +03:00
|
|
|
os.close(fd)
|
2022-02-21 11:38:10 +03:00
|
|
|
|
|
|
|
def write_to_child(self, data):
|
2022-02-24 07:35:13 +03:00
|
|
|
if isinstance(data, str):
|
|
|
|
data = data.encode('utf-8')
|
|
|
|
self.write_buf += data
|
2022-02-21 11:38:10 +03:00
|
|
|
|
2022-02-21 15:27:25 +03:00
|
|
|
def send_cmd_to_child(self, cmd):
|
|
|
|
self.write_to_child(cmd + '\r')
|
|
|
|
|
|
|
|
def process_input_from_child(self, timeout=10):
|
2022-02-24 07:35:13 +03:00
|
|
|
rd, wd, err = select.select([self.master_fd], [self.master_fd] if self.write_buf else [], [self.master_fd], timeout)
|
|
|
|
if err:
|
|
|
|
raise OSError('master_fd is in error condition')
|
|
|
|
while wd:
|
|
|
|
try:
|
|
|
|
n = os.write(self.master_fd, self.write_buf)
|
|
|
|
except (BlockingIOError, OSError):
|
|
|
|
n = 0
|
|
|
|
if not n:
|
|
|
|
break
|
|
|
|
self.write_buf = self.write_buf[n:]
|
|
|
|
|
2022-02-21 11:38:10 +03:00
|
|
|
bytes_read = 0
|
2022-02-24 07:35:13 +03:00
|
|
|
while rd:
|
2022-02-21 11:38:10 +03:00
|
|
|
try:
|
|
|
|
data = os.read(self.master_fd, io.DEFAULT_BUFFER_SIZE)
|
|
|
|
except (BlockingIOError, OSError):
|
|
|
|
data = b''
|
|
|
|
if not data:
|
|
|
|
break
|
|
|
|
bytes_read += len(data)
|
2022-02-22 09:17:44 +03:00
|
|
|
self.received_bytes += data
|
2022-02-21 11:38:10 +03:00
|
|
|
parse_bytes(self.screen, data)
|
|
|
|
return bytes_read
|
|
|
|
|
2022-02-21 15:27:25 +03:00
|
|
|
def wait_till(self, q, timeout=10):
|
2022-02-23 18:21:56 +03:00
|
|
|
end_time = time.monotonic() + timeout
|
|
|
|
while not q() and time.monotonic() <= end_time:
|
|
|
|
self.process_input_from_child(timeout=max(0, end_time - time.monotonic()))
|
2022-02-21 15:27:25 +03:00
|
|
|
if not q():
|
2022-02-22 09:17:44 +03:00
|
|
|
raise TimeoutError(f'The condition was not met. Screen contents: \n {repr(self.screen_contents())}')
|
2022-02-21 15:27:25 +03:00
|
|
|
|
|
|
|
def set_window_size(self, rows=25, columns=80):
|
2022-02-21 18:31:16 +03:00
|
|
|
if hasattr(self, 'screen'):
|
|
|
|
self.screen.resize(rows, columns)
|
2022-02-21 15:27:25 +03:00
|
|
|
x_pixels = columns * self.cell_width
|
|
|
|
y_pixels = rows * self.cell_height
|
2022-02-21 11:38:10 +03:00
|
|
|
s = struct.pack('HHHH', rows, columns, x_pixels, y_pixels)
|
|
|
|
fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, s)
|
|
|
|
|
|
|
|
def screen_contents(self):
|
|
|
|
lines = []
|
|
|
|
for i in range(self.screen.lines):
|
|
|
|
x = str(self.screen.line(i))
|
|
|
|
if x:
|
|
|
|
lines.append(x)
|
|
|
|
return '\n'.join(lines)
|
2022-02-21 15:27:25 +03:00
|
|
|
|
|
|
|
def last_cmd_output(self, as_ansi=False, add_wrap_markers=False):
|
2022-02-23 15:44:40 +03:00
|
|
|
from kitty.window import cmd_output
|
|
|
|
return cmd_output(self.screen, as_ansi=as_ansi, add_wrap_markers=add_wrap_markers)
|