simple textbox editing widget with emacslike keybindings import curses import curses ascii def rectanglewin uly ulx lry lrx win vlineuly1 ulx curses acsvline lry uly 1 win hlineuly ulx1 curses acshline lrx ulx 1 win hlinelry ulx1 curses acshline lrx ulx 1 win vlineuly1 lrx curses acsvline lry uly 1 win addchuly ulx curses acsulcorner win addchuly lrx curses acsurcorner win addchlry lrx curses acslrcorner win addchlry ulx curses acsllcorner class textbox def initself win insertmodefalse self win win self insertmode insertmode self updatemaxyx self stripspaces 1 self lastcmd none win keypad1 def updatemaxyxself maxy maxx self win getmaxyx self maxy maxy 1 self maxx maxx 1 def endoflineself y the trycatch ignores the error we trigger from some curses versions by trying to write into the lowestrightmost spot in the window remember where to put the cursor back since we are in insertmode first undo the effect of self endofline draw a rectangle with corners at the provided upper left and lower right coordinates editing widget using the interior of a window object supports the following emacs like key bindings ctrl a go to left edge of window ctrl b cursor left wrapping to previous line if appropriate ctrl d delete character under cursor ctrl e go to right edge stripspaces off or end of line stripspaces on ctrl f cursor right wrapping to next line when appropriate ctrl g terminate returning the window contents ctrl h delete character backward ctrl j terminate if the window is 1 line otherwise insert newline ctrl k if line is blank delete it otherwise clear to end of line ctrl l refresh screen ctrl n cursor down move down one line ctrl o insert a blank line at cursor location ctrl p cursor up move up one line move operations do nothing if the cursor is at an edge where the movement is not possible the following synonyms are supported where possible key_left ctrl b key_right ctrl f key_up ctrl p key_down ctrl n key_backspace ctrl h go to the location of the first blank on the given line returning the index of the last non blank character the try catch ignores the error we trigger from some curses versions by trying to write into the lowest rightmost spot in the window remember where to put the cursor back since we are in insert_mode a d e f g j k first undo the effect of self _end_of_line l n o p
import curses
import curses.ascii

def rectangle(win, uly, ulx, lry, lrx):
    
    win.vline(uly+1, ulx, curses.ACS_VLINE, lry - uly - 1)
    win.hline(uly, ulx+1, curses.ACS_HLINE, lrx - ulx - 1)
    win.hline(lry, ulx+1, curses.ACS_HLINE, lrx - ulx - 1)
    win.vline(uly+1, lrx, curses.ACS_VLINE, lry - uly - 1)
    win.addch(uly, ulx, curses.ACS_ULCORNER)
    win.addch(uly, lrx, curses.ACS_URCORNER)
    win.addch(lry, lrx, curses.ACS_LRCORNER)
    win.addch(lry, ulx, curses.ACS_LLCORNER)

