Chapter 7 Exercise Set 1: Unix Pipes and Curses

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)

Generating a simple menu

This program creates a simple menu.

import curses

def main(stdscr):
    # Constants
    enter_key = 10
    
    # Menu choices
    choices = [
        "1st Choice",
        "2nd Choice", 
        "3rd Choice",
        "Choose me!",
        "Not me!"
    ]
    
    highlight = 0
    
    # Setup
    curses.curs_set(0)  # Hide cursor
    stdscr.keypad(True)  # Enable special keys
    
    # Get screen dimensions
    height, width = stdscr.getmaxyx()
    
    # Create menu window
    menu_height = len(choices) + 4
    menu_width = width - 2
    menu_y = height - menu_height
    menu_x = 1
    
    menuwin = curses.newwin(menu_height, menu_width, menu_y, menu_x)
    menuwin.keypad(True)  # Enable special keys for the menu window
    menuwin.box()
    
    stdscr.refresh()
    menuwin.refresh()
    
    while True:
        # Display menu items
        for i, choice in enumerate(choices):
            if i == highlight:
                menuwin.attron(curses.A_REVERSE)
            menuwin.addstr(i + 2, 3, choice)
            menuwin.attroff(curses.A_REVERSE)
        
        menuwin.refresh()
        
        # Get user input
        choice = menuwin.getch()
        
        # Handle navigation
        if choice == curses.KEY_UP:
            if highlight > 0:
                highlight -= 1
        elif choice == curses.KEY_DOWN:
            if highlight < len(choices) - 1:
                highlight += 1
        elif choice == enter_key:
            break
    
    # Display final choice
    stdscr.addstr(2, 3, f"You chose: {choices[highlight]}")
    stdscr.refresh()
    stdscr.getch()

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

It uses the up and down arrow keys to select from among a list of choices. Pressing the Enter key will write out a line at the top of the screen with a messaging about which menu option was selected.

menu1.py screenshot

Pressing any key after that will exit the application.

Getting User Text Input

Sometimes we want the user to be able to type text input. This program sets up a messaging interface. We’ll revisit this code in the Chapter 10 exercises.

import curses

def main(stdscr):
    # Setup
    curses.echo() #make the input visible
    height, width = (24,80) #we're hard-coding this for now. 
                            #make sure you manually adjust to 24x80 
                            #before running

    
    # Create a window to display the chat history
    chat_height = height-3
    chat_win = curses.newwin(chat_height, width, 0, 0)
    
    # Create a window to type and read input
    input_win = curses.newwin(3, width, chat_height, 0)
    input_win.border()
    input_win.addstr(1, 2, "> ")
    input_win.refresh()
    
    messages = []
    
    while True:
        # Show user input
        curses.echo()

        #getstr moves the cursor to position (1,4) relative to this window,
        #  waits for the user to hit the enter key,
        # and then returns whatever the user typed.
        user_input = input_win.getstr(1, 4, width - 5).decode('utf-8').strip()
        
        #turn off user input until we're done updating the screen
        curses.noecho()
        
        if user_input:
            # Add your new message to the chat history
            messages.append(f"You: {user_input}")
            
            # Redraw full chat history
            chat_win.erase()
            for i, msg in enumerate(messages[-chat_height:]):
                chat_win.addstr(i, 0, msg)
            chat_win.refresh()
            
            # overwrite the previous input with a bunch of blanks
            input_win.addstr(1, 4, " " * (width - 5))
            input_win.refresh()

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