blob: 97ee36bdb5654a486dc2cf902cf8adc5fe1f6ec7 [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
3import sys, curses, ascii
4
5def rectangle(win, uly, ulx, lry, lrx):
6 "Draw a rectangle."
7 win.vline(uly+1, ulx, curses.ACS_VLINE, lry - uly - 1)
8 win.hline(uly, ulx+1, curses.ACS_HLINE, lrx - ulx - 1)
9 win.hline(lry, ulx+1, curses.ACS_HLINE, lrx - ulx - 1)
10 win.vline(uly+1, lrx, curses.ACS_VLINE, lry - uly - 1)
11 win.addch(uly, ulx, curses.ACS_ULCORNER)
12 win.addch(uly, lrx, curses.ACS_URCORNER)
13 win.addch(lry, lrx, curses.ACS_LRCORNER)
14 win.addch(lry, ulx, curses.ACS_LLCORNER)
15
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000016class Textbox:
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000017 """Editing widget using the interior of a window object.
18 Supports the following Emacs-like key bindings:
19
20 Ctrl-A Go to left edge of window.
21 Ctrl-B Cursor left, wrapping to previous line if appropriate.
22 Ctrl-D Delete character under cursor.
23 Ctrl-E Go to right edge (nospaces off) or end of line (nospaces on).
24 Ctrl-F Cursor right, wrapping to next line when appropriate.
25 Ctrl-G Terminate, returning the window contents.
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000026 Ctrl-H Delete character backward.
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000027 Ctrl-J Terminate if the window is 1 line, otherwise insert newline.
28 Ctrl-K If line is blank, delete it, otherwise clear to end of line.
29 Ctrl-L Refresh screen
30 Ctrl-N Cursor down; move down one line.
31 Ctrl-O Insert a blank line at cursor location.
32 Ctrl-P Cursor up; move up one line.
33
34 Move operations do nothing if the cursor is at an edge where the movement
35 is not possible. The following synonyms are supported where possible:
36
37 KEY_LEFT = Ctrl-B, KEY_RIGHT = Ctrl-F, KEY_UP = Ctrl-P, KEY_DOWN = Ctrl-N
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000038 KEY_BACKSPACE = Ctrl-h
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000039 """
40 def __init__(self, win):
41 self.win = win
42 (self.maxy, self.maxx) = win.getmaxyx()
43 self.maxy = self.maxy - 1
44 self.maxx = self.maxx - 1
45 self.stripspaces = 1
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000046 self.lastcmd = None
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000047 win.keypad(1)
48
49 def firstblank(self, y):
50 "Go to the location of the first blank on the given line."
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000051 last = self.maxx
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000052 while 1:
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000053 if ascii.ascii(self.win.inch(y, last)) != ascii.SP:
54 last = last + 1
55 break
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000056 elif last == 0:
57 break
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000058 last = last - 1
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000059 return last
60
61 def do_command(self, ch):
62 "Process a single editing command."
63 (y, x) = self.win.getyx()
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000064 self.lastcmd = ch
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000065 if ascii.isprint(ch):
66 if y < self.maxy or x < self.maxx:
67 # The try-catch ignores the error we trigger from some curses
68 # versions by trying to write into the lowest-rightmost spot
69 # in the self.window.
70 try:
71 self.win.addch(ch)
72 except ERR:
73 pass
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000074 elif ch == ascii.SOH: # ^a
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000075 self.win.move(y, 0)
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000076 elif ch in (ascii.STX,curses.KEY_LEFT, ascii.BS,curses.KEY_BACKSPACE):
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000077 if x > 0:
78 self.win.move(y, x-1)
79 elif y == 0:
80 pass
81 elif self.stripspaces:
82 self.win.move(y-1, self.firstblank(y-1))
83 else:
84 self.win.move(y-1, self.maxx)
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000085 if ch in (ascii.BS, curses.KEY_BACKSPACE):
86 self.win.delch()
87 elif ch == ascii.EOT: # ^d
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000088 self.win.delch()
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000089 elif ch == ascii.ENQ: # ^e
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000090 if self.stripspaces:
91 self.win.move(y, self.firstblank(y, maxx))
92 else:
93 self.win.move(y, self.maxx)
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000094 elif ch in (ascii.ACK, curses.KEY_RIGHT): # ^f
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000095 if x < self.maxx:
96 self.win.move(y, x+1)
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +000097 elif y == self.maxy:
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +000098 pass
99 else:
100 self.win.move(y+1, 0)
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000101 elif ch == ascii.BEL: # ^g
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000102 return 0
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000103 elif ch == ascii.NL: # ^j
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000104 if self.maxy == 0:
105 return 0
106 elif y < self.maxy:
107 self.win.move(y+1, 0)
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000108 elif ch == ascii.VT: # ^k
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000109 if x == 0 and self.firstblank(y) == 0:
110 self.win.deleteln()
111 else:
112 self.win.clrtoeol()
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000113 elif ch == ascii.FF: # ^l
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000114 self.win.refresh()
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000115 elif ch in (ascii.SO, curses.KEY_DOWN): # ^n
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000116 if y < self.maxy:
117 self.win.move(y+1, x)
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000118 elif ch == ascii.SI: # ^o
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000119 self.win.insertln()
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000120 elif ch in (ascii.DLE, curses.KEY_UP): # ^p
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000121 if y > 0:
122 self.win.move(y-1, x)
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000123 return 1
124
125 def gather(self):
126 "Collect and return the contents of the window."
127 result = ""
128 for y in range(self.maxy+1):
129 self.win.move(y, 0)
130 stop = self.firstblank(y)
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000131 #sys.stderr.write("y=%d, firstblank(y)=%d\n" % (y, stop))
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000132 if stop == 0 and self.stripspaces:
133 continue
134 for x in range(self.maxx+1):
135 if self.stripspaces and x == stop:
136 break
137 result = result + chr(ascii.ascii(self.win.inch(y, x)))
138 if self.maxy > 0:
139 result = result + "\n"
140 return result
141
142 def edit(self, validate=None):
143 "Edit in the widget window and collect the results."
144 while 1:
145 ch = self.win.getch()
146 if validate:
147 ch = validate(ch)
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000148 if not ch:
149 continue
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000150 if not self.do_command(ch):
151 break
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000152 self.win.refresh()
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000153 return self.gather()
154
155if __name__ == '__main__':
156 def test_editbox(stdscr):
157 win = curses.newwin(4, 9, 15, 20)
158 rectangle(stdscr, 14, 19, 19, 29)
159 stdscr.refresh()
Andrew M. Kuchlinge8d7dbf2000-06-27 00:53:12 +0000160 return Textbox(win).edit()
Andrew M. Kuchling2b9d0bc2000-06-26 23:55:42 +0000161
162 str = curses.wrapper(test_editbox)
163 print str