text: #@+leo-ver=5-thin
  #@+node:tbrown.20100226095909.12777: * @file ../plugins/leoscreen.py
  #@+<< docstring >>
  #@+node:tbrown.20100226095909.12778: ** << docstring >>
  '''Allows interaction with shell apps via screen.
  
  status: daily-use py2.7 Wed Aug  5 09:30:38 2015
  
  Analysis environments like SQL, R, scipy, ipython, etc. can be
  used by pasting sections of text from an editor (Leo) and a
  shell window.  Results can be pasted back into the editor.
  
  This plugin streamlines the process by communicating with
  ``GNU screen``, the shell multiplexer
  
  **Commands**
  
  leoscreen-run-text
    Send the text selected in Leo's body text to the shell app.
    Selects the next line for your convenience.
  
  leoscreen-run-all-text
    Send all the text in Leo's body text to the shell app.
    Selects the next node for your convenience.
  
  leoscreen-run-all-here
    Like leoscreen-run-all-text without the inconvenient selection
    of the next node.
  
  leoscreen-get-line
    Insert a line of the last result from the shell into Leo's body text
    at the current insert point.  Lines are pulled one at a time starting
    from the end of the output.  Can be used repeatedly to get the
    output you want into Leo.
  
  leoscreen-get-all
    Insert all of the last result from the shell into Leo's body text
    at the current insert point.
  
  leoscreen-get-note
    Insert all of the last result from the shell into a new child node of
    the current node.
  
  leoscreen-show-all
    Show the output from the last result from the shell in a temporary
    read only window. **Important**: The output is not stored.
  
  leoscreen-show-note
    Insert all of the last result from the shell into a new child node of
    the current node and display that node a a stickynote (requires stickynote
    plugin).
  
  leoscreen-next
    Switch screen session to next window.
  
  leoscreen-prev
    Switch screen session to preceding window.
  
  leoscreen-other
    Switch screen session to last window displayed.
  
  leoscreen-get-prefix
    Interactively get prefix for inserting text into body (#, --, //, etc/)
    Can also set using::
  
        c.leo_screen.get_line_prefix = '#'
  
  leoscreen-more-prompt
    Skip one less line at the end of output when fetching output into Leo.
    Adjusts lines skipped to avoid pulling in the applications prompt line.
  
  leoscreen-less-prompt
    Skip one more line at the end of output when fetching output into Leo
    Adjusts lines skipped to avoid pulling in the applications prompt line.
  
  leoscreen-jump-to-error
    Jump to the python error reported in the shell window, if the file's
    loaded in the current Leo session.  Just looks for a line::
  
        File "somefile.py", line NNN, in xxx
  
    and looks for a node starting with "@" and ending with "somefile.py", then
    jumps to line NNN in that file.
  
  leoscreen-jump-to-error-up
    `leoscreen-jump-to-error` jumps to the inner most stack-frame in the last
    traceback, this goes up one stack-frame. Note it will go through stack frames
    in previous tracebacks, so you need to pay attention to what's in the shell
    window. `leoscreen-jump-to-error` always resets to the inner most stack-frame
    in the last traceback.
  
  leoscreen-select-screen
    If you have multiple separate screen sessions (as seen with `screen -list`)
    running, this lets you pick the one to interact with.  By default it's the
    one from which Leo was launched.
  
  **Settings**
  
  leoscreen_prefix
    Prepended to output pulled in to Leo. The substring SPACE in this
    setting will be replaced with a space character, to allow for trailing
    spaces.
  
  leoscreen_time_fmt
    time.strftime format for note type output headings.
  
  **Theory of operation**
  
  leoscreen creates a instance at c.leo_screen which has some methods which might
  be useful in ``@button`` and other Leo contexts.
  
  **Example SQL setup**
  
  In a Leo file full of interactive SQL analysis, I have::
  
      @settings
          @string leoscreen_prefix = --SPACE
      @button rollback
          import time
          c.leo_screen.run_text('ROLLBACK;  -- %s\n' % time.asctime())
      @button commit
          import time
          cmd = 'COMMIT;  -- %s' % time.asctime()
          c.leo_screen.run_text(cmd)
          c.leo_screen.insert_line(cmd)
  
  which creates a button to rollback messed up queries, another to commit
  (requiring additional action to supply the newline as a safeguard) and
  sets the prefix to "-- " for text pulled back from the SQL session into
  Leo.
  
  **Implementation note**: screen behave's differently if screen -X is executed
  with the same stdout as the target screen, vs. a different stdout. Although
  stdout is ignored, Popen() needs to ensure it's not just inherited.
  
  '''
  #@-<< docstring >>
  
  # By TNB.
  # Use and distribute under the same terms as leo itself.
  
  #@+<< imports >>
  #@+node:tbrown.20100226095909.12779: ** << imports >>
  import subprocess
  import os
  import time
  import tempfile
  import difflib
  from leo.core import leoGlobals as g
  try:
      from leo.plugins import stickynotes
  except ImportError:
      stickynotes = None
  from leo.plugins.attrib_edit import ListDialog
  #@-<< imports >>
  #@+others
  #@+node:tbrown.20100226095909.12781: ** init
  def init():
      '''Return True if the plugin has loaded successfully.'''
      g.registerHandler('after-create-leo-frame',onCreate)
      g.plugin_signon(__name__)
      return True
  #@+node:tbrown.20100226095909.12782: ** onCreate
  def onCreate (tag,key):
      """Bind an instance of leoscreen_Controller to c"""
      c = key.get('c')
  
      leoscreen_Controller(c)
  #@+node:tbrown.20100226095909.12783: ** class leoscreen_Controller
  class leoscreen_Controller:
  
      '''A per-commander class that manages screen interaction.'''
  
      #@+others
      #@+node:tbrown.20100226095909.12784: *3* __init__& reloadSettings (leoscreen_Controller)
      def __init__ (self, c):
          """set up vars., prepare temporary file"""
          self.c = c
          c.leo_screen = self
          self.use_screen = None  # to select a particular screen session
          # skip line -1, which is usually a prompt and not interesting
          self.first_line = -2
          # pulling in lines from output, this is the next one to get
          self.next_unread_line = self.first_line
          # output from last command
          self.output = []
          self.old_output = []
          # file name for hardcopy and paste commands
          fd, self.tmpfile = tempfile.mkstemp()
          os.close(fd)
          # Settings
          self.reloadSettings()
          self._get_output()  # prime output diffing system
          self.popups = []  # store references to popup windows
          self.stack_frame = 0
              # used by jump to error commands, 0 = innermost frame
          
      def reloadSettings(self):
          c = self.c
          c.registerReloadSettings(self)
          # line prefix for pasting results into leo (#, --, //, C, etc.)
          x = self.c.config.getString('leoscreen-prefix')
          self.get_line_prefix = x.replace('SPACE', ' ') if x else ''
          self.time_fmt = self.c.config.getString('leoscreen-time-fmt') or '%Y-%m-%d %H:%M:%S'
      #@+node:tbrown.20100226095909.12785: *3* __del__
      def __del__(self):
          """remove temporary file"""
          try:
              os.unlink(self.tmpfile)
          except IOError:
              pass
      #@+node:tbrown.20100226095909.12786: *3* screen_cmd
      def screen_cmd(self, cmds):
          """Execute a screen command via screen -X"""
          cmd = [
              'screen',
          ]
  
          if self.use_screen:
              cmd.extend(['-d', '-r', self.use_screen.split()[0]])
  
          cmd.extend([
              '-X', 'eval',
              'msgwait 0',    # avoid waiting for message display
          ])
          cmd.extend(cmds)
  
          cmd.extend([
              'msgwait 5',
          ])
  
          proc = subprocess.Popen(cmd,
              stdout=subprocess.PIPE,  # don't just inherit, which alters
              stderr=subprocess.PIPE)  # screen's behavior
          out, err = proc.communicate()
          return out, err
      #@+node:tbrown.20100226095909.12787: *3* run_text
      def run_text(self, txt, c=None):
          """Send txt to screen"""
  
          if c and c != self.c:
              return
  
          if not c:
              c = self.c
  
          if not txt:
              return
              # otherwise there's an annoying delay for "Slurped zero chars" msg.
  
          if self.output:
              self.old_output = self.output
          self.output = []  # forget previous output (mostly)
  
          open(self.tmpfile,'w').write(txt)
  
          self.screen_cmd([
              'readbuf "%s"'%self.tmpfile,
              'paste .',
          ])
      #@+node:tbrown.20100421115534.21602: *3* insert_line
      def insert_line(self, line, c=None):
          """insert a line of text into the current body"""
  
          if not c:
              c = self.c
  
          editor = c.frame.body.wrapper
  
          insert_point = editor.getInsertPoint()
          editor.insert(insert_point, self.get_line_prefix+line+'\n')
          editor.setInsertPoint(insert_point)
          c.setChanged()
      #@+node:tbrown.20100528205637.5725: *3* _get_output
      def _get_output(self):
          """grab some output"""
          self.screen_cmd(['hardcopy -h "%s"'%self.tmpfile])
          # seems new output file isn't visible to the process
          # without this call
          cmd = ['ls', self.tmpfile]
          proc = subprocess.Popen(cmd,
              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
          proc.communicate()
          f = open(self.tmpfile, encoding='latin-1')
          self.output = f.read().strip().split('\n')
          self.next_unread_line = self.first_line
      #@+node:tbrown.20100226095909.12788: *3* get_line
      def get_line(self, c=None):
          """Get the next line of output from the last command"""
  
          if c and c != self.c:
              return None
          if not c:
              c = self.c
          if not self.output:
              self._get_output()
          if not self.output:
              g.es('No output retreived')
              return ''
          line = self.output[self.next_unread_line]
          self.next_unread_line -= 1
          return line
      #@+node:tbrown.20100422203442.5579: *3* get_all
      def get_all(self, c=None):
          """Get all output from the last command"""
  
          if c and c != self.c:
              return None
          if not c:
              c = self.c
          self.output = None  # trick get_line into getting output
          self.get_line()     # updates self.output, ignore returned line
          sm = difflib.SequenceMatcher(None, self.old_output, self.output)
          x = sm.find_longest_match(0, len(self.old_output)-1, 0, len(self.output)-1)
          ans = self.output[:]
          del ans[x.b:x.b+x.size]
          return '\n'.join(ans[:self.first_line])
      #@+node:tbrown.20100502155649.5599: *3* get_note
      def get_note(self, c=None):
          """Get all output from the last command"""
  
          if c and c != self.c:
              return
  
          if not c:
              c = self.c
  
          dat = self.get_all(c)
  
          p = c.currentPosition()
          n = p.insertAsLastChild()
          n.h = time.strftime(self.time_fmt)
          n.b = dat
          c.setChanged()
          c.selectPosition(n)
          c.redraw()
      #@+node:tbrown.20100424115939.5735: *3* show
      def show(self, what, title=None, plain=False):
  
          try:
              from leo.core.leoQt import QtWidgets, QtGui
              QTextEdit, QTextCursor = QtWidgets.QTextEdit, QtGui.QTextCursor
          except ImportError:
              g.es("Need Qt for show command")
              return
  
          if not title:
              title = what.split('\n', 1)[0].strip()
  
          te = QTextEdit()
          te.setReadOnly(True)
          if plain:
              te.setText(what)
          else:
              te.setHtml("<pre>%s</pre>" % what)
          te.setLineWrapMode(QTextEdit.NoWrap)
          te.resize(800, 600)
          te.setWindowTitle(title)
          te.moveCursor(QTextCursor.End)
          te.show()
          self.popups.append(te)
      #@+node:tbrown.20100502155649.5605: *3* show_note
      def show_note(self):
          if stickynotes:
              stickynotes.stickynote_f({'c':self.c})
          else:
              g.es('stickynotes not available')
  
      #@+node:tbrown.20100421115534.14949: *3* get_prefix
      def get_prefix(self):
          """get the prefix for insertions from get_line"""
  
          x = g.app.gui.runAskOkCancelStringDialog(
              self.c,'Prefix for text loading' ,'Prefix for text loading')
  
          if x is not None:
              self.get_line_prefix = x
      #@+node:tbrown.20150805094115.1: *3* select_screen
      def select_screen(self):
          """select_screen - select which screen session to target
          """
          cmd = ['screen', '-ls']
          proc = subprocess.Popen(cmd,
              stdout=subprocess.PIPE,
              stderr=subprocess.PIPE)
          out, err = proc.communicate()
          out = out.decode('utf-8')
  
          screens = [[('CURRENT: ' if i == self.use_screen else '')+i, False, i]
                     for i in out.split('\n') if i.startswith('\t')]
  
          ld = ListDialog(None, 'Pick screen', 'Pick screen', screens)
          ld.exec_()
  
          screen = [i for i in screens if i[1]]
          if screen:
              self.use_screen = screen[0][2]
          else:
              self.use_screen = None
      #@-others
  #@+node:tbrown.20100226095909.12789: ** cmd_get_line (leoscreen_Controller)
  @g.command('leoscreen-get-line')
  def cmd_get_line(event):
      """get next line of results"""
      c = event.get('c')
      line = c.leo_screen.get_line(c)
      c.leo_screen.insert_line(line)
  #@+node:tbrown.20100423084809.19285: ** cmd_get_all (leoscreen_Controller)
  @g.command('leoscreen-get-all')
  def cmd_get_all(event):
      """get all of results"""
      c = event.get('c')
      line = c.leo_screen.get_all(c)
      c.leo_screen.insert_line(line)
  #@+node:tbrown.20100502155649.5597: ** cmd_get_note (leoscreen_Controller)
  @g.command('leoscreen-get-note')
  def cmd_get_note(event):
      """get all of results"""
      c = event.get('c')
      c.leo_screen.get_note()
  #@+node:tbrown.20100502155649.5603: ** cmd_show_note (leoscreen_Controller)
  @g.command('leoscreen-show-note')
  def cmd_show_note(event):
      """get all of results"""
      c = event.get('c')
      c.leo_screen.get_note()
      c.leo_screen.show_note()
  #@+node:tbrown.20100502155649.5595: ** cmd_show_all (leoscreen_Controller)
  @g.command('leoscreen-show-all')
  def cmd_show_all(event):
      """get all of results"""
      c = event.get('c')
      line = c.leo_screen.get_all(c)
      c.leo_screen.show(line)
  #@+node:tbrown.20100226095909.12790: ** cmd_run_text (leoscreen_Controller)
  @g.command('leoscreen-run-text')
  def cmd_run_text(event):
      """pass selected text to shell app. via screen"""
      c = event.get('c')
      w = c.frame.body.wrapper
      txt = w.getSelectedText()
      # select next line ready for next select/send cycle
      b = w.getAllText()
      i = w.getInsertPoint()
      try:
          j = b[i:].index('\n')+i+1
          w.setSelectionRange(i,j)
      except ValueError:  # no more \n in text
          w.setSelectionRange(i,i)
      c.leo_screen.run_text(txt,c)
  #@+node:tbrown.20120905091352.20333: ** cmd_run_all_text (leoscreen_Controller)
  @g.command('leoscreen-run-all-text')
  def cmd_run_all_text(event, move=True):
      """pass whole body text to shell app. via screen and move to next body"""
      c = event.get('c')
      txt = c.p.b
      if txt[-1] != '\n':
          txt += '\n'
      c.leo_screen.run_text(txt,c)
      if move:
          c.selectThreadNext()
      c.redraw()
  #@+node:tbrown.20121108162853.20118: ** cmd_run_all_here (leoscreen_Controller)
  @g.command('leoscreen-run-all-here')
  def cmd_run_all_here(event):
      """non-advancing variant of cmd_run_all_text()"""
      cmd_run_all_text(event, move=False)
  #@+node:tbrown.20100226095909.12791: ** cmd_next,prev,other (leoscreen_Controller)
  @g.command('leoscreen-next')
  def cmd_next(event):
      """execute screen command next"""
      c = event.get('c')
      c.leo_screen.screen_cmd(['next'])
  
  @g.command('leoscreen-prev')
  def cmd_prev(event):
      """execute screen command prev"""
      c = event.get('c')
      c.leo_screen.screen_cmd(['prev'])
  
  @g.command('leoscreen-other')
  def cmd_other(event):
      """execute screen command other"""
      c = event.get('c')
      c.leo_screen.screen_cmd(['other'])
  #@+node:tbrown.20100421115534.14948: ** cmd_get_prefix (leoscreen_Controller)
  @g.command('leoscreen-get-prefix')
  def cmd_get_prefix(event):
      """call get_prefix"""
      c = event.get('c')
      c.leo_screen.get_prefix()
  #@+node:tbrown.20100424115939.5581: ** cmd_more/less prompt (leoscreen_Controller)
  @g.command('leoscreen-more-prompt')
  def cmd_more_prompt(event):
      """call get_prefix"""
      c = event.get('c')
      c.leo_screen.first_line += 1
  
  @g.command('leoscreen-less-prompt')
  def cmd_less_prompt(event):
      """call get_prefix"""
      c = event.get('c')
      c.leo_screen.first_line -= 1
  #@+node:tbrown.20120516075804.26095: ** cmd_jump_to_error (leoscreen_Controller)
  @g.command('leoscreen-jump-to-error')
  def cmd_jump_to_error(event):
      c = event.get('c')
      c.leo_screen.stack_frame = 0
      jump_to_error_internal(c)
  
  @g.command('leoscreen-jump-to-error-up')
  def cmd_jump_to_error_up(event):
      c = event.get('c')
      c.leo_screen.stack_frame += 1
      jump_to_error_internal(c)
  
  def jump_to_error_internal(c):
  
      import re
      regex = re.compile(r'  File "(.*)", line (\d+), in')
  
      lines = c.leo_screen.get_all(c)
      lines = lines.split('\n')
  
      skipped = 0
      for i in reversed(lines):
          match = regex.match(i)
          if match:
              if skipped == c.leo_screen.stack_frame:
                  g.es("Line %s in %s"%(match.group(2), match.group(1)))
                  filename = g.os_path_basename(match.group(1))
                  for p in c.all_unique_positions():
                      if p.h.startswith('@') and p.h.endswith(filename):
                          c.selectPosition(p)
                          c.goToLineNumber(int(match.group(2)))
                          c.bodyWantsFocusNow()
                          break
                  break
              skipped += 1
      else:
          g.es("%d error frames found in console content"%skipped)
  #@+node:tbrown.20150805095656.1: ** cmd_select_screen
  @g.command('leoscreen-select-screen')
  def cmd_select_screen(event):
      c = event.get('c')
      c.leo_screen.select_screen()
  
  #@-others
  #@@language python
  #@@tabwidth -4
  #@-leo
  