Lightweight Python library for direct terminal control on POSIX systems (Linux/macOS). Works with raw ANSI escape codes, terminal modes, cursor positioning, input handling, and color management — without heavy abstractions.
git clone https://github.com/hoefkensj/libTerm
cd libTerm
python -m venv .venv && source .venv/bin/activate
pip install -e .
python -m libTerm.examples.ex_arrowkeysOr install directly into your project:
pip install libTermA direct interface to terminal I/O and ANSI control codes. Useful for:
- Terminal games (snake, roguelikes, real-time animations)
- Interactive CLI tools with custom key handling
- Live dashboards that update without clearing the screen
- Learning ANSI sequences and POSIX terminal control
- Terminal modes: Switch between NORMAL (echo, canonical) and CONTROL (raw input, no echo, cursor hidden)
- Cursor control: Position, move, show/hide, save/restore positions on a stack
- Raw keyboard input: Non-blocking event polling, read all available input at once
- Color detection: Query current foreground/background colors from terminal
- Terminal size: Track terminal width and height, detect resize events
- Direct ANSI access: Use raw escape codes alongside the API
from libTerm import Term, Mode, Coord
term = Term()
# Query terminal state
print(f"Size: {term.size.xy}") # (width, height)
print(f"Cursor at: {term.cursor.xy}") # (x, y)
print(f"BG color: {term.color.bg}") # Current background colorSwitch between normal line-editing and raw control input:
from libTerm import Term, Mode
term = Term()
# Enter raw input mode (no echo, no line buffering, cursor hidden)
term.mode = Mode.CONTROL
# Poll for keyboard events without blocking
if term.stdin.event:
key = term.stdin.read()
if key == 'q':
break
# Restore normal mode (echo on, canonical input, cursor visible)
term.mode = Mode.NORMALRead raw input one event at a time:
# Check if input is available (non-blocking)
if term.stdin.event:
key_bytes = term.stdin.read()
# Arrow keys come as ANSI sequences
if key_bytes == '\x1b[A': # Up
print("UP")
elif key_bytes == '\x1b[B': # Down
print("DOWN")
elif key_bytes == '\x1b[C': # Right
print("RIGHT")
elif key_bytes == '\x1b[D': # Left
print("LEFT")vs. using input():
input()blocks and echoes, waits for Enterterm.stdin.event + term.stdin.read()is non-blocking, captures individual bytes, allows arrow keys and Ctrl sequences
Position the cursor, move it, or hide it:
from libTerm import Coord
# Set cursor position (1-indexed)
term.cursor.xy = Coord(10, 5)
print("*")
# Move relative to current position
term.cursor.move.down(3)
term.cursor.move.right(5)
print("*")
# Show / hide cursor
term.cursor.hide()
term.cursor.show()vs. raw ANSI:
# Without libTerm (raw ANSI):
print("\x1b[5;10H*") # Move to (10, 5), print *
print("\x1b[3B\x1b[5C*") # Move down 3, right 5, print *
print("\x1b[?25l") # Hide cursor
print("\x1b[?25h") # Show cursorlibTerm makes this discoverable and less error-prone.
Save and restore cursor positions easily:
term.cursor.store.save() # Push current position
print("Hello at position 1")
term.cursor.xy = Coord(1, 10)
print("Hello at position 2")
term.cursor.store.undo() # Pop back to saved position
print("Back to position 1")Immutable coordinate type:
c = Coord(5, 10)
print(c.x, c.y) # 5, 10
print(c[0], c[1]) # 5, 10 (indexing)
print(c + Coord(2, 3)) # Coord(7, 13)Query size and detect when terminal is resized:
width, height = term.size.xy
print(f"{width} x {height}")
# Detect resize events
if term.size.changed:
print("Terminal was resized!")vs. os.get_terminal_size():
- libTerm tracks resize events
- Provides a convenient object interface
- Integrates with the terminal state system
Query the current foreground and background colors:
fg = term.color.fg
bg = term.color.bg
# Color has R, G, B components (0-255 per channel)
print(f"Background: RGB({bg.R}, {bg.G}, {bg.B})")libTerm doesn't hide ANSI. Mix raw codes with the API:
# Use Ansi enum for common sequences
from libTerm import Ansi
print(Ansi.hide) # Hide cursor
print(Ansi.show) # Show cursor
print("\x1b[31mRED text\x1b[m") # Raw ANSI still works- libTerm: Direct, close to ANSI, minimal setup, no system package dependency
- Curses: Heavier abstraction, window-based model, more complex for simple tasks
Choose libTerm if you want raw terminal access without heavy abstractions.
- Raw ANSI: Error-prone escape sequences, hard to remember, no structure
- libTerm: Named enums, methods with clear intent, still lets you write raw ANSI
Choose libTerm if you want safety and discoverability alongside low-level control.
- Rich/Textual: High-level TUI framework, great for dashboards, lots of abstraction
- libTerm: Low-level, minimal, close to the terminal
Choose libTerm if you want to build your own abstractions or learn terminal internals.
Located in src/libTerm/examples/:
| File | Shows |
|---|---|
ex_basic.py |
Terminal state inspection |
ex_arrowkeys.py |
Raw arrow key polling |
ex_snake_manual.py |
Keyboard-controlled game |
ex_snake_automatic.py |
Animation without input |
ex_colors.py |
Color detection |
ex_buffers.py |
Alternate screen buffer |
ex_printkeys.py |
Debug: see all key codes |
Run an example:
python -m libTerm.examples.ex_arrowkeys- Linux: Full support
- macOS: Full support
- Windows: Partial (alternate implementations in progress)
libTerm is intentionally low-level:
- Stay close to ANSI and POSIX
- Avoid heavy abstractions
- Let you control the terminal directly
- Make common tasks simple, complex tasks possible
If you need a full TUI framework, use Rich or Textual. If you want control and simplicity, use libTerm.
Pull requests welcome. Report bugs and experimental ideas.
Happy terminal hacking!