Skip to content

Commit c9cc832

Browse files
committed
Add basic tests for zsh shell integration
1 parent 595698d commit c9cc832

File tree

5 files changed

+109
-22
lines changed

5 files changed

+109
-22
lines changed

kitty/shell_integration.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ def is_new_zsh_install(env: Dict[str, str]) -> bool:
7474
# the latter will bail if there are rc files in $HOME
7575
zdotdir = env.get('ZDOTDIR')
7676
if not zdotdir:
77-
zdotdir = os.path.expanduser('~')
77+
zdotdir = env.get('HOME', os.path.expanduser('~'))
78+
assert isinstance(zdotdir, str)
7879
if zdotdir == '~':
7980
return True
8081
for q in ('.zshrc', '.zshenv', '.zprofile', '.zlogin'):

kitty/utils.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,19 @@ class ScreenSize(NamedTuple):
148148
cell_height: int
149149

150150

151+
def read_screen_size(fd: int = -1) -> ScreenSize:
152+
import array
153+
import fcntl
154+
import termios
155+
buf = array.array('H', [0, 0, 0, 0])
156+
if fd < 0:
157+
fd = sys.stdout.fileno()
158+
fcntl.ioctl(fd, termios.TIOCGWINSZ, cast(bytearray, buf))
159+
rows, cols, width, height = tuple(buf)
160+
cell_width, cell_height = width // (cols or 1), height // (rows or 1)
161+
return ScreenSize(rows, cols, width, height, cell_width, cell_height)
162+
163+
151164
class ScreenSizeGetter:
152165
changed = True
153166
Size = ScreenSize
@@ -160,14 +173,7 @@ def __init__(self, fd: Optional[int]):
160173

161174
def __call__(self) -> ScreenSize:
162175
if self.changed:
163-
import array
164-
import fcntl
165-
import termios
166-
buf = array.array('H', [0, 0, 0, 0])
167-
fcntl.ioctl(self.fd, termios.TIOCGWINSZ, cast(bytearray, buf))
168-
rows, cols, width, height = tuple(buf)
169-
cell_width, cell_height = width // (cols or 1), height // (rows or 1)
170-
self.ans = ScreenSize(rows, cols, width, height, cell_width, cell_height)
176+
self.ans = read_screen_size()
171177
self.changed = False
172178
return cast(ScreenSize, self.ans)
173179

kitty_tests/__init__.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
import select
88
import shlex
99
import struct
10-
import sys
1110
import termios
11+
import time
1212
from pty import CHILD, fork
1313
from unittest import TestCase
1414

@@ -19,7 +19,7 @@
1919
from kitty.options.parse import merge_result_dicts
2020
from kitty.options.types import Options, defaults
2121
from kitty.types import MouseEvent
22-
from kitty.utils import no_echo, write_all
22+
from kitty.utils import read_screen_size, write_all
2323

2424

2525
class Callbacks:
@@ -139,9 +139,9 @@ def create_screen(self, cols=5, lines=5, scrollback=5, cell_width=10, cell_heigh
139139
s = Screen(c, lines, cols, scrollback, cell_width, cell_height, 0, c)
140140
return s
141141

142-
def create_pty(self, argv, cols=80, lines=25, scrollback=100, cell_width=10, cell_height=20, options=None, cwd=None):
142+
def create_pty(self, argv, cols=80, lines=25, scrollback=100, cell_width=10, cell_height=20, options=None, cwd=None, env=None):
143143
self.set_options(options)
144-
return PTY(argv, lines, cols, scrollback, cell_width, cell_height, cwd)
144+
return PTY(argv, lines, cols, scrollback, cell_width, cell_height, cwd, env)
145145

146146
def assertEqualAttributes(self, c1, c2):
147147
x1, y1, c1.x, c1.y = c1.x, c1.y, 0, 0
@@ -154,23 +154,27 @@ def assertEqualAttributes(self, c1, c2):
154154

155155
class PTY:
156156

157-
def __init__(self, argv, rows=25, columns=80, scrollback=100, cell_width=10, cell_height=20, cwd=None):
157+
def __init__(self, argv, rows=25, columns=80, scrollback=100, cell_width=10, cell_height=20, cwd=None, env=None):
158158
pid, self.master_fd = fork()
159159
self.is_child = pid == CHILD
160160
if self.is_child:
161+
while read_screen_size().width != columns * cell_width:
162+
time.sleep(0.01)
161163
if cwd:
162164
os.chdir(cwd)
165+
if env:
166+
os.environ.clear()
167+
os.environ.update(env)
163168
if isinstance(argv, str):
164169
argv = shlex.split(argv)
165-
with no_echo():
166-
sys.stdin.readline()
167170
os.execlp(argv[0], *argv)
168171
os.set_blocking(self.master_fd, False)
172+
self.cell_width = cell_width
173+
self.cell_height = cell_height
169174
self.set_window_size(rows=rows, columns=columns)
170175
new = termios.tcgetattr(self.master_fd)
171176
new[3] = new[3] & ~termios.ECHO
172177
termios.tcsetattr(self.master_fd, termios.TCSADRAIN, new)
173-
self.write_to_child('ready\r\n')
174178
self.callbacks = Callbacks()
175179
self.screen = Screen(self.callbacks, rows, columns, scrollback, cell_width, cell_height, 0, self.callbacks)
176180

@@ -186,7 +190,11 @@ def wait_for_input_from_child(self, timeout=10):
186190
rd = select.select([self.master_fd], [], [], timeout)[0]
187191
return bool(rd)
188192

189-
def process_input_from_child(self):
193+
def send_cmd_to_child(self, cmd):
194+
self.write_to_child(cmd + '\r')
195+
196+
def process_input_from_child(self, timeout=10):
197+
self.wait_for_input_from_child(timeout=10)
190198
bytes_read = 0
191199
while True:
192200
try:
@@ -199,7 +207,16 @@ def process_input_from_child(self):
199207
parse_bytes(self.screen, data)
200208
return bytes_read
201209

202-
def set_window_size(self, rows=25, columns=80, x_pixels=0, y_pixels=0):
210+
def wait_till(self, q, timeout=10):
211+
st = time.monotonic()
212+
while not q() and time.monotonic() - st < timeout:
213+
self.process_input_from_child(timeout=timeout - (time.monotonic() - st))
214+
if not q():
215+
raise TimeoutError('The condition was not met')
216+
217+
def set_window_size(self, rows=25, columns=80):
218+
x_pixels = columns * self.cell_width
219+
y_pixels = rows * self.cell_height
203220
s = struct.pack('HHHH', rows, columns, x_pixels, y_pixels)
204221
fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, s)
205222

@@ -210,3 +227,9 @@ def screen_contents(self):
210227
if x:
211228
lines.append(x)
212229
return '\n'.join(lines)
230+
231+
def last_cmd_output(self, as_ansi=False, add_wrap_markers=False):
232+
lines = []
233+
from kitty.window import CommandOutput
234+
self.screen.cmd_output(CommandOutput.last_run, lines.append, as_ansi, add_wrap_markers)
235+
return ''.join(lines)

kitty_tests/shell_integration.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env python
2+
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
3+
4+
5+
import os
6+
from contextlib import contextmanager
7+
from tempfile import TemporaryDirectory
8+
9+
from kitty.constants import terminfo_dir
10+
from kitty.fast_data_types import CURSOR_BEAM
11+
from kitty.shell_integration import setup_zsh_env
12+
13+
from . import BaseTest
14+
15+
16+
def safe_env_for_running_shell(home_dir, rc='', shell='zsh'):
17+
ans = {
18+
'PATH': os.environ['PATH'],
19+
'HOME': home_dir,
20+
'TERM': 'xterm-kitty',
21+
'TERMINFO': terminfo_dir,
22+
'KITTY_SHELL_INTEGRATION': 'enabled',
23+
}
24+
if shell == 'zsh':
25+
ans['ZLE_RPROMPT_INDENT'] = '0'
26+
with open(os.path.join(home_dir, '.zshenv'), 'w') as f:
27+
print('unset GLOBAL_RCS', file=f)
28+
with open(os.path.join(home_dir, '.zshrc'), 'w') as f:
29+
print(rc, file=f)
30+
setup_zsh_env(ans)
31+
return ans
32+
33+
34+
class ShellIntegration(BaseTest):
35+
36+
@contextmanager
37+
def run_shell(self, shell='zsh', rc=''):
38+
with TemporaryDirectory() as home_dir:
39+
pty = self.create_pty(f'{shell} -il', cwd=home_dir, env=safe_env_for_running_shell(home_dir, rc))
40+
i = 10
41+
while i > 0 and not pty.screen_contents().strip():
42+
pty.process_input_from_child()
43+
i -= 1
44+
yield pty
45+
46+
def test_zsh_integration(self):
47+
ps1, rps1 = 'left>', '<right'
48+
with self.run_shell(
49+
rc=f'''
50+
PS1="{ps1}"
51+
RPS1="{rps1}"
52+
''') as pty:
53+
self.ae(pty.callbacks.titlebuf, '~')
54+
q = ps1 + ' ' * (pty.screen.columns - len(ps1) - len(rps1)) + rps1
55+
self.ae(pty.screen_contents(), q)
56+
pty.wait_till(lambda: pty.screen.cursor.shape == CURSOR_BEAM)
57+
pty.send_cmd_to_child('mkdir test && ls -a')
58+
pty.wait_till(lambda: pty.screen_contents().count('left>') == 2)
59+
self.ae(pty.last_cmd_output(), str(pty.screen.line(1)))

kitty_tests/ssh.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,15 @@ class SSHTest(BaseTest):
1414

1515
def test_basic_pty_operations(self):
1616
pty = self.create_pty('echo hello')
17-
self.assertTrue(pty.wait_for_input_from_child())
1817
pty.process_input_from_child()
1918
self.ae(pty.screen_contents(), 'hello')
2019
pty = self.create_pty(self.cmd_to_run_python_code('''\
2120
import array, fcntl, sys, termios
2221
buf = array.array('H', [0, 0, 0, 0])
2322
fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, buf)
2423
print(' '.join(map(str, buf)))'''), lines=13, cols=77)
25-
self.assertTrue(pty.wait_for_input_from_child())
2624
pty.process_input_from_child()
27-
self.ae(pty.screen_contents(), '13 77 0 0')
25+
self.ae(pty.screen_contents(), '13 77 770 260')
2826

2927
def test_ssh_connection_data(self):
3028
def t(cmdline, binary='ssh', host='main', port=None, identity_file=''):

0 commit comments

Comments
 (0)