PageRenderTime 228ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/terminal/terminal/terminal.py

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