curses

A low-level terminal control library included in Python's standard library. Serves as the foundation for TUI application development on Unix-like systems.

TUIStandard LibraryTerminalncursesLow-level

curses

curses is a low-level terminal control library included in Python's standard library. It provides Python access to the ncurses library on Unix-like systems and serves as the foundational technology for many TUI libraries. It enables direct screen control and flexible customization.

Key Features

Standard Library

  • No Additional Installation: Included in Python's standard library
  • Stability: Long-term proven track record and stable operation
  • Compatibility: High compatibility with Unix-like systems

Low-Level Control

  • Direct Control: Direct control of screen, cursor, and keyboard
  • High Performance: Optimized screen updates
  • Flexibility: Fine-grained control capabilities

Foundation Technology

  • Base for Other Libraries: Many TUI libraries are built on top of curses
  • Learning Value: Learn the fundamentals of TUI programming
  • Customization: Enables development of custom TUI libraries

Installation

# No additional installation required as it's part of the standard library
import curses

Basic Usage

Initialization and Basic Structure

import curses

def main(stdscr):
    # Screen initialization
    curses.curs_set(0)  # Hide cursor
    stdscr.nodelay(1)   # Non-blocking key input
    stdscr.timeout(100) # 100ms timeout
    
    # Color initialization
    curses.start_color()
    curses.use_default_colors()
    curses.init_pair(1, curses.COLOR_RED, -1)
    curses.init_pair(2, curses.COLOR_GREEN, -1)
    curses.init_pair(3, curses.COLOR_BLUE, -1)
    
    height, width = stdscr.getmaxyx()
    
    while True:
        stdscr.clear()
        
        # Text display
        stdscr.addstr(0, 0, "Hello, curses!")
        stdscr.addstr(1, 0, f"Terminal size: {width}x{height}")
        
        # Colored text
        stdscr.addstr(3, 0, "Red text", curses.color_pair(1))
        stdscr.addstr(4, 0, "Green text", curses.color_pair(2))
        stdscr.addstr(5, 0, "Blue text", curses.color_pair(3))
        
        # Key input handling
        key = stdscr.getch()
        if key == ord('q'):
            break
        elif key != -1:
            stdscr.addstr(7, 0, f"Key pressed: {chr(key) if 32 <= key <= 126 else key}")
        
        stdscr.refresh()

# Run curses application
curses.wrapper(main)

Window Creation and Management

import curses

def main(stdscr):
    curses.curs_set(0)
    curses.start_color()
    curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)
    curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_RED)
    
    height, width = stdscr.getmaxyx()
    
    # Create main window
    main_win = curses.newwin(height-2, width-2, 1, 1)
    main_win.box()
    main_win.addstr(0, 2, " Main Window ")
    
    # Create sub window
    sub_win = curses.newwin(10, 30, 5, 10)
    sub_win.bkgd(' ', curses.color_pair(1))
    sub_win.box()
    sub_win.addstr(1, 2, "Sub Window")
    sub_win.addstr(3, 2, "Press 'q' to quit")
    
    # Popup window
    popup_win = curses.newwin(6, 25, 8, 40)
    popup_win.bkgd(' ', curses.color_pair(2))
    popup_win.box()
    popup_win.addstr(1, 2, "Popup Window")
    popup_win.addstr(3, 2, "Any key to close")
    
    # Initial draw
    stdscr.refresh()
    main_win.refresh()
    sub_win.refresh()
    
    show_popup = False
    
    while True:
        key = stdscr.getch()
        
        if key == ord('q'):
            break
        elif key == ord('p'):
            show_popup = not show_popup
        elif show_popup and key != -1:
            show_popup = False
        
        # Show/hide popup
        if show_popup:
            popup_win.refresh()
        else:
            popup_win.clear()
            popup_win.refresh()
            # Redraw main windows
            main_win.refresh()
            sub_win.refresh()

curses.wrapper(main)

Practical Examples

Simple Text Editor

import curses

