Home | History | Annotate | Download | only in curses
      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 Textbox:
     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.maxy, self.maxx) = win.getmaxyx()
     47         self.maxy = self.maxy - 1
     48         self.maxx = self.maxx - 1
     49         self.stripspaces = 1
     50         self.lastcmd = None
     51         win.keypad(1)
     52 
     53     def _end_of_line(self, y):
     54         """Go to the location of the first blank on the given line,
     55         returning the index of the last non-blank character."""
     56         last = self.maxx
     57         while True:
     58             if curses.ascii.ascii(self.win.inch(y, last)) != curses.ascii.SP:
     59                 last = min(self.maxx, last+1)
     60                 break
     61             elif last == 0:
     62                 break
     63             last = last - 1
     64         return last
     65 
     66     def _insert_printable_char(self, ch):
     67         (y, x) = self.win.getyx()
     68         if y < self.maxy or x < self.maxx:
     69             if self.insert_mode:
     70                 oldch = self.win.inch()
     71             # The try-catch ignores the error we trigger from some curses
     72             # versions by trying to write into the lowest-rightmost spot
     73             # in the window.
     74             try:
     75                 self.win.addch(ch)
     76             except curses.error:
     77                 pass
     78             if self.insert_mode:
     79                 (backy, backx) = self.win.getyx()
     80                 if curses.ascii.isprint(oldch):
     81                     self._insert_printable_char(oldch)
     82                     self.win.move(backy, backx)
     83 
     84     def do_command(self, ch):
     85         "Process a single editing command."
     86         (y, x) = self.win.getyx()
     87         self.lastcmd = ch
     88         if curses.ascii.isprint(ch):
     89             if y < self.maxy or x < self.maxx:
     90                 self._insert_printable_char(ch)
     91         elif ch == curses.ascii.SOH:                           # ^a
     92             self.win.move(y, 0)
     93         elif ch in (curses.ascii.STX,curses.KEY_LEFT, curses.ascii.BS,curses.KEY_BACKSPACE):
     94             if x > 0:
     95                 self.win.move(y, x-1)
     96             elif y == 0:
     97                 pass
     98             elif self.stripspaces:
     99                 self.win.move(y-1, self._end_of_line(y-1))
    100             else:
    101                 self.win.move(y-1, self.maxx)
    102             if ch in (curses.ascii.BS, curses.KEY_BACKSPACE):
    103                 self.win.delch()
    104         elif ch == curses.ascii.EOT:                           # ^d
    105             self.win.delch()
    106         elif ch == curses.ascii.ENQ:                           # ^e
    107             if self.stripspaces:
    108                 self.win.move(y, self._end_of_line(y))
    109             else:
    110                 self.win.move(y, self.maxx)
    111         elif ch in (curses.ascii.ACK, curses.KEY_RIGHT):       # ^f
    112             if x < self.maxx:
    113                 self.win.move(y, x+1)
    114             elif y == self.maxy:
    115                 pass
    116             else:
    117                 self.win.move(y+1, 0)
    118         elif ch == curses.ascii.BEL:                           # ^g
    119             return 0
    120         elif ch == curses.ascii.NL:                            # ^j
    121             if self.maxy == 0:
    122                 return 0
    123             elif y < self.maxy:
    124                 self.win.move(y+1, 0)
    125         elif ch == curses.ascii.VT:                            # ^k
    126             if x == 0 and self._end_of_line(y) == 0:
    127                 self.win.deleteln()
    128             else:
    129                 # first undo the effect of self._end_of_line
    130                 self.win.move(y, x)
    131                 self.win.clrtoeol()
    132         elif ch == curses.ascii.FF:                            # ^l
    133             self.win.refresh()
    134         elif ch in (curses.ascii.SO, curses.KEY_DOWN):         # ^n
    135             if y < self.maxy:
    136                 self.win.move(y+1, x)
    137                 if x > self._end_of_line(y+1):
    138                     self.win.move(y+1, self._end_of_line(y+1))
    139         elif ch == curses.ascii.SI:                            # ^o
    140             self.win.insertln()
    141         elif ch in (curses.ascii.DLE, curses.KEY_UP):          # ^p
    142             if y > 0:
    143                 self.win.move(y-1, x)
    144                 if x > self._end_of_line(y-1):
    145                     self.win.move(y-1, self._end_of_line(y-1))
    146         return 1
    147 
    148     def gather(self):
    149         "Collect and return the contents of the window."
    150         result = ""
    151         for y in range(self.maxy+1):
    152             self.win.move(y, 0)
    153             stop = self._end_of_line(y)
    154             if stop == 0 and self.stripspaces:
    155                 continue
    156             for x in range(self.maxx+1):
    157                 if self.stripspaces and x > stop:
    158                     break
    159                 result = result + chr(curses.ascii.ascii(self.win.inch(y, x)))
    160             if self.maxy > 0:
    161                 result = result + "\n"
    162         return result
    163 
    164     def edit(self, validate=None):
    165         "Edit in the widget window and collect the results."
    166         while 1:
    167             ch = self.win.getch()
    168             if validate:
    169                 ch = validate(ch)
    170             if not ch:
    171                 continue
    172             if not self.do_command(ch):
    173                 break
    174             self.win.refresh()
    175         return self.gather()
    176 
    177 if __name__ == '__main__':
    178     def test_editbox(stdscr):
    179         ncols, nlines = 9, 4
    180         uly, ulx = 15, 20
    181         stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.")
    182         win = curses.newwin(nlines, ncols, uly, ulx)
    183         rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols)
    184         stdscr.refresh()
    185         return Textbox(win).edit()
    186 
    187     str = curses.wrapper(test_editbox)
    188     print 'Contents of text box:', repr(str)
    189