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