File: emulation.py

package info (click to toggle)
pyqonsole 0.2.0-2
  • links: PTS
  • area: main
  • in suites: etch, etch-m68k
  • size: 448 kB
  • ctags: 647
  • sloc: python: 4,383; ansic: 111; makefile: 52; sh: 2
file content (278 lines) | stat: -rw-r--r-- 10,106 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# Copyright (c) 2005-2006 LOGILAB S.A. (Paris, FRANCE).
# Copyright (c) 2005-2006 CEA Grenoble 
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the CECILL license, available at
# http://www.inria.fr/valorisation/logiciels/Licence.CeCILL-V2.pdf
#
"""Provide the Emulation class.

This class acts as the controler between the Screen class (Model) and
Widget class (View). As Widget uses Qt, Emulation also depends on Qt.
But it is very easy to use another toolkit.

A note on refreshing

   Although the modifications to the current screen image could immediately
   be propagated via `Widget' to the graphical surface, we have chosen
   another way here.

   The reason for doing so is twofold.

   First, experiments show that directly displaying the operation results
   in slowing down the overall performance of emulations. Displaying
   individual characters using X11 creates a lot of overhead.

   Second, by using the following refreshing method, the screen operations
   can be completely separated from the displaying. This greatly simplifies
   the programmer's task of coding and maintaining the screen operations,
   since one need not worry about differential modifications on the
   display affecting the operation of concern.

   We use a refreshing algorithm here that has been adoped from rxvt/kvt.

   By this, refreshing is driven by a timer, which is (re)started whenever
   a new bunch of data to be interpreted by the emulation arives at `onRcvBlock'.
   As soon as no more data arrive for `BULK_TIMEOUT' milliseconds, we trigger
   refresh. This rule suits both bulk display operation as done by curses as
   well as individual characters typed.
   (BULK_TIMEOUT < 1000 / max characters received from keyboard per second).

   Additionally, we trigger refreshing by newlines comming in to make visual
   snapshots of lists as produced by `cat', `ls' and likely programs, thereby
   producing the illusion of a permanent and immediate display operation.

   As a sort of catch-all needed for cases where none of the above
   conditions catch, the screen refresh is also triggered by a count
   of incoming bulks (`bulk_incnt').

Based on the konsole code from Lars Doelle.

@author: Lars Doelle
@author: Benjamin Longuet
@author: Frederic Mantegazza
@author: Cyrille Boullier
@author: Sylvain Thenault
@copyright: 2003, 2005, 2006
@organization: CEA-Grenoble
@organization: Logilab
@license: CECILL
"""

__revision__ = '$Id: emulation.py,v 1.25 2006-02-15 10:24:01 alf Exp $'

import qt

from pyqonsole import Signalable, keytrans
from pyqonsole.screen import Screen


NOTIFYNORMAL = 0
NOTIFYBELL = 1
NOTIFYACTIVITY = 2
NOTIFYSILENCE = 3

BULK_TIMEOUT = 20


