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):
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
61 def _end_of_line(self
, y
):
62 "Go to the location of the first blank on the given line."
65 if ascii
.ascii(self
.pad
.inch(y
, last
)) != ascii
.SP
:
66 last
= min(self
.maxx
, last
+1)
73 def do_command(self
, ch
):
74 "Process a single editing command."
75 (y
, x
) = self
.pad
.getyx()
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
87 elif ch
== ascii
.SOH
: # ^a
89 elif ch
in (ascii
.STX
,curses
.KEY_LEFT
, ascii
.BS
,curses
.KEY_BACKSPACE
):
94 elif self
.stripspaces
:
95 self
.pad
.move(y
-1, self
._end
_of
_line
(y
-1))
97 self
.pad
.move(y
-1, self
.maxx
)
98 if ch
in (ascii
.BS
, curses
.KEY_BACKSPACE
):
100 elif ch
== ascii
.EOT
: # ^d
102 elif ch
== ascii
.ENQ
: # ^e
104 self
.pad
.move(y
, self
._end
_of
_line
(y
))
106 self
.pad
.move(y
, self
.maxx
)
107 elif ch
in (ascii
.ACK
, curses
.KEY_RIGHT
): # ^f
109 self
.pad
.move(y
, x
+1)
113 self
.pad
.move(y
+1, 0)
114 elif ch
== ascii
.BEL
: # ^g
116 elif ch
== ascii
.NL
: # ^j
120 self
.pad
.move(y
+1, 0)
121 elif ch
== ascii
.VT
: # ^k
122 if x
== 0 and self
._end
_of
_line
(y
) == 0:
125 # first undo the effect of self._end_of_line
128 elif ch
== ascii
.NAK
: # ^u
132 elif ch
== ascii
.FF
: # ^l
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
139 if self
.historyPoint
== 0:
140 line
= self
.postHistory
142 line
= self
.history
[-self
.historyPoint
]
144 self
.pad
.addstr(line
)
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
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:
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
163 self
.pad
.addstr(self
.history
[-self
.historyPoint
])
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))
171 "Collect and return the contents of the window."
173 for y
in range(self
.maxy
+1):
175 stop
= self
._end
_of
_line
(y
)
176 if stop
== 0 and self
.stripspaces
:
178 for x
in range(self
.maxx
+1):
179 if self
.stripspaces
and x
> stop
:
181 result
= result
+ chr(ascii
.ascii(self
.pad
.inch(y
, x
)))
183 result
= result
+ "\n"
187 (y
, x
) = self
.pad
.getyx()
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."
197 ch
= self
.pad
.getch()
202 if not self
.do_command(ch
):
207 if __name__
== '__main__':
208 def test_editbox(stdscr
):
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
)
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)