blob: a05b82bdb6ef1fc69da73846b79031efcdeff5ec [file] [log] [blame]
Andrew M. Kuchlinge0d00902000-07-11 10:38:24 +00001"""Simple textbox editing widget with Emacs-like keybindings."""
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +00002
Martin v. Löwis9d51fc82002-02-23 22:31:53 +00003import curses, ascii
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +00004
5def rectangle(win, uly, ulx, lry, lrx):
Andrew M. Kuchlinga13ea552004-10-19 19:21:20 +00006 """Draw a rectangle with corners at the provided upper-left
7 and lower-right coordinates.
8 """
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +00009 win.vline(uly+1, ulx, curses.ACS_VLINE, lry - uly - 1)
10 win.hline(uly, ulx+1, curses.ACS_HLINE, lrx - ulx - 1)
11 win.hline(lry, ulx+1, curses.ACS_HLINE, lrx - ulx - 1)
12 win.vline(uly+1, lrx, curses.ACS_VLINE, lry - uly - 1)
13 win.addch(uly, ulx, curses.ACS_ULCORNER)
14 win.addch(uly, lrx, curses.ACS_URCORNER)
15 win.addch(lry, lrx, curses.ACS_LRCORNER)
16 win.addch(lry, ulx, curses.ACS_LLCORNER)
17
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000018class Textbox:
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000019 """Editing widget using the interior of a window object.
20 Supports the following Emacs-like key bindings:
21
22 Ctrl-A Go to left edge of window.
23 Ctrl-B Cursor left, wrapping to previous line if appropriate.
24 Ctrl-D Delete character under cursor.
Eric S. Raymond5af256d2000-08-04 07:33:18 +000025 Ctrl-E Go to right edge (stripspaces off) or end of line (stripspaces on).
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000026 Ctrl-F Cursor right, wrapping to next line when appropriate.
27 Ctrl-G Terminate, returning the window contents.
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000028 Ctrl-H Delete character backward.
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000029 Ctrl-J Terminate if the window is 1 line, otherwise insert newline.
30 Ctrl-K If line is blank, delete it, otherwise clear to end of line.
Eric S. Raymond5af256d2000-08-04 07:33:18 +000031 Ctrl-L Refresh screen.
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000032 Ctrl-N Cursor down; move down one line.
33 Ctrl-O Insert a blank line at cursor location.
34 Ctrl-P Cursor up; move up one line.
35
36 Move operations do nothing if the cursor is at an edge where the movement
37 is not possible. The following synonyms are supported where possible:
38
39 KEY_LEFT = Ctrl-B, KEY_RIGHT = Ctrl-F, KEY_UP = Ctrl-P, KEY_DOWN = Ctrl-N
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000040 KEY_BACKSPACE = Ctrl-h
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000041 """
Christian Heimesfdab48e2008-01-20 09:06:41 +000042 def __init__(self, win, insert_mode=False):
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000043 self.win = win
Christian Heimesfdab48e2008-01-20 09:06:41 +000044 self.insert_mode = insert_mode
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000045 (self.maxy, self.maxx) = win.getmaxyx()
46 self.maxy = self.maxy - 1
47 self.maxx = self.maxx - 1
48 self.stripspaces = 1
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000049 self.lastcmd = None
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000050 win.keypad(1)
51
Eric S. Raymond5af256d2000-08-04 07:33:18 +000052 def _end_of_line(self, y):
Christian Heimesfdab48e2008-01-20 09:06:41 +000053 """Go to the location of the first blank on the given line,
54 returning the index of the last non-blank character."""
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000055 last = self.maxx
Christian Heimesfdab48e2008-01-20 09:06:41 +000056 while True:
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000057 if ascii.ascii(self.win.inch(y, last)) != ascii.SP:
Andrew M. Kuchling76276172005-06-02 00:10:04 +000058 last = min(self.maxx, last+1)
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000059 break
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000060 elif last == 0:
61 break
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000062 last = last - 1
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000063 return last
64
Christian Heimesfdab48e2008-01-20 09:06:41 +000065 def _insert_printable_char(self, ch):
66 (y, x) = self.win.getyx()
67 if y < self.maxy or x < self.maxx:
68 if self.insert_mode:
69 oldch = self.win.inch()
70 # The try-catch ignores the error we trigger from some curses
71 # versions by trying to write into the lowest-rightmost spot
72 # in the window.
73 try:
74 self.win.addch(ch)
75 except curses.error:
76 pass
77 if self.insert_mode:
78 (backy, backx) = self.win.getyx()
79 if ascii.isprint(oldch):
80 self._insert_printable_char(oldch)
81 self.win.move(backy, backx)
82
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000083 def do_command(self, ch):
84 "Process a single editing command."
85 (y, x) = self.win.getyx()
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000086 self.lastcmd = ch
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000087 if ascii.isprint(ch):
88 if y < self.maxy or x < self.maxx:
Christian Heimesfdab48e2008-01-20 09:06:41 +000089 self._insert_printable_char(ch)
Guido van Rossumbffa52f2002-09-29 00:25:51 +000090 elif ch == ascii.SOH: # ^a
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000091 self.win.move(y, 0)
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000092 elif ch in (ascii.STX,curses.KEY_LEFT, ascii.BS,curses.KEY_BACKSPACE):
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000093 if x > 0:
94 self.win.move(y, x-1)
95 elif y == 0:
96 pass
97 elif self.stripspaces:
Eric S. Raymond5af256d2000-08-04 07:33:18 +000098 self.win.move(y-1, self._end_of_line(y-1))
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000099 else:
100 self.win.move(y-1, self.maxx)
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000101 if ch in (ascii.BS, curses.KEY_BACKSPACE):
102 self.win.delch()
Guido van Rossumbffa52f2002-09-29 00:25:51 +0000103 elif ch == ascii.EOT: # ^d
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000104 self.win.delch()
Guido van Rossumbffa52f2002-09-29 00:25:51 +0000105 elif ch == ascii.ENQ: # ^e
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000106 if self.stripspaces:
Eric S. Raymond5af256d2000-08-04 07:33:18 +0000107 self.win.move(y, self._end_of_line(y))
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000108 else:
109 self.win.move(y, self.maxx)
Guido van Rossumbffa52f2002-09-29 00:25:51 +0000110 elif ch in (ascii.ACK, curses.KEY_RIGHT): # ^f
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000111 if x < self.maxx:
112 self.win.move(y, x+1)
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000113 elif y == self.maxy:
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000114 pass
115 else:
116 self.win.move(y+1, 0)
Guido van Rossumbffa52f2002-09-29 00:25:51 +0000117 elif ch == ascii.BEL: # ^g
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000118 return 0
Guido van Rossumbffa52f2002-09-29 00:25:51 +0000119 elif ch == ascii.NL: # ^j
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000120 if self.maxy == 0:
121 return 0
122 elif y < self.maxy:
123 self.win.move(y+1, 0)
Guido van Rossumbffa52f2002-09-29 00:25:51 +0000124 elif ch == ascii.VT: # ^k
Eric S. Raymond5af256d2000-08-04 07:33:18 +0000125 if x == 0 and self._end_of_line(y) == 0:
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000126 self.win.deleteln()
127 else:
Andrew M. Kuchlingccab0012004-10-19 19:29:40 +0000128 # first undo the effect of self._end_of_line
129 self.win.move(y, x)
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000130 self.win.clrtoeol()
Guido van Rossumbffa52f2002-09-29 00:25:51 +0000131 elif ch == ascii.FF: # ^l
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000132 self.win.refresh()
Guido van Rossumbffa52f2002-09-29 00:25:51 +0000133 elif ch in (ascii.SO, curses.KEY_DOWN): # ^n
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000134 if y < self.maxy:
135 self.win.move(y+1, x)
Eric S. Raymond5af256d2000-08-04 07:33:18 +0000136 if x > self._end_of_line(y+1):
137 self.win.move(y+1, self._end_of_line(y+1))
Guido van Rossumbffa52f2002-09-29 00:25:51 +0000138 elif ch == ascii.SI: # ^o
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000139 self.win.insertln()
Guido van Rossumbffa52f2002-09-29 00:25:51 +0000140 elif ch in (ascii.DLE, curses.KEY_UP): # ^p
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000141 if y > 0:
142 self.win.move(y-1, x)
Eric S. Raymond5af256d2000-08-04 07:33:18 +0000143 if x > self._end_of_line(y-1):
144 self.win.move(y-1, self._end_of_line(y-1))
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000145 return 1
Guido van Rossumbffa52f2002-09-29 00:25:51 +0000146
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000147 def gather(self):
148 "Collect and return the contents of the window."
149 result = ""
150 for y in range(self.maxy+1):
151 self.win.move(y, 0)
Eric S. Raymond5af256d2000-08-04 07:33:18 +0000152 stop = self._end_of_line(y)
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000153 if stop == 0 and self.stripspaces:
154 continue
155 for x in range(self.maxx+1):
Christian Heimesfdab48e2008-01-20 09:06:41 +0000156 if self.stripspaces and x > stop:
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000157 break
158 result = result + chr(ascii.ascii(self.win.inch(y, x)))
159 if self.maxy > 0:
160 result = result + "\n"
161 return result
162
163 def edit(self, validate=None):
164 "Edit in the widget window and collect the results."
165 while 1:
166 ch = self.win.getch()
167 if validate:
168 ch = validate(ch)
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000169 if not ch:
170 continue
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000171 if not self.do_command(ch):
172 break
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000173 self.win.refresh()
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000174 return self.gather()
175
176if __name__ == '__main__':
177 def test_editbox(stdscr):
Andrew M. Kuchlinga13ea552004-10-19 19:21:20 +0000178 ncols, nlines = 9, 4
179 uly, ulx = 15, 20
Andrew M. Kuchling8520b942004-10-19 19:36:09 +0000180 stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.")
Andrew M. Kuchlinga13ea552004-10-19 19:21:20 +0000181 win = curses.newwin(nlines, ncols, uly, ulx)
182 rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols)
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000183 stdscr.refresh()
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000184 return Textbox(win).edit()
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000185
186 str = curses.wrapper(test_editbox)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000187 print('Contents of text box:', repr(str))