class SimpleEditor:
    def __init__(self, stdscr):
        self.stdscr = stdscr
        self.lines = [""]
        self.cursor_y = 0
        self.cursor_x = 0
        self.scroll_y = 0
        
        curses.curs_set(1)  # Show cursor
        curses.start_color()
        curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)
        
        self.height, self.width = stdscr.getmaxyx()
        self.text_height = self.height - 2  # Excluding status line
    
    def draw(self):
        self.stdscr.clear()
        
        # Status line
        status = f"Line: {self.cursor_y + 1}, Col: {self.cursor_x + 1} | Press Ctrl+Q to quit"
        self.stdscr.addstr(0, 0, status[:self.width-1], curses.color_pair(1))
        
        # Text display
        for i in range(self.text_height):
            line_num = i + self.scroll_y
            if line_num < len(self.lines):
                line = self.lines[line_num]
                if len(line) <= self.width - 1:
                    self.stdscr.addstr(i + 1, 0, line)
                else:
                    self.stdscr.addstr(i + 1, 0, line[:self.width-1])
        
        # Set cursor position
        screen_y = self.cursor_y - self.scroll_y + 1
        if 0 <= screen_y < self.height:
            self.stdscr.move(screen_y, min(self.cursor_x, self.width - 1))
        
        self.stdscr.refresh()
    
    def handle_key(self, key):
        if key == 17:  # Ctrl+Q
            return False
        elif key == curses.KEY_UP:
            if self.cursor_y > 0:
                self.cursor_y -= 1
                self.cursor_x = min(self.cursor_x, len(self.lines[self.cursor_y]))
                self.adjust_scroll()
        elif key == curses.KEY_DOWN:
            if self.cursor_y < len(self.lines) - 1:
                self.cursor_y += 1
                self.cursor_x = min(self.cursor_x, len(self.lines[self.cursor_y]))
                self.adjust_scroll()
        elif key == curses.KEY_LEFT:
            if self.cursor_x > 0:
                self.cursor_x -= 1
            elif self.cursor_y > 0:
                self.cursor_y -= 1
                self.cursor_x = len(self.lines[self.cursor_y])
                self.adjust_scroll()
        elif key == curses.KEY_RIGHT:
            if self.cursor_x < len(self.lines[self.cursor_y]):
                self.cursor_x += 1
            elif self.cursor_y < len(self.lines) - 1:
                self.cursor_y += 1
                self.cursor_x = 0
                self.adjust_scroll()
        elif key == curses.KEY_BACKSPACE or key == 127:
            if self.cursor_x > 0:
                line = self.lines[self.cursor_y]
                self.lines[self.cursor_y] = line[:self.cursor_x-1] + line[self.cursor_x:]
                self.cursor_x -= 1
            elif self.cursor_y > 0:
                # Merge with previous line
                prev_line = self.lines[self.cursor_y - 1]
                curr_line = self.lines[self.cursor_y]
                self.lines[self.cursor_y - 1] = prev_line + curr_line
                del self.lines[self.cursor_y]
                self.cursor_y -= 1
                self.cursor_x = len(prev_line)
                self.adjust_scroll()
        elif key == 10 or key == 13:  # Enter
            line = self.lines[self.cursor_y]
            self.lines[self.cursor_y] = line[:self.cursor_x]
            self.lines.insert(self.cursor_y + 1, line[self.cursor_x:])
            self.cursor_y += 1
            self.cursor_x = 0
            self.adjust_scroll()
        elif 32 <= key <= 126:  # Printable characters
            line = self.lines[self.cursor_y]
            self.lines[self.cursor_y] = line[:self.cursor_x] + chr(key) + line[self.cursor_x:]
            self.cursor_x += 1
        
        return True
    
    def adjust_scroll(self):
        # Scroll adjustment
        if self.cursor_y < self.scroll_y:
            self.scroll_y = self.cursor_y
        elif self.cursor_y >= self.scroll_y + self.text_height:
            self.scroll_y = self.cursor_y - self.text_height + 1
    
    def run(self):
        while True:
            self.draw()
            key = self.stdscr.getch()
            if not self.handle_key(key):
                break

def main(stdscr):
    editor = SimpleEditor(stdscr)
    editor.run()

curses.wrapper(main)

Menu System

import curses

