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:

interrogate_env.py screenshot