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