183760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh"""Simple textbox editing widget with Emacs-like keybindings."""
283760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh
383760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsiehimport curses
483760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsiehimport curses.ascii
583760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh
683760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsiehdef rectangle(win, uly, ulx, lry, lrx):
783760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    """Draw a rectangle with corners at the provided upper-left
883760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    and lower-right coordinates.
983760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    """
1083760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    win.vline(uly+1, ulx, curses.ACS_VLINE, lry - uly - 1)
1183760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    win.hline(uly, ulx+1, curses.ACS_HLINE, lrx - ulx - 1)
1283760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    win.hline(lry, ulx+1, curses.ACS_HLINE, lrx - ulx - 1)
1383760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    win.vline(uly+1, lrx, curses.ACS_VLINE, lry - uly - 1)
1483760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    win.addch(uly, ulx, curses.ACS_ULCORNER)
1583760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    win.addch(uly, lrx, curses.ACS_URCORNER)
1683760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    win.addch(lry, lrx, curses.ACS_LRCORNER)
1783760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    win.addch(lry, ulx, curses.ACS_LLCORNER)
1883760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh
1983760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsiehclass Textbox:
2083760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    """Editing widget using the interior of a window object.
2183760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh     Supports the following Emacs-like key bindings:
2283760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh
2383760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    Ctrl-A      Go to left edge of window.
2483760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    Ctrl-B      Cursor left, wrapping to previous line if appropriate.
2583760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    Ctrl-D      Delete character under cursor.
2683760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    Ctrl-E      Go to right edge (stripspaces off) or end of line (stripspaces on).
2783760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    Ctrl-F      Cursor right, wrapping to next line when appropriate.
2883760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    Ctrl-G      Terminate, returning the window contents.
2983760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    Ctrl-H      Delete character backward.
3083760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    Ctrl-J      Terminate if the window is 1 line, otherwise insert newline.
3183760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    Ctrl-K      If line is blank, delete it, otherwise clear to end of line.
3283760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    Ctrl-L      Refresh screen.
3383760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    Ctrl-N      Cursor down; move down one line.
3483760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    Ctrl-O      Insert a blank line at cursor location.
3583760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    Ctrl-P      Cursor up; move up one line.
3683760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh
3783760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    Move operations do nothing if the cursor is at an edge where the movement
3883760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    is not possible.  The following synonyms are supported where possible:
3983760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh
4083760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    KEY_LEFT = Ctrl-B, KEY_RIGHT = Ctrl-F, KEY_UP = Ctrl-P, KEY_DOWN = Ctrl-N
4183760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    KEY_BACKSPACE = Ctrl-h
4283760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    """
4383760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    def __init__(self, win, insert_mode=False):
4483760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        self.win = win
4583760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        self.insert_mode = insert_mode
4683760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        (self.maxy, self.maxx) = win.getmaxyx()
4783760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        self.maxy = self.maxy - 1
4883760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        self.maxx = self.maxx - 1
4983760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        self.stripspaces = 1
5083760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        self.lastcmd = None
5183760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        win.keypad(1)
5283760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh
5383760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    def _end_of_line(self, y):
5483760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        """Go to the location of the first blank on the given line,
5583760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        returning the index of the last non-blank character."""
5683760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        last = self.maxx
5783760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        while True:
5883760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            if curses.ascii.ascii(self.win.inch(y, last)) != curses.ascii.SP:
5983760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                last = min(self.maxx, last+1)
6083760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                break
6183760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            elif last == 0:
6283760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                break
6383760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            last = last - 1
6483760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        return last
6583760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh
6683760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    def _insert_printable_char(self, ch):
6783760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        (y, x) = self.win.getyx()
6883760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        if y < self.maxy or x < self.maxx:
6983760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            if self.insert_mode:
7083760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                oldch = self.win.inch()
7183760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            # The try-catch ignores the error we trigger from some curses
7283760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            # versions by trying to write into the lowest-rightmost spot
7383760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            # in the window.
7483760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            try:
7583760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                self.win.addch(ch)
7683760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            except curses.error:
7783760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                pass
7883760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            if self.insert_mode:
7983760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                (backy, backx) = self.win.getyx()
8083760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                if curses.ascii.isprint(oldch):
8183760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                    self._insert_printable_char(oldch)
8283760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                    self.win.move(backy, backx)
8383760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh
8483760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    def do_command(self, ch):
8583760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        "Process a single editing command."
8683760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        (y, x) = self.win.getyx()
8783760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        self.lastcmd = ch
8883760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        if curses.ascii.isprint(ch):
8983760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            if y < self.maxy or x < self.maxx:
9083760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                self._insert_printable_char(ch)
9183760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        elif ch == curses.ascii.SOH:                           # ^a
9283760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            self.win.move(y, 0)
9383760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        elif ch in (curses.ascii.STX,curses.KEY_LEFT, curses.ascii.BS,curses.KEY_BACKSPACE):
9483760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            if x > 0:
9583760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                self.win.move(y, x-1)
9683760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            elif y == 0:
9783760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                pass
9883760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            elif self.stripspaces:
9983760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                self.win.move(y-1, self._end_of_line(y-1))
10083760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            else:
10183760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                self.win.move(y-1, self.maxx)
10283760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            if ch in (curses.ascii.BS, curses.KEY_BACKSPACE):
10383760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                self.win.delch()
10483760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        elif ch == curses.ascii.EOT:                           # ^d
10583760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            self.win.delch()
10683760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        elif ch == curses.ascii.ENQ:                           # ^e
10783760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            if self.stripspaces:
10883760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                self.win.move(y, self._end_of_line(y))
10983760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            else:
11083760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                self.win.move(y, self.maxx)
11183760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        elif ch in (curses.ascii.ACK, curses.KEY_RIGHT):       # ^f
11283760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            if x < self.maxx:
11383760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                self.win.move(y, x+1)
11483760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            elif y == self.maxy:
11583760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                pass
11683760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            else:
11783760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                self.win.move(y+1, 0)
11883760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        elif ch == curses.ascii.BEL:                           # ^g
11983760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            return 0
12083760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        elif ch == curses.ascii.NL:                            # ^j
12183760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            if self.maxy == 0:
12283760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                return 0
12383760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            elif y < self.maxy:
12483760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                self.win.move(y+1, 0)
12583760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        elif ch == curses.ascii.VT:                            # ^k
12683760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            if x == 0 and self._end_of_line(y) == 0:
12783760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                self.win.deleteln()
12883760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            else:
12983760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                # first undo the effect of self._end_of_line
13083760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                self.win.move(y, x)
13183760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                self.win.clrtoeol()
13283760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        elif ch == curses.ascii.FF:                            # ^l
13383760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            self.win.refresh()
13483760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        elif ch in (curses.ascii.SO, curses.KEY_DOWN):         # ^n
13583760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            if y < self.maxy:
13683760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                self.win.move(y+1, x)
13783760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                if x > self._end_of_line(y+1):
13883760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                    self.win.move(y+1, self._end_of_line(y+1))
13983760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        elif ch == curses.ascii.SI:                            # ^o
14083760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            self.win.insertln()
14183760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        elif ch in (curses.ascii.DLE, curses.KEY_UP):          # ^p
14283760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            if y > 0:
14383760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                self.win.move(y-1, x)
14483760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                if x > self._end_of_line(y-1):
14583760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                    self.win.move(y-1, self._end_of_line(y-1))
14683760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        return 1
14783760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh
14883760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    def gather(self):
14983760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        "Collect and return the contents of the window."
15083760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        result = ""
15183760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        for y in range(self.maxy+1):
15283760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            self.win.move(y, 0)
15383760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            stop = self._end_of_line(y)
15483760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            if stop == 0 and self.stripspaces:
15583760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                continue
15683760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            for x in range(self.maxx+1):
15783760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                if self.stripspaces and x > stop:
15883760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                    break
15983760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                result = result + chr(curses.ascii.ascii(self.win.inch(y, x)))
16083760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            if self.maxy > 0:
16183760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                result = result + "\n"
16283760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        return result
16383760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh
16483760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    def edit(self, validate=None):
16583760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        "Edit in the widget window and collect the results."
16683760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        while 1:
16783760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            ch = self.win.getch()
16883760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            if validate:
16983760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                ch = validate(ch)
17083760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            if not ch:
17183760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                continue
17283760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            if not self.do_command(ch):
17383760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh                break
17483760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh            self.win.refresh()
17583760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        return self.gather()
17683760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh
17783760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsiehif __name__ == '__main__':
17883760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    def test_editbox(stdscr):
17983760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        ncols, nlines = 9, 4
18083760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        uly, ulx = 15, 20
18183760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.")
18283760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        win = curses.newwin(nlines, ncols, uly, ulx)
18383760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols)
18483760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        stdscr.refresh()
18583760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh        return Textbox(win).edit()
18683760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh
18783760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    str = curses.wrapper(test_editbox)
18883760d213fb3bec7b4117d266fcfbf6fe2ba14abAndrew Hsieh    print 'Contents of text box:', repr(str)
189