0001 """Simple textbox editing widget with Emacs-like keybindings.""" 0002 0003 import curses, ascii 0004 0005 def rectangle(win, uly, ulx, lry, lrx): 0006 """Draw a rectangle with corners at the provided upper-left 0007 and lower-right coordinates. 0008 """ 0009 win.vline(uly+1, ulx, curses.ACS_VLINE, lry - uly - 1) 0010 win.hline(uly, ulx+1, curses.ACS_HLINE, lrx - ulx - 1) 0011 win.hline(lry, ulx+1, curses.ACS_HLINE, lrx - ulx - 1) 0012 win.vline(uly+1, lrx, curses.ACS_VLINE, lry - uly - 1) 0013 win.addch(uly, ulx, curses.ACS_ULCORNER) 0014 win.addch(uly, lrx, curses.ACS_URCORNER) 0015 win.addch(lry, lrx, curses.ACS_LRCORNER) 0016 win.addch(lry, ulx, curses.ACS_LLCORNER) 0017 0018 class Textbox: 0019 """Editing widget using the interior of a window object. 0020 Supports the following Emacs-like key bindings: 0021 0022 Ctrl-A Go to left edge of window. 0023 Ctrl-B Cursor left, wrapping to previous line if appropriate. 0024 Ctrl-D Delete character under cursor. 0025 Ctrl-E Go to right edge (stripspaces off) or end of line (stripspaces on). 0026 Ctrl-F Cursor right, wrapping to next line when appropriate. 0027 Ctrl-G Terminate, returning the window contents. 0028 Ctrl-H Delete character backward. 0029 Ctrl-J Terminate if the window is 1 line, otherwise insert newline. 0030 Ctrl-K If line is blank, delete it, otherwise clear to end of line. 0031 Ctrl-L Refresh screen. 0032 Ctrl-N Cursor down; move down one line. 0033 Ctrl-O Insert a blank line at cursor location. 0034 Ctrl-P Cursor up; move up one line. 0035 0036 Move operations do nothing if the cursor is at an edge where the movement 0037 is not possible. The following synonyms are supported where possible: 0038 0039 KEY_LEFT = Ctrl-B, KEY_RIGHT = Ctrl-F, KEY_UP = Ctrl-P, KEY_DOWN = Ctrl-N 0040 KEY_BACKSPACE = Ctrl-h 0041 """ 0042 def __init__(self, win): 0043 self.win = win 0044 (self.maxy, self.maxx) = win.getmaxyx() 0045 self.maxy = self.maxy - 1 0046 self.maxx = self.maxx - 1 0047 self.stripspaces = 1 0048 self.lastcmd = None 0049 win.keypad(1) 0050 0051 def _end_of_line(self, y): 0052 "Go to the location of the first blank on the given line." 0053 last = self.maxx 0054 while 1: 0055 if ascii.ascii(self.win.inch(y, last)) != ascii.SP: 0056 last = last + 1 0057 break 0058 elif last == 0: 0059 break 0060 last = last - 1 0061 return last 0062 0063 def do_command(self, ch): 0064 "Process a single editing command." 0065 (y, x) = self.win.getyx() 0066 self.lastcmd = ch 0067 if ascii.isprint(ch): 0068 if y < self.maxy or x < self.maxx: 0069 # The try-catch ignores the error we trigger from some curses 0070 # versions by trying to write into the lowest-rightmost spot 0071 # in the window. 0072 try: 0073 self.win.addch(ch) 0074 except curses.error: 0075 pass 0076 elif ch == ascii.SOH: # ^a 0077 self.win.move(y, 0) 0078 elif ch in (ascii.STX,curses.KEY_LEFT, ascii.BS,curses.KEY_BACKSPACE): 0079 if x > 0: 0080 self.win.move(y, x-1) 0081 elif y == 0: 0082 pass 0083 elif self.stripspaces: 0084 self.win.move(y-1, self._end_of_line(y-1)) 0085 else: 0086 self.win.move(y-1, self.maxx) 0087 if ch in (ascii.BS, curses.KEY_BACKSPACE): 0088 self.win.delch() 0089 elif ch == ascii.EOT: # ^d 0090 self.win.delch() 0091 elif ch == ascii.ENQ: # ^e 0092 if self.stripspaces: 0093 self.win.move(y, self._end_of_line(y)) 0094 else: 0095 self.win.move(y, self.maxx) 0096 elif ch in (ascii.ACK, curses.KEY_RIGHT): # ^f 0097 if x < self.maxx: 0098 self.win.move(y, x+1) 0099 elif y == self.maxy: 0100 pass 0101 else: 0102 self.win.move(y+1, 0) 0103 elif ch == ascii.BEL: # ^g 0104 return 0 0105 elif ch == ascii.NL: # ^j 0106 if self.maxy == 0: 0107 return 0 0108 elif y < self.maxy: 0109 self.win.move(y+1, 0) 0110 elif ch == ascii.VT: # ^k 0111 if x == 0 and self._end_of_line(y) == 0: 0112 self.win.deleteln() 0113 else: 0114 # first undo the effect of self._end_of_line 0115 self.win.move(y, x) 0116 self.win.clrtoeol() 0117 elif ch == ascii.FF: # ^l 0118 self.win.refresh() 0119 elif ch in (ascii.SO, curses.KEY_DOWN): # ^n 0120 if y < self.maxy: 0121 self.win.move(y+1, x) 0122 if x > self._end_of_line(y+1): 0123 self.win.move(y+1, self._end_of_line(y+1)) 0124 elif ch == ascii.SI: # ^o 0125 self.win.insertln() 0126 elif ch in (ascii.DLE, curses.KEY_UP): # ^p 0127 if y > 0: 0128 self.win.move(y-1, x) 0129 if x > self._end_of_line(y-1): 0130 self.win.move(y-1, self._end_of_line(y-1)) 0131 return 1 0132 0133 def gather(self): 0134 "Collect and return the contents of the window." 0135 result = "" 0136 for y in range(self.maxy+1): 0137 self.win.move(y, 0) 0138 stop = self._end_of_line(y) 0139 if stop == 0 and self.stripspaces: 0140 continue 0141 for x in range(self.maxx+1): 0142 if self.stripspaces and x == stop: 0143 break 0144 result = result + chr(ascii.ascii(self.win.inch(y, x))) 0145 if self.maxy > 0: 0146 result = result + "\n" 0147 return result 0148 0149 def edit(self, validate=None): 0150 "Edit in the widget window and collect the results." 0151 while 1: 0152 ch = self.win.getch() 0153 if validate: 0154 ch = validate(ch) 0155 if not ch: 0156 continue 0157 if not self.do_command(ch): 0158 break 0159 self.win.refresh() 0160 return self.gather() 0161 0162 if __name__ == '__main__': 0163 def test_editbox(stdscr): 0164 ncols, nlines = 9, 4 0165 uly, ulx = 15, 20 0166 stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.") 0167 win = curses.newwin(nlines, ncols, uly, ulx) 0168 rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols) 0169 stdscr.refresh() 0170 return Textbox(win).edit() 0171 0172 str = curses.wrapper(test_editbox) 0173 print 'Contents of text box:', repr(str) 0174
Generated by PyXR 0.9.4