class Textbox:
    
    def __init__(self, win, insert_mode=False):
        self.win = win
        self.insert_mode = insert_mode
        self._update_max_yx()
        self.stripspaces = 1
        self.lastcmd = None
        win.keypad(1)

    def _update_max_yx(self):
        maxy, maxx = self.win.getmaxyx()
        self.maxy = maxy - 1
        self.maxx = maxx - 1

    def _end_of_line(self, y):
        
        self._update_max_yx()
        last = self.maxx
        while True:
            if curses.ascii.ascii(self.win.inch(y, last)) != curses.ascii.SP:
                last = min(self.maxx, last+1)
                break
            elif last == 0:
                break
            last = last - 1
        return last

    def _insert_printable_char(self, ch):
        self._update_max_yx()
        (y, x) = self.win.getyx()
        backyx = None
        while y < self.maxy or x < self.maxx:
            if self.insert_mode:
                oldch = self.win.inch()
            
            
            
            try:
                self.win.addch(ch)
            except curses.error:
                pass
            if not self.insert_mode or not curses.ascii.isprint(oldch):
                break
            ch = oldch
            (y, x) = self.win.getyx()
            
            if backyx is None:
                backyx = y, x

        if backyx is not None:
            self.win.move(*backyx)

    def do_command(self, ch):
        "Process a single editing command."
        self._update_max_yx()
        (y, x) = self.win.getyx()
        self.lastcmd = ch
        if curses.ascii.isprint(ch):
            if y < self.maxy or x < self.maxx:
                self._insert_printable_char(ch)
        elif ch == curses.ascii.SOH:                           
            self.win.move(y, 0)
        elif ch in (curses.ascii.STX,curses.KEY_LEFT,
                    curses.ascii.BS,
                    curses.KEY_BACKSPACE,
                    curses.ascii.DEL):
            if x > 0:
                self.win.move(y, x-1)
            elif y == 0:
                pass
            elif self.stripspaces:
                self.win.move(y-1, self._end_of_line(y-1))
            else:
                self.win.move(y-1, self.maxx)
            if ch in (curses.ascii.BS, curses.KEY_BACKSPACE, curses.ascii.DEL):
                self.win.delch()
        elif ch == curses.ascii.EOT:                           
            self.win.delch()
        elif ch == curses.ascii.ENQ:                           
            if self.stripspaces:
                self.win.move(y, self._end_of_line(y))
            else:
                self.win.move(y, self.maxx)
        elif ch in (curses.ascii.ACK, curses.KEY_RIGHT):       
            if x < self.maxx:
                self.win.move(y, x+1)
            elif y == self.maxy:
                pass
            else:
                self.win.move(y+1, 0)
        elif ch == curses.ascii.BEL:                           
            return 0
        elif ch == curses.ascii.NL:                            
            if self.maxy == 0:
                return 0
            elif y < self.maxy:
                self.win.move(y+1, 0)
        elif ch == curses.ascii.VT:                            
            if x == 0 and self._end_of_line(y) == 0:
                self.win.deleteln()
            else:
                
                self.win.move(y, x)
                self.win.clrtoeol()
        elif ch == curses.ascii.FF:                            
            self.win.refresh()
        elif ch in (curses.ascii.SO, curses.KEY_DOWN):         
            if y < self.maxy:
                self.win.move(y+1, x)
                if x > self._end_of_line(y+1):
                    self.win.move(y+1, self._end_of_line(y+1))
        elif ch == curses.ascii.SI:                            
            self.win.insertln()
        elif ch in (curses.ascii.DLE, curses.KEY_UP):          
            if y > 0:
                self.win.move(y-1, x)
                if x > self._end_of_line(y-1):
                    self.win.move(y-1, self._end_of_line(y-1))
        return 1

    def gather(self):
        "Collect and return the contents of the window."
        result = ""
        self._update_max_yx()
        for y in range(self.maxy+1):
            self.win.move(y, 0)
            stop = self._end_of_line(y)
            if stop == 0 and self.stripspaces:
                continue
            for x in range(self.maxx+1):
                if self.stripspaces and x > stop:
                    break
                result = result + chr(curses.ascii.ascii(self.win.inch(y, x)))
            if self.maxy > 0:
                result = result + "\n"
        return result

    def edit(self, validate=None):
        "Edit in the widget window and collect the results."
        while 1:
            ch = self.win.getch()
            if validate:
                ch = validate(ch)
            if not ch:
                continue
            if not self.do_command(ch):
                break
            self.win.refresh()
        return self.gather()

if __name__ == '__main__':
    def test_editbox(stdscr):
        ncols, nlines = 9, 4
        uly, ulx = 15, 20
        stdscr.addstr(uly-2, ulx, "Use Ctrl-G to end editing.")
        win = curses.newwin(nlines, ncols, uly, ulx)
        rectangle(stdscr, uly-1, ulx-1, uly + nlines, ulx + ncols)
        stdscr.refresh()
        return Textbox(win).edit()

    str = curses.wrapper(test_editbox)
    print('Contents of text box:', repr(str))