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