10c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi"""Simple textbox editing widget with Emacs-like keybindings."""
20c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
30c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport curses
40c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport curses.ascii
50c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
60c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yidef rectangle(win, uly, ulx, lry, lrx):
70c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    """Draw a rectangle with corners at the provided upper-left
80c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    and lower-right coordinates.
90c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    """
100c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    win.vline(uly+1, ulx, curses.ACS_VLINE, lry - uly - 1)
110c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    win.hline(uly, ulx+1, curses.ACS_HLINE, lrx - ulx - 1)
120c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    win.hline(lry, ulx+1, curses.ACS_HLINE, lrx - ulx - 1)
130c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    win.vline(uly+1, lrx, curses.ACS_VLINE, lry - uly - 1)
140c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    win.addch(uly, ulx, curses.ACS_ULCORNER)
150c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    win.addch(uly, lrx, curses.ACS_URCORNER)
160c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    win.addch(lry, lrx, curses.ACS_LRCORNER)
170c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    win.addch(lry, ulx, curses.ACS_LLCORNER)
180c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
190c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiclass Textbox:
200c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    """Editing widget using the interior of a window object.
210c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi     Supports the following Emacs-like key bindings:
220c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
230c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    Ctrl-A      Go to left edge of window.
240c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    Ctrl-B      Cursor left, wrapping to previous line if appropriate.
250c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    Ctrl-D      Delete character under cursor.
260c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    Ctrl-E      Go to right edge (stripspaces off) or end of line (stripspaces on).
270c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    Ctrl-F      Cursor right, wrapping to next line when appropriate.
280c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    Ctrl-G      Terminate, returning the window contents.
290c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    Ctrl-H      Delete character backward.
300c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    Ctrl-J      Terminate if the window is 1 line, otherwise insert newline.
310c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    Ctrl-K      If line is blank, delete it, otherwise clear to end of line.
320c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    Ctrl-L      Refresh screen.
330c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    Ctrl-N      Cursor down; move down one line.
340c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    Ctrl-O      Insert a blank line at cursor location.
350c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    Ctrl-P      Cursor up; move up one line.
360c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
370c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    Move operations do nothing if the cursor is at an edge where the movement
380c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    is not possible.  The following synonyms are supported where possible:
390c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
400c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    KEY_LEFT = Ctrl-B, KEY_RIGHT = Ctrl-F, KEY_UP = Ctrl-P, KEY_DOWN = Ctrl-N
410c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    KEY_BACKSPACE = Ctrl-h
420c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    """
430c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    def __init__(self, win, insert_mode=False):
440c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        self.win = win
450c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        self.insert_mode = insert_mode
460c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        (self.maxy, self.maxx) = win.getmaxyx()
470c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        self.maxy = self.maxy - 1
480c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        self.maxx = self.maxx - 1
490c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        self.stripspaces = 1
500c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        self.lastcmd = None
510c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        win.keypad(1)
520c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
530c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    def _end_of_line(self, y):
540c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        """Go to the location of the first blank on the given line,
550c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        returning the index of the last non-blank character."""
560c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        last = self.maxx
570c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        while True:
580c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            if curses.ascii.ascii(self.win.inch(y, last)) != curses.ascii.SP:
590c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                last = min(self.maxx, last+1)
600c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                break
610c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            elif last == 0:
620c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                break
630c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            last = last - 1
640c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        return last
650c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
660c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    def _insert_printable_char(self, ch):
670c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        (y, x) = self.win.getyx()
680c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        if y < self.maxy or x < self.maxx:
690c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            if self.insert_mode:
700c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                oldch = self.win.inch()
710c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            # The try-catch ignores the error we trigger from some curses
720c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            # versions by trying to write into the lowest-rightmost spot
730c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            # in the window.
740c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            try:
750c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                self.win.addch(ch)
760c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            except curses.error:
770c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                pass
780c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            if self.insert_mode:
790c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                (backy, backx) = self.win.getyx()
800c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                if curses.ascii.isprint(oldch):
810c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                    self._insert_printable_char(oldch)
820c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                    self.win.move(backy, backx)
830c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
840c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    def do_command(self, ch):
850c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        "Process a single editing command."
860c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        (y, x) = self.win.getyx()
870c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        self.lastcmd = ch
880c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        if curses.ascii.isprint(ch):
890c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            if y < self.maxy or x < self.maxx:
900c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                self._insert_printable_char(ch)
910c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        elif ch == curses.ascii.SOH:                           # ^a
920c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            self.win.move(y, 0)
930c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        elif ch in (curses.ascii.STX,curses.KEY_LEFT, curses.ascii.BS,curses.KEY_BACKSPACE):
940c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            if x > 0:
950c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                self.win.move(y, x-1)
960c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            elif y == 0:
970c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                pass
980c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            elif self.stripspaces:
990c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                self.win.move(y-1, self._end_of_line(y-1))
1000c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            else:
1010c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                self.win.move(y-1, self.maxx)
1020c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            if ch in (curses.ascii.BS, curses.KEY_BACKSPACE):
1030c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                self.win.delch()
1040c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        elif ch == curses.ascii.EOT:                           # ^d
1050c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            self.win.delch()
1060c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        elif ch == curses.ascii.ENQ:                           # ^e
1070c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            if self.stripspaces:
1080c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                self.win.move(y, self._end_of_line(y))
1090c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            else:
1100c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                self.win.move(y, self.maxx)
1110c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        elif ch in (curses.ascii.ACK, curses.KEY_RIGHT):       # ^f
1120c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            if x < self.maxx:
1130c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                self.win.move(y, x+1)
1140c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            elif y == self.maxy:
1150c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                pass
1160c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            else:
1170c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                self.win.move(y+1, 0)
1180c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        elif ch == curses.ascii.BEL:                           # ^g
1190c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            return 0
1200c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        elif ch == curses.ascii.NL:                            # ^j
1210c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            if self.maxy == 0:
1220c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                return 0
1230c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            elif y < self.maxy:
1240c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                self.win.move(y+1, 0)
1250c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        elif ch == curses.ascii.VT:                            # ^k
1260c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            if x == 0 and self._end_of_line(y) == 0:
1270c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                self.win.deleteln()
1280c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            else:
1290c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                # first undo the effect of self._end_of_line
1300c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                self.win.move(y, x)
1310c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                self.win.clrtoeol()
1320c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        elif ch == curses.ascii.FF:                            # ^l
1330c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            self.win.refresh()
1340c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        elif ch in (curses.ascii.SO, curses.KEY_DOWN):         # ^n
1350c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            if y < self.maxy:
1360c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                self.win.move(y+1, x)
1370c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                if x > self._end_of_line(y+1):
1380c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                    self.win.move(y+1, self._end_of_line(y+1))
1390c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        elif ch == curses.ascii.SI:                            # ^o
1400c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            self.win.insertln()
1410c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        elif ch in (curses.ascii.DLE, curses.KEY_UP):          # ^p
1420c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            if y > 0:
1430c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                self.win.move(y-1, x)
1440c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                if x > self._end_of_line(y-1):
1450c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                    self.win.move(y-1, self._end_of_line(y-1))
1460c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        return 1
1470c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
1480c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    def gather(self):
1490c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        "Collect and return the contents of the window."
1500c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        result = ""
1510c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        for y in range(self.maxy+1):
1520c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            self.win.move(y, 0)
1530c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            stop = self._end_of_line(y)
1540c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            if stop == 0 and self.stripspaces:
1550c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                continue
1560c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            for x in range(self.maxx+1):
1570c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                if self.stripspaces and x > stop:
1580c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                    break
1590c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                result = result + chr(curses.ascii.ascii(self.win.inch(y, x)))
1600c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            if self.maxy > 0:
1610c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                result = result + "\n"
1620c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        return result
1630c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
1640c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    def edit(self, validate=None):
1650c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        "Edit in the widget window and collect the results."
1660c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        while 1:
1670c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            ch = self.win.getch()
1680c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            if validate:
1690c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                ch = validate(ch)
1700c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            if not ch:
1710c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                continue
1720c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            if not self.do_command(ch):
1730c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi                break
1740c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi            self.win.refresh()
1750c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        return self.gather()
1760c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
1770c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiif __name__ == '__main__':
1780c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    def test_editbox(stdscr):
1790c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        ncols, nlines = 9, 4
1800c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        uly, ulx = 15, 20
1810c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.")
1820c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        win = curses.newwin(nlines, ncols, uly, ulx)
1830c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols)
1840c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        stdscr.refresh()
1850c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi        return Textbox(win).edit()
1860c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi
1870c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    str = curses.wrapper(test_editbox)
1880c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi    print 'Contents of text box:', repr(str)
189