(root)/
Python-3.12.0/
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,
     106                      curses.ascii.BS,
     107                      curses.KEY_BACKSPACE,
     108                      curses.ascii.DEL):
     109              if x > 0:
     110                  self.win.move(y, x-1)
     111              elif y == 0:
     112                  pass
     113              elif self.stripspaces:
     114                  self.win.move(y-1, self._end_of_line(y-1))
     115              else:
     116                  self.win.move(y-1, self.maxx)
     117              if ch in (curses.ascii.BS, curses.KEY_BACKSPACE, curses.ascii.DEL):
     118                  self.win.delch()
     119          elif ch == curses.ascii.EOT:                           # ^d
     120              self.win.delch()
     121          elif ch == curses.ascii.ENQ:                           # ^e
     122              if self.stripspaces:
     123                  self.win.move(y, self._end_of_line(y))
     124              else:
     125                  self.win.move(y, self.maxx)
     126          elif ch in (curses.ascii.ACK, curses.KEY_RIGHT):       # ^f
     127              if x < self.maxx:
     128                  self.win.move(y, x+1)
     129              elif y == self.maxy:
     130                  pass
     131              else:
     132                  self.win.move(y+1, 0)
     133          elif ch == curses.ascii.BEL:                           # ^g
     134              return 0
     135          elif ch == curses.ascii.NL:                            # ^j
     136              if self.maxy == 0:
     137                  return 0
     138              elif y < self.maxy:
     139                  self.win.move(y+1, 0)
     140          elif ch == curses.ascii.VT:                            # ^k
     141              if x == 0 and self._end_of_line(y) == 0:
     142                  self.win.deleteln()
     143              else:
     144                  # first undo the effect of self._end_of_line
     145                  self.win.move(y, x)
     146                  self.win.clrtoeol()
     147          elif ch == curses.ascii.FF:                            # ^l
     148              self.win.refresh()
     149          elif ch in (curses.ascii.SO, curses.KEY_DOWN):         # ^n
     150              if y < self.maxy:
     151                  self.win.move(y+1, x)
     152                  if x > self._end_of_line(y+1):
     153                      self.win.move(y+1, self._end_of_line(y+1))
     154          elif ch == curses.ascii.SI:                            # ^o
     155              self.win.insertln()
     156          elif ch in (curses.ascii.DLE, curses.KEY_UP):          # ^p
     157              if y > 0:
     158                  self.win.move(y-1, x)
     159                  if x > self._end_of_line(y-1):
     160                      self.win.move(y-1, self._end_of_line(y-1))
     161          return 1
     162  
     163      def gather(self):
     164          "Collect and return the contents of the window."
     165          result = ""
     166          self._update_max_yx()
     167          for y in range(self.maxy+1):
     168              self.win.move(y, 0)
     169              stop = self._end_of_line(y)
     170              if stop == 0 and self.stripspaces:
     171                  continue
     172              for x in range(self.maxx+1):
     173                  if self.stripspaces and x > stop:
     174                      break
     175                  result = result + chr(curses.ascii.ascii(self.win.inch(y, x)))
     176              if self.maxy > 0:
     177                  result = result + "\n"
     178          return result
     179  
     180      def edit(self, validate=None):
     181          "Edit in the widget window and collect the results."
     182          while 1:
     183              ch = self.win.getch()
     184              if validate:
     185                  ch = validate(ch)
     186              if not ch:
     187                  continue
     188              if not self.do_command(ch):
     189                  break
     190              self.win.refresh()
     191          return self.gather()
     192  
     193  if __name__ == '__main__':
     194      def test_editbox(stdscr):
     195          ncols, nlines = 9, 4
     196          uly, ulx = 15, 20
     197          stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.")
     198          win = curses.newwin(nlines, ncols, uly, ulx)
     199          rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols)
     200          stdscr.refresh()
     201          return Textbox(win).edit()
     202  
     203      str = curses.wrapper(test_editbox)
     204      print('Contents of text box:', repr(str))