(root)/
Python-3.11.7/
Lib/
curses/
textpad.py
       1  """Simple textbox editing widget with Emacs-like keybindings."""
       2  
       3  import curses
       4  import curses.ascii
       5  
       6  def rectangle(win, uly, ulx, lry, lrx):
       7      """Draw a rectangle with corners at the provided upper-left
       8      and lower-right coordinates.
       9      """
      10      win.vline(uly+1, ulx, curses.ACS_VLINE, lry - uly - 1)
      11      win.hline(uly, ulx+1, curses.ACS_HLINE, lrx - ulx - 1)
      12      win.hline(lry, ulx+1, curses.ACS_HLINE, lrx - ulx - 1)
      13      win.vline(uly+1, lrx, curses.ACS_VLINE, lry - uly - 1)
      14      win.addch(uly, ulx, curses.ACS_ULCORNER)
      15      win.addch(uly, lrx, curses.ACS_URCORNER)
      16      win.addch(lry, lrx, curses.ACS_LRCORNER)
      17      win.addch(lry, ulx, curses.ACS_LLCORNER)
      18  
      19  class ESC[4;38;5;81mTextbox:
      20      """Editing widget using the interior of a window object.
      21       Supports the following Emacs-like key bindings:
      22  
      23      Ctrl-A      Go to left edge of window.
      24      Ctrl-B      Cursor left, wrapping to previous line if appropriate.
      25      Ctrl-D      Delete character under cursor.
      26      Ctrl-E      Go to right edge (stripspaces off) or end of line (stripspaces on).
      27      Ctrl-F      Cursor right, wrapping to next line when appropriate.
      28      Ctrl-G      Terminate, returning the window contents.
      29      Ctrl-H      Delete character backward.
      30      Ctrl-J      Terminate if the window is 1 line, otherwise insert newline.
      31      Ctrl-K      If line is blank, delete it, otherwise clear to end of line.
      32      Ctrl-L      Refresh screen.
      33      Ctrl-N      Cursor down; move down one line.
      34      Ctrl-O      Insert a blank line at cursor location.
      35      Ctrl-P      Cursor up; move up one line.
      36  
      37      Move operations do nothing if the cursor is at an edge where the movement
      38      is not possible.  The following synonyms are supported where possible:
      39  
      40      KEY_LEFT = Ctrl-B, KEY_RIGHT = Ctrl-F, KEY_UP = Ctrl-P, KEY_DOWN = Ctrl-N
      41      KEY_BACKSPACE = Ctrl-h
      42      """
      43      def __init__(self, win, insert_mode=False):
      44          self.win = win
      45          self.insert_mode = insert_mode
      46          self._update_max_yx()
      47          self.stripspaces = 1
      48          self.lastcmd = None
      49          win.keypad(1)
      50  
      51      def _update_max_yx(self):
      52          maxy, maxx = self.win.getmaxyx()
      53          self.maxy = maxy - 1
      54          self.maxx = maxx - 1
      55  
      56      def _end_of_line(self, y):
      57          """Go to the location of the first blank on the given line,
      58          returning the index of the last non-blank character."""
      59          self._update_max_yx()
      60          last = self.maxx
      61          while True:
      62              if curses.ascii.ascii(self.win.inch(y, last)) != curses.ascii.SP:
      63                  last = min(self.maxx, last+1)
      64                  break
      65              elif last == 0:
      66                  break
      67              last = last - 1
      68          return last
      69  
      70      def _insert_printable_char(self, ch):
      71          self._update_max_yx()
      72          (y, x) = self.win.getyx()
      73          backyx = None
      74          while y < self.maxy or x < self.maxx:
      75              if self.insert_mode:
      76                  oldch = self.win.inch()
      77              # The try-catch ignores the error we trigger from some curses
      78              # versions by trying to write into the lowest-rightmost spot
      79              # in the window.
      80              try:
      81                  self.win.addch(ch)
      82              except curses.error:
      83                  pass
      84              if not self.insert_mode or not curses.ascii.isprint(oldch):
      85                  break
      86              ch = oldch
      87              (y, x) = self.win.getyx()
      88              # Remember where to put the cursor back since we are in insert_mode
      89              if backyx is None:
      90                  backyx = y, x
      91  
      92          if backyx is not None:
      93              self.win.move(*backyx)
      94  
      95      def do_command(self, ch):
      96          "Process a single editing command."
      97          self._update_max_yx()
      98          (y, x) = self.win.getyx()
      99          self.lastcmd = ch
     100          if curses.ascii.isprint(ch):
     101              if y < self.maxy or x < self.maxx:
     102                  self._insert_printable_char(ch)
     103          elif ch == curses.ascii.SOH:                           # ^a
     104              self.win.move(y, 0)
     105          elif ch in (curses.ascii.STX,curses.KEY_LEFT, curses.ascii.BS,curses.KEY_BACKSPACE):
     106              if x > 0:
     107                  self.win.move(y, x-1)
     108              elif y == 0:
     109                  pass
     110              elif self.stripspaces:
     111                  self.win.move(y-1, self._end_of_line(y-1))
     112              else:
     113                  self.win.move(y-1, self.maxx)
     114              if ch in (curses.ascii.BS, curses.KEY_BACKSPACE):
     115                  self.win.delch()
     116          elif ch == curses.ascii.EOT:                           # ^d
     117              self.win.delch()
     118          elif ch == curses.ascii.ENQ:                           # ^e
     119              if self.stripspaces:
     120                  self.win.move(y, self._end_of_line(y))
     121              else:
     122                  self.win.move(y, self.maxx)
     123          elif ch in (curses.ascii.ACK, curses.KEY_RIGHT):       # ^f
     124              if x < self.maxx:
     125                  self.win.move(y, x+1)
     126              elif y == self.maxy:
     127                  pass
     128              else:
     129                  self.win.move(y+1, 0)
     130          elif ch == curses.ascii.BEL:                           # ^g
     131              return 0
     132          elif ch == curses.ascii.NL:                            # ^j
     133              if self.maxy == 0:
     134                  return 0
     135              elif y < self.maxy:
     136                  self.win.move(y+1, 0)
     137          elif ch == curses.ascii.VT:                            # ^k
     138              if x == 0 and self._end_of_line(y) == 0:
     139                  self.win.deleteln()
     140              else:
     141                  # first undo the effect of self._end_of_line
     142                  self.win.move(y, x)
     143                  self.win.clrtoeol()
     144          elif ch == curses.ascii.FF:                            # ^l
     145              self.win.refresh()
     146          elif ch in (curses.ascii.SO, curses.KEY_DOWN):         # ^n
     147              if y < self.maxy:
     148                  self.win.move(y+1, x)
     149                  if x > self._end_of_line(y+1):
     150                      self.win.move(y+1, self._end_of_line(y+1))
     151          elif ch == curses.ascii.SI:                            # ^o
     152              self.win.insertln()
     153          elif ch in (curses.ascii.DLE, curses.KEY_UP):          # ^p
     154              if y > 0:
     155                  self.win.move(y-1, x)
     156                  if x > self._end_of_line(y-1):
     157                      self.win.move(y-1, self._end_of_line(y-1))
     158          return 1
     159  
     160      def gather(self):
     161          "Collect and return the contents of the window."
     162          result = ""
     163          self._update_max_yx()
     164          for y in range(self.maxy+1):
     165              self.win.move(y, 0)
     166              stop = self._end_of_line(y)
     167              if stop == 0 and self.stripspaces:
     168                  continue
     169              for x in range(self.maxx+1):
     170                  if self.stripspaces and x > stop:
     171                      break
     172                  result = result + chr(curses.ascii.ascii(self.win.inch(y, x)))
     173              if self.maxy > 0:
     174                  result = result + "\n"
     175          return result
     176  
     177      def edit(self, validate=None):
     178          "Edit in the widget window and collect the results."
     179          while 1:
     180              ch = self.win.getch()
     181              if validate:
     182                  ch = validate(ch)
     183              if not ch:
     184                  continue
     185              if not self.do_command(ch):
     186                  break
     187              self.win.refresh()
     188          return self.gather()
     189  
     190  if __name__ == '__main__':
     191      def test_editbox(stdscr):
     192          ncols, nlines = 9, 4
     193          uly, ulx = 15, 20
     194          stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.")
     195          win = curses.newwin(nlines, ncols, uly, ulx)
     196          rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols)
     197          stdscr.refresh()
     198          return Textbox(win).edit()
     199  
     200      str = curses.wrapper(test_editbox)
     201      print('Contents of text box:', repr(str))