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:
cursesapplications 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 usestdscr.nodelay(True)and check forcurses.ERRfromgetch(). - Error Handling (Manual Setup): If you don’t use
curses.wrapper, you must manually initialize withcurses.initscr()and clean up withcurses.endwin(), typically in atry...finallyblock.curses.wrapperhandles 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()returnsbytes, so you often need todecode('utf-8')it.
These examples should give you a solid foundation for building more complex TUI applications with Python’s curses module!