Ticket #2157: conio.py

File conio.py, 9.8 KB (added by synapsis, 8 years ago)

console support for Twisted, version 2

Line 
1# -*- test-case-name: twisted.test.test_conio -*-
2"""This module implements POSIX replacements for stdin/stdout/stderr
3that support asyncronous read/write to a Windows console.
4
5Some details about Windows console:
6- a process can have attached only one console
7- there can be only one input buffer
8- there can be more then one output buffer
9
10Moreover this module tries to offer an higher level and convenient
11interface for termios commands.
12"""
13
14import errno
15import pywintypes
16import win32api
17import win32console
18
19
20
21_ENABLE_NORMAL_MODE = win32console.ENABLE_ECHO_INPUT | win32console.ENABLE_LINE_INPUT
22_ENABLE_WINDOW_INPUT = win32console.ENABLE_WINDOW_INPUT
23
24
25class ConIn(object):
26    """I implement a file like object that supports asyncronous reading
27    from a console.
28
29    This class should be considered a singleton, don't instantiate new
30    objects and instead use the global stdin object.
31    """
32   
33    def __init__(self, handle):
34        # handle should be std handle for STD_INPUT_HANDLE
35        self.handle = handle
36
37        # The code page in use
38        # I assume that this does not change
39        self.cp = "cp%d" % win32console.GetConsoleCP()
40
41        # The temporary (accumulation) buffer used to store the data as
42        # it arrives from the console
43        self._buf = []
44       
45        # The buffer used to store data ready to be read
46        self.buffer = ''
47
48        # Enable the receiving of input records when the console
49        # window (buffer) is changed
50        defaultMode = handle.GetConsoleMode()
51        handle.SetConsoleMode(defaultMode | _ENABLE_WINDOW_INPUT)
52
53        # The callback to be called upon the receiving of a windows
54        # change record
55        self._windowChangeCallback = None
56       
57        # To optimize the code we use different functions for normal
58        # and raw mode
59        self.read = self._read
60        self.readline = self._readline
61
62    #
63    # termios interface
64    #
65    def enableRawMode(self, enabled=True):
66        """Enable raw mode.
67
68        XXX check me
69        """
70
71        # Flush buffer
72        self._buf = []
73        self.buffer = ''
74
75        # Flush the console buffer, too
76        self.handle.FlushConsoleInputBuffer()
77
78        mode = self.handle.GetConsoleMode()
79
80        if enabled:
81            self.read = self._read_raw
82            self.readline = self._readline_raw
83
84            # Set mode on the console, too
85            # XXX check me (this seems not to work)
86            self.handle.SetConsoleMode(mode & ~_ENABLE_NORMAL_MODE)
87        else:
88            self.read = self._read
89            self.readline = self._readline
90
91            # Set mode on the console, too
92            self.handle.SetConsoleMode(mode | _ENABLE_NORMAL_MODE)
93
94    def setWindowChangeCallback(self, callback):
95        """callback is called when the console window buffer is
96        changed.
97
98        Note: WINDOW_BUFFER_SIZE_EVENT is only raised when changing
99              the window *buffer* size from the console menu
100        """
101       
102        self._windowChangeCallback = callback
103
104           
105    #
106    # File object interface
107    #
108    def close(self):
109        win32api.CloseHandle(self.handle)
110
111    def flush(self):
112        # Flush both internal buffer and system console buffer
113        self.buffer = ''
114        self._buf = []
115
116        self.handle.FlushConsoleInputBuffer() 
117
118    def fileno(self):
119        return self.handle
120
121    def isatty(self): 
122        return True
123
124    def next(self):
125        raise NotImplementedError("Not yet implemented")
126
127    def _read(self, size=None):
128        """Read size bytes from the console.
129        An exception is raised when the operation would block.
130
131        XXX Just return the empty string instead of raising an exception?
132        """
133
134        # This can fail if stdout has been closed
135        info = stdout.handle.GetConsoleScreenBufferInfo()
136        rowSize = info["MaximumWindowSize"].X
137
138        # Initialize the current cursor position
139        if not self._buf:
140            self.pos = info["CursorPosition"]
141           
142        while 1:
143            n = self.handle.GetNumberOfConsoleInputEvents()
144            if n == 0:
145                break
146               
147            records = self.handle.ReadConsoleInput(n)
148               
149            # Process input
150            for record in records:
151                if record.EventType == win32console.WINDOW_BUFFER_SIZE_EVENT:
152                    rowSize = record.Size.X
153                    if self._windowChangeCallback:
154                        self._windowChangeCallback()
155                if record.EventType != win32console.KEY_EVENT \
156                        or not record.KeyDown:
157                    continue
158
159                char = record.Char
160                n = record.RepeatCount
161                if char == '\b':
162                    pos = stdout.handle.GetConsoleScreenBufferInfo()["CursorPosition"]
163                   
164                    # Move the cursor
165                    x = pos.X - n
166                    if x >= 0:
167                        pos.X = x
168                    # XXX assuming |x| < rowSize (I'm lazy)
169                    elif pos.Y > self.pos.Y:
170                        pos.X = rowSize - 1
171                        pos.Y -= 1
172
173                    stdout.handle.SetConsoleCursorPosition(pos)
174                    stdout.handle.WriteConsoleOutputCharacter(' ' * n, pos)
175                   
176                    # Delete the characters from accumulation buffer
177                    self._buf = self._buf[:-n]
178                    continue
179                elif char == '\0':
180                    vCode = record.VirtualKeyCode
181                    # XXX TODO handle keyboard navigation
182                    continue
183                elif char == '\r':
184                    char = '\n' * n
185
186                    self._buf.append(char)
187                    stdout.handle.WriteConsole(char) # do echo
188                   
189                    # We have some data ready to be read
190                    self.buffer = ''.join(self._buf)
191                    self._buf = []
192                    self.pos = info["CursorPosition"]
193
194                    if size is None:
195                        size = len(self.buffer)
196                       
197                    data = self.buffer[:size]
198                    self.buffer = self.buffer[size:]
199                    return data
200
201                char = char * n
202                data = char.encode(self.cp)
203                stdout.handle.WriteConsole(data) # do echo
204               
205                self._buf.append(data)
206
207        if self.buffer:
208            data = self.buffer[:size]
209            self.buffer = self.buffer[size:]
210            return data
211        else:
212            raise IOError(errno.EAGAIN)
213
214    def _read_raw(self, size=None):
215        """Read size bytes from the console, in raw mode.
216
217        XXX check me.
218        """
219
220        while 1: # XXX is this loop really needed?
221            n = self.handle.GetNumberOfConsoleInputEvents()
222            if n == 0:
223                break
224               
225            records = self.handle.ReadConsoleInput(n)
226               
227            # Process input
228            for record in records:
229                if record.EventType == win32console.WINDOW_BUFFER_SIZE_EVENT:
230                    if self._windowChangeCallback:
231                        self._windowChangeCallback()
232                if record.EventType != win32console.KEY_EVENT \
233                        or not record.KeyDown:
234                    continue
235
236                char = record.Char
237                n = record.RepeatCount
238                if char == '\0':
239                    vCode = record.VirtualKeyCode
240                    # XXX TODO handle keyboard navigation
241                    continue
242                elif char == '\r':
243                    char = '\n' * n
244
245                char = char * n
246                data = char.encode(self.cp)
247               
248                self._buf.append(data)
249
250
251        buffer = ''.join(self._buf)
252        if buffer:
253            if size is None:
254                size = len(buffer)
255
256            data = buffer[:size]
257            # Keep the remaining data in the accumulation buffer
258            self._buf = [buffer[size:]]
259            return data
260        else:
261            return ''
262
263    def _readline(self, size=None):
264        # XXX check me
265        return self._read(size)
266
267    def _readline_raw(self, size=None):
268        raise NotImplementedError("Not yet implemented")
269
270   
271   
272class ConOut(object):
273    """I implement a file like object that supports asyncronous writing
274    to a console.
275
276    This class should be considered private, don't instantiate new
277    objects and instead use the global stdout and stderr objects.
278
279    Note that there is no option to make WriteConsole non blocking,
280    but is seems that this function does not block at all.
281    When a blocking operation like text selection is in action, the
282    process is halted.
283    """
284
285    def __init__(self, handle):
286        # handle should be std handle for STD_OUTOUT_HANDLE or STD_ERROR_HANDLE
287        self.handle = handle
288
289       
290    #
291    # File object interface
292    #
293    def close(self):
294        win32api.CloseHandle(self.handle)
295
296    def flush(self):
297        # There is no buffering
298        pass
299
300    def fileno(self):
301        return self.handle
302
303    def isatty(self): 
304        return True
305
306    def write(self, s):
307        """Write a string to the console.
308        """
309
310        return self.handle.WriteConsole(s)
311
312    def writelines(self, seq):
313        """Write a sequence of strings to the console.
314        """
315       
316        s = ''.join(seq)
317        return self.handle.WriteConsole(s)
318
319
320
321# The public interface of this module
322# XXX TODO replace sys.stdin, sys.stdout and sys.stderr?
323stdin = ConIn(win32console.GetStdHandle(win32console.STD_INPUT_HANDLE))
324stdout = ConOut(win32console.GetStdHandle(win32console.STD_OUTPUT_HANDLE))
325stderr = ConOut(win32console.GetStdHandle(win32console.STD_ERROR_HANDLE))
326
327
328__all__ = [stdin, stdout, stderr]