1 """Simple textbox editing widget with Emacs-like keybindings.""" 2 3 import curses 4 import curses.ascii 5 6 def 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 19 class 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.maxy, self.maxx) = win.getmaxyx() 47 self.maxy = self.maxy - 1 48 self.maxx = self.maxx - 1 49 self.stripspaces = 1 50 self.lastcmd = None 51 win.keypad(1) 52 53 def _end_of_line(self, y): 54 """Go to the location of the first blank on the given line, 55 returning the index of the last non-blank character.""" 56 last = self.maxx 57 while True: 58 if curses.ascii.ascii(self.win.inch(y, last)) != curses.ascii.SP: 59 last = min(self.maxx, last+1) 60 break 61 elif last == 0: 62 break 63 last = last - 1 64 return last 65 66 def _insert_printable_char(self, ch): 67 (y, x) = self.win.getyx() 68 if y < self.maxy or x < self.maxx: 69 if self.insert_mode: 70 oldch = self.win.inch() 71 # The try-catch ignores the error we trigger from some curses 72 # versions by trying to write into the lowest-rightmost spot 73 # in the window. 74 try: 75 self.win.addch(ch) 76 except curses.error: 77 pass 78 if self.insert_mode: 79 (backy, backx) = self.win.getyx() 80 if curses.ascii.isprint(oldch): 81 self._insert_printable_char(oldch) 82 self.win.move(backy, backx) 83 84 def do_command(self, ch): 85 "Process a single editing command." 86 (y, x) = self.win.getyx() 87 self.lastcmd = ch 88 if curses.ascii.isprint(ch): 89 if y < self.maxy or x < self.maxx: 90 self._insert_printable_char(ch) 91 elif ch == curses.ascii.SOH: # ^a 92 self.win.move(y, 0) 93 elif ch in (curses.ascii.STX,curses.KEY_LEFT, curses.ascii.BS,curses.KEY_BACKSPACE): 94 if x > 0: 95 self.win.move(y, x-1) 96 elif y == 0: 97 pass 98 elif self.stripspaces: 99 self.win.move(y-1, self._end_of_line(y-1)) 100 else: 101 self.win.move(y-1, self.maxx) 102 if ch in (curses.ascii.BS, curses.KEY_BACKSPACE): 103 self.win.delch() 104 elif ch == curses.ascii.EOT: # ^d 105 self.win.delch() 106 elif ch == curses.ascii.ENQ: # ^e 107 if self.stripspaces: 108 self.win.move(y, self._end_of_line(y)) 109 else: 110 self.win.move(y, self.maxx) 111 elif ch in (curses.ascii.ACK, curses.KEY_RIGHT): # ^f 112 if x < self.maxx: 113 self.win.move(y, x+1) 114 elif y == self.maxy: 115 pass 116 else: 117 self.win.move(y+1, 0) 118 elif ch == curses.ascii.BEL: # ^g 119 return 0 120 elif ch == curses.ascii.NL: # ^j 121 if self.maxy == 0: 122 return 0 123 elif y < self.maxy: 124 self.win.move(y+1, 0) 125 elif ch == curses.ascii.VT: # ^k 126 if x == 0 and self._end_of_line(y) == 0: 127 self.win.deleteln() 128 else: 129 # first undo the effect of self._end_of_line 130 self.win.move(y, x) 131 self.win.clrtoeol() 132 elif ch == curses.ascii.FF: # ^l 133 self.win.refresh() 134 elif ch in (curses.ascii.SO, curses.KEY_DOWN): # ^n 135 if y < self.maxy: 136 self.win.move(y+1, x) 137 if x > self._end_of_line(y+1): 138 self.win.move(y+1, self._end_of_line(y+1)) 139 elif ch == curses.ascii.SI: # ^o 140 self.win.insertln() 141 elif ch in (curses.ascii.DLE, curses.KEY_UP): # ^p 142 if y > 0: 143 self.win.move(y-1, x) 144 if x > self._end_of_line(y-1): 145 self.win.move(y-1, self._end_of_line(y-1)) 146 return 1 147 148 def gather(self): 149 "Collect and return the contents of the window." 150 result = "" 151 for y in range(self.maxy+1): 152 self.win.move(y, 0) 153 stop = self._end_of_line(y) 154 if stop == 0 and self.stripspaces: 155 continue 156 for x in range(self.maxx+1): 157 if self.stripspaces and x > stop: 158 break 159 result = result + chr(curses.ascii.ascii(self.win.inch(y, x))) 160 if self.maxy > 0: 161 result = result + "\n" 162 return result 163 164 def edit(self, validate=None): 165 "Edit in the widget window and collect the results." 166 while 1: 167 ch = self.win.getch() 168 if validate: 169 ch = validate(ch) 170 if not ch: 171 continue 172 if not self.do_command(ch): 173 break 174 self.win.refresh() 175 return self.gather() 176 177 if __name__ == '__main__': 178 def test_editbox(stdscr): 179 ncols, nlines = 9, 4 180 uly, ulx = 15, 20 181 stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.") 182 win = curses.newwin(nlines, ncols, uly, ulx) 183 rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols) 184 stdscr.refresh() 185 return Textbox(win).edit() 186 187 str = curses.wrapper(test_editbox) 188 print 'Contents of text box:', repr(str) 189