1"""This implements a virtual screen. This is used to support ANSI terminal
2emulation. The screen representation and state is implemented in this class.
3Most of the methods are inspired by ANSI screen control codes. The ANSI class
4extends this class to add parsing of ANSI escape codes.
5
6PEXPECT LICENSE
7
8    This license is approved by the OSI and FSF as GPL-compatible.
9        http://opensource.org/licenses/isc-license.txt
10
11    Copyright (c) 2012, Noah Spurrier <noah@noah.org>
12    PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY
13    PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE
14    COPYRIGHT NOTICE AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES.
15    THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
16    WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
17    MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
18    ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
19    WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
20    ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
21    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
22
23"""
24
25import copy
26
27NUL = 0    # Fill character; ignored on input.
28ENQ = 5    # Transmit answerback message.
29BEL = 7    # Ring the bell.
30BS  = 8    # Move cursor left.
31HT  = 9    # Move cursor to next tab stop.
32LF = 10    # Line feed.
33VT = 11    # Same as LF.
34FF = 12    # Same as LF.
35CR = 13    # Move cursor to left margin or newline.
36SO = 14    # Invoke G1 character set.
37SI = 15    # Invoke G0 character set.
38XON = 17   # Resume transmission.
39XOFF = 19  # Halt transmission.
40CAN = 24   # Cancel escape sequence.
41SUB = 26   # Same as CAN.
42ESC = 27   # Introduce a control sequence.
43DEL = 127  # Fill character; ignored on input.
44SPACE = chr(32) # Space or blank character.
45
46def constrain (n, min, max):
47
48    """This returns a number, n constrained to the min and max bounds. """
49
50    if n < min:
51        return min
52    if n > max:
53        return max
54    return n
55
56class screen:
57
58    """This object maintains the state of a virtual text screen as a
59    rectangluar array. This maintains a virtual cursor position and handles
60    scrolling as characters are added. This supports most of the methods needed
61    by an ANSI text screen. Row and column indexes are 1-based (not zero-based,
62    like arrays). """
63
64    def __init__ (self, r=24,c=80):
65
66        """This initializes a blank scree of the given dimentions."""
67
68        self.rows = r
69        self.cols = c
70        self.cur_r = 1
71        self.cur_c = 1
72        self.cur_saved_r = 1
73        self.cur_saved_c = 1
74        self.scroll_row_start = 1
75        self.scroll_row_end = self.rows
76        self.w = [ [SPACE] * self.cols for c in range(self.rows)]
77
78    def __str__ (self):
79
80        """This returns a printable representation of the screen. The end of
81        each screen line is terminated by a newline. """
82
83        return '\n'.join ([ ''.join(c) for c in self.w ])
84
85    def dump (self):
86
87        """This returns a copy of the screen as a string. This is similar to
88        __str__ except that lines are not terminated with line feeds. """
89
90        return ''.join ([ ''.join(c) for c in self.w ])
91
92    def pretty (self):
93
94        """This returns a copy of the screen as a string with an ASCII text box
95        around the screen border. This is similar to __str__ except that it
96        adds a box. """
97
98        top_bot = '+' + '-'*self.cols + '+\n'
99        return top_bot + '\n'.join(['|'+line+'|' for line in str(self).split('\n')]) + '\n' + top_bot
100
101    def fill (self, ch=SPACE):
102
103        self.fill_region (1,1,self.rows,self.cols, ch)
104
105    def fill_region (self, rs,cs, re,ce, ch=SPACE):
106
107        rs = constrain (rs, 1, self.rows)
108        re = constrain (re, 1, self.rows)
109        cs = constrain (cs, 1, self.cols)
110        ce = constrain (ce, 1, self.cols)
111        if rs > re:
112            rs, re = re, rs
113        if cs > ce:
114            cs, ce = ce, cs
115        for r in range (rs, re+1):
116            for c in range (cs, ce + 1):
117                self.put_abs (r,c,ch)
118
119    def cr (self):
120
121        """This moves the cursor to the beginning (col 1) of the current row.
122        """
123
124        self.cursor_home (self.cur_r, 1)
125
126    def lf (self):
127
128        """This moves the cursor down with scrolling.
129        """
130
131        old_r = self.cur_r
132        self.cursor_down()
133        if old_r == self.cur_r:
134            self.scroll_up ()
135            self.erase_line()
136
137    def crlf (self):
138
139        """This advances the cursor with CRLF properties.
140        The cursor will line wrap and the screen may scroll.
141        """
142
143        self.cr ()
144        self.lf ()
145
146    def newline (self):
147
148        """This is an alias for crlf().
149        """
150
151        self.crlf()
152
153    def put_abs (self, r, c, ch):
154
155        """Screen array starts at 1 index."""
156
157        r = constrain (r, 1, self.rows)
158        c = constrain (c, 1, self.cols)
159        ch = str(ch)[0]
160        self.w[r-1][c-1] = ch
161
162    def put (self, ch):
163
164        """This puts a characters at the current cursor position.
165        """
166
167        self.put_abs (self.cur_r, self.cur_c, ch)
168
169    def insert_abs (self, r, c, ch):
170
171        """This inserts a character at (r,c). Everything under
172        and to the right is shifted right one character.
173        The last character of the line is lost.
174        """
175
176        r = constrain (r, 1, self.rows)
177        c = constrain (c, 1, self.cols)
178        for ci in range (self.cols, c, -1):
179            self.put_abs (r,ci, self.get_abs(r,ci-1))
180        self.put_abs (r,c,ch)
181
182    def insert (self, ch):
183
184        self.insert_abs (self.cur_r, self.cur_c, ch)
185
186    def get_abs (self, r, c):
187
188        r = constrain (r, 1, self.rows)
189        c = constrain (c, 1, self.cols)
190        return self.w[r-1][c-1]
191
192    def get (self):
193
194        self.get_abs (self.cur_r, self.cur_c)
195
196    def get_region (self, rs,cs, re,ce):
197
198        """This returns a list of lines representing the region.
199        """
200
201        rs = constrain (rs, 1, self.rows)
202        re = constrain (re, 1, self.rows)
203        cs = constrain (cs, 1, self.cols)
204        ce = constrain (ce, 1, self.cols)
205        if rs > re:
206            rs, re = re, rs
207        if cs > ce:
208            cs, ce = ce, cs
209        sc = []
210        for r in range (rs, re+1):
211            line = ''
212            for c in range (cs, ce + 1):
213                ch = self.get_abs (r,c)
214                line = line + ch
215            sc.append (line)
216        return sc
217
218    def cursor_constrain (self):
219
220        """This keeps the cursor within the screen area.
221        """
222
223        self.cur_r = constrain (self.cur_r, 1, self.rows)
224        self.cur_c = constrain (self.cur_c, 1, self.cols)
225
226    def cursor_home (self, r=1, c=1): # <ESC>[{ROW};{COLUMN}H
227
228        self.cur_r = r
229        self.cur_c = c
230        self.cursor_constrain ()
231
232    def cursor_back (self,count=1): # <ESC>[{COUNT}D (not confused with down)
233
234        self.cur_c = self.cur_c - count
235        self.cursor_constrain ()
236
237    def cursor_down (self,count=1): # <ESC>[{COUNT}B (not confused with back)
238
239        self.cur_r = self.cur_r + count
240        self.cursor_constrain ()
241
242    def cursor_forward (self,count=1): # <ESC>[{COUNT}C
243
244        self.cur_c = self.cur_c + count
245        self.cursor_constrain ()
246
247    def cursor_up (self,count=1): # <ESC>[{COUNT}A
248
249        self.cur_r = self.cur_r - count
250        self.cursor_constrain ()
251
252    def cursor_up_reverse (self): # <ESC> M   (called RI -- Reverse Index)
253
254        old_r = self.cur_r
255        self.cursor_up()
256        if old_r == self.cur_r:
257            self.scroll_up()
258
259    def cursor_force_position (self, r, c): # <ESC>[{ROW};{COLUMN}f
260
261        """Identical to Cursor Home."""
262
263        self.cursor_home (r, c)
264
265    def cursor_save (self): # <ESC>[s
266
267        """Save current cursor position."""
268
269        self.cursor_save_attrs()
270
271    def cursor_unsave (self): # <ESC>[u
272
273        """Restores cursor position after a Save Cursor."""
274
275        self.cursor_restore_attrs()
276
277    def cursor_save_attrs (self): # <ESC>7
278
279        """Save current cursor position."""
280
281        self.cur_saved_r = self.cur_r
282        self.cur_saved_c = self.cur_c
283
284    def cursor_restore_attrs (self): # <ESC>8
285
286        """Restores cursor position after a Save Cursor."""
287
288        self.cursor_home (self.cur_saved_r, self.cur_saved_c)
289
290    def scroll_constrain (self):
291
292        """This keeps the scroll region within the screen region."""
293
294        if self.scroll_row_start <= 0:
295            self.scroll_row_start = 1
296        if self.scroll_row_end > self.rows:
297            self.scroll_row_end = self.rows
298
299    def scroll_screen (self): # <ESC>[r
300
301        """Enable scrolling for entire display."""
302
303        self.scroll_row_start = 1
304        self.scroll_row_end = self.rows
305
306    def scroll_screen_rows (self, rs, re): # <ESC>[{start};{end}r
307
308        """Enable scrolling from row {start} to row {end}."""
309
310        self.scroll_row_start = rs
311        self.scroll_row_end = re
312        self.scroll_constrain()
313
314    def scroll_down (self): # <ESC>D
315
316        """Scroll display down one line."""
317
318        # Screen is indexed from 1, but arrays are indexed from 0.
319        s = self.scroll_row_start - 1
320        e = self.scroll_row_end - 1
321        self.w[s+1:e+1] = copy.deepcopy(self.w[s:e])
322
323    def scroll_up (self): # <ESC>M
324
325        """Scroll display up one line."""
326
327        # Screen is indexed from 1, but arrays are indexed from 0.
328        s = self.scroll_row_start - 1
329        e = self.scroll_row_end - 1
330        self.w[s:e] = copy.deepcopy(self.w[s+1:e+1])
331
332    def erase_end_of_line (self): # <ESC>[0K -or- <ESC>[K
333
334        """Erases from the current cursor position to the end of the current
335        line."""
336
337        self.fill_region (self.cur_r, self.cur_c, self.cur_r, self.cols)
338
339    def erase_start_of_line (self): # <ESC>[1K
340
341        """Erases from the current cursor position to the start of the current
342        line."""
343
344        self.fill_region (self.cur_r, 1, self.cur_r, self.cur_c)
345
346    def erase_line (self): # <ESC>[2K
347
348        """Erases the entire current line."""
349
350        self.fill_region (self.cur_r, 1, self.cur_r, self.cols)
351
352    def erase_down (self): # <ESC>[0J -or- <ESC>[J
353
354        """Erases the screen from the current line down to the bottom of the
355        screen."""
356
357        self.erase_end_of_line ()
358        self.fill_region (self.cur_r + 1, 1, self.rows, self.cols)
359
360    def erase_up (self): # <ESC>[1J
361
362        """Erases the screen from the current line up to the top of the
363        screen."""
364
365        self.erase_start_of_line ()
366        self.fill_region (self.cur_r-1, 1, 1, self.cols)
367
368    def erase_screen (self): # <ESC>[2J
369
370        """Erases the screen with the background color."""
371
372        self.fill ()
373
374    def set_tab (self): # <ESC>H
375
376        """Sets a tab at the current position."""
377
378        pass
379
380    def clear_tab (self): # <ESC>[g
381
382        """Clears tab at the current position."""
383
384        pass
385
386    def clear_all_tabs (self): # <ESC>[3g
387
388        """Clears all tabs."""
389
390        pass
391
392#        Insert line             Esc [ Pn L
393#        Delete line             Esc [ Pn M
394#        Delete character        Esc [ Pn P
395#        Scrolling region        Esc [ Pn(top);Pn(bot) r
396
397