Python curses dilemma

11,624

Solution 1

You could do this:

def main():
    curses.initscr()

    try:
        curses.cbreak()
        for i in range(3):
            time.sleep(1)
            curses.flash()
            pass
        print( "Hello World" )
    finally:
        curses.endwin()

Or more nicely, make a context wrapper:

class CursesWindow(object):
    def __enter__(self):
        curses.initscr()

    def __exit__(self):
        curses.endwin()

def main():
    with CursesWindow():
        curses.cbreak()
        for i in range(3):
            time.sleep(1)
            curses.flash()
            pass
        print( "Hello World" )

Solution 2

I believe you are looking for curses.wrapper See http://docs.python.org/dev/library/curses.html#curses.wrapper

It will do curses.cbreak(), curses.noecho() and curses_screen.keypad(1) on init and reverse them on exit, even if the exit was an exception.

Your program goes as a function to the wrapper, example:

def main(screen):
    """screen is a curses screen passed from the wrapper"""
    ...

if __name__ == '__main__':
    curses.wrapper(main)

Solution 3

My advice: For testing purposes, call your script using a simple wrapper shell script; have the shell script perform a reset command to bring your terminal settings back into a usable state:

#!/bin/sh
eval "$@"
stty -sane
reset

... call that as run.sh and be happy. This should run your command almost exactly as your shell would if you entered the arguments as a command (more exactly if you wrap the arguments in hard quotes).

To ensure that your program will leave the terminal in a robust state, in the the face of uncaught exceptions and abnormal terminations ... either use the curses.wrapper() method to call your top level entry point (probably main() or whatever main_curses_ui() you choose to implement) or wrap your code in your own sequence of curses.* methods to restore cursor visibility, restore "cbreak" (canonical/cooked input) mode, restore the normal "echo" settings and whatever else you may have mucked with.

You can also use the Python: atexit Handlers to register all your clean-up actions. But there might still be cases where your code doesn't get called --- some sorts of non-catchable signals and any situation where os._exit() is invoked.

My little shell script wrapper should be fairly robust even in those cases.

Solution 4

You can:

  • wrap your code in a try/finally block that calls curses.endwin()
  • capture the interrupt signal specifically via the signal library
  • use the atexit library.

The first option is probably the simplest for a basic case (if you're not running much code).

The second option is the most specific, if you want to do something special for Ctrl+C.

The last option is the most robust if you always want to do certain shutdown actions, no matter how your program is ending.

Share:
11,624
math4tots
Author by

math4tots

Updated on June 18, 2022

Comments

  • math4tots
    math4tots about 2 years

    I'm playing around a little with Python and curses.

    When I run

    import time
    import curses
    
    def main():
        curses.initscr()
        curses.cbreak()
        for i in range(3):
            time.sleep(1)
            curses.flash()
            pass
        print( "Hello World" )
        curses.endwin()
    
    if __name__ == '__main__':
        main()
    

    if I wait all the way through, curses.endwin() gets called so everything works out fine. However, if I cut it short with Ctrl-C, curses.endwin() never gets called so it screws up my terminal session.

    What is the proper way to handle this situation? How can I make sure that no matter how I try to end/interrupt the program (e.g. Ctrl-C, Ctrl-Z), it doesn't mess up the terminal?

  • asmeurer
    asmeurer almost 11 years
    Is atexit really more robust than finally?
  • Amber
    Amber almost 11 years
    @asmeurer It's more a matter of the finally version requires you to guarantee that all of your program code is within the try block. In, say, a multithreaded environment, that's not necessarily the case.