10c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi"""Simple textbox editing widget with Emacs-like keybindings.""" 20c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi 30c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport curses 40c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiimport curses.ascii 50c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi 60c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yidef rectangle(win, uly, ulx, lry, lrx): 70c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi """Draw a rectangle with corners at the provided upper-left 80c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi and lower-right coordinates. 90c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi """ 100c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi win.vline(uly+1, ulx, curses.ACS_VLINE, lry - uly - 1) 110c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi win.hline(uly, ulx+1, curses.ACS_HLINE, lrx - ulx - 1) 120c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi win.hline(lry, ulx+1, curses.ACS_HLINE, lrx - ulx - 1) 130c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi win.vline(uly+1, lrx, curses.ACS_VLINE, lry - uly - 1) 140c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi win.addch(uly, ulx, curses.ACS_ULCORNER) 150c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi win.addch(uly, lrx, curses.ACS_URCORNER) 160c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi win.addch(lry, lrx, curses.ACS_LRCORNER) 170c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi win.addch(lry, ulx, curses.ACS_LLCORNER) 180c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi 190c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiclass Textbox: 200c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi """Editing widget using the interior of a window object. 210c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi Supports the following Emacs-like key bindings: 220c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi 230c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi Ctrl-A Go to left edge of window. 240c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi Ctrl-B Cursor left, wrapping to previous line if appropriate. 250c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi Ctrl-D Delete character under cursor. 260c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi Ctrl-E Go to right edge (stripspaces off) or end of line (stripspaces on). 270c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi Ctrl-F Cursor right, wrapping to next line when appropriate. 280c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi Ctrl-G Terminate, returning the window contents. 290c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi Ctrl-H Delete character backward. 300c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi Ctrl-J Terminate if the window is 1 line, otherwise insert newline. 310c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi Ctrl-K If line is blank, delete it, otherwise clear to end of line. 320c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi Ctrl-L Refresh screen. 330c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi Ctrl-N Cursor down; move down one line. 340c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi Ctrl-O Insert a blank line at cursor location. 350c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi Ctrl-P Cursor up; move up one line. 360c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi 370c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi Move operations do nothing if the cursor is at an edge where the movement 380c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi is not possible. The following synonyms are supported where possible: 390c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi 400c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi KEY_LEFT = Ctrl-B, KEY_RIGHT = Ctrl-F, KEY_UP = Ctrl-P, KEY_DOWN = Ctrl-N 410c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi KEY_BACKSPACE = Ctrl-h 420c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi """ 430c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi def __init__(self, win, insert_mode=False): 440c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win = win 450c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.insert_mode = insert_mode 460c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi (self.maxy, self.maxx) = win.getmaxyx() 470c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.maxy = self.maxy - 1 480c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.maxx = self.maxx - 1 490c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.stripspaces = 1 500c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.lastcmd = None 510c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi win.keypad(1) 520c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi 530c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi def _end_of_line(self, y): 540c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi """Go to the location of the first blank on the given line, 550c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi returning the index of the last non-blank character.""" 560c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi last = self.maxx 570c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi while True: 580c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if curses.ascii.ascii(self.win.inch(y, last)) != curses.ascii.SP: 590c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi last = min(self.maxx, last+1) 600c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi break 610c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi elif last == 0: 620c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi break 630c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi last = last - 1 640c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi return last 650c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi 660c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi def _insert_printable_char(self, ch): 670c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi (y, x) = self.win.getyx() 680c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if y < self.maxy or x < self.maxx: 690c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if self.insert_mode: 700c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi oldch = self.win.inch() 710c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi # The try-catch ignores the error we trigger from some curses 720c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi # versions by trying to write into the lowest-rightmost spot 730c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi # in the window. 740c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi try: 750c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.addch(ch) 760c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi except curses.error: 770c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi pass 780c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if self.insert_mode: 790c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi (backy, backx) = self.win.getyx() 800c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if curses.ascii.isprint(oldch): 810c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self._insert_printable_char(oldch) 820c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.move(backy, backx) 830c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi 840c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi def do_command(self, ch): 850c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi "Process a single editing command." 860c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi (y, x) = self.win.getyx() 870c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.lastcmd = ch 880c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if curses.ascii.isprint(ch): 890c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if y < self.maxy or x < self.maxx: 900c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self._insert_printable_char(ch) 910c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi elif ch == curses.ascii.SOH: # ^a 920c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.move(y, 0) 930c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi elif ch in (curses.ascii.STX,curses.KEY_LEFT, curses.ascii.BS,curses.KEY_BACKSPACE): 940c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if x > 0: 950c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.move(y, x-1) 960c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi elif y == 0: 970c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi pass 980c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi elif self.stripspaces: 990c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.move(y-1, self._end_of_line(y-1)) 1000c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi else: 1010c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.move(y-1, self.maxx) 1020c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if ch in (curses.ascii.BS, curses.KEY_BACKSPACE): 1030c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.delch() 1040c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi elif ch == curses.ascii.EOT: # ^d 1050c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.delch() 1060c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi elif ch == curses.ascii.ENQ: # ^e 1070c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if self.stripspaces: 1080c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.move(y, self._end_of_line(y)) 1090c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi else: 1100c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.move(y, self.maxx) 1110c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi elif ch in (curses.ascii.ACK, curses.KEY_RIGHT): # ^f 1120c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if x < self.maxx: 1130c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.move(y, x+1) 1140c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi elif y == self.maxy: 1150c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi pass 1160c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi else: 1170c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.move(y+1, 0) 1180c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi elif ch == curses.ascii.BEL: # ^g 1190c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi return 0 1200c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi elif ch == curses.ascii.NL: # ^j 1210c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if self.maxy == 0: 1220c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi return 0 1230c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi elif y < self.maxy: 1240c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.move(y+1, 0) 1250c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi elif ch == curses.ascii.VT: # ^k 1260c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if x == 0 and self._end_of_line(y) == 0: 1270c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.deleteln() 1280c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi else: 1290c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi # first undo the effect of self._end_of_line 1300c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.move(y, x) 1310c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.clrtoeol() 1320c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi elif ch == curses.ascii.FF: # ^l 1330c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.refresh() 1340c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi elif ch in (curses.ascii.SO, curses.KEY_DOWN): # ^n 1350c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if y < self.maxy: 1360c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.move(y+1, x) 1370c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if x > self._end_of_line(y+1): 1380c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.move(y+1, self._end_of_line(y+1)) 1390c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi elif ch == curses.ascii.SI: # ^o 1400c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.insertln() 1410c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi elif ch in (curses.ascii.DLE, curses.KEY_UP): # ^p 1420c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if y > 0: 1430c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.move(y-1, x) 1440c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if x > self._end_of_line(y-1): 1450c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.move(y-1, self._end_of_line(y-1)) 1460c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi return 1 1470c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi 1480c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi def gather(self): 1490c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi "Collect and return the contents of the window." 1500c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi result = "" 1510c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi for y in range(self.maxy+1): 1520c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.move(y, 0) 1530c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi stop = self._end_of_line(y) 1540c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if stop == 0 and self.stripspaces: 1550c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi continue 1560c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi for x in range(self.maxx+1): 1570c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if self.stripspaces and x > stop: 1580c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi break 1590c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi result = result + chr(curses.ascii.ascii(self.win.inch(y, x))) 1600c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if self.maxy > 0: 1610c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi result = result + "\n" 1620c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi return result 1630c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi 1640c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi def edit(self, validate=None): 1650c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi "Edit in the widget window and collect the results." 1660c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi while 1: 1670c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi ch = self.win.getch() 1680c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if validate: 1690c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi ch = validate(ch) 1700c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if not ch: 1710c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi continue 1720c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi if not self.do_command(ch): 1730c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi break 1740c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi self.win.refresh() 1750c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi return self.gather() 1760c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi 1770c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yiif __name__ == '__main__': 1780c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi def test_editbox(stdscr): 1790c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi ncols, nlines = 9, 4 1800c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi uly, ulx = 15, 20 1810c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.") 1820c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi win = curses.newwin(nlines, ncols, uly, ulx) 1830c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols) 1840c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi stdscr.refresh() 1850c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi return Textbox(win).edit() 1860c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi 1870c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi str = curses.wrapper(test_editbox) 1880c5958b1636c47ed7c284f859c8e805fd06a0e6Bill Yi print 'Contents of text box:', repr(str) 189