Skip to main content

A python library for building interactive command line interfaces effortlessly. Inspired by clack.cc

Project description

logo
language PyPI - Version PyPI - Downloads

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

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:

  1. /core: Low-level components for building custom CLIs
  2. /prompts: Ready-to-use, styled prompt components
  3. /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 prompt
  • error: Current error message if validation fails
  • _cursor: Internal cursor position
  • cols: Terminal width

Key Methods

  • prompt(): Start the prompt and handle user input
  • handle_key(key: str): Process keyboard input
  • render(): Render the current frame
  • on(event: str, callback: Callable): Add event listener
  • emit(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

  1. Clone the repository:
git clone https://github.com/Bbalduzz/pyclack.git
cd pyclack
  1. Create a virtual environment:
python -m venv venv
source venv/bin/activate  # Unix
venv\Scripts\activate     # Windows
  1. 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

pyclack_cli-0.3.1.tar.gz (6.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyclack_cli-0.3.1-py3-none-any.whl (5.9 kB view details)

Uploaded Python 3

File details

Details for the file pyclack_cli-0.3.1.tar.gz.

File metadata

  • Download URL: pyclack_cli-0.3.1.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

Hashes for pyclack_cli-0.3.1.tar.gz
Algorithm Hash digest
SHA256 baf266becde2ff4395b361f274d4f2b23593022fb91b66fd2e83e2c8fb108f4d
MD5 d5cc423b95181cb2d21f28086defd483
BLAKE2b-256 6c44b213b8e78d4ce3f21d36266b550543250c9e07e3014d1c8aec16e94d2e13

See more details on using hashes here.

File details

Details for the file pyclack_cli-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: pyclack_cli-0.3.1-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

Hashes for pyclack_cli-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 315a978c2b43e8c4e3ffbe4b164bb2fcb9e7fd0407b71cb89d2091102865e2de
MD5 099b3e7d25c54ba58a0d03773b39b9dd
BLAKE2b-256 e8d28e87f256e416b2c75f29d3bfe821c62c5a5b7d90536835fd8a972cf0c0ba

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page