简体   繁体   中英

Python curses - resize terminal while inputting text

I am trying to do a simple GUI with curses but I am stuck with some problems when resizing and redrawing the screen. Here is my code:

import curses
import curses.textpad as textpad

class editor (object):
    def __init__(self, stdscreen):
        self.screen = stdscreen
        self.height,self.width = self.screen.getmaxyx()
        self.draw_screen()
        while True:
            # Check if the screen has been resized
            resize = curses.is_term_resized(self.height, self.width)
            if resize:
                self.height,self.width = self.screen.getmaxyx()
                self.draw_screen()
            curses.noecho()
            text = textpad.Textbox(self.text).edit(validate=self.validate)
            self.body.addstr(0,0, str(text), curses.A_NORMAL)
            self.body.refresh()
            self.text.erase()
    def draw_screen (self):
        self.screen.clear()
        self.header=self.screen.subwin(1,self.width,0,0)
        self.body=self.screen.subwin(self.height-3,self.width,1,0)
        self.text=self.screen.subwin(1,self.width,self.height-2,0)
        self.footer=self.screen.subwin(self.height-1,0)
        header_text = "{0:<{n}}".format("Header", n=self.width-1)
        self.header.addstr(0,0, header_text, curses.A_REVERSE)
        self.footer.addstr(0,0, "^W", curses.A_REVERSE)
        self.footer.addstr(0,2, " Quit", curses.A_NORMAL)
        self.header.noutrefresh()
        self.body.noutrefresh()
        self.footer.noutrefresh()
        self.text.noutrefresh()
        curses.doupdate()
    def validate (self, ch):
        # If resize event redraw screen
        if ch==curses.KEY_RESIZE:
            self.height,self.width = self.screen.getmaxyx()
            self.draw_screen()
        if ch==23: # ^w quit
            exit()
        else:
            return ch

if __name__=="__main__":
    curses.wrapper(editor)

When the terminal is not been resized it works like expected, but when I enlarge the terminal window, while the Header and footer moves to the top and bottom row respectively, the cursor remains in the previous position until the string is accepted by pressing Enter. Same when shrinking, except that in this case the cursor goes over the footer and the content of the footer is returned. Trying to debug the problem I found that the sub-windows seem to move all right, but the cursor is not moving with them. I also tried a

self.text.move(0,0)

to move the cursor back to the right position, but it didn't work.

Bonus: How can I preserve the text inserted before the resize action and add it to the resized displayed screen?

Edit: After some more debugging I came up with a pseudo solution that "kind of" works (but is ugly and hacky).

import curses
import curses.textpad as textpad

class my_textbox(textpad.Textbox):

    def edit(self, validate=None):
        while 1:
            ch = self.win.getch()
            # Return a list instead of a string if the last key was a resize
            if ch==curses.KEY_RESIZE:
                return[self.gather(),True]
            if validate:
                ch = validate(ch)
            if not ch:
                continue
            if not self.do_command(ch):
                break
            self.win.refresh()
        return self.gather()

class editor (object):
    def __init__(self, stdscreen):
        self.screen = stdscreen
        self.height,self.width = self.screen.getmaxyx()
        # Initialize a variable to store the previous text
        self.ptext=None
        curses.noecho()
        self.draw_screen()
        while True:
            # Check if the screen has been resized
            resize = curses.is_term_resized(self.height, self.width)
            if resize:
                self.height,self.width = self.screen.getmaxyx()
                self.draw_screen()
            text = my_textbox(self.text).edit(validate=self.validate)
            # If the window has been resized (aka textbox returned a list)
            if isinstance (text, list):
                text=text[0]
                # Store the previous text
                self.ptext=text.strip()
                continue
            self.body.addstr(0,0, str(text), curses.A_NORMAL)
            self.body.refresh()
            self.text.erase()
    def draw_screen (self):
        self.screen.clear()
        self.header=self.screen.subwin(1,self.width,0,0)
        self.body=self.screen.subwin(self.height-3,self.width,1,0)
        self.text=self.screen.subwin(1,self.width,self.height-2,0)
        self.footer=self.screen.subwin(self.height-1,0)
        header_text = "{0:<{n}}".format("Header", n=self.width-1)
        self.header.addstr(0,0, header_text, curses.A_REVERSE)
        # If there was text previous to resize
        if self.ptext:
            # To prevent _curses.error: addwstr() returned ERR
            # when shrinking window cut out the last character
            # (!) Raises ERR anyway when shrinking with double click from
            # titlebar, also, when resizing is too fast, ptext become a series of ^?
            if len(self.ptext)<self.width-1:
                self.text.addstr(0,0,self.ptext.strip())
            else:
                self.ptext=self.ptext[:self.width-1]
                self.text.addstr(0,0,self.ptext.strip())
        self.footer.addstr(0,0, "^W", curses.A_REVERSE)
        self.footer.addstr(0,2, " Quit", curses.A_NORMAL)
        self.header.noutrefresh()
        self.body.noutrefresh()
        self.footer.noutrefresh()
        self.text.noutrefresh()
        curses.doupdate()

    def validate (self, ch):
        if ch==23: # ^w quit
            exit()
        else:
            return ch

if __name__=="__main__":
    curses.wrapper(editor)

what I've done :

I redefined the edit method of the Textbox class, so that when the windows is resized it returns a list instead of a string.

In the input loop when the window is resized the previous text is stored ad a new cycle is started.

If previous text is present the drawing function adds it to the subwindow.

notes:

Resizing by double-click on the titlebar throws throws an error and if resizing is "too fast" the string is replaced by garbage ("^?", which is actually 2^64-1 before conversion by curses).

I suppose that because most of the functionality of (n)curses have been created with non resizeable terminals in mind the implementation does make sense after all but, at least for someone relatively new to python like me, is not intuitive at all.

The description sounds right: your application has to re-layout the text and redraw the contents of the screen.

To do this, it has to keep a record of the information. Some of that is predefined such as "Header" in this line

header_text = "{0:<{n}}".format("Header", n=self.width-1)

But the contents of sub windows may be lost if the screen is made smaller (since resizeterm in ncurses will truncate those windows):

self.header=self.screen.subwin(1,self.width,0,0)

as well as any window, eg, created with newwin . Python's textpad class does that.

curses (in particular, ncurses which is used by Python) supports a quasi-window called "pad" , which you could use in some cases. But it is not readily usable by its textpad class (since the underlying calls for refreshing the pad differ from those for a window ).

However (this would take some real work), you could create a pad which is as large as the screen which you would like to manage, and make the textpad a child of that, and then do your own re-layout independent of resizeterm . Python's documentation for the related resize_term notes (copied from the ncurses manual ):

Backend function used by resizeterm() , performing most of the work; when resizing the windows, resize_term() blank-fills the areas that are extended. The calling application should fill in these areas with appropriate data. The resize_term() function attempts to resize all windows. However, due to the calling convention of pads , it is not possible to resize these without additional interaction with the application.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM