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