Using Curses with Python¶
Python’s curses module provides “terminal handling for character-cell displays”.
Here is a “Hello Curses!” program in a file named hello_curses.py:
import curses
from curses import wrapper
def main(stdscr):
stdscr.clear()
stdscr.addstr(11, 32, "Hello CSC 221!")
stdscr.refresh()
stdscr.getch()
wrapper(main)
Notice that we are not using our usual print function here to display
output, nor are we using input. Instead, we create a screen object,
stdscr, and use its addstr method to print a string and its getch
method to get a character.
In our next example, read_chars.py, we save the character returned by
getchar an print out both its character and numeric representation:
import curses
def main(stdscr):
curses.noecho()
stdscr.addstr(9, 25, "Enter a character or '!' to quite: ")
stdscr.refresh()
ch = stdscr.getch()
while ch != ord('!'):
stdscr.clear()
stdscr.addstr(9, 25, "Enter a character or '!' to quite: ")
stdscr.addstr(
12, 18,
f"You entered '{chr(ch)}', which has a numeric value of {ch}."
)
stdscr.move(9, 60)
stdscr.refresh()
ch = stdscr.getch()
if __name__ == "__main__":
curses.wrapper(main)
This method returns the numeric value of the key pressed, so we need to use
Python’s char function convert into a printable character.
Exploring the window environment¶
This next example, interrogate_env.py, uses several of the available curses functions to get information about the window environment:
import curses
COLOR_ORANGE = 8
def display_heading(win):
win.attron(curses.A_STANDOUT)
win.addstr(2, 22, "ncurses")
win.attroff(curses.A_STANDOUT)
win.addstr(2, 22 + len("ncurses"), " Environment Interrogator")
win.box()
win.refresh()
def test_color_support(win):
colors = "YES" if curses.has_colors() else "NO"
win.addstr(2, 5, "Supports color: ")
if colors == "YES":
curses.start_color()
curses.init_pair(1, curses.COLOR_CYAN, curses.COLOR_BLACK)
win.attron(curses.color_pair(1))
win.addstr(2, 5 + len("Supports color: "), colors)
if colors == "YES":
win.attroff(curses.color_pair(1))
def test_change_color_support(win):
change_color = "YES" if curses.can_change_color() else "NO"
win.addstr(4, 5, "Supports change color: ")
if change_color == "YES":
curses.init_color(COLOR_ORANGE, 1000, 500, 0)
curses.init_pair(2, COLOR_ORANGE, curses.COLOR_BLACK)
win.attron(curses.color_pair(2))
win.addstr(4, 5 + len("Supports change color: "), change_color)
if change_color == "YES":
win.attroff(curses.color_pair(2))
def display_window_sizes(win0, win1, win2):
rows, cols = win0.getmaxyx()
win2.addstr(6, 5, f"Main window size: {rows} by {cols}")
win2_rows, win2_cols = win2.getmaxyx()
win2.addstr(8, 5, f"Current window size: {win2_rows} by {win2_cols}")
win1_rows, win1_cols = win1.getmaxyx()
win2.addstr(10, 5, f"Top window size: {win1_rows} by {win1_cols}")
beg_y, beg_x = win2.getbegyx()
win2.addstr(12, 5, f"Current window top left pos: {beg_y}, {beg_x}")
def main(stdscr):
curses.noecho()
stdscr.refresh()
win1 = curses.newwin(5, 76, 1, 2)
win2 = curses.newwin(15, 46, 6, 18)
display_heading(win1)
test_color_support(win2)
test_change_color_support(win2)
display_window_sizes(stdscr, win1, win2)
win2.box()
win2.refresh()
stdscr.addstr(22, 25, "Enter a character to quit: ")
stdscr.getch()
if __name__ == "__main__":
curses.wrapper(main)
Running this will on a terminal that supports colors and color changes will produce a screen something like this:
Additional Curses Configuration¶
There are a few things that will be useful to setup as we get deeper into our exploration of Curses.
First, The convention is that terminal size should be 80 columns x 24 rows. (The history of this convention is fascinating - see this StackOverflow thread for more context ). It’s possible to resize modern terminal windows, but we’ll want to standardize on these dimensions for many of our upcoming projects.
The other challenging thing about using Curses is that its output takes up the
entire terminal, and uses the stdout system stream to control the output.
This means that, while our program is using curses, we can not use Python’s
standard print() function. There are lots of ways to get around this, but
our favorite is to use named pipes
to redirect output to a different terminal window.
To set this up on a unix system, create a named pipe with this command:
$ mkfifo /tmp/debug_pipe
Then in a new terminal window, type this:
$ tail -f /tmp/debug_pipe
This command will listen to the pipe and print out anything that is written to it.
Here’s some starter Python code that sets the terminal size and writes a sample message to the pipe.
import curses
import subprocess
output_pipe = open("/tmp/debug_pipe", "w")
def main(stdscr):
width = 80
height = 24
rows, cols = stdscr.getmaxyx()
curses.start_color()
curses.resize_term(height, width)
subprocess.call(["/usr/bin/resize", "-s", str(height), str(width)])
output_pipe.write("Resized screen to 80x24\n")
output_pipe.flush()
stdscr.addstr(6, 32, f"Original window size: {rows} by {cols}")
stdscr.addstr(11, 32, "Now, This screen is 24x80!")
stdscr.refresh()
# clear any output that might have come from resize, and then
# wait for user to press another key to exit
curses.flushinp()
stdscr.getch()
if __name__ == "__main__":
curses.wrapper(main)