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.
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
Feature | curses | Blessed | Rich | Textual |
---|---|---|---|---|
Installation | Standard | pip | pip | pip |
Complexity | High | Low | Medium | Medium |
Control Level | Low-level | Mid-level | High-level | High-level |
Performance | Highest | High | Medium | Medium |
Learning Curve | High | Low | Medium | Medium |
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.