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