Building interactive command line interfaces effortlessly.
Documentation
Table of Contents
Overview
PyClack is a Python library designed to simplify the creation of interactive command-line interfaces. It provides both low-level components for custom CLI development and high-level, pre-styled prompts for immediate use.
Key Features
- Rich set of interactive prompts (text, password, select, multiselect, confirm)
- Unicode support with automatic fallbacks
- Consistent styling and color themes
- Keyboard navigation (arrow keys, vim-style hjkl)
- Error handling and validation
- Support for async/await
- Spinner for async operations
- Extensive customization options
Installation
PyClack can be installed using pip with different installation options:
pip install pyclack-cli # Base installation
pip install pyclack-cli[core] # Core features
pip install pyclack-cli[prompts] # Prompts features
pip install pyclack-cli[all] # Everything
Requirements
- readchar library: https://github.com/magmax/python-readchar
Architecture
The library follows a two-tier architecture:
- Core components provide the foundational building blocks with maximum flexibility
- Prompts package offers pre-styled, production-ready components for immediate use
PyClack is organized into three main packages:
/core: Low-level components for building custom CLIs/prompts: Ready-to-use, styled prompt components/utils: Utility functions for styling and terminal operations
Core Components
Base Prompt Class
The Prompt class in /core is the foundation for all interactive prompts in PyClack. It handles:
- Keyboard input processing
- Terminal rendering
- State management
- Event handling
- Cursor positioning
Base Prompt Class Structure
from pyclack.core import Prompt
from typing import Optional, Callable, Any, Union
class Prompt:
def __init__(
self,
render: Callable[['Prompt'], Optional[str]], # Render function
placeholder: str = '', # Placeholder text
initial_value: Any = None, # Starting value
validate: Optional[Callable[[Any], Optional[str]]] = None, # Validation
debug: bool = False, # Debug mode
track_value: bool = True # Value tracking
)
Key Properties
state: Current prompt state ('initial', 'active', 'cancel', 'submit', 'error')value: Current value of the prompterror: Current error message if validation fails_cursor: Internal cursor positioncols: Terminal width
Key Methods
prompt(): Start the prompt and handle user inputhandle_key(key: str): Process keyboard inputrender(): Render the current frameon(event: str, callback: Callable): Add event listeneremit(event: str, *args): Emit an event
Implementing a Custom Prompt
Here's a step-by-step guide to creating a custom prompt. Let's implement a simple numeric input prompt as an example:
from pyclack.core import Prompt
from pyclack.utils.styling import Color
from typing import Optional, Callable, Any, Union
class NumericPrompt(Prompt):
def __init__(
self,
render: Callable[['NumericPrompt'], str],
min_value: float = float('-inf'),
max_value: float = float('inf'),
initial_value: float = 0,
debug: bool = False
):
# Initialize parent class
super().__init__(
render=render,
initial_value=initial_value,
validate=self._validate, # Custom validation
debug=debug
)
self.min_value = min_value
self.max_value = max_value
self._text_buffer = []
self.value_with_cursor = ''
# Set up event handlers
self.on('key', self._handle_key)
self.on('finalize', self._handle_finalize)
def _validate(self, value: str) -> Optional[str]:
"""Custom validation logic."""
try:
num = float(value)
if num < self.min_value:
return f"Value must be ≥ {self.min_value}"
if num > self.max_value:
return f"Value must be ≤ {self.max_value}"
return None
except ValueError:
return "Please enter a valid number"
def _handle_key(self, char: str):
"""Handle numeric input and decimal point."""
if char == readchar.key.BACKSPACE:
if self._cursor > 0:
self._text_buffer.pop(self._cursor - 1)
self._cursor -= 1
elif char.isdigit() or (char == '.' and '.' not in self._text_buffer):
self._text_buffer.insert(self._cursor, char)
self._cursor += 1
self.value = ''.join(self._text_buffer)
self._update_value_with_cursor()
def _handle_finalize(self, *args):
"""Handle final value conversion."""
try:
self.value = float(self.value) if self.value else 0
except ValueError:
self.value = 0
self.value_with_cursor = str(self.value)
def _update_value_with_cursor(self):
"""Update display value with cursor."""
if self._cursor >= len(self.value):
self.value_with_cursor = f"{self.value}{Color.inverse(Color.hidden('_'))}"
else:
s1 = self.value[:self._cursor]
s2 = self.value[self._cursor:]
self.value_with_cursor = f"{s1}{Color.inverse(s2[0])}{s2[1:]}"
async def prompt(self) -> Union[float, object]:
"""Start the prompt and return final value."""
self._text_buffer = list(str(self.initial_value)) if self.initial_value else []
self._cursor = len(self._text_buffer)
self._update_value_with_cursor()
result = await super().prompt()
return float(result) if result is not None else result
Usage Example
async def main():
def render(prompt):
return f"Enter a number ({prompt.min_value}-{prompt.max_value}): {prompt.value_with_cursor}"
numeric = NumericPrompt(
render=render,
min_value=0,
max_value=100,
initial_value=50
)
result = await numeric.prompt()
print(f"You entered: {result}")
Key Implementation Concepts
1. State Management
The prompt maintains its state internally:
self.state = 'initial' # One of: initial, active, cancel, submit, error
2. Event System
Subscribe to events using the on() method:
self.on('key', self._handle_key) # Key press events
self.on('finalize', self._handle_finalize) # Value finalization
self.on('cursor', self._handle_cursor) # Cursor movement
3. Rendering
The render function determines the prompt's appearance:
def render(prompt):
return f"Value: {prompt.value_with_cursor}"
4. Value Tracking
Track and update the value:
self.value = ''.join(self._text_buffer) # Current value
self._update_value_with_cursor() # Display value
5. Input Handling
Process keyboard input in _handle_key:
def _handle_key(self, char: str):
if char.isdigit(): # Allow only digits
self._text_buffer.insert(self._cursor, char)
self._cursor += 1
6. Validation
Implement validation logic:
def _validate(self, value: str) -> Optional[str]:
try:
num = float(value)
return None # Valid
except ValueError:
return "Invalid number" # Error message
Core components: /core
TextPrompt
Text input component with cursor movement and editing:
from pyclack.core import TextPrompt
async def custom_text():
prompt = TextPrompt(
render=lambda p: f"Enter text: {p.value_with_cursor}",
placeholder="Type here...",
initial_value=""
)
result = await prompt.prompt()
PasswordPrompt
Secure password input with masked characters:
from pyclack.core import PasswordPrompt
async def custom_password():
prompt = PasswordPrompt(
render=lambda p: f"Password: {p.masked}",
mask="*"
)
result = await prompt.prompt()
SelectPrompt
Single-selection menu:
from pyclack.core import SelectPrompt, Option
async def custom_select():
options = [
Option("apple", "Apple"),
Option("banana", "Banana")
]
prompt = SelectPrompt(
render=lambda p: f"Select: {p.options[p.cursor].label}",
options=options
)
result = await prompt.prompt()
MultiSelectPrompt
Multiple-selection component with checkboxes:
from pyclack.core import MultiSelectPrompt, Option
async def custom_multiselect():
options = [
Option("red", "Red"),
Option("blue", "Blue")
]
prompt = MultiSelectPrompt(
render=lambda p: "Selected: " +
", ".join(opt.label for opt in p.options if opt.value in p.value),
options=options
)
result = await prompt.prompt()
Spinner
Loading indicator for async operations:
from pyclack.core import Spinner
spinner = Spinner()
spinner.start("Loading...")
# Do work
spinner.stop("Completed!")
Ready-to-Use : /prompts
The prompts package provides pre-styled components ready for immediate use.
Text Input
from pyclack.prompts import text
result = await text(
message="What's your name?",
placeholder="Enter name",
initial_value="",
validate=lambda x: "Too short" if len(x) < 3 else None
)
Password Input
from pyclack.prompts import password
result = await password(
message="Enter your password:",
mask="•",
validate=lambda x: "Too short" if len(x) < 8 else None
)
Select Menu
from pyclack.prompts import select, Option
result = await select(
message="Choose a fruit:",
options=[
Option("apple", "Apple", "Sweet and crunchy"),
Option("banana", "Banana", "Yellow fruit")
],
initial_value="apple"
)
Multiple Selection
from pyclack.prompts import multiselect, Option
result = await multiselect(
message="Select colors:",
options=[
Option("red", "Red"),
Option("blue", "Blue"),
Option("green", "Green")
],
required=True
)
Confirmation Dialog
from pyclack.prompts import confirm
result = await confirm(
message="Do you want to continue?",
active="Yes",
inactive="No",
initial_value=True
)
Spinner
from pyclack.prompts import spinner
import asyncio
# As context manager
async with spinner("Installing dependencies...") as spin:
await asyncio.sleep(1)
spin.update("Almost done...")
# As decorator
@with_spinner("Loading...")
async def long_task():
await asyncio.sleep(2)
Advanced Usage
Custom Styling
The utils.styling module provides utilities for terminal styling:
from pyclack.utils.styling import Color
# Available colors
text = Color.cyan("Cyan text")
text = Color.red("Red text")
text = Color.green("Green text")
text = Color.yellow("Yellow text")
text = Color.blue("Blue text")
text = Color.magenta("Magenta text")
text = Color.gray("Gray text")
# Text effects
text = Color.dim("Dimmed text")
text = Color.inverse("Inverse text")
text = Color.hidden("Hidden text")
text = Color.strikethrough("Strikethrough text")
Validation
All prompts support custom validation:
async def main():
result = await text(
message="Enter email:",
validate=lambda x: "Invalid email"
if not re.match(r"[^@]+@[^@]+\.[^@]+", x)
else None
)
Error Handling
from pyclack.prompts import text, is_cancel
async def main():
result = await text("Enter name:")
if is_cancel(result):
print("User cancelled")
return
Unicode Support
PyClack automatically detects Unicode support and falls back to ASCII characters when needed:
from pyclack.utils.styling import is_unicode_supported
if is_unicode_supported():
# Use Unicode symbols
symbol = "â—†"
else:
# Use ASCII fallback
symbol = "*"
Contributing
Development Setup
- Clone the repository:
git clone https://github.com/Bbalduzz/pyclack.git
cd pyclack
- Create a virtual environment:
python -m venv venv
source venv/bin/activate # Unix
venv\Scripts\activate # Windows
- Install development dependencies:
pip install -e ".[all]"
Coding Standards
- Follow PEP 8 guidelines
- Include type hints for all functions
- Document all public APIs
- Write unit tests for new features
- Maintain Unicode fallbacks for all symbols
Getting Help
- Submit issues on GitHub
- Check the GitHub repository for updates
- Read the source code for detailed implementation
Metadata
Release files for pyclack-cli 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pyclack_cli-0.4.0.tar.gz | 20.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyclack_cli-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 44.4 kB
Release files / pyclack_cli-0.4.0.tar.gz
| Download URL | pyclack_cli-0.4.0.tar.gz |
|---|---|
| Size | 20.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c636cee4c8360a2d368cd425b4e0087f3627e61e421582c314355ff70b070eb4
|
|
BLAKE2b-256 checksum How to use checksums |
cf1597812b01a5b6ba84634655aa7e829f86b89e2ec8b268114ecd40c2837180
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.0.1 CPython/3.12.3
|
Release files / pyclack_cli-0.4.0-py3-none-any.whl
| Download URL | pyclack_cli-0.4.0-py3-none-any.whl |
|---|---|
| Size | 23.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a574c144695451d3f3ec0253e555cef5132a252b715895e829a646e4cab7b36c
|
|
BLAKE2b-256 checksum How to use checksums |
bd1c556078e9a195ef2435b2e665dfcb2227de69efb5e7990faff6beca9aaccc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.0.1 CPython/3.12.3
|