codekingpro/portable-devtools
114k
1"""Simple textbox editing widget with Emacs-like keybindings."""2 3import curses4import curses.ascii5 6def rectangle(win, uly, ulx, lry, lrx):7 """Draw a rectangle with corners at the provided upper-left8 and lower-right coordinates.9 """10 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 19class Textbox:20 """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.26 Ctrl-E Go to right edge (stripspaces off) or end of line (stripspaces on).27 Ctrl-F Cursor right, wrapping to next line when appropriate.28 Ctrl-G Terminate, returning the window contents.29 Ctrl-H Delete character backward.30 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.32 Ctrl-L Refresh screen.33 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 movement38 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-N41 KEY_BACKSPACE = Ctrl-h42 """43 def __init__(self, win, insert_mode=False):44 self.win = win45 self.insert_mode = insert_mode46 self._update_max_yx()47 self.stripspaces = 148 self.lastcmd = None49 win.keypad(1)50 51 def _update_max_yx(self):52 maxy, maxx = self.win.getmaxyx()53 self.maxy = maxy - 154 self.maxx = maxx - 155 56 def _end_of_line(self, y):57 """Go to the location of the first blank on the given line,58 returning the index of the last non-blank character."""59 self._update_max_yx()60 last = self.maxx61 while True:62 if curses.ascii.ascii(self.win.inch(y, last)) != curses.ascii.SP:63 last = min(self.maxx, last+1)64 break65 elif last == 0:66 break67 last = last - 168 return last69 70 def _insert_printable_char(self, ch):71 self._update_max_yx()72 (y, x) = self.win.getyx()73 backyx = None74 while y < self.maxy or x < self.maxx:75 if self.insert_mode:76 oldch = self.win.inch()77 # The try-catch ignores the error we trigger from some curses78 # versions by trying to write into the lowest-rightmost spot79 # in the window.80 try:81 self.win.addch(ch)82 except curses.error:83 pass84 if not self.insert_mode or not curses.ascii.isprint(oldch):85 break86 ch = oldch87 (y, x) = self.win.getyx()88 # Remember where to put the cursor back since we are in insert_mode89 if backyx is None:90 backyx = y, x91 92 if backyx is not None:93 self.win.move(*backyx)94 95 def do_command(self, ch):96 "Process a single editing command."97 self._update_max_yx()98 (y, x) = self.win.getyx()99 self.lastcmd = ch100 if curses.ascii.isprint(ch):101 if y < self.maxy or x < self.maxx:102 self._insert_printable_char(ch)103 elif ch == curses.ascii.SOH: # ^a104 self.win.move(y, 0)105 elif ch in (curses.ascii.STX,curses.KEY_LEFT,106 curses.ascii.BS,107 curses.KEY_BACKSPACE,108 curses.ascii.DEL):109 if x > 0:110 self.win.move(y, x-1)111 elif y == 0:112 pass113 elif self.stripspaces:114 self.win.move(y-1, self._end_of_line(y-1))115 else:116 self.win.move(y-1, self.maxx)117 if ch in (curses.ascii.BS, curses.KEY_BACKSPACE, curses.ascii.DEL):118 self.win.delch()119 elif ch == curses.ascii.EOT: # ^d120 self.win.delch()121 elif ch == curses.ascii.ENQ: # ^e122 if self.stripspaces:123 self.win.move(y, self._end_of_line(y))124 else:125 self.win.move(y, self.maxx)126 elif ch in (curses.ascii.ACK, curses.KEY_RIGHT): # ^f127 if x < self.maxx:128 self.win.move(y, x+1)129 elif y == self.maxy:130 pass131 else:132 self.win.move(y+1, 0)133 elif ch == curses.ascii.BEL: # ^g134 return 0135 elif ch == curses.ascii.NL: # ^j136 if self.maxy == 0:137 return 0138 elif y < self.maxy:139 self.win.move(y+1, 0)140 elif ch == curses.ascii.VT: # ^k141 if x == 0 and self._end_of_line(y) == 0:142 self.win.deleteln()143 else:144 # first undo the effect of self._end_of_line145 self.win.move(y, x)146 self.win.clrtoeol()147 elif ch == curses.ascii.FF: # ^l148 self.win.refresh()149 elif ch in (curses.ascii.SO, curses.KEY_DOWN): # ^n150 if y < self.maxy:151 self.win.move(y+1, x)152 if x > self._end_of_line(y+1):153 self.win.move(y+1, self._end_of_line(y+1))154 elif ch == curses.ascii.SI: # ^o155 self.win.insertln()156 elif ch in (curses.ascii.DLE, curses.KEY_UP): # ^p157 if y > 0:158 self.win.move(y-1, x)159 if x > self._end_of_line(y-1):160 self.win.move(y-1, self._end_of_line(y-1))161 return 1162 163 def gather(self):164 "Collect and return the contents of the window."165 result = ""166 self._update_max_yx()167 for y in range(self.maxy+1):168 self.win.move(y, 0)169 stop = self._end_of_line(y)170 if stop == 0 and self.stripspaces:171 continue172 for x in range(self.maxx+1):173 if self.stripspaces and x > stop:174 break175 result = result + chr(curses.ascii.ascii(self.win.inch(y, x)))176 if self.maxy > 0:177 result = result + "\n"178 return result179 180 def edit(self, validate=None):181 "Edit in the widget window and collect the results."182 while 1:183 ch = self.win.getch()184 if validate:185 ch = validate(ch)186 if not ch:187 continue188 if not self.do_command(ch):189 break190 self.win.refresh()191 return self.gather()192 193if __name__ == '__main__':194 def test_editbox(stdscr):195 ncols, nlines = 9, 4196 uly, ulx = 15, 20197 stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.")198 win = curses.newwin(nlines, ncols, uly, ulx)199 rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols)200 stdscr.refresh()201 return Textbox(win).edit()202 203 str = curses.wrapper(test_editbox)204 print('Contents of text box:', repr(str))205 