blob: 2079953a06614ba7ffc8f669ed9849f9b4d2ceb4 [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
Serhiy Storchakabdf9e0e2016-12-28 10:16:06 +020046 self._update_max_yx()
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000047 self.stripspaces = 1
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000048 self.lastcmd = None
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000049 win.keypad(1)
50
Serhiy Storchakabdf9e0e2016-12-28 10:16:06 +020051 def _update_max_yx(self):
52 maxy, maxx = self.win.getmaxyx()
53 self.maxy = maxy - 1
54 self.maxx = maxx - 1
55
Eric S. Raymond5af256d2000-08-04 07:33:18 +000056 def _end_of_line(self, y):
Christian Heimesfdab48e2008-01-20 09:06:41 +000057 """Go to the location of the first blank on the given line,
58 returning the index of the last non-blank character."""
Serhiy Storchakabdf9e0e2016-12-28 10:16:06 +020059 self._update_max_yx()
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000060 last = self.maxx
Christian Heimesfdab48e2008-01-20 09:06:41 +000061 while True:
Georg Brandl86b2fb92008-07-16 03:43:04 +000062 if curses.ascii.ascii(self.win.inch(y, last)) != curses.ascii.SP:
Andrew M. Kuchling76276172005-06-02 00:10:04 +000063 last = min(self.maxx, last+1)
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000064 break
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000065 elif last == 0:
66 break
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000067 last = last - 1
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000068 return last
69
Christian Heimesfdab48e2008-01-20 09:06:41 +000070 def _insert_printable_char(self, ch):
Serhiy Storchakabdf9e0e2016-12-28 10:16:06 +020071 self._update_max_yx()
Christian Heimesfdab48e2008-01-20 09:06:41 +000072 (y, x) = self.win.getyx()
Serhiy Storchakabdf9e0e2016-12-28 10:16:06 +020073 backyx = None
74 while y < self.maxy or x < self.maxx:
Christian Heimesfdab48e2008-01-20 09:06:41 +000075 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
Serhiy Storchakabdf9e0e2016-12-28 10:16:06 +020084 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)
Christian Heimesfdab48e2008-01-20 09:06:41 +000094
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000095 def do_command(self, ch):
96 "Process a single editing command."
Serhiy Storchakabdf9e0e2016-12-28 10:16:06 +020097 self._update_max_yx()
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000098 (y, x) = self.win.getyx()
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000099 self.lastcmd = ch
Georg Brandl86b2fb92008-07-16 03:43:04 +0000100 if curses.ascii.isprint(ch):
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000101 if y < self.maxy or x < self.maxx:
Christian Heimesfdab48e2008-01-20 09:06:41 +0000102 self._insert_printable_char(ch)
Georg Brandl86b2fb92008-07-16 03:43:04 +0000103 elif ch == curses.ascii.SOH: # ^a
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000104 self.win.move(y, 0)
Georg Brandl86b2fb92008-07-16 03:43:04 +0000105 elif ch in (curses.ascii.STX,curses.KEY_LEFT, curses.ascii.BS,curses.KEY_BACKSPACE):
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000106 if x > 0:
107 self.win.move(y, x-1)
108 elif y == 0:
109 pass
110 elif self.stripspaces:
Eric S. Raymond5af256d2000-08-04 07:33:18 +0000111 self.win.move(y-1, self._end_of_line(y-1))
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000112 else:
113 self.win.move(y-1, self.maxx)
Georg Brandl86b2fb92008-07-16 03:43:04 +0000114 if ch in (curses.ascii.BS, curses.KEY_BACKSPACE):
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000115 self.win.delch()
Georg Brandl86b2fb92008-07-16 03:43:04 +0000116 elif ch == curses.ascii.EOT: # ^d
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000117 self.win.delch()
Georg Brandl86b2fb92008-07-16 03:43:04 +0000118 elif ch == curses.ascii.ENQ: # ^e
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000119 if self.stripspaces:
Eric S. Raymond5af256d2000-08-04 07:33:18 +0000120 self.win.move(y, self._end_of_line(y))
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000121 else:
122 self.win.move(y, self.maxx)
Georg Brandl86b2fb92008-07-16 03:43:04 +0000123 elif ch in (curses.ascii.ACK, curses.KEY_RIGHT): # ^f
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000124 if x < self.maxx:
125 self.win.move(y, x+1)
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000126 elif y == self.maxy:
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000127 pass
128 else:
129 self.win.move(y+1, 0)
Georg Brandl86b2fb92008-07-16 03:43:04 +0000130 elif ch == curses.ascii.BEL: # ^g
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000131 return 0
Georg Brandl86b2fb92008-07-16 03:43:04 +0000132 elif ch == curses.ascii.NL: # ^j
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000133 if self.maxy == 0:
134 return 0
135 elif y < self.maxy:
136 self.win.move(y+1, 0)
Georg Brandl86b2fb92008-07-16 03:43:04 +0000137 elif ch == curses.ascii.VT: # ^k
Eric S. Raymond5af256d2000-08-04 07:33:18 +0000138 if x == 0 and self._end_of_line(y) == 0:
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000139 self.win.deleteln()
140 else:
Andrew M. Kuchlingccab0012004-10-19 19:29:40 +0000141 # first undo the effect of self._end_of_line
142 self.win.move(y, x)
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000143 self.win.clrtoeol()
Georg Brandl86b2fb92008-07-16 03:43:04 +0000144 elif ch == curses.ascii.FF: # ^l
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000145 self.win.refresh()
Georg Brandl86b2fb92008-07-16 03:43:04 +0000146 elif ch in (curses.ascii.SO, curses.KEY_DOWN): # ^n
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000147 if y < self.maxy:
148 self.win.move(y+1, x)
Eric S. Raymond5af256d2000-08-04 07:33:18 +0000149 if x > self._end_of_line(y+1):
150 self.win.move(y+1, self._end_of_line(y+1))
Georg Brandl86b2fb92008-07-16 03:43:04 +0000151 elif ch == curses.ascii.SI: # ^o
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000152 self.win.insertln()
Georg Brandl86b2fb92008-07-16 03:43:04 +0000153 elif ch in (curses.ascii.DLE, curses.KEY_UP): # ^p
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000154 if y > 0:
155 self.win.move(y-1, x)
Eric S. Raymond5af256d2000-08-04 07:33:18 +0000156 if x > self._end_of_line(y-1):
157 self.win.move(y-1, self._end_of_line(y-1))
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000158 return 1
Guido van Rossumbffa52f2002-09-29 00:25:51 +0000159
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000160 def gather(self):
161 "Collect and return the contents of the window."
162 result = ""
Serhiy Storchakabdf9e0e2016-12-28 10:16:06 +0200163 self._update_max_yx()
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000164 for y in range(self.maxy+1):
165 self.win.move(y, 0)
Eric S. Raymond5af256d2000-08-04 07:33:18 +0000166 stop = self._end_of_line(y)
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000167 if stop == 0 and self.stripspaces:
168 continue
169 for x in range(self.maxx+1):
Christian Heimesfdab48e2008-01-20 09:06:41 +0000170 if self.stripspaces and x > stop:
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000171 break
Georg Brandl86b2fb92008-07-16 03:43:04 +0000172 result = result + chr(curses.ascii.ascii(self.win.inch(y, x)))
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000173 if self.maxy > 0:
174 result = result + "\n"
175 return result
176
177 def edit(self, validate=None):
178 "Edit in the widget window and collect the results."
179 while 1:
180 ch = self.win.getch()
181 if validate:
182 ch = validate(ch)
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000183 if not ch:
184 continue
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000185 if not self.do_command(ch):
186 break
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000187 self.win.refresh()
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000188 return self.gather()
189
190if __name__ == '__main__':
191 def test_editbox(stdscr):
Andrew M. Kuchlinga13ea552004-10-19 19:21:20 +0000192 ncols, nlines = 9, 4
193 uly, ulx = 15, 20
Andrew M. Kuchling8520b942004-10-19 19:36:09 +0000194 stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.")
Andrew M. Kuchlinga13ea552004-10-19 19:21:20 +0000195 win = curses.newwin(nlines, ncols, uly, ulx)
196 rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols)
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000197 stdscr.refresh()
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000198 return Textbox(win).edit()
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000199
200 str = curses.wrapper(test_editbox)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000201 print('Contents of text box:', repr(str))