Some simple ncurses code examples for Python

Posted on: Posted on

Python’s curses module provides a wrapper around the ncurses library, allowing you to create text-based user interfaces (TUIs) in the terminal.

Here are several examples, starting from basic setup to more interactive elements.

General Setup & Best Practice

It’s highly recommended to use curses.wrapper for your main function. This handles the initialization and cleanup of the curses environment automatically, including error handling.

import curses
import time

def main(stdscr):
    # Curses initialization (handled by wrapper for most part)
    # These are common settings:
    curses.noecho()  # Don't echo keys pressed to the screen
    curses.cbreak()  # React to keys instantly, without requiring Enter
    stdscr.keypad(True) # Enable special keys (e.g., arrow keys)

    # Your curses code goes here
    # ...

    # Keep the window open until a key is pressed
    stdscr.addstr("Press any key to exit.")
    stdscr.getch()

# This is the standard way to run a curses application
if __name__ == "__main__":
    curses.wrapper(main)

1. Basic “Hello, World!” and Screen Manipulation

This example demonstrates how to display text, get screen dimensions, and refresh the screen.

import curses

def hello_world_example(stdscr):
    # Clear the screen
    stdscr.clear()

    # Get screen dimensions
    height, width = stdscr.getmaxyx()

    # Display a message at the center of the screen
    message = "Hello, ncurses in Python!"
    status_msg = "Window size: {}x{}".format(width, height)
    exit_msg = "Press any key to exit."

    stdscr.addstr(height // 2, (width - len(message)) // 2, message)
    stdscr.addstr(height - 2, (width - len(status_msg)) // 2, status_msg)
    stdscr.addstr(height - 1, (width - len(exit_msg)) // 2, exit_msg)

    # Refresh the screen to show the changes
    stdscr.refresh()

    # Wait for user input before exiting
    stdscr.getch()

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

2. User Input (Simple String)

This example shows how to take user input. Note the temporary use of curses.echo() so the user can see what they are typing.

import curses

def user_input_example(stdscr):
    stdscr.clear()
    curses.echo() # Turn echo on so the user can see what they type

    # Prompt for input
    prompt = "Enter your name: "
    stdscr.addstr(0, 0, prompt)

    # Get the input string (getstr returns bytes, so decode it)
    stdscr.refresh() # Make sure prompt is visible before getting input
    name = stdscr.getstr().decode('utf-8')

    curses.noecho() # Turn echo off again

    # Display the entered name
    stdscr.addstr(2, 0, f"Hello, {name}!")
    stdscr.addstr(4, 0, "Press any key to exit.")

    stdscr.refresh()
    stdscr.getch()

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

3. Colors and Attributes

This example demonstrates how to use colors and text attributes (like bold, reverse video).

import curses

def colors_example(stdscr):
    stdscr.clear()

    # Check if the terminal supports colors
    if not curses.has_colors():
        stdscr.addstr("Your terminal does not support colors. Press any key to exit.")
        stdscr.getch()
        return

    # Start color system
    curses.start_color()

    # Define color pairs (ID, foreground_color, background_color)
    # Curses provides 8 basic colors:
    # curses.COLOR_BLACK, curses.COLOR_RED, curses.COLOR_GREEN,
    # curses.COLOR_YELLOW, curses.COLOR_BLUE, curses.COLOR_MAGENTA,
    # curses.COLOR_CYAN, curses.COLOR_WHITE
    curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK)
    curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK)
    curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLUE)
    curses.init_pair(4, curses.COLOR_CYAN, curses.COLOR_WHITE)

    # Display text with different colors and attributes
    stdscr.addstr(0, 0, "This is default text.")
    stdscr.addstr(1, 0, "This is red text.", curses.color_pair(1))
    stdscr.addstr(2, 0, "This is green text.", curses.color_pair(2))
    stdscr.addstr(3, 0, "Yellow on Blue background.", curses.color_pair(3))
    stdscr.addstr(4, 0, "Cyan on White background.", curses.color_pair(4))

    # Attributes (can be combined with colors using |)
    stdscr.addstr(6, 0, "Bold text.", curses.A_BOLD)
    stdscr.addstr(7, 0, "Underlined text.", curses.A_UNDERLINE)
    stdscr.addstr(8, 0, "Reversed text (foreground/background swapped).", curses.A_REVERSE)
    stdscr.addstr(9, 0, "Bold and Red text.", curses.A_BOLD | curses.color_pair(1))

    stdscr.addstr(11, 0, "Press any key to exit.")

    stdscr.refresh()
    stdscr.getch()

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

4. Windows and Subwindows

