Build interactive command-line applications with async Python. Create REPLs (Read-Eval-Print Loops) that feel like modern chat interfaces, complete with keyboard shortcuts, command history, and clipboard support.
Build interactive terminal apps where users type messages and get responses - like chat bots, database queries, or system monitoring tools. REPL Toolkit handles the terminal UI, keyboard shortcuts, and command routing so you focus on your application's logic.
- Support both typed commands (
/help,/save) and keyboard shortcuts (F1,Ctrl+S) - Works with async backends (API calls, database queries, etc.)
- Handle images from clipboard
- Full customization of commands and shortcuts
Install with pip:
pip install repl-toolkitCreate a simple echo bot:
import asyncio
from repl_toolkit import AsyncREPL
class EchoBot:
async def handle_input(self, user_input: str) -> bool:
print(f"You said: {user_input}")
return True
async def main():
backend = EchoBot()
repl = AsyncREPL()
await repl.run(backend)
asyncio.run(main())Run it:
python echo_bot.pyNow you have an interactive chat interface with:
- Multiline input (Enter for new line, Alt+Enter to send)
- Handles special commands (
/help,/exit) and keyboard shortcuts (F1,Ctrl+C) - Command history (Up/Down arrows)
- Async support for API calls or other I/O
Let's make a todo list app with commands and keyboard shortcuts:
import asyncio
from repl_toolkit import AsyncREPL, ActionRegistry, Action, ActionContext
class TodoBackend:
def __init__(self):
self.todos = []
async def handle_input(self, user_input: str) -> bool:
self.todos.append(user_input)
print(f"Added: {user_input}")
return True
def setup_actions(backend):
"""Create actions for todo management."""
registry = ActionRegistry()
# List todos with F2 or /list
def list_handler(context: ActionContext):
if not backend.todos:
print("No todos yet!")
else:
print("\nTodos:")
for i, todo in enumerate(backend.todos, 1):
print(f" {i}. {todo}")
registry.register_action(Action(
name="list_todos",
description="Show all todos",
handler=list_handler,
keys="F2",
command="/list",
))
# Clear list
def clear_handler(context: ActionContext):
backend.todos.clear()
print("Cleared all todos!")
registry.register_action(Action(
name="clear_todos",
description="Clear all todos",
handler=clear_handler,
command="/clear"
))
return registry
async def main():
backend = TodoBackend()
actions = setup_actions(backend)
repl = AsyncREPL(action_registry=actions)
await repl.run(backend, "Todo list ready! Type something to add a todo.")
asyncio.run(main())Usage:
- Type anything to add a todo
- Type
/listor pressF2to see all todos - Type
/clearto clear the list - Type
/helpor pressF1to see all commands - Commands execute immediately when you press Enter
- For normal text, press Alt+Enter to send
REPL Toolkit uses Python's standard logging framework for all error reporting. This gives you full control over whether errors are displayed, how they're formatted, and where they're sent.
To see errors, configure logging in your application:
import logging
# Option 1: Simple setup - show all errors
logging.basicConfig(level=logging.WARNING)
# Option 2: Custom format
logging.basicConfig(
level=logging.WARNING,
format='%(levelname)s: %(message)s'
)
# Option 3: File output
logging.basicConfig(
filename='repl.log',
level=logging.DEBUG
)
# Option 4: Detailed control
logger = logging.getLogger('repl_toolkit')
logger.setLevel(logging.WARNING)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
logger.addHandler(handler)What gets logged:
ERROR: Action execution failures, clipboard errors, critical issuesWARNING: Non-critical issues (missing dependencies, invalid configs)DEBUG: Detailed execution flow (useful for troubleshooting)
Production recommendation:
# Show warnings and errors, hide debug details
logging.basicConfig(level=logging.WARNING)Development recommendation:
# See everything for troubleshooting
logging.basicConfig(level=logging.DEBUG, format='%(name)s: %(message)s')If you don't configure logging, errors will be silent (Python's default behavior).
Your REPL can accept text and images from the clipboard. Users press F6 to paste:
import asyncio
from repl_toolkit import AsyncREPL
class ImageBot:
async def handle_input(self, user_input: str, images=None) -> bool:
print(f"Message: {user_input}")
if images:
for img_id, img_data in images.items():
print(f" Received {img_data.media_type} image: {len(img_data.data)} bytes")
# img_data.data contains the raw image bytes
# Send to your API, save to disk, etc.
return True
async def main():
backend = ImageBot()
repl = AsyncREPL(enable_image_paste=True) # Enabled by default
await repl.run(backend)
asyncio.run(main())Text handling is as expected.
With images, users can:
- Copy an image to clipboard (screenshot, copy from browser, etc.)
- Type their message in the REPL
- Press
F6or type/pasteto insert the image - Press
Alt+Enterto send
The image appears as {{image:img_001}} in the text, and your backend receives both the text and the actual image data.
You then need to process the image in whatever way your application needs to:
from repl_toolkit import parse_image_references, iter_content_parts
class SmartBackend:
async def handle_input(self, user_input: str, images=None) -> bool:
# Option 1: Simple check
if images:
print(f"Got {len(images)} images")
# Option 2: Parse placeholders
parsed = parse_image_references(user_input)
print(f"Text references these images: {parsed.image_ids}")
# Option 3: Iterate through message parts
for content, image in iter_content_parts(user_input, images):
if image:
# Process image
print(f"Image: {len(image.data)} bytes of {image.media_type}")
# save_image(image.data)
# upload_to_api(image.data, image.media_type)
elif content:
# Process text
print(f"Text: {content}")
return TrueReal-world examples:
- Claude/ChatGPT clients - Send images to multimodal AI APIs
- Support tools - Users paste screenshots with bug reports
- Documentation - Generate docs with embedded images
- Data annotation - Label images with text descriptions
See examples/image_paste_demo.py for a complete working example.
Add tab completion for commands and custom values:
from repl_toolkit import AsyncREPL, PrefixCompleter
# Complete slash commands
completer = PrefixCompleter(["/help", "/save", "/load", "/exit"])
# Or complete custom values
completer = PrefixCompleter(["apple", "banana", "cherry"])
repl = AsyncREPL(completer=completer)Features:
- Complete commands at line start or after newline
- Won't complete mid-sentence (e.g., won't complete in "Please type /help")
- Prefix-only matching (no fuzzy search)
See repl_toolkit/completion/README.md for advanced completion features including shell command expansion and environment variables.
Process input from stdin instead of interactive prompts - perfect for:
- Piping data:
cat input.txt | python my_tool.py - Batch processing:
python my_tool.py < batch.txt - Automated testing
import asyncio
from repl_toolkit import run_headless_mode
class BatchProcessor:
async def handle_input(self, text: str) -> bool:
result = text.upper() # Your processing logic
print(f"Processed: {result}")
return True
async def main():
backend = BatchProcessor()
success = await run_headless_mode(backend)
return 0 if success else 1
exit(asyncio.run(main()))Input format:
First line of input
Second line
/send
More content here
Another line
/send
- Content accumulates until
/send - Each
/sendtriggers backend processing - EOF auto-sends remaining content
- Commands like
/helpwork normally
See examples/headless_usage.py for a complete example with statistics tracking.
Actions provide both typed commands and keyboard shortcuts for the same functionality:
from repl_toolkit import Action, ActionContext
def save_handler(context: ActionContext):
"""Save the conversation."""
# Get arguments if it was a command
if context.args:
filename = context.args[0]
else:
filename = "default.txt"
# Your save logic here
print(f"Saved to {filename}")
# Know how it was triggered
if context.triggered_by == "shortcut":
print("(via Ctrl+S)")
action = Action(
name="save",
description="Save conversation",
category="File",
handler=save_handler,
keys="c-s", # Ctrl+S
command="/save",
command_usage="/save [filename]"
)
registry = ActionRegistry()
registry.register_action(action)Every REPL includes these by default:
|---------|----------|--------------|
| /help [action] | F1 | Show help for all actions or a specific one |
| /shortcuts | - | List all keyboard shortcuts |
| /exit or /quit | - | Exit the application |
| /paste | F6 | Paste image or text from clipboard |
Commands execute immediately when you press Enter. For normal text messages, press Alt+Enter to send.
Your action handlers receive context about how they were called:
def my_handler(context: ActionContext):
# How was it triggered?
if context.triggered_by == "command":
print("User typed the command")
elif context.triggered_by == "shortcut":
print("User pressed the keyboard shortcut")
# Command arguments (if triggered by command)
if context.args:
print(f"Arguments: {context.args}")
# Access the full command
if context.command:
print(f"Full command: {context.command}")
# Access other components
context.registry # The action registry
context.repl # The REPL instance
context.backend # Your backendAdd tab completion for commands, file paths, or custom values:
from repl_toolkit import PrefixCompleter, ShellExpansionCompleter
# Complete slash commands
completer = PrefixCompleter(["/help", "/history", "/model"])
# Or use shell-style completion with variables and commands
completer = ShellExpansionCompleter(
commands=["/help", "/history"],
enable_env_vars=True, # Complete $VAR
enable_command_sub=True # Complete $(command)
)See repl_toolkit/completion/README.md for advanced completion features including shell command expansion and environment variables.
A more complete example with commands, shortcuts, and conversation history:
import asyncio
from repl_toolkit import AsyncREPL, ActionRegistry, Action, ActionContext
class ChatBackend:
def __init__(self):
self.history = []
self.model = "gpt-4"
async def handle_input(self, user_input: str) -> bool:
self.history.append({"role": "user", "content": user_input})
# Your AI API call here
response = f"[{self.model}] Echo: {user_input}"
self.history.append({"role": "assistant", "content": response})
print(response)
return True
def create_actions(backend):
registry = ActionRegistry()
# Show conversation history
registry.register_action(Action(
name="show_history",
description="Show conversation history",
handler=lambda ctx: print("\n".join(
f"{msg['role']}: {msg['content']}" for msg in backend.history
)),
command="/history",
keys="F3"
))
# Switch model
def switch_model(ctx: ActionContext):
if ctx.args:
backend.model = ctx.args[0]
print(f"Switched to {backend.model}")
else:
print(f"Current model: {backend.model}")
registry.register_action(Action(
name="switch_model",
description="Switch AI model",
handler=switch_model,
command="/model",
command_usage="/model <model-name>"
))
# Clear history
registry.register_action(Action(
name="clear",
description="Clear conversation history",
handler=lambda ctx: (backend.history.clear(), print("History cleared!")),
command="/clear",
keys="F4"
))
return registry
async def main():
backend = ChatBackend()
actions = create_actions(backend)
repl = AsyncREPL(
action_registry=actions,
prompt_string="<b>You:</b> ",
history_path=Path.home() / ".chatbot_history"
)
await repl.run(backend, "Chat bot ready! Type /help for commands.")
asyncio.run(main())from repl_toolkit import (
AsyncREPL, # Interactive REPL with UI
run_headless_mode, # Process stdin without UI
ActionRegistry, # Manage commands and shortcuts
Action, # Define a command/shortcut
ActionContext, # Context passed to handlers
PrefixCompleter, # Tab completion
ShellExpansionCompleter, # Advanced tab completion
)repl = AsyncREPL(
action_registry=None, # Optional action registry
completer=None, # Optional tab completer
prompt_string="User: ", # Custom prompt
history_path=None, # Optional history file
enable_image_paste=True, # Image clipboard support
)action = Action(
name="my_action", # Unique identifier
description="What it does", # Help text
category="General", # Optional: group in help
handler=my_function, # Called when triggered
keys="F2", # Optional: keyboard shortcut
keys_description="...", # Optional: help for shortcut
command="/cmd", # Optional: typed command
command_usage="/cmd [args]", # Optional: usage text
enabled=True # Optional: enable/disable
)Key formats:
- Function keys:
"F1","F2", ...,"F12" - Control:
"c-s"(Ctrl+S),"c-q"(Ctrl+Q) - Alt/Escape:
"escape"followed by key - Control+Shift:
"c-s-v"(Ctrl+Shift+V)
REPL Toolkit is designed for any interactive terminal application:
- AI Chat Clients - Talk to Claude, ChatGPT, or local models with keyboard shortcuts for common operations
- Database Query Tools - Interactive SQL or NoSQL clients with command history, query shortcuts, and result formatting
- System Monitoring - Watch logs, metrics, or system status with commands to filter, search, or change views
- Configuration Editors - Interactive config file editing with validation and shortcuts for common changes
- Game Consoles - Debug commands and cheats during development with quick shortcuts for common operations
- Log Analyzers - Query and filter logs interactively with custom commands for common patterns
- Data Processing - Interactive data transformation with commands for different operations and live preview
- API Clients - Test and explore APIs interactively with shortcuts for common requests
- Development Tools - Any tool that needs interactive command input with a good user experience
The toolkit handles the terminal UI, keyboard shortcuts, and command routing so you can focus on your application's logic.
The examples/ directory contains working examples:
# Basic usage - simple echo bot
python examples/basic_usage.py
# Advanced - chat bot with history and commands
python examples/advanced_usage.py
# Headless - process input from stdin
echo -e "Line 1\nLine 2\n/send" | python examples/headless_usage.py
# Image paste demo
python examples/image_paste_demo.py
# Tab completion
python examples/completion_demo.pyPass additional context to your action handlers:
def my_handler(context: ActionContext):
# Built-in context
command = context.command
args = context.args
triggered_by = context.triggered_by
# Your custom data via backend
backend = context.backend
user_settings = backend.user_settingsfrom repl_toolkit import ShellExpansionCompleter
completer = ShellExpansionCompleter(
commands=["/help", "/load"],
enable_env_vars=True, # $HOME, $USER, etc.
enable_command_sub=True, # $(echo foo)
enable_tilde=True, # ~/Documents
)
# Expand $VAR and $(command) on tabEnable/disable actions at runtime:
# Disable an action
registry.get_action("save").enabled = False
# Re-enable it
registry.get_action("save").enabled = TrueThe toolkit uses prompt_toolkit under the hood. You can use HTML-style formatting in prompts and output:
from prompt_toolkit import HTML
# In prompts
repl = AsyncREPL(prompt_string=HTML("<b>You:</b> "))
# In output
print(HTML("<green>Success!</green>"))
print(HTML("<red>Error!</red>"))Commands don't work
- Make sure you're using
ActionRegistryand registering actions - Check action names are unique
- Commands execute immediately on Enter; press Alt+Enter for normal text
Keyboard shortcuts don't work
- Verify key format:
"F1","c-s", etc. - Some terminals don't support all key combinations
- Try function keys (
F1-F12) - they work everywhere
Image paste doesn't work
- Install pyclip:
pip install pyclip - Verify clipboard access on your system
- Linux may need
xcliporxsel:apt-get install xclip
History not saving
- Check
history_pathis writable - Parent directory must exist
- Example:
Path.home() / ".myapp_history"
Tab completion not working
- Pass
completertoAsyncREPLconstructor - Verify completion items are strings
- Commands only complete at line start or after newlines
- Python 3.8+
- prompt-toolkit 3.0+
- pyclip 0.7+ (optional, for image paste support)
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass:
pytest - Submit a pull request
MIT License - see LICENSE file for details.
Built on prompt-toolkit for terminal handling.
See CHANGELOG.md for version history.