Python Curses

From Johns Cool Things
Jump to: navigation, search

Python Curses

Curses is a library (originally for C) that was designed for doing complex terminal effects. It allows the user to move the cursor to any position in the screen, print special characters, print different colors, and interpret keypresses as events, instead of having them buffered on the command line. This means that one can make rich user interfaces in the terminal with this library.

Lets look at the basic setup:

Start curses:

import curses
stdscr = curses.initscr()
curses.cbreak()
stdscr.keypad(1)

Clean it up:

curses.nocbreak()
stdscr.keypad(0)
curses.echo()
curses.endwin()

You will always want the cleanup code to run, reguardless of how the program exits, or else your terminal will freak out. So to make sure that this code is called, we will use the try: finally: blocks.

Good Curses stub program

#!/usr/bin/python

import curses
stdscr = curses.initscr()
curses.cbreak()
stdscr.keypad(1) 

try:
# Run your code here
    pass
finally:
    curses.nocbreak()
    stdscr.keypad(0)
    curses.echo()
    curses.endwin()

First Curses script

Now lets say we want to print an X character at the location row 10, column 5. We will write the character with stdscr.addch(row,col,'X'). However, this does not show up on the screen until we refresh (for efficiency) so we can call stdscr.refresh() to cause this to happen. Finally we will want to call time.sleep(3) so that we can actually see the results (normally, curses screens are cleared when the cleanup code is run).

#!/usr/bin/python

import curses
import time
stdscr = curses.initscr()
curses.cbreak()
stdscr.keypad(1)

try:
# Run your code here
    stdscr.addch(10,5,"X")
    stdscr.refresh()
    time.sleep(3)
finally:
    curses.nocbreak()
    stdscr.keypad(0)
    curses.echo()
    curses.endwin()

Getting screen dimensions

Now lets say we want to draw X's down the diagonal. We will need to know when to stop, so we will need to know the width and the height of the screen. This is done with the stdscr.getmaxyx() function. This function returns a tuple with the (height, width) values.

#!/usr/bin/python

import curses
import time
stdscr = curses.initscr()
curses.cbreak()
curses.noecho()
stdscr.keypad(1)

try:
# Run your code here
    height,width = stdscr.getmaxyx()
    num = min(height,width)
    for x in range(num):
        stdscr.addch(x,x,'X')
    stdscr.refresh()
    time.sleep(3)
finally:
    curses.nocbreak()
    stdscr.keypad(0)
    curses.echo()
    curses.endwin()

Printing Strings and Special characters

If we want to add a string, instead of an individual character, we can use the function stdscr.addstr(row,col,str) which will print the given string, left aligned, to that location.

Now, lets say we want to print out some lines, as delimiters. Maybe we want to draw a staircase of some sort, or a maze. The characters on your standard 104 keyboard are not sufficient to the task, so we need to access the extended characters in the ACS set. These are given names like curses.ACS_<character_name>. For example, a horizontal line would be curses.ACS_HLINE, and a vertical line would be curses.ACS_VLINE. If we want a corner, we can specify which kind of corner with (U|L)(L|R)CORNER, for Upper|Lower,Left|Right,corner. So if we want the lower left corner, that would be curses.ACS_LLCORNER, at the upper right corner would be curses.ACS_URCORNER. There are also TEEs and the PLUS (cross).

#!/usr/bin/python

import curses
import time
stdscr = curses.initscr()
curses.cbreak()
curses.noecho()
stdscr.keypad(1)

try:
# Run your code here
    height,width = stdscr.getmaxyx()
    num = min(height,width)
    for x in range(num):
        stdscr.addch(x,x,'X')

    # Print Hello World along the top
    stdscr.addstr(0,10,"Hello World")

    #Draw some lines
    for x in range(8):
        stdscr.addch(4,x,curses.ACS_HLINE)
    stdscr.addch(4,8,curses.ACS_PLUS)
    stdscr.addch(4,9,curses.ACS_LRCORNER)

    stdscr.refresh()
    time.sleep(3)
finally:
    curses.nocbreak()
    stdscr.keypad(0)
    curses.echo()
    curses.endwin()

Getting user input

Now lets say we want to allow the user to input some value, lets say a character. We use the stdscr.getch() function for this. getch returns an integer, and we will need to compare it against ord('x') if we want to compare it to an ascii character.

#!/usr/bin/python

import curses
import time
stdscr = curses.initscr()
curses.cbreak()
curses.noecho()
stdscr.keypad(1)

try:
# Run your code here
    height,width = stdscr.getmaxyx()
    num = min(height,width)
    for x in range(num):
        stdscr.addch(x,x,'X')

    # Print Hello World along the top
    stdscr.addstr(0,10,"Hello World")

    #Draw some lines
    for x in range(8):
        stdscr.addch(4,x,curses.ACS_HLINE)
    stdscr.addch(4,8,curses.ACS_PLUS)
    stdscr.addch(4,9,curses.ACS_LRCORNER)

    stdscr.refresh()
    key = 'X'
    while key != ord('q'):
        key = stdscr.getch()
        stdscr.addch(20,25,key)
        stdscr.refresh()
finally:
    curses.nocbreak()
    stdscr.keypad(0)
    curses.echo()
    curses.endwin()

Alternatively, key presses on the arrow keys can be found as (curses.KEY_UP curses.KEY_LEFT curses.KEY_RIGHT curses.KEY_DOWN)

Cleaning up

Since Python 2.6 we have had the extremely nice "with" statement in python, which can clean up some code immensely. Ideally, it would be nice to have a collection of standard curses configurations in the standard curses library, which would be compatible with the with statement. Unless and until such is provided as a standard, you can implement your own as follows.

#!/usr/bin/python

import curses

# -----------------------------------------------------------------------------
class curses_screen:
    def __enter__(self):
        self.stdscr = curses.initscr()
        curses.cbreak()
        curses.noecho()
        self.stdscr.keypad(1)
        return self.stdscr
    def __exit__(self,a,b,c):
        curses.nocbreak()
        self.stdscr.keypad(0)
        curses.echo()
        curses.endwin()

# -----------------------------------------------------------------------------
with curses_screen() as stdscr:
    pass # Do your stuff here...