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, curses.ascii.BS,curses.KEY_BACKSPACE): 106 if x > 0: 107 self.win.move(y, x-1) 108 elif y == 0: 109 pass 110 elif self.stripspaces: 111 self.win.move(y-1, self._end_of_line(y-1)) 112 else: 113 self.win.move(y-1, self.maxx) 114 if ch in (curses.ascii.BS, curses.KEY_BACKSPACE): 115 self.win.delch() 116 elif ch == curses.ascii.EOT: # ^d 117 self.win.delch() 118 elif ch == curses.ascii.ENQ: # ^e 119 if self.stripspaces: 120 self.win.move(y, self._end_of_line(y)) 121 else: 122 self.win.move(y, self.maxx) 123 elif ch in (curses.ascii.ACK, curses.KEY_RIGHT): # ^f 124 if x < self.maxx: 125 self.win.move(y, x+1) 126 elif y == self.maxy: 127 pass 128 else: 129 self.win.move(y+1, 0) 130 elif ch == curses.ascii.BEL: # ^g 131 return 0 132 elif ch == curses.ascii.NL: # ^j 133 if self.maxy == 0: 134 return 0 135 elif y < self.maxy: 136 self.win.move(y+1, 0) 137 elif ch == curses.ascii.VT: # ^k 138 if x == 0 and self._end_of_line(y) == 0: 139 self.win.deleteln() 140 else: 141 # first undo the effect of self._end_of_line 142 self.win.move(y, x) 143 self.win.clrtoeol() 144 elif ch == curses.ascii.FF: # ^l 145 self.win.refresh() 146 elif ch in (curses.ascii.SO, curses.KEY_DOWN): # ^n 147 if y < self.maxy: 148 self.win.move(y+1, x) 149 if x > self._end_of_line(y+1): 150 self.win.move(y+1, self._end_of_line(y+1)) 151 elif ch == curses.ascii.SI: # ^o 152 self.win.insertln() 153 elif ch in (curses.ascii.DLE, curses.KEY_UP): # ^p 154 if y > 0: 155 self.win.move(y-1, x) 156 if x > self._end_of_line(y-1): 157 self.win.move(y-1, self._end_of_line(y-1)) 158 return 1 159 160 def gather(self): 161 "Collect and return the contents of the window." 162 result = "" 163 self._update_max_yx() 164 for y in range(self.maxy+1): 165 self.win.move(y, 0) 166 stop = self._end_of_line(y) 167 if stop == 0 and self.stripspaces: 168 continue 169 for x in range(self.maxx+1): 170 if self.stripspaces and x > stop: 171 break 172 result = result + chr(curses.ascii.ascii(self.win.inch(y, x))) 173 if self.maxy > 0: 174 result = result + "\n" 175 return result 176 177 def edit(self, validate=None): 178 "Edit in the widget window and collect the results." 179 while 1: 180 ch = self.win.getch() 181 if validate: 182 ch = validate(ch) 183 if not ch: 184 continue 185 if not self.do_command(ch): 186 break 187 self.win.refresh() 188 return self.gather() 189 190if __name__ == '__main__': 191 def test_editbox(stdscr): 192 ncols, nlines = 9, 4 193 uly, ulx = 15, 20 194 stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.") 195 win = curses.newwin(nlines, ncols, uly, ulx) 196 rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols) 197 stdscr.refresh() 198 return Textbox(win).edit() 199 200 str = curses.wrapper(test_editbox) 201 print('Contents of text box:', repr(str)) 202