version 0.5.0
[flinks.git] / flinkspkg / TextboxPad.py
blob08ffd09505eabcf788576adcd05cc4c7bf9f9161
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, getCompletions=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, self.getCompletions = (history, getCompletions)
55 self.historyPoint = 0
56 self.currentCompletion = 0
57 self.lastcmd = ''
59 self.stripspaces = 1
60 self.lastcmd = None
61 pad.keypad(1)
63 def _end_of_line(self, y):
64 "Go to the location of the first blank on the given line."
65 last = self.maxx
66 while 1:
67 if ascii.ascii(self.pad.inch(y, last)) != ascii.SP:
68 last = min(self.maxx, last+1)
69 break
70 elif last == 0:
71 break
72 last = last - 1
73 return last
75 def do_command(self, ch):
76 "Process a single editing command."
77 (y, x) = self.pad.getyx()
78 if ascii.isprint(ch):
79 if y < self.maxy or x < self.maxx:
80 # The try-catch ignores the error we trigger from some curses
81 # versions by trying to write into the lowest-rightmost spot
82 # in the window.
83 try:
84 self.pad.insch(ch)
85 self.pad.move(y, x+1)
86 except curses.error:
87 pass
88 elif ch == ascii.SOH: # ^a
89 self.pad.move(y, 0)
90 elif ch in (ascii.STX,curses.KEY_LEFT, ascii.BS,curses.KEY_BACKSPACE):
91 if x > 0:
92 self.pad.move(y, x-1)
93 elif y == 0:
94 pass
95 elif self.stripspaces:
96 self.pad.move(y-1, self._end_of_line(y-1))
97 else:
98 self.pad.move(y-1, self.maxx)
99 if ch in (ascii.BS, curses.KEY_BACKSPACE):
100 self.pad.delch()
101 elif ch == ascii.EOT: # ^d
102 self.pad.delch()
103 elif ch == ascii.ENQ: # ^e
104 if self.stripspaces:
105 self.pad.move(y, self._end_of_line(y))
106 else:
107 self.pad.move(y, self.maxx)
108 elif ch in (ascii.ACK, curses.KEY_RIGHT): # ^f
109 if x < self.maxx:
110 self.pad.move(y, x+1)
111 elif y == self.maxy:
112 pass
113 else:
114 self.pad.move(y+1, 0)
115 elif ch == ascii.BEL: # ^g
116 return 0
117 elif (ch == ascii.TAB and # ^i
118 self.getCompletions is not None):
119 # completion, modelled after vim's: successive tabs cycle through
120 # the possible completions.
121 if self.lastcmd == ascii.TAB and self.completions:
122 self.currentCompletion = ((self.currentCompletion + 1) %
123 len(self.completions))
124 else:
125 self.completions = self.getCompletions(
126 self.getCurrentContents())
127 self.currentCompletion = 0
128 if self.completions:
129 self.pad.deleteln()
130 self.pad.move(y,0)
131 self.pad.addstr(self.completions[self.currentCompletion])
132 elif ch == ascii.NL: # ^j
133 if self.maxy == 0:
134 return 0
135 elif y < self.maxy:
136 self.pad.move(y+1, 0)
137 elif ch == ascii.VT: # ^k
138 if x == 0 and self._end_of_line(y) == 0:
139 self.pad.deleteln()
140 else:
141 # first undo the effect of self._end_of_line
142 self.pad.move(y, x)
143 self.pad.clrtoeol()
144 elif ch == ascii.NAK: # ^u
145 self.pad.move(y,0)
146 for i in range(x):
147 self.pad.delch()
148 elif ch == ascii.FF: # ^l
149 self.refresh()
150 elif ch in (ascii.SO, curses.KEY_DOWN): # ^n
151 if self.history and self.maxy == 0:
152 if self.historyPoint > 0:
153 self.historyPoint -= 1
154 self.pad.deleteln()
155 if self.historyPoint == 0:
156 line = self.postHistory
157 else:
158 line = self.history[-self.historyPoint]
159 self.pad.move(y,0)
160 self.pad.addstr(line)
161 elif y < self.maxy:
162 self.pad.move(y+1, x)
163 if x > self._end_of_line(y+1):
164 self.pad.move(y+1, self._end_of_line(y+1))
165 elif ch == ascii.SI: # ^o
166 self.pad.insertln()
167 elif ch in (ascii.DLE, curses.KEY_UP): # ^p
168 if self.history and self.maxy == 0:
169 if self.historyPoint < len(self.history):
170 if self.historyPoint == 0:
171 self.pad.move(y,0)
172 self.postHistory = self.getCurrentContents()
173 self.historyPoint += 1
174 self.pad.deleteln()
175 self.pad.move(y,0)
176 self.pad.addstr(self.history[-self.historyPoint])
177 elif y > 0:
178 self.pad.move(y-1, x)
179 if x > self._end_of_line(y-1):
180 self.pad.move(y-1, self._end_of_line(y-1))
181 self.lastcmd = ch
182 return 1
184 def getCurrentContents(self):
185 (y, x) = self.pad.getyx()
186 result = ''
187 for xt in range(self._end_of_line(y)):
188 result += \
189 chr(ascii.ascii(self.pad.inch(y, xt)))
190 self.pad.move(y, x)
191 return result
193 def gather(self):
194 "Collect and return the contents of the window."
195 result = ""
196 for y in range(self.maxy+1):
197 self.pad.move(y, 0)
198 stop = self._end_of_line(y)
199 if stop == 0 and self.stripspaces:
200 continue
201 for x in range(self.maxx+1):
202 if self.stripspaces and x > stop:
203 break
204 result = result + chr(ascii.ascii(self.pad.inch(y, x)))
205 if self.maxy > 0:
206 result = result + "\n"
207 return result
209 def refresh(self):
210 (y, x) = self.pad.getyx()
211 self.pad.refresh(
212 max(0, y - self.sheight),
213 max(0, x - self.swidth),
214 self.sminrow, self.smincol, self.smaxrow, self.smaxcol)
216 def edit(self, validate=None):
217 "Edit in the widget window and collect the results."
218 self.refresh()
219 while 1:
220 ch = self.pad.getch()
221 if validate:
222 ch = validate(ch)
223 if not ch:
224 continue
225 if not self.do_command(ch):
226 break
227 self.refresh()
228 return self.gather()
230 if __name__ == '__main__':
231 def test_editbox(stdscr):
232 ncols, nlines = 9, 4
233 uly, ulx = 15, 20
234 stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.")
235 pad = curses.newpad(nlines*3, ncols*2)
236 rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols)
237 stdscr.refresh()
238 return TextboxPad(pad, uly, ulx, uly+nlines-1, ulx+ncols-1).edit()
240 str = curses.wrapper(test_editbox)
241 print(('Contents of text box:', repr(str)))