14adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao"""Simple textbox editing widget with Emacs-like keybindings."""
24adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao
34adfde8bc82dd39f59e0445588c3e599ada477dJosh Gaoimport curses
44adfde8bc82dd39f59e0445588c3e599ada477dJosh Gaoimport curses.ascii
54adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao
64adfde8bc82dd39f59e0445588c3e599ada477dJosh Gaodef rectangle(win, uly, ulx, lry, lrx):
74adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    """Draw a rectangle with corners at the provided upper-left
84adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    and lower-right coordinates.
94adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    """
104adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    win.vline(uly+1, ulx, curses.ACS_VLINE, lry - uly - 1)
114adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    win.hline(uly, ulx+1, curses.ACS_HLINE, lrx - ulx - 1)
124adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    win.hline(lry, ulx+1, curses.ACS_HLINE, lrx - ulx - 1)
134adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    win.vline(uly+1, lrx, curses.ACS_VLINE, lry - uly - 1)
144adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    win.addch(uly, ulx, curses.ACS_ULCORNER)
154adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    win.addch(uly, lrx, curses.ACS_URCORNER)
164adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    win.addch(lry, lrx, curses.ACS_LRCORNER)
174adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    win.addch(lry, ulx, curses.ACS_LLCORNER)
184adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao
194adfde8bc82dd39f59e0445588c3e599ada477dJosh Gaoclass Textbox:
204adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    """Editing widget using the interior of a window object.
214adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao     Supports the following Emacs-like key bindings:
224adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao
234adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    Ctrl-A      Go to left edge of window.
244adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    Ctrl-B      Cursor left, wrapping to previous line if appropriate.
254adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    Ctrl-D      Delete character under cursor.
264adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    Ctrl-E      Go to right edge (stripspaces off) or end of line (stripspaces on).
274adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    Ctrl-F      Cursor right, wrapping to next line when appropriate.
284adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    Ctrl-G      Terminate, returning the window contents.
294adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    Ctrl-H      Delete character backward.
304adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    Ctrl-J      Terminate if the window is 1 line, otherwise insert newline.
314adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    Ctrl-K      If line is blank, delete it, otherwise clear to end of line.
324adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    Ctrl-L      Refresh screen.
334adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    Ctrl-N      Cursor down; move down one line.
344adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    Ctrl-O      Insert a blank line at cursor location.
354adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    Ctrl-P      Cursor up; move up one line.
364adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao
374adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    Move operations do nothing if the cursor is at an edge where the movement
384adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    is not possible.  The following synonyms are supported where possible:
394adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao
404adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    KEY_LEFT = Ctrl-B, KEY_RIGHT = Ctrl-F, KEY_UP = Ctrl-P, KEY_DOWN = Ctrl-N
414adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    KEY_BACKSPACE = Ctrl-h
424adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    """
434adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    def __init__(self, win, insert_mode=False):
444adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        self.win = win
454adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        self.insert_mode = insert_mode
464adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        (self.maxy, self.maxx) = win.getmaxyx()
474adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        self.maxy = self.maxy - 1
484adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        self.maxx = self.maxx - 1
494adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        self.stripspaces = 1
504adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        self.lastcmd = None
514adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        win.keypad(1)
524adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao
534adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    def _end_of_line(self, y):
544adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        """Go to the location of the first blank on the given line,
554adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        returning the index of the last non-blank character."""
564adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        last = self.maxx
574adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        while True:
584adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            if curses.ascii.ascii(self.win.inch(y, last)) != curses.ascii.SP:
594adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                last = min(self.maxx, last+1)
604adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                break
614adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            elif last == 0:
624adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                break
634adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            last = last - 1
644adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        return last
654adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao
664adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    def _insert_printable_char(self, ch):
674adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        (y, x) = self.win.getyx()
684adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        if y < self.maxy or x < self.maxx:
694adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            if self.insert_mode:
704adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                oldch = self.win.inch()
714adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            # The try-catch ignores the error we trigger from some curses
724adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            # versions by trying to write into the lowest-rightmost spot
734adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            # in the window.
744adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            try:
754adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                self.win.addch(ch)
764adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            except curses.error:
774adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                pass
784adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            if self.insert_mode:
794adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                (backy, backx) = self.win.getyx()
804adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                if curses.ascii.isprint(oldch):
814adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                    self._insert_printable_char(oldch)
824adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                    self.win.move(backy, backx)
834adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao
844adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    def do_command(self, ch):
854adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        "Process a single editing command."
864adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        (y, x) = self.win.getyx()
874adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        self.lastcmd = ch
884adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        if curses.ascii.isprint(ch):
894adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            if y < self.maxy or x < self.maxx:
904adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                self._insert_printable_char(ch)
914adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        elif ch == curses.ascii.SOH:                           # ^a
924adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            self.win.move(y, 0)
934adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        elif ch in (curses.ascii.STX,curses.KEY_LEFT, curses.ascii.BS,curses.KEY_BACKSPACE):
944adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            if x > 0:
954adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                self.win.move(y, x-1)
964adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            elif y == 0:
974adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                pass
984adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            elif self.stripspaces:
994adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                self.win.move(y-1, self._end_of_line(y-1))
1004adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            else:
1014adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                self.win.move(y-1, self.maxx)
1024adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            if ch in (curses.ascii.BS, curses.KEY_BACKSPACE):
1034adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                self.win.delch()
1044adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        elif ch == curses.ascii.EOT:                           # ^d
1054adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            self.win.delch()
1064adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        elif ch == curses.ascii.ENQ:                           # ^e
1074adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            if self.stripspaces:
1084adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                self.win.move(y, self._end_of_line(y))
1094adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            else:
1104adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                self.win.move(y, self.maxx)
1114adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        elif ch in (curses.ascii.ACK, curses.KEY_RIGHT):       # ^f
1124adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            if x < self.maxx:
1134adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                self.win.move(y, x+1)
1144adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            elif y == self.maxy:
1154adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                pass
1164adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            else:
1174adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                self.win.move(y+1, 0)
1184adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        elif ch == curses.ascii.BEL:                           # ^g
1194adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            return 0
1204adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        elif ch == curses.ascii.NL:                            # ^j
1214adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            if self.maxy == 0:
1224adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                return 0
1234adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            elif y < self.maxy:
1244adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                self.win.move(y+1, 0)
1254adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        elif ch == curses.ascii.VT:                            # ^k
1264adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            if x == 0 and self._end_of_line(y) == 0:
1274adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                self.win.deleteln()
1284adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            else:
1294adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                # first undo the effect of self._end_of_line
1304adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                self.win.move(y, x)
1314adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                self.win.clrtoeol()
1324adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        elif ch == curses.ascii.FF:                            # ^l
1334adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            self.win.refresh()
1344adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        elif ch in (curses.ascii.SO, curses.KEY_DOWN):         # ^n
1354adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            if y < self.maxy:
1364adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                self.win.move(y+1, x)
1374adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                if x > self._end_of_line(y+1):
1384adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                    self.win.move(y+1, self._end_of_line(y+1))
1394adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        elif ch == curses.ascii.SI:                            # ^o
1404adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            self.win.insertln()
1414adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        elif ch in (curses.ascii.DLE, curses.KEY_UP):          # ^p
1424adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            if y > 0:
1434adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                self.win.move(y-1, x)
1444adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                if x > self._end_of_line(y-1):
1454adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                    self.win.move(y-1, self._end_of_line(y-1))
1464adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        return 1
1474adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao
1484adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    def gather(self):
1494adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        "Collect and return the contents of the window."
1504adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        result = ""
1514adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        for y in range(self.maxy+1):
1524adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            self.win.move(y, 0)
1534adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            stop = self._end_of_line(y)
1544adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            if stop == 0 and self.stripspaces:
1554adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                continue
1564adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            for x in range(self.maxx+1):
1574adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                if self.stripspaces and x > stop:
1584adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                    break
1594adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                result = result + chr(curses.ascii.ascii(self.win.inch(y, x)))
1604adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            if self.maxy > 0:
1614adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                result = result + "\n"
1624adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        return result
1634adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao
1644adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    def edit(self, validate=None):
1654adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        "Edit in the widget window and collect the results."
1664adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        while 1:
1674adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            ch = self.win.getch()
1684adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            if validate:
1694adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                ch = validate(ch)
1704adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            if not ch:
1714adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                continue
1724adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            if not self.do_command(ch):
1734adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao                break
1744adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao            self.win.refresh()
1754adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        return self.gather()
1764adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao
1774adfde8bc82dd39f59e0445588c3e599ada477dJosh Gaoif __name__ == '__main__':
1784adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    def test_editbox(stdscr):
1794adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        ncols, nlines = 9, 4
1804adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        uly, ulx = 15, 20
1814adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.")
1824adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        win = curses.newwin(nlines, ncols, uly, ulx)
1834adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols)
1844adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        stdscr.refresh()
1854adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao        return Textbox(win).edit()
1864adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao
1874adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    str = curses.wrapper(test_editbox)
1884adfde8bc82dd39f59e0445588c3e599ada477dJosh Gao    print 'Contents of text box:', repr(str)
189