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