1# Urwid terminal emulation widget unit tests
2#    Copyright (C) 2010  aszlig
3#    Copyright (C) 2011  Ian Ward
4#
5#    This library is free software; you can redistribute it and/or
6#    modify it under the terms of the GNU Lesser General Public
7#    License as published by the Free Software Foundation; either
8#    version 2.1 of the License, or (at your option) any later version.
9#
10#    This library is distributed in the hope that it will be useful,
11#    but WITHOUT ANY WARRANTY; without even the implied warranty of
12#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13#    Lesser General Public License for more details.
14#
15#    You should have received a copy of the GNU Lesser General Public
16#    License along with this library; if not, write to the Free Software
17#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18#
19# Urwid web site: http://excess.org/urwid/
20
21import errno
22import os
23import sys
24import unittest
25
26from itertools import dropwhile
27
28from urwid.listbox import ListBox
29from urwid.decoration import BoxAdapter
30from urwid import vterm
31from urwid import signals
32from urwid.compat import B
33
34class DummyCommand(object):
35    QUITSTRING = B('|||quit|||')
36
37    def __init__(self):
38        self.reader, self.writer = os.pipe()
39
40    def __call__(self):
41        # reset
42        stdout = getattr(sys.stdout, 'buffer', sys.stdout)
43        stdout.write(B('\x1bc'))
44
45        while True:
46            data = self.read(1024)
47            if self.QUITSTRING == data:
48                break
49            stdout.write(data)
50            stdout.flush()
51
52    def read(self, size):
53        while True:
54            try:
55                return os.read(self.reader, size)
56            except OSError as e:
57                if e.errno != errno.EINTR:
58                    raise
59
60    def write(self, data):
61        os.write(self.writer, data)
62
63    def quit(self):
64        self.write(self.QUITSTRING)
65
66
67class TermTest(unittest.TestCase):
68    def setUp(self):
69        self.command = DummyCommand()
70
71        self.term = vterm.Terminal(self.command)
72        self.resize(80, 24)
73
74    def tearDown(self):
75        self.command.quit()
76
77    def connect_signal(self, signal):
78        self._sig_response = None
79
80        def _set_signal_response(widget, *args, **kwargs):
81            self._sig_response = (args, kwargs)
82        self._set_signal_response = _set_signal_response
83
84        signals.connect_signal(self.term, signal, self._set_signal_response)
85
86    def expect_signal(self, *args, **kwargs):
87        self.assertEqual(self._sig_response, (args, kwargs))
88
89    def disconnect_signal(self, signal):
90        signals.disconnect_signal(self.term, signal, self._set_signal_response)
91
92    def caught_beep(self, obj):
93        self.beeped = True
94
95    def resize(self, width, height, soft=False):
96        self.termsize = (width, height)
97        if not soft:
98            self.term.render(self.termsize, focus=False)
99
100    def write(self, data):
101        data = B(data)
102        self.command.write(data.replace(B('\e'), B('\x1b')))
103
104    def flush(self):
105        self.write(chr(0x7f))
106
107    def read(self, raw=False, focus=False):
108        self.term.wait_and_feed()
109        rendered = self.term.render(self.termsize, focus=focus)
110        if raw:
111            is_empty = lambda c: c == (None, None, B(' '))
112            content = list(rendered.content())
113            lines = [list(dropwhile(is_empty, reversed(line)))
114                     for line in content]
115            return [list(reversed(line)) for line in lines if len(line)]
116        else:
117            content = rendered.text
118            lines = [line.rstrip() for line in content]
119            return B('\n').join(lines).rstrip()
120
121    def expect(self, what, desc=None, raw=False, focus=False):
122        if not isinstance(what, list):
123            what = B(what)
124        got = self.read(raw=raw, focus=focus)
125        if desc is None:
126            desc = ''
127        else:
128            desc += '\n'
129        desc += 'Expected:\n%r\nGot:\n%r' % (what, got)
130        self.assertEqual(got, what, desc)
131
132    def test_simplestring(self):
133        self.write('hello world')
134        self.expect('hello world')
135
136    def test_linefeed(self):
137        self.write('hello\x0aworld')
138        self.expect('hello\nworld')
139
140    def test_linefeed2(self):
141        self.write('aa\b\b\eDbb')
142        self.expect('aa\nbb')
143
144    def test_carriage_return(self):
145        self.write('hello\x0dworld')
146        self.expect('world')
147
148    def test_insertlines(self):
149        self.write('\e[0;0flast\e[0;0f\e[10L\e[0;0ffirst\nsecond\n\e[11D')
150        self.expect('first\nsecond\n\n\n\n\n\n\n\n\nlast')
151
152    def test_deletelines(self):
153        self.write('1\n2\n3\n4\e[2;1f\e[2M')
154        self.expect('1\n4')
155
156    def test_nul(self):
157        self.write('a\0b')
158        self.expect('ab')
159
160    def test_movement(self):
161        self.write('\e[10;20H11\e[10;0f\e[20C\e[K')
162        self.expect('\n' * 9 + ' ' * 19 + '1')
163        self.write('\e[A\e[B\e[C\e[D\b\e[K')
164        self.expect('')
165        self.write('\e[50A2')
166        self.expect(' ' * 19 + '2')
167        self.write('\b\e[K\e[50B3')
168        self.expect('\n' * 23 + ' ' * 19 + '3')
169        self.write('\b\e[K' + '\eM' * 30 + '\e[100C4')
170        self.expect(' ' * 79 + '4')
171        self.write('\e[100D\e[K5')
172        self.expect('5')
173
174    def edgewall(self):
175        edgewall = '1-\e[1;%(x)df-2\e[%(y)d;1f3-\e[%(y)d;%(x)df-4\x0d'
176        self.write(edgewall % {'x': self.termsize[0] - 1,
177                               'y': self.termsize[1] - 1})
178
179    def test_horizontal_resize(self):
180        self.resize(80, 24)
181        self.edgewall()
182        self.expect('1-' + ' ' * 76 + '-2' + '\n' * 22
183                         + '3-' + ' ' * 76 + '-4')
184        self.resize(78, 24, soft=True)
185        self.flush()
186        self.expect('1-' + '\n' * 22 + '3-')
187        self.resize(80, 24, soft=True)
188        self.flush()
189        self.expect('1-' + '\n' * 22 + '3-')
190
191    def test_vertical_resize(self):
192        self.resize(80, 24)
193        self.edgewall()
194        self.expect('1-' + ' ' * 76 + '-2' + '\n' * 22
195                         + '3-' + ' ' * 76 + '-4')
196        for y in range(23, 1, -1):
197            self.resize(80, y, soft=True)
198            self.write('\e[%df\e[J3-\e[%d;%df-4' % (y, y, 79))
199            desc = "try to rescale to 80x%d." % y
200            self.expect('\n' * (y - 2) + '3-' + ' ' * 76 + '-4', desc)
201        self.resize(80, 24, soft=True)
202        self.flush()
203        self.expect('1-' + ' ' * 76 + '-2' + '\n' * 22
204                         + '3-' + ' ' * 76 + '-4')
205
206    def write_movements(self, arg):
207        fmt = 'XXX\n\e[faaa\e[Bccc\e[Addd\e[Bfff\e[Cbbb\e[A\e[Deee'
208        self.write(fmt.replace('\e[', '\e['+arg))
209
210    def test_defargs(self):
211        self.write_movements('')
212        self.expect('aaa   ddd      eee\n   ccc   fff bbb')
213
214    def test_nullargs(self):
215        self.write_movements('0')
216        self.expect('aaa   ddd      eee\n   ccc   fff bbb')
217
218    def test_erase_line(self):
219        self.write('1234567890\e[5D\e[K\n1234567890\e[5D\e[1K\naaaaaaaaaaaaaaa\e[2Ka')
220        self.expect('12345\n      7890\n               a')
221
222    def test_erase_display(self):
223        self.write('1234567890\e[5D\e[Ja')
224        self.expect('12345a')
225        self.write('98765\e[8D\e[1Jx')
226        self.expect('   x5a98765')
227
228    def test_scrolling_region_simple(self):
229        self.write('\e[10;20r\e[10f1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\e[faa')
230        self.expect('aa' + '\n' * 9 + '2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12')
231
232    def test_scrolling_region_reverse(self):
233        self.write('\e[2J\e[1;2r\e[5Baaa\r\eM\eM\eMbbb\nXXX')
234        self.expect('\n\nbbb\nXXX\n\naaa')
235
236    def test_scrolling_region_move(self):
237        self.write('\e[10;20r\e[2J\e[10Bfoo\rbar\rblah\rmooh\r\e[10Aone\r\eM\eMtwo\r\eM\eMthree\r\eM\eMa')
238        self.expect('ahree\n\n\n\n\n\n\n\n\n\nmooh')
239
240    def test_scrolling_twice(self):
241        self.write('\e[?6h\e[10;20r\e[2;5rtest')
242        self.expect('\ntest')
243
244    def test_cursor_scrolling_region(self):
245        self.write('\e[?6h\e[10;20r\e[10f1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\e[faa')
246        self.expect('\n' * 9 + 'aa\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12')
247
248    def test_scrolling_region_simple_with_focus(self):
249        self.write('\e[10;20r\e[10f1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\e[faa')
250        self.expect('aa' + '\n' * 9 + '2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12', focus=True)
251
252    def test_scrolling_region_reverse_with_focus(self):
253        self.write('\e[2J\e[1;2r\e[5Baaa\r\eM\eM\eMbbb\nXXX')
254        self.expect('\n\nbbb\nXXX\n\naaa', focus=True)
255
256    def test_scrolling_region_move_with_focus(self):
257        self.write('\e[10;20r\e[2J\e[10Bfoo\rbar\rblah\rmooh\r\e[10Aone\r\eM\eMtwo\r\eM\eMthree\r\eM\eMa')
258        self.expect('ahree\n\n\n\n\n\n\n\n\n\nmooh', focus=True)
259
260    def test_scrolling_twice_with_focus(self):
261        self.write('\e[?6h\e[10;20r\e[2;5rtest')
262        self.expect('\ntest', focus=True)
263
264    def test_cursor_scrolling_region_with_focus(self):
265        self.write('\e[?6h\e[10;20r\e[10f1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\e[faa')
266        self.expect('\n' * 9 + 'aa\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12', focus=True)
267
268    def test_relative_region_jump(self):
269        self.write('\e[21H---\e[10;20r\e[?6h\e[18Htest')
270        self.expect('\n' * 19 + 'test\n---')
271
272    def test_set_multiple_modes(self):
273        self.write('\e[?6;5htest')
274        self.expect('test')
275        self.assertTrue(self.term.term_modes.constrain_scrolling)
276        self.assertTrue(self.term.term_modes.reverse_video)
277        self.write('\e[?6;5l')
278        self.expect('test')
279        self.assertFalse(self.term.term_modes.constrain_scrolling)
280        self.assertFalse(self.term.term_modes.reverse_video)
281
282    def test_wrap_simple(self):
283        self.write('\e[?7h\e[1;%dHtt' % self.term.width)
284        self.expect(' ' * (self.term.width - 1) + 't\nt')
285
286    def test_wrap_backspace_tab(self):
287        self.write('\e[?7h\e[1;%dHt\b\b\t\ta' % self.term.width)
288        self.expect(' ' * (self.term.width - 1) + 'a')
289
290    def test_cursor_visibility(self):
291        self.write('\e[?25linvisible')
292        self.expect('invisible', focus=True)
293        self.assertEqual(self.term.term.cursor, None)
294        self.write('\rvisible\e[?25h\e[K')
295        self.expect('visible', focus=True)
296        self.assertNotEqual(self.term.term.cursor, None)
297
298    def test_get_utf8_len(self):
299        length = self.term.term.get_utf8_len(int("11110000", 2))
300        self.assertEqual(length, 3)
301        length = self.term.term.get_utf8_len(int("11000000", 2))
302        self.assertEqual(length, 1)
303        length = self.term.term.get_utf8_len(int("11111101", 2))
304        self.assertEqual(length, 5)
305
306    def test_encoding_unicode(self):
307        vterm.util._target_encoding = 'utf-8'
308        self.write('\e%G\xe2\x80\x94')
309        self.expect('\xe2\x80\x94')
310
311    def test_encoding_unicode_ascii(self):
312        vterm.util._target_encoding = 'ascii'
313        self.write('\e%G\xe2\x80\x94')
314        self.expect('?')
315
316    def test_encoding_wrong_unicode(self):
317        vterm.util._target_encoding = 'utf-8'
318        self.write('\e%G\xc0\x99')
319        self.expect('')
320
321    def test_encoding_vt100_graphics(self):
322        vterm.util._target_encoding = 'ascii'
323        self.write('\e)0\e(0\x0fg\x0eg\e)Bn\e)0g\e)B\e(B\x0fn')
324        self.expect([[
325            (None, '0', B('g')), (None, '0', B('g')),
326            (None, None, B('n')), (None, '0', B('g')),
327            (None, None, B('n'))
328        ]], raw=True)
329
330    def test_ibmpc_mapping(self):
331        vterm.util._target_encoding = 'ascii'
332
333        self.write('\e[11m\x18\e[10m\x18')
334        self.expect([[(None, 'U', B('\x18'))]], raw=True)
335
336        self.write('\ec\e)U\x0e\x18\x0f\e[3h\x18\e[3l\x18')
337        self.expect([[(None, None, B('\x18'))]], raw=True)
338
339        self.write('\ec\e[11m\xdb\x18\e[10m\xdb')
340        self.expect([[
341            (None, 'U', B('\xdb')), (None, 'U', B('\x18')),
342            (None, None, B('\xdb'))
343        ]], raw=True)
344
345    def test_set_title(self):
346        self._the_title = None
347
348        def _change_title(widget, title):
349            self._the_title = title
350
351        self.connect_signal('title')
352        self.write('\e]666parsed right?\e\\te\e]0;test title\007st1')
353        self.expect('test1')
354        self.expect_signal(B('test title'))
355        self.write('\e]3;stupid title\e\\\e[0G\e[2Ktest2')
356        self.expect('test2')
357        self.expect_signal(B('stupid title'))
358        self.disconnect_signal('title')
359
360    def test_set_leds(self):
361        self.connect_signal('leds')
362        self.write('\e[0qtest1')
363        self.expect('test1')
364        self.expect_signal('clear')
365        self.write('\e[3q\e[H\e[Ktest2')
366        self.expect('test2')
367        self.expect_signal('caps_lock')
368        self.disconnect_signal('leds')
369
370    def test_in_listbox(self):
371        listbox = ListBox([BoxAdapter(self.term, 80)])
372        rendered = listbox.render((80, 24))
373