A python library for building interactive command line interfaces effortlessly. Inspired by clack.cc
Project description
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
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyclack_cli-0.3.0.tar.gz.
File metadata
- Download URL: pyclack_cli-0.3.0.tar.gz
- Upload date:
- Size: 6.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.0.1 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
48d7d58941baddeabaf070bf9c0b4c247fc224de708e543dea9641319a5ba2d9
|
|
| MD5 |
50bc9cc56af1cc69c5841a71e843b491
|
|
| BLAKE2b-256 |
ed45ab0674348fe3dd9aeba734509174582e729df8d2208ae5c8d93f70c269bb
|
File details
Details for the file pyclack_cli-0.3.0-py3-none-any.whl.
File metadata
- Download URL: pyclack_cli-0.3.0-py3-none-any.whl
- Upload date:
- Size: 5.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.0.1 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f61bb31155fc644e7e60e597378e3552c3f02b1993ea482c69e2200301320a6b
|
|
| MD5 |
74cd45cc7dfc8e81f229a33ccb80df72
|
|
| BLAKE2b-256 |
83dc577378759986cc61b8be2a5cd45fed8b7a70fc961621fe894358a404c0b0
|