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.