Files
dotfiles/.config/kitty/test_window_picker.py
T

328 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Run with: kitty +runpy 'import runpy; runpy.run_path("test_window_picker.py", run_name="__main__")'."""
import io
import json
from pathlib import Path
import runpy
from types import SimpleNamespace as NS
import unittest
from unittest.mock import Mock, patch
from kitty.config import parse_config
from kitty.fast_data_types import Screen, wcswidth
from kitty.key_encoding import KeyEvent
ROOT = Path(__file__).resolve().parent
ui = runpy.run_path(str(ROOT / 'window_picker.py'))
launcher = runpy.run_path(str(ROOT / 'choose_tab.py'))
def parse_bytes(screen, data):
while data:
buffer = screen.test_create_write_buffer()
size = screen.test_commit_write_buffer(data, buffer)
screen.test_parse_written_data()
data = data[size:]
def example_payload():
entries = []
snapshots = {}
for group in range(2):
ids = list(range(group * 6 + 1, group * 6 + 7))
entries.append(dict(kind='window', label=f'- Project {group + 1}: 6 tabs',
title=f'Project {group + 1}', tab_id=ids[0], tabs=ids))
for index, tab_id in enumerate(ids):
title = f'project-{tab_id}'
entries.append(dict(kind='tab', label=f'└─> {index + 1}: {title}',
title=title, tab_id=tab_id))
panes = [dict(id=tab_id * 10, title='shell', active=True,
rect=[0, 0, 100, 100],
lines=['\x1b[32m$ build project\x1b[0m', 'result: OK', '❯'])]
if index == 0:
panes[0]['rect'] = [0, 0, 50, 100]
panes.append(dict(id=tab_id * 10 + 1, title='logs', active=False,
rect=[50, 0, 100, 100], lines=['\x1b[33mwarning\x1b[0m']))
snapshots[str(tab_id)] = dict(title=title, panes=panes)
return dict(entries=entries, snapshots=snapshots)
class PickerTests(unittest.TestCase):
def picker(self, collapsed=False, rows=30, cols=100):
payload = example_payload()
picker = ui['WindowPicker'](payload['entries'], collapsed, payload['snapshots'])
picker.screen_size = NS(rows=rows, cols=cols)
picker.write = Mock()
picker.quit_loop = Mock()
return picker
def rendered(self, picker):
picker.draw()
screen = Screen(None, picker.screen_size.rows, picker.screen_size.cols)
parse_bytes(screen, picker.write.call_args.args[0].encode())
return screen, '\n'.join(str(screen.line(i)) for i in range(screen.lines))
def test_window_vs_tab_preview(self):
picker = self.picker()
screen, text = self.rendered(picker)
self.assertIn('Project 1 — snapshot', text)
self.assertIn('1:project-1', text)
self.assertIn('4:project-4', text)
self.assertIn('warning', text)
preview_text = '\n'.join(text.splitlines()[15:-1])
self.assertEqual(preview_text.count('┌'), 5) # Frame + four tab badges.
self.assertNotIn('││', preview_text)
self.assertIn('[] page', text)
self.assertIn('\x1b[32m', ''.join(screen.line(i).as_ansi() for i in range(screen.lines)))
picker.move(1)
_, text = self.rendered(picker)
self.assertIn('project-1 — snapshot', text)
preview_text = '\n'.join(text.splitlines()[15:])
self.assertNotIn('4:project-4', preview_text)
self.assertIn('│ 1 │', preview_text)
self.assertIn('│ 2 │', preview_text)
self.assertEqual(preview_text.count('┌'), 3) # Frame + two small badges.
picker.move(1)
_, text = self.rendered(picker)
preview_text = '\n'.join(text.splitlines()[15:-1])
self.assertEqual(preview_text.count('┌'), 2) # Single pane: frame + number badge.
def test_paging_does_not_change_selection(self):
picker = self.picker()
picker.on_text(']')
self.assertEqual(picker.selected, 0)
_, text = self.rendered(picker)
self.assertIn('tabs 5–6/6 [2/2]', text)
self.assertIn('5:project-5', text)
picker.on_text(']')
self.assertEqual(picker.preview_pages[id(picker.entries[0])], 1)
picker.on_text('[')
self.assertEqual(picker.preview_pages[id(picker.entries[0])], 0)
picker.entries[0]['tab_id'] = 6
picker.preview_pages.clear()
_, text = self.rendered(picker)
self.assertIn('6:project-6', text)
def test_resize_and_small_terminal(self):
picker = self.picker()
picker.on_text(']')
picker.on_resize(NS(rows=24, cols=160))
_, text = self.rendered(picker)
self.assertIn('Project 1 — snapshot', text)
self.assertNotIn('[2/2]', text)
for rows, cols in ((17, 100), (30, 23), (1, 1)):
picker.on_resize(NS(rows=rows, cols=cols))
self.assertEqual(picker.preview_height(), 0)
_, text = self.rendered(picker)
self.assertNotIn('snapshot', text)
def test_folding_search_and_selection(self):
picker = self.picker(collapsed=True)
self.assertEqual(picker.visible_indices(), [0, 7])
picker.on_text('j')
self.assertEqual(picker.entries[picker.selected]['title'], 'Project 2')
picker.on_key(KeyEvent(key='RIGHT'))
self.assertEqual(len(picker.entries), 8)
picker.on_text('/PROJECT-12')
self.assertEqual(picker.entries[picker.selected]['tab_id'], 12)
picker.on_key(KeyEvent(key='ENTER'))
picker.quit_loop.assert_not_called()
picker.on_key(KeyEvent(key='ENTER'))
self.assertEqual(picker.answer, 12)
picker.on_text('/not found')
self.assertIn('no matches', picker.write.call_args.args[0])
picker.on_key(KeyEvent(key='ESCAPE'))
self.assertEqual(picker.query, 'PROJECT-12')
picker.on_text('q')
picker.quit_loop.assert_called_with(0)
def test_search_next_previous_and_arrow_navigation(self):
picker = self.picker()
picker.on_text('/project-')
picker.on_key(KeyEvent(key='ENTER'))
selected = picker.selected
picker.on_text('n')
self.assertGreater(picker.selected, selected)
picker.on_text('N')
self.assertEqual(picker.selected, selected)
picker.on_key(KeyEvent(key='DOWN'))
picker.on_key(KeyEvent(key='UP'))
self.assertEqual(picker.selected, selected)
def test_unavailable_empty_and_tiny_panes(self):
picker = self.picker()
picker.selected = 1
picker.snapshots['1']['panes'][0]['lines'] = None
_, text = self.rendered(picker)
self.assertIn('Preview unavailable', text)
picker.snapshots.clear()
_, text = self.rendered(picker)
self.assertIn('Preview unavailable', text)
empty = ui['WindowPicker']([])
empty.screen_size = NS(rows=24, cols=80)
empty.write = Mock()
empty.draw()
picker = self.picker(rows=18, cols=24)
picker.snapshots['1']['panes'][0]['rect'] = [0, 0, 1, 100]
picker.snapshots['1']['panes'][1]['rect'] = [1, 0, 100, 100]
_, text = self.rendered(picker)
self.assertIn('too small', text)
def test_nested_layout_and_clipping(self):
preview = ui['Preview'](1, 80, 24)
panes = [dict(id=i, title=str(i), active=i == 0, rect=rect,
lines=['\x1b[31m' + '界' * 100] * 50)
for i, rect in enumerate(([0, 0, 50, 50], [0, 50, 50, 100], [50, 0, 100, 100]))]
preview.box(0, 0, 80, 24, 'Nested')
preview.tab(dict(panes=panes), 1, 1, 78, 22)
screen = Screen(None, 24, 80)
parse_bytes(screen, ''.join(preview.output).encode())
# Overlong colored lines cannot overwrite the outer border.
self.assertTrue(str(screen.line(5)).endswith('│'))
self.assertTrue(str(screen.line(23)).startswith('└'))
def test_safe_ansi_and_unicode_width(self):
clip = ui['clip_ansi']
result = clip('\x1b[32m界界abc\x1b[0m', 5)
self.assertEqual(ui['clean_text'](result), '界界a')
self.assertEqual(wcswidth(ui['clean_text'](result)), 5)
self.assertIn('\x1b[32m', result)
malicious = '\x1b]52;c;secret\x07\x1b[2J\x1bPpayload\x1b\\safe\x07'
self.assertEqual(ui['clean_text'](clip(malicious, 80)), 'safe')
self.assertNotIn('secret', clip(malicious, 80))
self.assertNotIn('\x1b[2J', clip(malicious, 80))
self.assertEqual(ui['clean_text'](clip('e\u0301界', 1)), 'e\u0301')
def test_stdin_payload_and_collapsed_flag(self):
payload = example_payload()
with patch.object(ui['Loop'], 'loop') as loop, patch('sys.stdin', io.StringIO(json.dumps(payload))):
result = ui['main'](['window_picker.py', '--collapsed'])
picker = loop.call_args.args[0]
self.assertEqual(picker.visible_indices(), [0, 7])
self.assertIsNone(result)
class CaptureTests(unittest.TestCase):
def fixture(self):
events = []
def capture():
events.append('capture')
return '\x1b[32m$ hello'
window = NS(id=10, title='shell', geometry=NS(left=0, top=0, right=100, bottom=100),
screen=NS(lines=2, visual_line=lambda i: NS(as_ansi=capture)))
tab = NS(id=1, name='Tab', title='Tab', active_window=window, num_window_groups=1,
windows=NS(iter_all_layoutable_groups=lambda **kw: [NS(active_window_id=10)]),
current_layout=NS(name='splits', tree='original tree', biases=[0.3, 0.7]),
_current_layout_name='splits', _last_used_layout='tall',
relayout=Mock(), mark_tab_bar_dirty=Mock())
def set_layout(name):
tab._last_used_layout = tab._current_layout_name
tab._current_layout_name = name
tab.current_layout = NS(name=name)
tab._set_current_layout = Mock(side_effect=set_layout)
window.tabref = lambda: tab
class Manager:
active_tab = tab
def __iter__(self):
return iter([tab])
boss = NS(window_id_map={10: window}, active_tab=tab, all_tabs=[tab],
os_window_map={99: Manager()}, set_active_window=Mock(),
run_kitten_with_metadata=Mock(side_effect=lambda *a, **k: (events.append('launch'), NS())[1]))
return events, window, tab, boss
def test_capture_before_launch_and_activation_after_removal(self):
events, window, tab, boss = self.fixture()
original = tab.current_layout
with patch('kitty.fast_data_types.get_os_window_title', return_value='OS title'):
handler = runpy.run_path(str(ROOT / 'choose_tab.py'))['handle_result']
handler(['choose_tab.py', '--collapsed'], None, 10, boss)
self.assertEqual(events, ['capture', 'capture', 'launch'])
self.assertEqual(tab.current_layout.name, 'stack')
call = boss.run_kitten_with_metadata.call_args
self.assertEqual(call.kwargs['args'], ('--collapsed',))
payload = json.loads(call.kwargs['input_data'])
self.assertEqual(payload['entries'][0]['title'], 'OS title')
self.assertEqual(payload['entries'][0]['kind'], 'window')
self.assertEqual(payload['snapshots']['1']['panes'][0]['lines'], ['\x1b[32m$ hello'] * 2)
call.kwargs['custom_callback'](1, 10, boss)
boss.set_active_window.assert_not_called()
call.kwargs['action_on_removal'](10, boss)
self.assertIs(tab.current_layout, original)
self.assertEqual((tab._current_layout_name, tab._last_used_layout), ('splits', 'tall'))
boss.set_active_window.assert_called_once_with(window, switch_os_window_if_needed=True)
boss.set_active_window.reset_mock()
boss.all_tabs = []
call.kwargs['action_on_removal'](10, boss)
boss.set_active_window.assert_not_called()
def test_full_tab_cancel_and_existing_stack(self):
events, window, tab, boss = self.fixture()
original = tab.current_layout
launcher['handle_result'](['choose_tab.py'], None, 10, boss)
call = boss.run_kitten_with_metadata.call_args
self.assertIs(call.kwargs['window'], window)
self.assertEqual(tab.current_layout.name, 'stack')
call.kwargs['custom_callback'](None, 10, boss)
call.kwargs['action_on_removal'](10, boss)
self.assertIs(tab.current_layout, original)
boss.set_active_window.assert_not_called()
tab.current_layout = NS(name='stack')
tab._set_current_layout.reset_mock()
launcher['handle_result'](['choose_tab.py'], None, 10, boss)
tab._set_current_layout.assert_not_called()
boss.run_kitten_with_metadata.call_args.kwargs['action_on_removal'](10, boss)
self.assertEqual(tab.current_layout.name, 'stack')
def test_full_tab_launch_failure_restores_splits(self):
for failed in (False, True):
events, window, tab, boss = self.fixture()
original = tab.current_layout
if failed:
boss.run_kitten_with_metadata.side_effect = RuntimeError('launch failed')
with self.assertRaises(RuntimeError):
launcher['handle_result'](['choose_tab.py'], None, 10, boss)
else:
boss.run_kitten_with_metadata.side_effect = None
boss.run_kitten_with_metadata.return_value = None
launcher['handle_result'](['choose_tab.py'], None, 10, boss)
self.assertIs(tab.current_layout, original)
self.assertEqual((tab._current_layout_name, tab._last_used_layout), ('splits', 'tall'))
def test_capture_failure_does_not_disable_chooser(self):
events, window, tab, boss = self.fixture()
window.screen.visual_line = Mock(side_effect=RuntimeError('closed'))
snapshot = launcher['snapshot_tab'](tab, boss)
self.assertIsNone(snapshot['panes'][0]['lines'])
tab.windows.iter_all_layoutable_groups = Mock(side_effect=RuntimeError('closed'))
self.assertEqual(launcher['snapshot_tab'](tab, boss)['panes'], [])
def test_capture_real_screen_colors(self):
events, window, tab, boss = self.fixture()
window.screen = Screen(None, 3, 40)
parse_bytes(window.screen, b'\x1b[32mgreen prompt\x1b[0m\r\nnext row')
snapshot = launcher['snapshot_tab'](tab, boss)
lines = snapshot['panes'][0]['lines']
self.assertIn('\x1b[32m', lines[0])
self.assertEqual(ui['clean_text'](lines[0]), 'green prompt')
self.assertEqual(ui['clean_text'](lines[1]), 'next row')
def test_config(self):
errors = []
with (ROOT / 'kitty.conf').open() as config:
parsed = parse_config(config, accumulate_bad_lines=errors)
self.assertEqual(errors, [])
definitions = [m.definition for m in parsed['map']]
self.assertIn('kitten choose_tab.py --collapsed', definitions)
self.assertIn('kitten choose_tab.py', definitions)
if __name__ == '__main__':
unittest.main(argv=['test_window_picker.py'])