This example shows how to create separate windows within the terminal. Each window is an independent drawing surface.

import curses

def windows_example(stdscr):
    stdscr.clear()
    height, width = stdscr.getmaxyx()

    # Create a new window (height, width, begin_y, begin_x)
    # The main window (stdscr) takes up the whole terminal.
    # New windows are relative to the terminal's top-left corner (0,0).
    win1_height = height // 2 - 1
    win1_width = width // 2 - 1
    window1 = curses.newwin(win1_height, win1_width, 1, 1) # Top-left

    win2_height = height // 2 - 1
    win2_width = width // 2 - 1
    window2 = curses.newwin(win2_height, win2_width, win1_height + 2, width // 2) # Bottom-right

    # Add borders to the windows
    window1.box() # Default border characters
    window2.box()

    # Add text to window 1
    msg1 = "Window 1 (Top-Left)"
    window1.addstr(win1_height // 2, (win1_width - len(msg1)) // 2, msg1)

    # Add text to window 2
    msg2 = "Window 2 (Bottom-Right)"
    window2.addstr(win2_height // 2, (win2_width - len(msg2)) // 2, msg2)

    # Refresh stdscr first (background)
    stdscr.addstr(0, 0, "Main screen content (stdscr)")
    stdscr.addstr(height - 1, 0, "Press any key to exit.")
    stdscr.refresh()

    # Refresh individual windows (they will draw on top of stdscr)
    window1.refresh()
    window2.refresh()

    stdscr.getch()

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

5. Simple Menu Navigation

This example demonstrates how to create a basic interactive menu using arrow keys and Enter.

import curses

def draw_menu(stdscr, selected_row_idx, menu_items):
    stdscr.clear()
    h, w = stdscr.getmaxyx()

    # Title
    title = "Simple Ncurses Menu"
    stdscr.addstr(0, (w - len(title)) // 2, title, curses.A_BOLD)

    # Menu items
    for idx, item in enumerate(menu_items):
        x = w // 2 - len(item) // 2
        y = h // 2 - len(menu_items) // 2 + idx
        if idx == selected_row_idx:
            stdscr.attron(curses.A_REVERSE) # Highlight selected item
            stdscr.addstr(y, x, item)
            stdscr.attroff(curses.A_REVERSE)
        else:
            stdscr.addstr(y, x, item)

    # Instructions
    instructions = "Use arrow keys to navigate, Enter to select, 'q' to quit."
    stdscr.addstr(h - 2, (w - len(instructions)) // 2, instructions, curses.A_DIM)

    stdscr.refresh()

def menu_example(stdscr):
    curses.noecho()
    curses.cbreak()
    stdscr.keypad(True)

    menu_items = ["Option 1", "Option 2", "Option 3", "Exit"]
    current_row = 0

    draw_menu(stdscr, current_row, menu_items)

    while True:
        key = stdscr.getch()

        if key == curses.KEY_UP and current_row > 0:
            current_row -= 1
        elif key == curses.KEY_DOWN and current_row < len(menu_items) - 1:
            current_row += 1
        elif key == curses.KEY_ENTER or key in [10, 13]: # 10 and 13 are ASCII for Enter
            # Perform action based on selection
            if current_row == len(menu_items) - 1: # 'Exit' option
                break
            else:
                stdscr.clear()
                stdscr.addstr(0, 0, f"You selected: {menu_items[current_row]}")
                stdscr.addstr(2, 0, "Press any key to return to menu...")
                stdscr.refresh()
                stdscr.getch()
        elif key == ord('q'): # 'q' key to quit
            break

        draw_menu(stdscr, current_row, menu_items)

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

Important Notes

  • Terminal Compatibility: curses applications might behave differently on various terminals (e.g., xterm, gnome-terminal, PuTTY).
  • Refreshing: Always call window.refresh() after making changes to a window to make them visible on the screen.
  • Input Blocking: stdscr.getch() blocks execution until a key is pressed. If you need non-blocking input (e.g., for animations), you can use stdscr.nodelay(True) and check for curses.ERR from getch().
  • Error Handling (Manual Setup): If you don’t use curses.wrapper, you must manually initialize with curses.initscr() and clean up with curses.endwin(), typically in a try...finally block. curses.wrapper handles this for you.
  • Coordinate System: (y, x) or (row, col) for coordinates, not (x, y). The top-left corner is (0, 0).
  • Unicode: stdscr.addstr() expects Unicode strings. stdscr.getstr() returns bytes, so you often need to decode('utf-8') it.

These examples should give you a solid foundation for building more complex TUI applications with Python’s curses module!

Leave a Reply

Your email address will not be published. Required fields are marked *