class Emulation(Signalable, qt.QObject):
    """This class acts as the controler between the Screen class (Model) and
    Widget class (View). It's actually a common abstract base class for
    different terminal implementations, and so should be subclassed.

    It is responsible to scan the escapes sequences of the terminal
    emulation and to map it to their corresponding semantic complements.
    Thus this module knows mainly about decoding escapes sequences and
    is a stateless device w.r.t. the semantics.

    It is also responsible to refresh the Widget by certain rules.
    """
    def __init__(self, gui):
        super(Emulation, self).__init__()
        self._gui = gui
        # 0 = primary, 1 = alternate
        self._screen = [Screen(self._gui.lines, self._gui.columns),
                        Screen(self._gui.lines, self._gui.columns)]
        self._scr = self._screen[0]
        # communicate with widget
        self._connected = False
        # codec
        self._codec = None
        self._decoder = None
        # key translator
        self._key_trans = None
        self.setKeymap(0)
        # bulk handling
        self._bulk_timer = qt.QTimer(self)
        self._bulk_nl_cnt = 0 # bulk new line counter
        self._bulk_in_cnt = 0 # bulk counter
        self._bulk_timer.connect(self._bulk_timer, qt.SIGNAL("timeout()"),
                                 self._showBulk)
        gui.myconnect("changedImageSizeSignal", self.onImageSizeChange)
        gui.myconnect("changedHistoryCursor", self.onHistoryCursorChange)
        gui.myconnect("keyPressedSignal", self.onKeyPress)
        gui.myconnect("beginSelectionSignal", self.onSelectionBegin)
        gui.myconnect("extendSelectionSignal", self.onSelectionExtend)
        gui.myconnect("endSelectionSignal", self.setSelection)
        gui.myconnect("clearSelectionSignal", self.clearSelection)
        gui.myconnect("isBusySelecting", self.isBusySelecting)
        gui.myconnect("testIsSelected", self.testIsSelected)
        
    def __del__(self):
        self._bulk_timer.stop()
        
    def _setScreen(self, n):
        """change between primary and alternate screen"""
        old = self._scr
        self._scr = self._screen[n]
        if not self._scr is old:
            self._scr.clearSelection()
            old.busy_selecting = False
            
    def setHistory(self, history_type):
        self._screen[0].setScroll(history_type)
        if self._connected:
            self._showBulk()
        
    def history(self):
        return self._screen[0].getScroll()
    
    def setKeymap(self, no):
        self._key_trans = keytrans.find(no)
    
    def keymap(self):
        return self._key_trans
        
    # Interpreting Codes
    # This section deals with decoding the incoming character stream.
    # Decoding means here, that the stream is first seperated into `tokens'
    # which are then mapped to a `meaning' provided as operations by the
    # `Screen' class.

    def onRcvChar(self, c):
        """process application unicode input to terminal"""
        raise NotImplementedError()

    def setMode(self):
        raise NotImplementedError()
    
    def resetMode(self):
        raise NotImplementedError()
    
    def sendString(self, string):
        self.myemit("sndBlock", (string,))
           
    # Keyboard handling
    
    def onKeyPress(self, ev):
        """char received from the gui"""
        raise NotImplementedError()
            
    def onRcvBlock(self, block):
        self.myemit("notifySessionState", (NOTIFYACTIVITY,))
        self._bulkStart()
        self._bulk_in_cnt += 1
        for c in block:
            result = self._decoder.toUnicode(c , 1)
            for char in result:
                self.onRcvChar(char.at(0).unicode())
            if c == '\n':
                self._bulkNewLine()
        self._bulkEnd()
        
    def onSelectionBegin(self, x, y):
        if self._connected:
            self._scr.setSelBeginXY(x, y)
            self._showBulk()
        
    def onSelectionExtend(self, x, y):
        if self._connected:
            self._scr.setSelExtendXY(x, y)
            self._showBulk()
        
    def setSelection(self, preserve_line_break):
        if self._connected:
            text = self._scr.getSelText(preserve_line_break)
            if text is not None:
                self._gui.setSelection(text)
            
    def isBusySelecting(self, busy):
        if self._connected:
            self._scr.busy_selecting = busy
        
    def testIsSelected(self, x, y, ref):
        if self._connected:
            ref[0] = self._scr.testIsSelected(x, y)
    
    def clearSelection(self):
        if self._connected:
            self._scr.clearSelection()
            self._showBulk()
    
    def setConnect(self, c):
        self._connected = c
        if self._connected:
            self.onImageSizeChange(self._gui.lines, self._gui.columns)
            self._showBulk()
        else:
            self._scr.clearSelection()
            
    def onImageSizeChange(self, lines, columns):
        """Triggered by image size change of the TEWidget `gui'.

        This event is simply propagated to the attached screens
        and to the related serial line.
        """
        if not self._connected:
            return
        #print 'emulation.onImageSizeChange', lines, columns
        self._screen[0].resizeImage(lines, columns)
        self._screen[1].resizeImage(lines, columns)
        self._showBulk()
        # Propagate event to serial line
        self.myemit("imageSizeChanged", (lines, columns))
    
    def onHistoryCursorChange(self, cursor):
        if self._connected:
            self._scr.hist_cursor = cursor
            self._showBulk()
        
    def _setCodec(self, c):
        """coded number, 0=locale, 1=utf8"""
        if c:
            self._codec = qt.QTextCodec.codecForName("utf8")
        else:
            self._codec = qt.QTextCodec.codecForLocale()
        self._decoder = self._codec.makeDecoder()
        
    def _setColumns(self, columns):
        # FIXME This goes strange ways
        # Can we put this straight or explain it at least?
        # XXX moreover no one is connected to this signal...
        self.myemit("changeColumns", (columns,))
        
    def _bulkNewLine(self):
        self._bulk_nl_cnt += 1
        self._bulk_in_cnt = 0  # Reset bulk counter since 'nl' rule applies
        
    def _showBulk(self):
        self._bulk_nl_cnt = 0
        self._bulk_in_cnt = 0
        if self._connected:
            image, wrapped = self._scr.getCookedImage() # Get the image
            self._gui.setImage(image, self._scr.lines, self._scr.columns) #  Actual refresh
            self._gui.setCursorPos(self._scr.getCursorX(), self._scr.getCursorY())
            # FIXME: Check that we do not trigger other draw event here
            self._gui.setLineWrapped(wrapped)
            self._gui.setScroll(self._scr.hist_cursor, self._scr.getHistLines())
            
    def _bulkStart(self):
        if self._bulk_timer.isActive():
            self._bulk_timer.stop()
            
    def _bulkEnd(self):
        if self._bulk_nl_cnt > self._gui.lines or self._bulk_in_cnt > 20:
            self._showBulk()
        else:
            self._bulk_timer.start(BULK_TIMEOUT, True)