"""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'])