1 """Simple textpad editing widget with Emacs-like keybindings, using curses pad
2 objects. Adapted from the standard library module curses.textpad"""
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.
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
)
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
44 def __init__(self
, pad
, sminrow
, smincol
, smaxrow
, smaxcol
, history
=None, getCompletions
=None):
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
)
56 self
.currentCompletion
= 0
63 def _end_of_line(self
, y
):
64 "Go to the location of the first blank on the given line."
67 if ascii
.ascii(self
.pad
.inch(y
, last
)) != ascii
.SP
:
68 last
= min(self
.maxx
, last
+1)
75 def do_command(self
, ch
):
76 "Process a single editing command."
77 (y
, x
) = self
.pad
.getyx()
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
88 elif ch
== ascii
.SOH
: # ^a
90 elif ch
in (ascii
.STX
,curses
.KEY_LEFT
, ascii
.BS
,curses
.KEY_BACKSPACE
):
95 elif self
.stripspaces
:
96 self
.pad
.move(y
-1, self
._end
_of
_line
(y
-1))
98 self
.pad
.move(y
-1, self
.maxx
)
99 if ch
in (ascii
.BS
, curses
.KEY_BACKSPACE
):
101 elif ch
== ascii
.EOT
: # ^d
103 elif ch
== ascii
.ENQ
: # ^e
105 self
.pad
.move(y
, self
._end
_of
_line
(y
))
107 self
.pad
.move(y
, self
.maxx
)
108 elif ch
in (ascii
.ACK
, curses
.KEY_RIGHT
): # ^f
110 self
.pad
.move(y
, x
+1)
114 self
.pad
.move(y
+1, 0)
115 elif ch
== ascii
.BEL
: # ^g
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
))
125 self
.completions
= self
.getCompletions(
126 self
.getCurrentContents())
127 self
.currentCompletion
= 0
131 self
.pad
.addstr(self
.completions
[self
.currentCompletion
])
132 elif ch
== ascii
.NL
: # ^j
136 self
.pad
.move(y
+1, 0)
137 elif ch
== ascii
.VT
: # ^k
138 if x
== 0 and self
._end
_of
_line
(y
) == 0:
141 # first undo the effect of self._end_of_line
144 elif ch
== ascii
.NAK
: # ^u
148 elif ch
== ascii
.FF
: # ^l
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
155 if self
.historyPoint
== 0:
156 line
= self
.postHistory
158 line
= self
.history
[-self
.historyPoint
]
160 self
.pad
.addstr(line
)
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
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:
172 self
.postHistory
= self
.getCurrentContents()
173 self
.historyPoint
+= 1
176 self
.pad
.addstr(self
.history
[-self
.historyPoint
])
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))
184 def getCurrentContents(self
):
185 (y
, x
) = self
.pad
.getyx()
187 for xt
in range(self
._end
_of
_line
(y
)):
189 chr(ascii
.ascii(self
.pad
.inch(y
, xt
)))
194 "Collect and return the contents of the window."
196 for y
in range(self
.maxy
+1):
198 stop
= self
._end
_of
_line
(y
)
199 if stop
== 0 and self
.stripspaces
:
201 for x
in range(self
.maxx
+1):
202 if self
.stripspaces
and x
> stop
:
204 result
= result
+ chr(ascii
.ascii(self
.pad
.inch(y
, x
)))
206 result
= result
+ "\n"
210 (y
, x
) = self
.pad
.getyx()
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."
220 ch
= self
.pad
.getch()
225 if not self
.do_command(ch
):
230 if __name__
== '__main__':
231 def test_editbox(stdscr
):
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
)
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)))