math4tots
math4tots

Reputation: 8870

Python curses dilemma

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?

Upvotes: 12

Views: 12647

Answers (5)

Jim Dennis
Jim Dennis

Reputation: 17500

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.

Upvotes: 3

Henrik Lindgren
Henrik Lindgren

Reputation: 669

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)

Upvotes: 47

Amber
Amber

Reputation: 526473

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.

Upvotes: 1

tkone
tkone

Reputation: 22728

You need to capture the signal and run endwin() during the capture.

For info on this, look at this SO answer: How do I capture SIGINT in Python?

Upvotes: -1

orlp
orlp

Reputation: 117641

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" )

Upvotes: 7

Related Questions