blob: 2b4b4cb669f53523b2db42bbb629e91c551ba3be [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
Georg Brandl86b2fb92008-07-16 03:43:04 +00003import curses
4import curses.ascii
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +00005
6def rectangle(win, uly, ulx, lry, lrx):
Andrew M. Kuchlinga13ea552004-10-19 19:21:20 +00007 """Draw a rectangle with corners at the provided upper-left
8 and lower-right coordinates.
9 """
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000010 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
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000019class Textbox:
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000020 """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.
Eric S. Raymond5af256d2000-08-04 07:33:18 +000026 Ctrl-E Go to right edge (stripspaces off) or end of line (stripspaces on).
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000027 Ctrl-F Cursor right, wrapping to next line when appropriate.
28 Ctrl-G Terminate, returning the window contents.
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000029 Ctrl-H Delete character backward.
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000030 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.
Eric S. Raymond5af256d2000-08-04 07:33:18 +000032 Ctrl-L Refresh screen.
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000033 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
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000041 KEY_BACKSPACE = Ctrl-h
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000042 """
Christian Heimesfdab48e2008-01-20 09:06:41 +000043 def __init__(self, win, insert_mode=False):
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000044 self.win = win
Christian Heimesfdab48e2008-01-20 09:06:41 +000045 self.insert_mode = insert_mode
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000046 (self.maxy, self.maxx) = win.getmaxyx()
47 self.maxy = self.maxy - 1
48 self.maxx = self.maxx - 1
49 self.stripspaces = 1
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000050 self.lastcmd = None
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000051 win.keypad(1)
52
Eric S. Raymond5af256d2000-08-04 07:33:18 +000053 def _end_of_line(self, y):
Christian Heimesfdab48e2008-01-20 09:06:41 +000054 """Go to the location of the first blank on the given line,
55 returning the index of the last non-blank character."""
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000056 last = self.maxx
Christian Heimesfdab48e2008-01-20 09:06:41 +000057 while True:
Georg Brandl86b2fb92008-07-16 03:43:04 +000058 if curses.ascii.ascii(self.win.inch(y, last)) != curses.ascii.SP:
Andrew M. Kuchling76276172005-06-02 00:10:04 +000059 last = min(self.maxx, last+1)
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000060 break
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000061 elif last == 0:
62 break
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000063 last = last - 1
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000064 return last
65
Christian Heimesfdab48e2008-01-20 09:06:41 +000066 def _insert_printable_char(self, ch):
67 (y, x) = self.win.getyx()
68 if y < self.maxy or x < self.maxx:
69 if self.insert_mode:
70 oldch = self.win.inch()
71 # The try-catch ignores the error we trigger from some curses
72 # versions by trying to write into the lowest-rightmost spot
73 # in the window.
74 try:
75 self.win.addch(ch)
76 except curses.error:
77 pass
78 if self.insert_mode:
79 (backy, backx) = self.win.getyx()
Georg Brandl86b2fb92008-07-16 03:43:04 +000080 if curses.ascii.isprint(oldch):
Christian Heimesfdab48e2008-01-20 09:06:41 +000081 self._insert_printable_char(oldch)
82 self.win.move(backy, backx)
83
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000084 def do_command(self, ch):
85 "Process a single editing command."
86 (y, x) = self.win.getyx()
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000087 self.lastcmd = ch
Georg Brandl86b2fb92008-07-16 03:43:04 +000088 if curses.ascii.isprint(ch):
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000089 if y < self.maxy or x < self.maxx:
Christian Heimesfdab48e2008-01-20 09:06:41 +000090 self._insert_printable_char(ch)
Georg Brandl86b2fb92008-07-16 03:43:04 +000091 elif ch == curses.ascii.SOH: # ^a
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000092 self.win.move(y, 0)
Georg Brandl86b2fb92008-07-16 03:43:04 +000093 elif ch in (curses.ascii.STX,curses.KEY_LEFT, curses.ascii.BS,curses.KEY_BACKSPACE):
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000094 if x > 0:
95 self.win.move(y, x-1)
96 elif y == 0:
97 pass
98 elif self.stripspaces:
Eric S. Raymond5af256d2000-08-04 07:33:18 +000099 self.win.move(y-1, self._end_of_line(y-1))
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000100 else:
101 self.win.move(y-1, self.maxx)
Georg Brandl86b2fb92008-07-16 03:43:04 +0000102 if ch in (curses.ascii.BS, curses.KEY_BACKSPACE):
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000103 self.win.delch()
Georg Brandl86b2fb92008-07-16 03:43:04 +0000104 elif ch == curses.ascii.EOT: # ^d
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000105 self.win.delch()
Georg Brandl86b2fb92008-07-16 03:43:04 +0000106 elif ch == curses.ascii.ENQ: # ^e
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000107 if self.stripspaces:
Eric S. Raymond5af256d2000-08-04 07:33:18 +0000108 self.win.move(y, self._end_of_line(y))
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000109 else:
110 self.win.move(y, self.maxx)
Georg Brandl86b2fb92008-07-16 03:43:04 +0000111 elif ch in (curses.ascii.ACK, curses.KEY_RIGHT): # ^f
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000112 if x < self.maxx:
113 self.win.move(y, x+1)
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000114 elif y == self.maxy:
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000115 pass
116 else:
117 self.win.move(y+1, 0)
Georg Brandl86b2fb92008-07-16 03:43:04 +0000118 elif ch == curses.ascii.BEL: # ^g
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000119 return 0
Georg Brandl86b2fb92008-07-16 03:43:04 +0000120 elif ch == curses.ascii.NL: # ^j
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000121 if self.maxy == 0:
122 return 0
123 elif y < self.maxy:
124 self.win.move(y+1, 0)
Georg Brandl86b2fb92008-07-16 03:43:04 +0000125 elif ch == curses.ascii.VT: # ^k
Eric S. Raymond5af256d2000-08-04 07:33:18 +0000126 if x == 0 and self._end_of_line(y) == 0:
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000127 self.win.deleteln()
128 else:
Andrew M. Kuchlingccab0012004-10-19 19:29:40 +0000129 # first undo the effect of self._end_of_line
130 self.win.move(y, x)
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000131 self.win.clrtoeol()
Georg Brandl86b2fb92008-07-16 03:43:04 +0000132 elif ch == curses.ascii.FF: # ^l
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000133 self.win.refresh()
Georg Brandl86b2fb92008-07-16 03:43:04 +0000134 elif ch in (curses.ascii.SO, curses.KEY_DOWN): # ^n
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000135 if y < self.maxy:
136 self.win.move(y+1, x)
Eric S. Raymond5af256d2000-08-04 07:33:18 +0000137 if x > self._end_of_line(y+1):
138 self.win.move(y+1, self._end_of_line(y+1))
Georg Brandl86b2fb92008-07-16 03:43:04 +0000139 elif ch == curses.ascii.SI: # ^o
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000140 self.win.insertln()
Georg Brandl86b2fb92008-07-16 03:43:04 +0000141 elif ch in (curses.ascii.DLE, curses.KEY_UP): # ^p
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000142 if y > 0:
143 self.win.move(y-1, x)
Eric S. Raymond5af256d2000-08-04 07:33:18 +0000144 if x > self._end_of_line(y-1):
145 self.win.move(y-1, self._end_of_line(y-1))
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000146 return 1
Guido van Rossumbffa52f2002-09-29 00:25:51 +0000147
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000148 def gather(self):
149 "Collect and return the contents of the window."
150 result = ""
151 for y in range(self.maxy+1):
152 self.win.move(y, 0)
Eric S. Raymond5af256d2000-08-04 07:33:18 +0000153 stop = self._end_of_line(y)
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000154 if stop == 0 and self.stripspaces:
155 continue
156 for x in range(self.maxx+1):
Christian Heimesfdab48e2008-01-20 09:06:41 +0000157 if self.stripspaces and x > stop:
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000158 break
Georg Brandl86b2fb92008-07-16 03:43:04 +0000159 result = result + chr(curses.ascii.ascii(self.win.inch(y, x)))
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000160 if self.maxy > 0:
161 result = result + "\n"
162 return result
163
164 def edit(self, validate=None):
165 "Edit in the widget window and collect the results."
166 while 1:
167 ch = self.win.getch()
168 if validate:
169 ch = validate(ch)
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000170 if not ch:
171 continue
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000172 if not self.do_command(ch):
173 break
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000174 self.win.refresh()
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000175 return self.gather()
176
177if __name__ == '__main__':
178 def test_editbox(stdscr):
Andrew M. Kuchlinga13ea552004-10-19 19:21:20 +0000179 ncols, nlines = 9, 4
180 uly, ulx = 15, 20
Andrew M. Kuchling8520b942004-10-19 19:36:09 +0000181 stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.")
Andrew M. Kuchlinga13ea552004-10-19 19:21:20 +0000182 win = curses.newwin(nlines, ncols, uly, ulx)
183 rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols)
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000184 stdscr.refresh()
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000185 return Textbox(win).edit()
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000186
187 str = curses.wrapper(test_editbox)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000188 print('Contents of text box:', repr(str))