class Menu:
    def __init__(self, stdscr, items, title="Menu"):
        self.stdscr = stdscr
        self.items = items
        self.title = title
        self.selected = 0
        
        curses.curs_set(0)
        curses.start_color()
        curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE)  # Selected item
        curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLUE)   # Title
        
        self.height, self.width = stdscr.getmaxyx()
        
        # Calculate menu window size
        self.menu_height = len(items) + 4
        self.menu_width = max(len(item) for item in items) + 6
        self.menu_width = max(self.menu_width, len(title) + 4)
        
        # Center placement
        self.start_y = (self.height - self.menu_height) // 2
        self.start_x = (self.width - self.menu_width) // 2
        
        self.win = curses.newwin(self.menu_height, self.menu_width, 
                                self.start_y, self.start_x)
    
    def draw(self):
        self.win.clear()
        self.win.box()
        
        # Title
        title_x = (self.menu_width - len(self.title)) // 2
        self.win.addstr(1, title_x, self.title, curses.color_pair(2))
        
        # Menu items
        for i, item in enumerate(self.items):
            y = i + 3
            x = 2
            
            if i == self.selected:
                self.win.addstr(y, x, f"> {item}", curses.color_pair(1))
            else:
                self.win.addstr(y, x, f"  {item}")
        
        self.win.refresh()
    
    def handle_key(self, key):
        if key == curses.KEY_UP:
            self.selected = (self.selected - 1) % len(self.items)
        elif key == curses.KEY_DOWN:
            self.selected = (self.selected + 1) % len(self.items)
        elif key == 10 or key == 13:  # Enter
            return self.selected
        elif key == 27:  # ESC
            return -1
        
        return None
    
    def run(self):
        while True:
            self.draw()
            key = self.stdscr.getch()
            result = self.handle_key(key)
            
            if result is not None:
                return result

def main(stdscr):
    # Main menu
    main_items = ["New File", "Open File", "Settings", "About", "Exit"]
    
    while True:
        menu = Menu(stdscr, main_items, "Main Menu")
        choice = menu.run()
        
        if choice == 0:  # New File
            stdscr.clear()
            stdscr.addstr(5, 5, "New File selected. Press any key to continue...")
            stdscr.refresh()
            stdscr.getch()
        elif choice == 1:  # Open File
            stdscr.clear()
            stdscr.addstr(5, 5, "Open File selected. Press any key to continue...")
            stdscr.refresh()
            stdscr.getch()
        elif choice == 2:  # Settings
            settings_items = ["Theme", "Fonts", "Shortcuts", "Back"]
            settings_menu = Menu(stdscr, settings_items, "Settings")
            settings_choice = settings_menu.run()
            
            if settings_choice != -1 and settings_choice != 3:
                stdscr.clear()
                stdscr.addstr(5, 5, f"Settings: {settings_items[settings_choice]} selected")
                stdscr.addstr(6, 5, "Press any key to continue...")
                stdscr.refresh()
                stdscr.getch()
        elif choice == 3:  # About
            stdscr.clear()
            stdscr.addstr(5, 5, "Simple curses application")
            stdscr.addstr(6, 5, "Press any key to continue...")
            stdscr.refresh()
            stdscr.getch()
        elif choice == 4 or choice == -1:  # Exit
            break

curses.wrapper(main)

Comparison with Other Libraries

FeaturecursesBlessedRichTextual
InstallationStandardpippippip
ComplexityHighLowMediumMedium
Control LevelLow-levelMid-levelHigh-levelHigh-level
PerformanceHighestHighMediumMedium
Learning CurveHighLowMediumMedium

Use Cases

  • System Tools: Low-level system administration tools
  • High-Performance Apps: Applications where performance is critical
  • Learning Purposes: Learning TUI programming fundamentals
  • Library Development: Foundation for custom TUI libraries

Considerations and Best Practices

Platform Support

import curses
import sys

def main():
    try:
        # May not work on Windows
        curses.wrapper(run_app)
    except ImportError:
        print("curses is not available on this platform")
        sys.exit(1)

Error Handling

import curses

def safe_addstr(win, y, x, text, attr=0):
    """Safe string drawing"""
    try:
        win.addstr(y, x, text, attr)
    except curses.error:
        # Ignore drawing errors outside screen boundaries
        pass

Community and Support

  • Official Documentation: Detailed coverage in Python's official documentation
  • Stability: Long-term proven track record
  • Compatibility: High compatibility with Unix-like systems
  • Learning Resources: Rich tutorials and examples

curses is an important library that provides high performance and flexibility as the foundational technology for TUI application development.