17db96d56Sopenharmony_ci"""Simple textbox editing widget with Emacs-like keybindings.""" 27db96d56Sopenharmony_ci 37db96d56Sopenharmony_ciimport curses 47db96d56Sopenharmony_ciimport curses.ascii 57db96d56Sopenharmony_ci 67db96d56Sopenharmony_cidef rectangle(win, uly, ulx, lry, lrx): 77db96d56Sopenharmony_ci """Draw a rectangle with corners at the provided upper-left 87db96d56Sopenharmony_ci and lower-right coordinates. 97db96d56Sopenharmony_ci """ 107db96d56Sopenharmony_ci win.vline(uly+1, ulx, curses.ACS_VLINE, lry - uly - 1) 117db96d56Sopenharmony_ci win.hline(uly, ulx+1, curses.ACS_HLINE, lrx - ulx - 1) 127db96d56Sopenharmony_ci win.hline(lry, ulx+1, curses.ACS_HLINE, lrx - ulx - 1) 137db96d56Sopenharmony_ci win.vline(uly+1, lrx, curses.ACS_VLINE, lry - uly - 1) 147db96d56Sopenharmony_ci win.addch(uly, ulx, curses.ACS_ULCORNER) 157db96d56Sopenharmony_ci win.addch(uly, lrx, curses.ACS_URCORNER) 167db96d56Sopenharmony_ci win.addch(lry, lrx, curses.ACS_LRCORNER) 177db96d56Sopenharmony_ci win.addch(lry, ulx, curses.ACS_LLCORNER) 187db96d56Sopenharmony_ci 197db96d56Sopenharmony_ciclass Textbox: 207db96d56Sopenharmony_ci """Editing widget using the interior of a window object. 217db96d56Sopenharmony_ci Supports the following Emacs-like key bindings: 227db96d56Sopenharmony_ci 237db96d56Sopenharmony_ci Ctrl-A Go to left edge of window. 247db96d56Sopenharmony_ci Ctrl-B Cursor left, wrapping to previous line if appropriate. 257db96d56Sopenharmony_ci Ctrl-D Delete character under cursor. 267db96d56Sopenharmony_ci Ctrl-E Go to right edge (stripspaces off) or end of line (stripspaces on). 277db96d56Sopenharmony_ci Ctrl-F Cursor right, wrapping to next line when appropriate. 287db96d56Sopenharmony_ci Ctrl-G Terminate, returning the window contents. 297db96d56Sopenharmony_ci Ctrl-H Delete character backward. 307db96d56Sopenharmony_ci Ctrl-J Terminate if the window is 1 line, otherwise insert newline. 317db96d56Sopenharmony_ci Ctrl-K If line is blank, delete it, otherwise clear to end of line. 327db96d56Sopenharmony_ci Ctrl-L Refresh screen. 337db96d56Sopenharmony_ci Ctrl-N Cursor down; move down one line. 347db96d56Sopenharmony_ci Ctrl-O Insert a blank line at cursor location. 357db96d56Sopenharmony_ci Ctrl-P Cursor up; move up one line. 367db96d56Sopenharmony_ci 377db96d56Sopenharmony_ci Move operations do nothing if the cursor is at an edge where the movement 387db96d56Sopenharmony_ci is not possible. The following synonyms are supported where possible: 397db96d56Sopenharmony_ci 407db96d56Sopenharmony_ci KEY_LEFT = Ctrl-B, KEY_RIGHT = Ctrl-F, KEY_UP = Ctrl-P, KEY_DOWN = Ctrl-N 417db96d56Sopenharmony_ci KEY_BACKSPACE = Ctrl-h 427db96d56Sopenharmony_ci """ 437db96d56Sopenharmony_ci def __init__(self, win, insert_mode=False): 447db96d56Sopenharmony_ci self.win = win 457db96d56Sopenharmony_ci self.insert_mode = insert_mode 467db96d56Sopenharmony_ci self._update_max_yx() 477db96d56Sopenharmony_ci self.stripspaces = 1 487db96d56Sopenharmony_ci self.lastcmd = None 497db96d56Sopenharmony_ci win.keypad(1) 507db96d56Sopenharmony_ci 517db96d56Sopenharmony_ci def _update_max_yx(self): 527db96d56Sopenharmony_ci maxy, maxx = self.win.getmaxyx() 537db96d56Sopenharmony_ci self.maxy = maxy - 1 547db96d56Sopenharmony_ci self.maxx = maxx - 1 557db96d56Sopenharmony_ci 567db96d56Sopenharmony_ci def _end_of_line(self, y): 577db96d56Sopenharmony_ci """Go to the location of the first blank on the given line, 587db96d56Sopenharmony_ci returning the index of the last non-blank character.""" 597db96d56Sopenharmony_ci self._update_max_yx() 607db96d56Sopenharmony_ci last = self.maxx 617db96d56Sopenharmony_ci while True: 627db96d56Sopenharmony_ci if curses.ascii.ascii(self.win.inch(y, last)) != curses.ascii.SP: 637db96d56Sopenharmony_ci last = min(self.maxx, last+1) 647db96d56Sopenharmony_ci break 657db96d56Sopenharmony_ci elif last == 0: 667db96d56Sopenharmony_ci break 677db96d56Sopenharmony_ci last = last - 1 687db96d56Sopenharmony_ci return last 697db96d56Sopenharmony_ci 707db96d56Sopenharmony_ci def _insert_printable_char(self, ch): 717db96d56Sopenharmony_ci self._update_max_yx() 727db96d56Sopenharmony_ci (y, x) = self.win.getyx() 737db96d56Sopenharmony_ci backyx = None 747db96d56Sopenharmony_ci while y < self.maxy or x < self.maxx: 757db96d56Sopenharmony_ci if self.insert_mode: 767db96d56Sopenharmony_ci oldch = self.win.inch() 777db96d56Sopenharmony_ci # The try-catch ignores the error we trigger from some curses 787db96d56Sopenharmony_ci # versions by trying to write into the lowest-rightmost spot 797db96d56Sopenharmony_ci # in the window. 807db96d56Sopenharmony_ci try: 817db96d56Sopenharmony_ci self.win.addch(ch) 827db96d56Sopenharmony_ci except curses.error: 837db96d56Sopenharmony_ci pass 847db96d56Sopenharmony_ci if not self.insert_mode or not curses.ascii.isprint(oldch): 857db96d56Sopenharmony_ci break 867db96d56Sopenharmony_ci ch = oldch 877db96d56Sopenharmony_ci (y, x) = self.win.getyx() 887db96d56Sopenharmony_ci # Remember where to put the cursor back since we are in insert_mode 897db96d56Sopenharmony_ci if backyx is None: 907db96d56Sopenharmony_ci backyx = y, x 917db96d56Sopenharmony_ci 927db96d56Sopenharmony_ci if backyx is not None: 937db96d56Sopenharmony_ci self.win.move(*backyx) 947db96d56Sopenharmony_ci 957db96d56Sopenharmony_ci def do_command(self, ch): 967db96d56Sopenharmony_ci "Process a single editing command." 977db96d56Sopenharmony_ci self._update_max_yx() 987db96d56Sopenharmony_ci (y, x) = self.win.getyx() 997db96d56Sopenharmony_ci self.lastcmd = ch 1007db96d56Sopenharmony_ci if curses.ascii.isprint(ch): 1017db96d56Sopenharmony_ci if y < self.maxy or x < self.maxx: 1027db96d56Sopenharmony_ci self._insert_printable_char(ch) 1037db96d56Sopenharmony_ci elif ch == curses.ascii.SOH: # ^a 1047db96d56Sopenharmony_ci self.win.move(y, 0) 1057db96d56Sopenharmony_ci elif ch in (curses.ascii.STX,curses.KEY_LEFT, curses.ascii.BS,curses.KEY_BACKSPACE): 1067db96d56Sopenharmony_ci if x > 0: 1077db96d56Sopenharmony_ci self.win.move(y, x-1) 1087db96d56Sopenharmony_ci elif y == 0: 1097db96d56Sopenharmony_ci pass 1107db96d56Sopenharmony_ci elif self.stripspaces: 1117db96d56Sopenharmony_ci self.win.move(y-1, self._end_of_line(y-1)) 1127db96d56Sopenharmony_ci else: 1137db96d56Sopenharmony_ci self.win.move(y-1, self.maxx) 1147db96d56Sopenharmony_ci if ch in (curses.ascii.BS, curses.KEY_BACKSPACE): 1157db96d56Sopenharmony_ci self.win.delch() 1167db96d56Sopenharmony_ci elif ch == curses.ascii.EOT: # ^d 1177db96d56Sopenharmony_ci self.win.delch() 1187db96d56Sopenharmony_ci elif ch == curses.ascii.ENQ: # ^e 1197db96d56Sopenharmony_ci if self.stripspaces: 1207db96d56Sopenharmony_ci self.win.move(y, self._end_of_line(y)) 1217db96d56Sopenharmony_ci else: 1227db96d56Sopenharmony_ci self.win.move(y, self.maxx) 1237db96d56Sopenharmony_ci elif ch in (curses.ascii.ACK, curses.KEY_RIGHT): # ^f 1247db96d56Sopenharmony_ci if x < self.maxx: 1257db96d56Sopenharmony_ci self.win.move(y, x+1) 1267db96d56Sopenharmony_ci elif y == self.maxy: 1277db96d56Sopenharmony_ci pass 1287db96d56Sopenharmony_ci else: 1297db96d56Sopenharmony_ci self.win.move(y+1, 0) 1307db96d56Sopenharmony_ci elif ch == curses.ascii.BEL: # ^g 1317db96d56Sopenharmony_ci return 0 1327db96d56Sopenharmony_ci elif ch == curses.ascii.NL: # ^j 1337db96d56Sopenharmony_ci if self.maxy == 0: 1347db96d56Sopenharmony_ci return 0 1357db96d56Sopenharmony_ci elif y < self.maxy: 1367db96d56Sopenharmony_ci self.win.move(y+1, 0) 1377db96d56Sopenharmony_ci elif ch == curses.ascii.VT: # ^k 1387db96d56Sopenharmony_ci if x == 0 and self._end_of_line(y) == 0: 1397db96d56Sopenharmony_ci self.win.deleteln() 1407db96d56Sopenharmony_ci else: 1417db96d56Sopenharmony_ci # first undo the effect of self._end_of_line 1427db96d56Sopenharmony_ci self.win.move(y, x) 1437db96d56Sopenharmony_ci self.win.clrtoeol() 1447db96d56Sopenharmony_ci elif ch == curses.ascii.FF: # ^l 1457db96d56Sopenharmony_ci self.win.refresh() 1467db96d56Sopenharmony_ci elif ch in (curses.ascii.SO, curses.KEY_DOWN): # ^n 1477db96d56Sopenharmony_ci if y < self.maxy: 1487db96d56Sopenharmony_ci self.win.move(y+1, x) 1497db96d56Sopenharmony_ci if x > self._end_of_line(y+1): 1507db96d56Sopenharmony_ci self.win.move(y+1, self._end_of_line(y+1)) 1517db96d56Sopenharmony_ci elif ch == curses.ascii.SI: # ^o 1527db96d56Sopenharmony_ci self.win.insertln() 1537db96d56Sopenharmony_ci elif ch in (curses.ascii.DLE, curses.KEY_UP): # ^p 1547db96d56Sopenharmony_ci if y > 0: 1557db96d56Sopenharmony_ci self.win.move(y-1, x) 1567db96d56Sopenharmony_ci if x > self._end_of_line(y-1): 1577db96d56Sopenharmony_ci self.win.move(y-1, self._end_of_line(y-1)) 1587db96d56Sopenharmony_ci return 1 1597db96d56Sopenharmony_ci 1607db96d56Sopenharmony_ci def gather(self): 1617db96d56Sopenharmony_ci "Collect and return the contents of the window." 1627db96d56Sopenharmony_ci result = "" 1637db96d56Sopenharmony_ci self._update_max_yx() 1647db96d56Sopenharmony_ci for y in range(self.maxy+1): 1657db96d56Sopenharmony_ci self.win.move(y, 0) 1667db96d56Sopenharmony_ci stop = self._end_of_line(y) 1677db96d56Sopenharmony_ci if stop == 0 and self.stripspaces: 1687db96d56Sopenharmony_ci continue 1697db96d56Sopenharmony_ci for x in range(self.maxx+1): 1707db96d56Sopenharmony_ci if self.stripspaces and x > stop: 1717db96d56Sopenharmony_ci break 1727db96d56Sopenharmony_ci result = result + chr(curses.ascii.ascii(self.win.inch(y, x))) 1737db96d56Sopenharmony_ci if self.maxy > 0: 1747db96d56Sopenharmony_ci result = result + "\n" 1757db96d56Sopenharmony_ci return result 1767db96d56Sopenharmony_ci 1777db96d56Sopenharmony_ci def edit(self, validate=None): 1787db96d56Sopenharmony_ci "Edit in the widget window and collect the results." 1797db96d56Sopenharmony_ci while 1: 1807db96d56Sopenharmony_ci ch = self.win.getch() 1817db96d56Sopenharmony_ci if validate: 1827db96d56Sopenharmony_ci ch = validate(ch) 1837db96d56Sopenharmony_ci if not ch: 1847db96d56Sopenharmony_ci continue 1857db96d56Sopenharmony_ci if not self.do_command(ch): 1867db96d56Sopenharmony_ci break 1877db96d56Sopenharmony_ci self.win.refresh() 1887db96d56Sopenharmony_ci return self.gather() 1897db96d56Sopenharmony_ci 1907db96d56Sopenharmony_ciif __name__ == '__main__': 1917db96d56Sopenharmony_ci def test_editbox(stdscr): 1927db96d56Sopenharmony_ci ncols, nlines = 9, 4 1937db96d56Sopenharmony_ci uly, ulx = 15, 20 1947db96d56Sopenharmony_ci stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.") 1957db96d56Sopenharmony_ci win = curses.newwin(nlines, ncols, uly, ulx) 1967db96d56Sopenharmony_ci rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols) 1977db96d56Sopenharmony_ci stdscr.refresh() 1987db96d56Sopenharmony_ci return Textbox(win).edit() 1997db96d56Sopenharmony_ci 2007db96d56Sopenharmony_ci str = curses.wrapper(test_editbox) 2017db96d56Sopenharmony_ci print('Contents of text box:', repr(str)) 202