1ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh"""Simple textbox editing widget with Emacs-like keybindings."""
2ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
3ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehimport curses
4ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehimport curses.ascii
5ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
6ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehdef rectangle(win, uly, ulx, lry, lrx):
7ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    """Draw a rectangle with corners at the provided upper-left
8ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    and lower-right coordinates.
9ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    """
10ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    win.vline(uly+1, ulx, curses.ACS_VLINE, lry - uly - 1)
11ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    win.hline(uly, ulx+1, curses.ACS_HLINE, lrx - ulx - 1)
12ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    win.hline(lry, ulx+1, curses.ACS_HLINE, lrx - ulx - 1)
13ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    win.vline(uly+1, lrx, curses.ACS_VLINE, lry - uly - 1)
14ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    win.addch(uly, ulx, curses.ACS_ULCORNER)
15ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    win.addch(uly, lrx, curses.ACS_URCORNER)
16ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    win.addch(lry, lrx, curses.ACS_LRCORNER)
17ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    win.addch(lry, ulx, curses.ACS_LLCORNER)
18ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
19ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehclass Textbox:
20ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    """Editing widget using the interior of a window object.
21ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh     Supports the following Emacs-like key bindings:
22ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
23ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    Ctrl-A      Go to left edge of window.
24ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    Ctrl-B      Cursor left, wrapping to previous line if appropriate.
25ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    Ctrl-D      Delete character under cursor.
26ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    Ctrl-E      Go to right edge (stripspaces off) or end of line (stripspaces on).
27ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    Ctrl-F      Cursor right, wrapping to next line when appropriate.
28ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    Ctrl-G      Terminate, returning the window contents.
29ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    Ctrl-H      Delete character backward.
30ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    Ctrl-J      Terminate if the window is 1 line, otherwise insert newline.
31ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    Ctrl-K      If line is blank, delete it, otherwise clear to end of line.
32ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    Ctrl-L      Refresh screen.
33ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    Ctrl-N      Cursor down; move down one line.
34ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    Ctrl-O      Insert a blank line at cursor location.
35ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    Ctrl-P      Cursor up; move up one line.
36ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
37ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    Move operations do nothing if the cursor is at an edge where the movement
38ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    is not possible.  The following synonyms are supported where possible:
39ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
40ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    KEY_LEFT = Ctrl-B, KEY_RIGHT = Ctrl-F, KEY_UP = Ctrl-P, KEY_DOWN = Ctrl-N
41ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    KEY_BACKSPACE = Ctrl-h
42ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    """
43ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    def __init__(self, win, insert_mode=False):
44ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        self.win = win
45ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        self.insert_mode = insert_mode
46ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        (self.maxy, self.maxx) = win.getmaxyx()
47ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        self.maxy = self.maxy - 1
48ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        self.maxx = self.maxx - 1
49ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        self.stripspaces = 1
50ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        self.lastcmd = None
51ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        win.keypad(1)
52ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
53ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    def _end_of_line(self, y):
54ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        """Go to the location of the first blank on the given line,
55ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        returning the index of the last non-blank character."""
56ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        last = self.maxx
57ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        while True:
58ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if curses.ascii.ascii(self.win.inch(y, last)) != curses.ascii.SP:
59ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                last = min(self.maxx, last+1)
60ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                break
61ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            elif last == 0:
62ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                break
63ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            last = last - 1
64ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        return last
65ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
66ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    def _insert_printable_char(self, ch):
67ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        (y, x) = self.win.getyx()
68ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        if y < self.maxy or x < self.maxx:
69ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if self.insert_mode:
70ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                oldch = self.win.inch()
71ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            # The try-catch ignores the error we trigger from some curses
72ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            # versions by trying to write into the lowest-rightmost spot
73ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            # in the window.
74ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            try:
75ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                self.win.addch(ch)
76ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            except curses.error:
77ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                pass
78ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if self.insert_mode:
79ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                (backy, backx) = self.win.getyx()
80ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                if curses.ascii.isprint(oldch):
81ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                    self._insert_printable_char(oldch)
82ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                    self.win.move(backy, backx)
83ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
84ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    def do_command(self, ch):
85ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        "Process a single editing command."
86ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        (y, x) = self.win.getyx()
87ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        self.lastcmd = ch
88ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        if curses.ascii.isprint(ch):
89ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if y < self.maxy or x < self.maxx:
90ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                self._insert_printable_char(ch)
91ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        elif ch == curses.ascii.SOH:                           # ^a
92ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            self.win.move(y, 0)
93ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        elif ch in (curses.ascii.STX,curses.KEY_LEFT, curses.ascii.BS,curses.KEY_BACKSPACE):
94ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if x > 0:
95ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                self.win.move(y, x-1)
96ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            elif y == 0:
97ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                pass
98ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            elif self.stripspaces:
99ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                self.win.move(y-1, self._end_of_line(y-1))
100ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            else:
101ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                self.win.move(y-1, self.maxx)
102ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if ch in (curses.ascii.BS, curses.KEY_BACKSPACE):
103ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                self.win.delch()
104ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        elif ch == curses.ascii.EOT:                           # ^d
105ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            self.win.delch()
106ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        elif ch == curses.ascii.ENQ:                           # ^e
107ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if self.stripspaces:
108ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                self.win.move(y, self._end_of_line(y))
109ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            else:
110ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                self.win.move(y, self.maxx)
111ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        elif ch in (curses.ascii.ACK, curses.KEY_RIGHT):       # ^f
112ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if x < self.maxx:
113ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                self.win.move(y, x+1)
114ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            elif y == self.maxy:
115ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                pass
116ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            else:
117ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                self.win.move(y+1, 0)
118ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        elif ch == curses.ascii.BEL:                           # ^g
119ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            return 0
120ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        elif ch == curses.ascii.NL:                            # ^j
121ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if self.maxy == 0:
122ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                return 0
123ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            elif y < self.maxy:
124ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                self.win.move(y+1, 0)
125ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        elif ch == curses.ascii.VT:                            # ^k
126ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if x == 0 and self._end_of_line(y) == 0:
127ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                self.win.deleteln()
128ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            else:
129ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                # first undo the effect of self._end_of_line
130ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                self.win.move(y, x)
131ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                self.win.clrtoeol()
132ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        elif ch == curses.ascii.FF:                            # ^l
133ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            self.win.refresh()
134ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        elif ch in (curses.ascii.SO, curses.KEY_DOWN):         # ^n
135ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if y < self.maxy:
136ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                self.win.move(y+1, x)
137ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                if x > self._end_of_line(y+1):
138ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                    self.win.move(y+1, self._end_of_line(y+1))
139ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        elif ch == curses.ascii.SI:                            # ^o
140ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            self.win.insertln()
141ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        elif ch in (curses.ascii.DLE, curses.KEY_UP):          # ^p
142ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if y > 0:
143ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                self.win.move(y-1, x)
144ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                if x > self._end_of_line(y-1):
145ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                    self.win.move(y-1, self._end_of_line(y-1))
146ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        return 1
147ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
148ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    def gather(self):
149ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        "Collect and return the contents of the window."
150ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        result = ""
151ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        for y in range(self.maxy+1):
152ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            self.win.move(y, 0)
153ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            stop = self._end_of_line(y)
154ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if stop == 0 and self.stripspaces:
155ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                continue
156ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            for x in range(self.maxx+1):
157ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                if self.stripspaces and x > stop:
158ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                    break
159ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                result = result + chr(curses.ascii.ascii(self.win.inch(y, x)))
160ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if self.maxy > 0:
161ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                result = result + "\n"
162ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        return result
163ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
164ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    def edit(self, validate=None):
165ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        "Edit in the widget window and collect the results."
166ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        while 1:
167ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            ch = self.win.getch()
168ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if validate:
169ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                ch = validate(ch)
170ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if not ch:
171ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                continue
172ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if not self.do_command(ch):
173ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                break
174ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            self.win.refresh()
175ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        return self.gather()
176ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
177ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehif __name__ == '__main__':
178ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    def test_editbox(stdscr):
179ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        ncols, nlines = 9, 4
180ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        uly, ulx = 15, 20
181ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.")
182ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        win = curses.newwin(nlines, ncols, uly, ulx)
183ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols)
184ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        stdscr.refresh()
185ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        return Textbox(win).edit()
186ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
187ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    str = curses.wrapper(test_editbox)
188ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    print 'Contents of text box:', repr(str)
189