PageRenderTime 174ms CodeModel.GetById 77ms app.highlight 84ms RepoModel.GetById 1ms app.codeStats 1ms

/terminal/terminal/terminal.py

http://editra-plugins.googlecode.com/
Python | 957 lines | 895 code | 21 blank | 41 comment | 24 complexity | 46b7c4a1f2553b442602dc0d66a31996 MD5 | raw file
  1# -*- coding: utf-8 -*-
  2###############################################################################
  3# Name: terminal.py                                                           #
  4# Purpose: Provides a terminal widget that can be embedded in any wxWidgets   #
  5#          window or run alone as its own shell window.                       #
  6# Author: Cody Precord <cprecord@editra.org>                                  #
  7# Copyright: (c) 2007 Cody Precord <staff@editra.org>                         #
  8# Licence: wxWindows Licence                                                  #
  9###############################################################################
 10
 11"""
 12This script started as an adaption of the vim plugin 'vimsh' by brian m sturk 
 13but has since been partially reimplemented to take advantage of newer libraries
 14and the features that wx offers.
 15
 16It currently should run on any unix like operating system that supports:
 17    - wxPython
 18    - Psuedo TTY's
 19
 20On windows things are basically working now but support is not as good as it is
 21on unix like operating systems.
 22
 23"""
 24
 25__author__ = "Cody Precord <cprecord@editra.org>"
 26__svnid__ = "$Id: terminal.py 669 2008-12-05 02:18:30Z CodyPrecord $"
 27__revision__ = "$Revision: 669 $"
 28
 29#-----------------------------------------------------------------------------#
 30# Imports
 31
 32import sys
 33import os
 34import signal
 35#import threading
 36import re
 37import time
 38import wx
 39import wx.stc
 40
 41# On windows need to use pipes as pty's are not available
 42try:
 43    if sys.platform == 'win32':
 44        import popen2
 45        import stat
 46        import msvcrt
 47        import ctypes
 48        USE_PTY = False
 49    else:
 50        import pty
 51        import tty
 52        import select
 53        USE_PTY = True
 54except ImportError, msg:
 55    print "[terminal] Error importing required libs: %s" % str(msg)
 56
 57#-----------------------------------------------------------------------------#
 58# Globals
 59
 60#---- Variables ----#
 61DEBUG = True
 62MAX_HIST = 50       # Max command history to save
 63
 64if sys.platform == 'win32':
 65    SHELL = 'cmd.exe'
 66else:
 67    if 'SHELL' in os.environ:
 68        SHELL = os.environ['SHELL']
 69    else:
 70        SHELL = '/bin/sh'
 71#---- End Variables ----#
 72
 73#---- Callables ----#
 74_ = wx.GetTranslation
 75
 76#---- End Callables ----#
 77
 78#---- ANSI color code support ----#
 79# ANSI_FORE_BLACK   = 1
 80# ANSI_FORE_RED     = 2
 81# ANSI_FORE_GREEN   = 3
 82# ANSI_FORE_YELLOW  = 4
 83# ANSI_FORE_BLUE    = 5
 84# ANSI_FORE_MAGENTA = 6
 85# ANSI_FORE_CYAN    = 7
 86# ANSI_FORE_WHITE   = 8
 87
 88# ANSI_BACK_BLACK
 89# ANSI_BACK_RED
 90# ANSI_BACK_GREEN
 91# ANSI_BACK_YELLOW
 92# ANSI_BACK_BLUE
 93# ANSI_BACK_MAGENTA
 94# ANSI_BACK_CYAN
 95# ANSI_BACK_WHITE
 96
 97ANSI = {
 98        ## Forground colours ##
 99        '' : (1, '#000000'),
100        '' : (2, '#FF0000'),
101        '' : (3, '#00FF00'),
102        '' : (4, '#FFFF00'),  # Yellow
103        '' : (5, '#0000FF'),
104        '' : (6, '#FF00FF'),
105        '' : (7, '#00FFFF'),
106        '' : (8, '#FFFFFF'),
107        #'' : default
108
109        ## Background colour ##
110        '' : (011, '#000000'),  # Black
111        '' : (012, '#FF0000'),  # Red
112        '' : (013, '#00FF00'),  # Green
113        '' : (014, '#FFFF00'),  # Yellow
114        '' : (015, '#0000FF'),  # Blue
115        '' : (016, '#FF00FF'),  # Magenta
116        '' : (017, '#00FFFF'),  # Cyan
117        '' : (020, '#FFFFFF'),  # White
118        #'' : default
119        }
120
121RE_COLOUR_START = re.compile('\[[34][0-9]m')
122RE_COLOUR_FORE = re.compile('\[3[0-9]m')
123RE_COLOUR_BLOCK = re.compile('\[[34][0-9]m*.*?\[m')
124RE_COLOUR_END = ''
125RE_CLEAR_ESC = re.compile('\[[0-9]+m')
126#---- End ANSI Colour Support ----#
127
128#---- Font Settings ----#
129# TODO make configurable from interface
130FONT = None
131FONT_FACE = None
132FONT_SIZE = None
133#----- End Font Settings ----#
134
135#-----------------------------------------------------------------------------#
136
137class Xterm(wx.stc.StyledTextCtrl):
138    """Creates a graphical terminal that works like the system shell
139    that it is running on (bash, command, ect...).
140
141    """
142    def __init__(self, parent, ID=wx.ID_ANY, pos=wx.DefaultPosition, 
143                 size=wx.DefaultSize, style=0):
144        wx.stc.StyledTextCtrl.__init__(self, parent, ID, pos, size, style)
145
146        # Attributes
147        ##  The lower this number is the more responsive some commands
148        ##  may be ( printing prompt, ls ), but also the quicker others
149        ##  may timeout reading their output ( ping, ftp )
150        self.delay = 0.03
151        self._fpos = 0          # First allowed cursor position
152        self._exited = False    # Is shell still running
153        self._setspecs = [0]
154        self._history = dict(cmds=[''], index=-1, lastexe='')  # Command history
155        self._menu = None
156
157        # Setup
158        self.__Configure()
159        self.__ConfigureStyles()
160        self.__ConfigureKeyCmds()
161        self._SetupPTY()
162
163        #---- Event Handlers ----#
164        # General Events
165        self.Bind(wx.EVT_IDLE, self.OnIdle)
166
167        # Stc events
168        self.Bind(wx.stc.EVT_STC_DO_DROP, self.OnDrop)
169
170        # Key events
171        self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
172        self.Bind(wx.EVT_CHAR, self.OnChar)
173        self.Bind(wx.EVT_KEY_UP, self.OnKeyUp)
174
175        # Menu Events
176        self.Bind(wx.EVT_MENU, lambda evt: self.Cut(), id=wx.ID_CUT)
177        self.Bind(wx.EVT_MENU, lambda evt: self.Copy(), id=wx.ID_COPY)
178        self.Bind(wx.EVT_MENU, lambda evt: self.Paste(), id=wx.ID_PASTE)
179        self.Bind(wx.EVT_MENU, lambda evt: self.SelectAll(), id=wx.ID_SELECTALL)
180
181        # Mouse Events
182        self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
183#         self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
184        self.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu)
185        self.Bind(wx.EVT_UPDATE_UI, self.OnUpdateUI)
186
187    def __del__(self):
188        DebugLog("[terminal][info] Terminal instance is being deleted")
189        self._CleanUp()
190
191    def __ConfigureKeyCmds(self):
192        """Clear the builtin keybindings that we dont want"""
193        self.CmdKeyClear(ord('U'), wx.stc.STC_SCMOD_CTRL)
194        self.CmdKeyClear(ord('Z'), wx.stc.STC_SCMOD_CTRL)
195        self.CmdKeyClear(wx.WXK_BACK, wx.stc.STC_SCMOD_CTRL | \
196                                      wx.stc.STC_SCMOD_SHIFT)
197        self.CmdKeyClear(wx.WXK_DELETE, wx.stc.STC_SCMOD_CTRL | \
198                                        wx.stc.STC_SCMOD_SHIFT)
199        self.CmdKeyClear(ord('['), wx.stc.STC_SCMOD_CTRL)
200        self.CmdKeyClear(ord(']'), wx.stc.STC_SCMOD_CTRL)
201        self.CmdKeyClear(ord('\\'), wx.stc.STC_SCMOD_CTRL)
202        self.CmdKeyClear(ord('/'), wx.stc.STC_SCMOD_CTRL)
203        self.CmdKeyClear(ord('L'), wx.stc.STC_SCMOD_CTRL)
204        self.CmdKeyClear(ord('D'), wx.stc.STC_SCMOD_CTRL)
205        self.CmdKeyClear(ord('Y'), wx.stc.STC_SCMOD_CTRL)
206        self.CmdKeyClear(ord('T'), wx.stc.STC_SCMOD_CTRL)
207        self.CmdKeyClear(wx.WXK_TAB, wx.stc.STC_SCMOD_NORM)
208
209    def __Configure(self):
210        """Configure the base settings of the control"""
211        if wx.Platform == '__WXMSW__':
212            self.SetEOLMode(wx.stc.STC_EOL_CRLF)
213        else:
214            self.SetEOLMode(wx.stc.STC_EOL_LF)
215        self.SetViewWhiteSpace(False)
216        self.SetTabWidth(0)
217        self.SetUseTabs(False)
218        self.SetWrapMode(True)
219        self.SetEndAtLastLine(False)
220        self.SetVisiblePolicy(1, wx.stc.STC_VISIBLE_STRICT)
221
222    def __ConfigureStyles(self):
223        """Configure the text coloring of the terminal"""
224        # Clear Styles
225        self.StyleResetDefault()
226        self.StyleClearAll()
227
228        # Set margins
229        self.SetMargins(4, 4)
230        self.SetMarginWidth(wx.stc.STC_MARGIN_NUMBER, 0)
231
232        # Caret styles
233        self.SetCaretWidth(4)
234        self.SetCaretForeground(wx.NamedColor("white"))
235
236        # Configure text styles
237        # TODO make this configurable
238        fore = "#FFFFFF" #"#000000"
239        back = "#000000" #"#DBE0C4"
240        global FONT
241        global FONT_SIZE
242        global FONT_FACE
243        FONT = wx.Font(11, wx.FONTFAMILY_MODERN, wx.FONTSTYLE_NORMAL, 
244                               wx.FONTWEIGHT_NORMAL)
245        FONT_FACE = FONT.GetFaceName()
246        FONT_SIZE = FONT.GetPointSize()
247        self.StyleSetSpec(0, "face:%s,size:%d,fore:%s,back:%s,bold" % \
248                             (FONT_FACE, FONT_SIZE, fore, back))
249        self.StyleSetSpec(wx.stc.STC_STYLE_DEFAULT, \
250                          "face:%s,size:%d,fore:%s,back:%s,bold" % \
251                          (FONT_FACE, FONT_SIZE, fore, back))
252        self.StyleSetSpec(wx.stc.STC_STYLE_CONTROLCHAR, \
253                          "face:%s,size:%d,fore:%s,back:%s" % \
254                          (FONT_FACE, FONT_SIZE, fore, back))
255        self.Colourise(0, -1)
256
257    #---- Protected Members ----#
258    def _ApplyStyles(self, data):
259        """Apply style bytes to regions of text that require them, starting
260        at self._fpos and using the postional data as on offset from that point.
261
262        @param data: list of tuples [ (style_start, colour, style_end) ]
263
264        """
265        spec = 0
266        for pos in data:
267            if len(pos[1]) > 1:
268                spec = ANSI[pos[1][1]][0] | ANSI[pos[1][0]][0]
269            elif len(pos[1]):
270                spec = ANSI[pos[1][0]][0]
271            else:
272                pass
273
274            if spec not in self._setspecs:
275                DebugLog("[terminal][styles] Setting Spec: %d" % spec)
276                self._setspecs.append(spec)
277                if len(pos[1]) > 1:
278                    if RE_COLOUR_FORE.match(pos[1][0]):
279                        fore = ANSI[pos[1][0]][1]
280                        back = ANSI[pos[1][1]][1]
281                    else:
282                        fore = ANSI[pos[1][1]][1]
283                        back = ANSI[pos[1][0]][1]
284
285                    self.StyleSetSpec(spec, 
286                                      "fore:%s,back:%s,face:%s,size:%d" % \
287                                      (fore, back, FONT_FACE, FONT_SIZE))
288                else:
289                    self.StyleSetSpec(spec, 
290                                      "fore:%s,back:#000000,face:%s,size:%d" % \
291                                     (ANSI[pos[1][0]][1], FONT_FACE, FONT_SIZE))
292
293            # Adjust styling start position if necessary
294            spos = self._fpos + pos[0]
295            if unichr(self.GetCharAt(spos)).isspace():
296                spos += 1
297
298            # Set style bytes for a given region
299            self.StartStyling(spos, 0xff)
300            self.SetStyling(pos[2] - pos[0] + 1, spec)
301
302    def _CheckAfterExe(self):
303        """Check std out for anything left after an execution"""
304        DebugLog("[terminal][info] Checking after cmd execution")
305        self.Read()
306        self.CheckForPassword()
307
308    def _CleanUp(self):
309        """Cleanup open file descriptors"""
310        if self._exited:
311            DebugLog("[terminal][exit] Already exited")
312            return
313
314        try:
315            DebugLog("[terminal][exit] Closing FD and killing process")
316            if not USE_PTY:
317                os.close(self.ind)
318                os.close(self.outd)
319
320            os.close(self.errd)       ##  all the same if pty
321            if USE_PTY:
322                os.kill(self.pid, signal.SIGKILL)
323                time.sleep(self.delay) # give some time for the process to die
324
325        except Exception, msg:
326            DebugLog("[terminal][err] %s" % str(msg))
327
328        DebugLog("[terminal][cleanup] Finished Cleanup")
329
330    def _EndRead(self, any_lines_read):
331        """Finish up after reading"""
332        # Mark earliest valid cursor position in buffer
333        self._fpos = self.GetCurrentPos()
334        if (not USE_PTY and any_lines_read):
335            self.LineDelete()
336            DebugLog("[terminal][endread] Deleted trailing line")
337            self._fpos = self.GetCurrentPos()
338
339        DebugLog("[terminal][info] Set valid cursor to > %d" % self._fpos)
340        self.PrintPrompt()
341        self.EnsureCaretVisible()
342        self.EnsureVisibleEnforcePolicy(self.GetCurrentLine())
343
344    def _HandleExit(self, cmd):
345        """Handle closing the shell connection"""
346        ##  Exit was typed, could be the spawned shell, or a subprocess like
347        ##  telnet/ssh/etc.
348        DebugLog("[terminal][exit] Exiting process")
349        if not self._exited:
350            try:
351                DebugLog("[terminal][exit] Shell still around, trying to close")
352                self.Write(cmd + os.linesep)
353                self._CheckAfterExe()
354            except Exception, msg:            
355                DebugLog("[terminal][err] Exception on exit: %s" % str(msg))
356
357                ## shell exited, self._exited may not have been set yet in
358                ## sigchld_handler.
359                DebugLog("[terminal][exit] Shell Exited is: " + str(self._exited))
360                self.ExitShell()
361
362    def _ProcessRead(self, lines):
363        """Process the raw lines from stdout"""
364        DebugLog("[terminal][info] Processing Read...")
365        lines_to_print = lines.split(os.linesep)
366
367        #  Filter out invalid blank lines from begining/end input
368        if sys.platform == 'win32':
369            m = re.search(re.escape(self._history['lastexe'].strip()), 
370                                    lines_to_print[0])
371            if m != None or lines_to_print[0] == "":
372                DebugLog('[terminal][info] Win32, removing leading blank line')
373                lines_to_print = lines_to_print[ 1: ]
374
375        # Check for extra blank line at end
376        num_lines = len(lines_to_print)
377        if num_lines > 1:
378            last_line = lines_to_print[num_lines - 1].strip()
379
380            if last_line == "":
381                lines_to_print = lines_to_print[ :-1 ]
382
383        # Look on StdErr for any error output
384        errors = self.CheckStdErr()
385        if errors:
386            DebugLog("[terminal][err] Process Read stderr --> " + '\n'.join(errors))
387            lines_to_print = errors + lines_to_print
388
389        return lines_to_print
390
391    def _SetupPTY(self):
392        """Setup the connection to the real terminal"""
393        if USE_PTY:
394            self.master, pty_name = pty.openpty()
395            DebugLog("[terminal][info] Slave Pty name: " + str(pty_name))
396
397            self.pid, self.fd = pty.fork()
398            print "GRRR", self.fd, self.pid, "WTF"
399
400            self.outd = self.fd
401            self.ind  = self.fd
402            self.errd = self.fd
403
404#            signal.signal(signal.SIGCHLD, self._SigChildHandler)
405            if self.pid == 0:
406                attrs = tty.tcgetattr(1)
407
408                attrs[6][tty.VMIN]  = 1
409                attrs[6][tty.VTIME] = 0
410                attrs[0] = attrs[0] | tty.BRKINT
411                attrs[0] = attrs[0] & tty.IGNBRK
412                attrs[3] = attrs[3] & ~tty.ICANON & ~tty.ECHO
413
414                tty.tcsetattr(1, tty.TCSANOW, attrs)
415#                 cwd = os.path.curdir
416                os.chdir(wx.GetHomeDir())
417#                 pty.spawn([SHELL, "-l"])
418                os.execv(SHELL, [SHELL, '-l'])
419#                 os.chdir(cwd)
420
421            else:
422                try:
423                    attrs = tty.tcgetattr(self.fd)
424                    termios_keys = attrs[6]
425                except:
426                    DebugLog('[terminal][err] tcgetattr failed')
427                    return
428
429                #  Get *real* key-sequence for standard input keys, i.e. EOF
430                self.eof_key   = termios_keys[tty.VEOF]
431                self.eol_key   = termios_keys[tty.VEOL]
432                self.erase_key = termios_keys[tty.VERASE]
433                self.intr_key  = termios_keys[tty.VINTR]
434                self.kill_key  = termios_keys[tty.VKILL]
435                self.susp_key  = termios_keys[tty.VSUSP]
436        else:
437            ##  Use pipes on Win32. not as reliable/nice but 
438            ##  works with limitations.
439            self.delay = 0.15
440
441            try:
442                import win32pipe
443                DebugLog('[terminal][info] using windows extensions')
444                self.stdin, self.stdout, self.stderr = win32pipe.popen3(SHELL)
445            except ImportError:
446                DebugLog('[terminal][info] not using windows extensions')
447                self.stdout, self.stdin, self.stderr = popen2.popen3(SHELL, -1, 'b')
448
449            self.outd = self.stdout.fileno()
450            self.ind  = self.stdin.fileno()
451            self.errd = self.stderr.fileno()
452
453            self.intr_key = ''
454            self.eof_key  = ''
455
456    def _SigChildHandler(self, sig, frame):
457        """Child process signal handler"""
458        DebugLog("[terminal][info] caught SIGCHLD")
459        self._WaitPid()
460
461    def _WaitPid(self):
462        """Mark the original shell process as having gone away if it
463        has exited.
464
465        """
466        if os.waitpid(self.pid, os.WNOHANG)[0]:
467            self._exited = True
468            DebugLog("[terminal][waitpid] Shell process has exited")
469        else:
470            DebugLog("[terminal][waitpid] Shell process hasn't exited")
471
472    #---- End Protected Members ----#
473
474    #---- Public Members ----#
475    def AddCmdHistory(self, cmd):
476        """Add a command to the history index so it can be quickly
477        recalled by using the up/down keys.
478
479        """
480        if cmd.isspace():
481            return
482
483        if len(self._history['cmds']) > MAX_HIST:
484            self._history['cmds'].pop()
485        self._history['cmds'].insert(0, cmd)
486        self._history['index'] = -1
487
488    def CanCopy(self):
489        """Check if copy is possible"""
490        return self.GetSelectionStart() != self.GetSelectionEnd()
491
492    def CanCut(self):
493        """Check if selection is valid to allow for cutting"""
494        s_start = self.GetSelectionStart()
495        s_end = self.GetSelectionEnd()
496        return s_start != s_end and s_start >= self._fpos and s_end >= self._fpos
497
498    def CheckForPassword(self):
499        """Check if the shell is waiting for a password or not"""
500        prev_line = self.GetLine(self.GetCurrentLine() - 1)
501        for regex in ['^\s*Password:', 'password:', 'Password required']:
502            if re.search(regex, prev_line):
503                try:
504                    print "FIX ME: CheckForPassword"
505                except KeyboardInterrupt:
506                    return
507
508                # send the password to the 
509#                 self.ExecuteCmd([password])
510
511    def CheckStdErr(self):
512        """Check for errors in the shell"""
513        errors  = ''
514        if sys.platform == 'win32':
515            err_txt  = self.PipeRead(self.errd, 0)
516            errors   = err_txt.split(os.linesep)
517
518            num_lines = len(errors)
519            last_line = errors[num_lines - 1].strip()
520
521            if last_line == "":
522                errors = errors[:-1]
523
524        return errors
525
526    def ClearScreen(self):
527        """Clear the screen so that all commands are scrolled out of
528        view and a new prompt is shown on the top of the screen.
529
530        """
531        self.AppendText(os.linesep * 5)
532        self.Write(os.linesep)
533        self._CheckAfterExe()
534        self.Freeze()
535        wx.PostEvent(self, wx.ScrollEvent(wx.wxEVT_SCROLLWIN_PAGEDOWN, 
536                                          self.GetId(), orient=wx.VERTICAL))
537        wx.CallAfter(self.Thaw)
538
539    def ExecuteCmd(self, cmd=None, null=True):
540        """Run the command entered in the buffer
541        @keyword cmd: send a specified command
542        @keyword null: should the command be null terminated
543
544        """
545        DebugLog("[terminal][exec] Running command: %s" % str(cmd))
546
547        try:
548            # Get text from prompt to eol when no command is given
549            if cmd is None:
550                cmd = self.GetTextRange(self._fpos, self.GetLength())
551
552            # Move output position past input command
553            self._fpos = self.GetLength()
554
555            # Process command
556            if len(cmd) and cmd[-1] != '\t':
557                cmd = cmd.strip()
558
559            if re.search(r'^\s*\bclear\b', cmd) or re.search(r'^\s*\bcls\b', cmd):
560                DebugLog('[terminal][exec] Clear Screen')
561                self.ClearScreen()
562            elif re.search(r'^\s*\exit\b', cmd):
563                DebugLog('[terminal][exec] Exit terminal session')
564                self._HandleExit(cmd)
565                self.SetCaretForeground(wx.BLACK)
566            else:
567                if null:
568                    self.Write(cmd + os.linesep)
569                else:
570                    self.Write(cmd)
571
572                self._history['lastexe'] = cmd
573                self._CheckAfterExe()
574
575            if len(cmd) and cmd != self._history['cmds'][0]:
576                self.AddCmdHistory(cmd)
577            self._history['lastexe'] = cmd
578
579        except KeyboardInterrupt:
580            pass
581
582    def ExitShell(self):
583        """Cause the shell to exit"""
584        if not self._exited:
585            self._CleanUp()
586
587        self.PrintLines(["[process complete]" + os.linesep,])
588        self.SetReadOnly(True)
589
590    def GetNextCommand(self):
591        """Get the next command from history based on the current
592        position in the history list. If the list is already at the
593        begining then an empty command will be returned.
594
595        @postcondition: current history postion is decremented towards 0
596        @return: string
597
598        """
599        if self._history['index'] > -1:
600            self._history['index'] -= 1
601
602        index = self._history['index']
603        if index == -1:
604            return ''
605        else:
606            return self._history['cmds'][index]
607
608    def GetPrevCommand(self):
609        """Get the previous command from history based on the current
610        position in the history list. If the list is already at the
611        end then the oldest command will be returned.
612
613        @postcondition: current history postion is decremented towards 0
614        @return: string
615
616        """
617        if self._history['index'] < len(self._history['cmds']) - 1\
618           and self._history['index'] < MAX_HIST:
619            self._history['index'] += 1
620
621        index = self._history['index']
622        return self._history['cmds'][index]
623
624    def NewPrompt(self):
625        """Put a new prompt on the screen and make all text from end of
626        prompt to left read only.
627
628        """
629        self.ExecuteCmd("")
630
631    def OnContextMenu(self, evt):
632        """Display the context menu"""
633        if self._menu is None:
634            self._menu = GetContextMenu()
635        print "HELLO", self._menu
636        self.PopupMenu(self._menu)
637
638    def OnDrop(self, evt):
639        """Handle drop events"""
640        if evt.GetPosition() < self._fpos:
641            evt.SetDragResult(wx.DragCancel)
642
643    def OnIdle(self, evt):
644        """While idle check for more output"""
645        if not self._exited:
646            self.Read()
647
648    def OnKeyDown(self, evt):
649        """Handle key down events"""
650        if self._exited:
651            return
652
653        key = evt.GetKeyCode()
654        if key == wx.WXK_RETURN:
655            self.CmdKeyExecute(wx.stc.STC_CMD_NEWLINE)
656            self.ExecuteCmd()
657        elif key == wx.WXK_TAB:
658            # TODO Tab Completion
659#             self.ExecuteCmd(self.GetTextRange(self._fpos, 
660#                              self.GetCurrentPos()) + '\t', 0)
661            pass
662        elif key in [wx.WXK_UP, wx.WXK_NUMPAD_UP]:
663            # Cycle through previous command history
664            cmd = self.GetPrevCommand()
665            self.SetCommand(cmd)
666        elif key in [wx.WXK_DOWN, wx.WXK_NUMPAD_DOWN]:
667            # Cycle towards most recent commands in history
668            cmd = self.GetNextCommand()
669            self.SetCommand(cmd)
670        elif key in [wx.WXK_LEFT, wx.WXK_NUMPAD_LEFT, 
671                     wx.WXK_BACK, wx.WXK_DELETE]:
672            if self.GetCurrentPos() > self._fpos:
673                evt.Skip()
674        elif key == wx.WXK_HOME:
675            # Go Prompt Start
676            self.GotoPos(self._fpos)
677        else:
678            evt.Skip()
679
680    def OnChar(self, evt):
681        """Handle character enter events"""
682        # Dont allow editing of earlier portion of buffer
683        if self.GetCurrentPos() < self._fpos:
684            return
685        evt.Skip()
686
687    def OnKeyUp(self, evt):
688        """Handle when the key comes up"""
689        key = evt.GetKeyCode()
690        sel_s, sel_e = self.GetSelection()
691        if evt.ControlDown() and key == ord('C') and sel_s == sel_e:
692            self.ExecuteCmd(self.intr_key, null=False)
693        else:
694            evt.Skip()
695
696    def OnLeftDown(self, evt):
697        """Set selection anchor"""
698        pos = evt.GetPosition()
699        self.SetSelectionStart(self.PositionFromPoint(pos))
700
701    def OnLeftUp(self, evt):
702        """Check click position to ensure caret doesn't 
703        move to invalid position.
704
705        """
706        evt.Skip()
707        pos = evt.GetPosition()
708        sel_s = self.GetSelectionStart()
709        sel_e = self.GetSelectionEnd()
710        if (self._fpos > self.PositionFromPoint(pos)) and (sel_s == sel_e):
711            wx.CallAfter(self.GotoPos, self._fpos)
712
713    def OnUpdateUI(self, evt):
714        """Enable or disable menu events"""
715        e_id = evt.GetId()
716        if e_id == wx.ID_CUT:
717            evt.Enable(self.CanCut())
718        elif e_id == wx.ID_COPY:
719            evt.Enable(self.CanCopy())
720        elif e_id == wx.ID_PASTE:
721            evt.Enable(self.CanPaste())
722        else:
723            evt.Skip()
724
725    def PrintLines(self, lines):
726        """Print lines to the terminal buffer
727        @param lines: list of strings
728
729        """
730        if len(lines) and lines[0].strip() == self._history['lastexe'] .strip():
731            lines.pop(0)
732
733        for line in lines:
734            DebugLog("[terminal][print] Current line is --> %s" % line)
735            m = False
736            if len(line) > 2 and "\r" in line[-2:]:
737                line = line.rstrip()
738                m = True
739
740            # Parse ANSI escape sequences
741            need_style = False
742            if r'' in line:
743                DebugLog('[terminal][print] found ansi escape sequence(s)')
744                # Construct a list of [ (style_start, (colours), style_end) ]
745                # where the start end positions are offsets of the curent _fpos
746                c_items = re.findall(RE_COLOUR_BLOCK, line)
747                tmp = line
748                positions = list()
749                for pat in c_items:
750                    ind = tmp.find(pat)
751                    colors = re.findall(RE_COLOUR_START, pat)
752                    tpat = pat
753                    for color in colors:
754                        tpat = tpat.replace(color, '')
755                    tpat = tpat.replace(RE_COLOUR_END, '')
756                    tmp = tmp.replace(pat, tpat, 1).replace(RE_COLOUR_END, '', 1)
757                    positions.append((ind, colors, (ind + len(tpat) - 1)))
758
759                # Try to remove any remaining escape sequences
760                line = tmp.replace(RE_COLOUR_END, '')
761                line = re.sub(RE_COLOUR_START, '', line)
762                line = re.sub(RE_CLEAR_ESC, '', line)
763                
764                need_style = True
765
766            # Put text in buffer
767            self.AppendText(line)
768
769            # Apply any colouring that is needed
770            if need_style:
771                DebugLog('[terminal][print] applying styles to output string')
772                self._ApplyStyles(positions)
773
774            # Move cursor to end of buffer
775            self._fpos = self.GetLength()
776            self.GotoPos(self._fpos)
777
778            ##  If there's a '\n' or using pipes and it's not the last line
779            if not USE_PTY or m:
780                DebugLog("[terminal][print] Appending new line since ^M or not using pty")
781                self.AppendText(os.linesep)
782
783    def PrintPrompt(self):
784        """Construct a windows prompt and print it to the screen.
785        Has no affect on other platforms as their prompt can be read from
786        the screen.
787
788        """
789        if wx.Platform != '__WXMSW__':
790            return
791        else:
792            cmd = self._history['lastexe']
793            if cmd.lower().startswith('cd '):
794                try:
795                    os.chdir(cmd[2:].strip())
796                except:
797                    pass
798
799            self.AppendText(u"%s>" % os.getcwd())
800            self._fpos = self.GetLength()
801            self.GotoPos(self._fpos)
802            self.EnsureCaretVisible()
803
804    def PipeRead(self, pipe, minimum_to_read):
805        """Read from pipe, used on Windows. This is needed because select
806        can only be used with sockets on Windows and not with any other type
807        of file descriptor.
808        @param pipe: Pipe to read from
809        @param minimum_to_read: minimum bytes to read at one time
810
811        """
812        DebugLog("[terminal][pipe] minimum to read is " + str(minimum_to_read))
813
814        time.sleep(self.delay)
815        data = u''
816
817        # Take a peek to see if any output is available
818        try:
819            handle = msvcrt.get_osfhandle(pipe)
820            avail = ctypes.c_long()
821            ctypes.windll.kernel32.PeekNamedPipe(handle, None, 0, 0,
822                                                 ctypes.byref(avail), None)
823        except IOError, msg:
824            DebugLog("[terminal][err] Pipe read failed: %s" % msg)
825            return data
826
827        count = avail.value
828        DebugLog("[terminal][pipe] PeekNamedPipe is " + str(count))
829
830        # If there is some output start reading it
831        while (count > 0):
832            tmp = os.read(pipe, 32)
833            data += tmp
834
835            if len(tmp) == 0:
836                DebugLog("[terminal][pipe] count %s but nothing read" % str(count))
837                break
838
839            #  Be sure to break the read, if asked to do so,
840            #  after we've read in a line termination.
841            if minimum_to_read != 0 and len(data) > 0 and data[len(data) - 1] == os.linesep:
842                if len(data) >= minimum_to_read:
843                    DebugLog("[terminal][pipe] read minimum and found termination")
844                    break
845                else:
846                    DebugLog("[terminal][pipe] more data to read: count is " + str(count))
847
848            # Check for more output
849            avail = ctypes.c_long()
850            ctypes.windll.kernel32.PeekNamedPipe(handle, None, 0, 0,
851                                             ctypes.byref(avail), None)
852            count = avail.value
853
854        return data
855
856    def Read(self):
857        """Read output from stdin"""
858        if self._exited:
859            return
860
861        num_iterations = 0  #  counter for periodic redraw
862        any_lines_read = 0  #  sentinel for reading anything at all
863
864        lines = ''
865        while 1:
866            if USE_PTY:
867                try:
868                    r = select.select([self.outd], [], [], self.delay)[0]
869                except select.error, msg:
870                    DebugLog("[terminal][err] Select failed: %s" % str(msg))
871                    r = [1, ]
872            else:
873                r = [1, ]  # pipes, unused
874
875            for file_iter in r:
876                if USE_PTY:
877                    tmp = os.read(self.outd, 32)
878                else:
879                    tmp = self.PipeRead(self.outd, 2048)
880
881                lines += tmp
882                if tmp == '':
883                    DebugLog('[terminal][read] No more data on stdout Read')
884                    r = []
885                    break
886
887                any_lines_read  = 1 
888                num_iterations += 1
889
890            if not len(r) and len(lines):
891                DebugLog('[terminal][read] End of Read, starting processing and printing' )
892                lines = self._ProcessRead(lines)
893                self.PrintLines(lines)
894                self._EndRead(any_lines_read)
895                break
896            elif not any_lines_read and not num_iterations:
897                break
898            else:
899                pass
900
901    def SetCommand(self, cmd):
902        """Set the command that is shown at the current prompt
903        @param cmd: command string to put on the prompt
904
905        """
906        self.SetTargetStart(self._fpos)
907        self.SetTargetEnd(self.GetLength())
908        self.ReplaceTarget(cmd)
909        self.GotoPos(self.GetLength())
910
911    def Write(self, cmd):
912        """Write out command to shell process
913        @param cmd: command string to write out to the shell proccess to run
914
915        """
916        DebugLog("[terminal][info] Writting out command: " + cmd)
917        os.write(self.ind, cmd)
918
919#-----------------------------------------------------------------------------#
920# Utility Functions
921def DebugLog(errmsg):
922    """Print debug messages"""
923    if DEBUG:
924        print errmsg
925
926def GetContextMenu():
927    """Create and return a context menu to override the builtin scintilla
928    one. To prevent it from allowing modifications to text that is to the
929    left of the prompt.
930
931    """
932    menu = wx.Menu()
933    menu.Append(wx.ID_CUT, _("Cut"))
934    menu.Append(wx.ID_COPY, _("Copy"))
935    menu.Append(wx.ID_PASTE, _("Paste"))
936    menu.AppendSeparator()
937    menu.Append(wx.ID_SELECTALL, _("Select All"))
938    menu.AppendSeparator()
939    menu.Append(wx.ID_SETUP, _("Preferences"))
940
941    return menu
942
943#-----------------------------------------------------------------------------#
944# For Testing
945
946if __name__ == '__main__':
947    APP = wx.PySimpleApp(False)
948    FRAME = wx.Frame(None, wx.ID_ANY, "Terminal Test")
949    TERM = Xterm(FRAME, wx.ID_ANY)
950    
951    FSIZER = wx.BoxSizer(wx.VERTICAL)
952    FSIZER.Add(TERM, 1, wx.EXPAND)
953    FRAME.SetSizer(FSIZER)
954    
955    FRAME.SetSize((600, 400))
956    FRAME.Show()
957    APP.MainLoop()