This guide shows you how to build polished command-line applications with the same professional UX patterns used in vibe-llama, featuring beautiful Rich-based interfaces, interactive dialogs, and real-time streaming updates.
- Overview & Architecture
- Core Dependencies
- Project Structure
- CLI Entry Point & Argument Parsing
- Rich Formatter System
- Interactive Dialogs with Prompt-Toolkit
- Streaming Events & Live Updates
- Logo & Branding
- Error Handling & User Feedback
- File Operations & Path Completion
- Complete Example Implementation
- Advanced Patterns
- Testing & Best Practices
The vibe-llama CLI follows a modular architecture with these key components:
- Main CLI Router: Uses argparse for subcommands and options
- Rich Formatter: Centralized formatting system for consistent UI elements
- Interactive Dialogs: Prompt-toolkit based UIs for user interaction
- Streaming System: Real-time updates with Rich Live components
- Async Operations: All I/O operations are async for better UX
First, set up your pyproject.toml
with the essential dependencies:
[project]
dependencies = [
"rich>=14.1.0", # Core UI library
"rich-gradient>=0.3.3", # Gradient text effects
"prompt-toolkit>=3.0.51", # Interactive terminal UIs
"httpx>=0.28.1", # Async HTTP client
]
Organize your project following this structure:
your-cli/
βββ src/
β βββ your_cli/
β βββ __init__.py
β βββ main.py # CLI entry point
β βββ logo.py # Branding and logo
β βββ commons/
β β βββ __init__.py
β β βββ formatter.py # Rich formatting utilities
β β βββ events.py # Streaming event system
β βββ handlers/
β β βββ __init__.py
β β βββ command.py # Command handlers
β βββ ui/
β βββ __init__.py
β βββ dialogs.py # Interactive dialogs
β βββ terminal.py # Terminal interface
βββ pyproject.toml
βββ README.md
#!/usr/bin/env python3
import argparse
import asyncio
from rich.console import Console
from .logo import print_logo
from .handlers import run_command_handler
from .ui import run_interactive_mode
def main() -> None:
console = Console()
parser = argparse.ArgumentParser(
prog="your-cli",
description="Your CLI tool description with Rich formatting support.",
)
# Add subcommands
subparsers = parser.add_subparsers(
dest="command",
help="Available commands",
required=True
)
# Interactive command
interactive_parser = subparsers.add_parser(
"interactive",
help="Launch interactive terminal user interface"
)
# Direct command example
direct_parser = subparsers.add_parser(
"process",
help="Process files directly from command line"
)
direct_parser.add_argument(
"-f", "--files",
nargs="+",
help="Files to process"
)
direct_parser.add_argument(
"-v", "--verbose",
action="store_true",
help="Enable verbose output"
)
args = parser.parse_args()
# Always show logo first
print_logo()
try:
if args.command == "interactive":
asyncio.run(run_interactive_mode())
elif args.command == "process":
asyncio.run(run_command_handler(args))
except KeyboardInterrupt:
console.print("\nπ Goodbye!", style="bold yellow")
return None
Create a centralized formatting system for consistent UI elements:
"""Rich formatting utilities for consistent CLI styling"""
import textwrap
from rich.console import Console, Group
from rich.text import Text
from rich.markdown import Markdown
from rich.syntax import Syntax
from rich.padding import Padding
from rich.rule import Rule
from rich.panel import Panel
class CLIFormatter:
"""Centralized Rich formatting utilities"""
@staticmethod
def _get_terminal_width() -> int:
"""Get current terminal width with fallback"""
try:
console = Console()
return console.size.width
except Exception:
return 80
@staticmethod
def success(message: str) -> Text:
"""Format success message"""
return Text(f"β
{message}", style="bold green")
@staticmethod
def error(message: str) -> Text:
"""Format error message"""
return Text(f"β {message}", style="bold red")
@staticmethod
def info(message: str) -> Text:
"""Format info message"""
return Text(f"βΉοΈ {message}", style="bold blue")
@staticmethod
def warning(message: str) -> Text:
"""Format warning message"""
return Text(f"β οΈ {message}", style="bold yellow")
@staticmethod
def tool_action(action_name: str, description: str = "") -> Text:
"""Format tool action with proper styling"""
if description:
full_text = f"βΊ {action_name}({description})"
else:
full_text = f"βΊ {action_name}"
return Text(full_text, style="bold")
@staticmethod
def status_update(text: str, prefix: str = "βΈ ") -> Text:
"""Format status updates"""
return Text(f"{prefix}{text}", style="bold")
@staticmethod
def indented_text(text: str, prefix: str = "β ", style: str = "dim white") -> Text:
"""Format text with visual indicator"""
if not text.strip():
return Text("")
formatted_text = f"{prefix}{text.strip()}"
return Text(formatted_text, style=style)
@staticmethod
def agent_response(text: str) -> Padding:
"""Format agent response with markdown support"""
if not text.strip():
return Padding(Text(""), (0, 0, 0, 0))
content = Padding(Markdown(text.strip()), (0, 0, 0, 2))
return content
@staticmethod
def code_output(code: str, title: str = "Generated Code", language: str = "python") -> Group:
"""Format code output with syntax highlighting"""
header = Text(title, style="bold green")
rule = Rule(style="dim green")
code_syntax = Syntax(
code, language, theme="monokai",
line_numbers=True, word_wrap=True
)
indented_code = Padding(code_syntax, (0, 0, 0, 2))
return Group(header, rule, indented_code)
@staticmethod
def file_list(files: list, title: str = "π Available Files") -> Group:
"""Format file list display"""
header = Text(title, style="bold yellow")
rule = Rule(style="dim yellow")
file_lines = []
for i, file in enumerate(files[:10]):
file_lines.append(Text(f" {i + 1}. {file}", style="dim white"))
if len(files) > 10:
file_lines.append(
Text(f" ... and {len(files) - 10} more files", style="dim")
)
return Group(header, rule, *file_lines)
@staticmethod
def progress_panel(message: str) -> Panel:
"""Create a progress panel"""
return Panel(
Text(message, style="bold cyan"),
border_style="cyan",
padding=(1, 2)
)
Create beautiful interactive dialogs for user input:
"""Interactive dialogs using prompt-toolkit"""
from typing import Optional, List, Tuple
from prompt_toolkit.shortcuts import checkboxlist_dialog, yes_no_dialog, input_dialog
from prompt_toolkit.formatted_text import HTML
from prompt_toolkit.styles import Style
# Define your app's color scheme
APP_STYLE = Style.from_dict({
"dialog": "bg:#F8E9D8",
"dialog.body": "bg:#45DFF8",
"dialog shadow": "bg:#a6a2ab",
"button.focused": "bg:#FFA6EA",
})
async def select_multiple_options(
title: str,
text: str,
options: List[Tuple[str, str]] # [(display_name, value), ...]
) -> Optional[List[str]]:
"""Multi-select checkbox dialog"""
dialog = checkboxlist_dialog(
title=HTML(f"<style fg='black'>{title}</style>"),
text=text,
values=options,
style=APP_STYLE,
)
return await dialog.run_async()
async def confirm_action(title: str, text: str) -> bool:
"""Yes/No confirmation dialog"""
dialog = yes_no_dialog(
title=HTML(f"<style fg='black'>{title}</style>"),
text=text,
style=APP_STYLE,
)
result = await dialog.run_async()
return result if result is not None else False
async def get_text_input(title: str, text: str, default: str = "") -> Optional[str]:
"""Text input dialog"""
dialog = input_dialog(
title=HTML(f"<style fg='black'>{title}</style>"),
text=text,
default=default,
style=APP_STYLE,
)
return await dialog.run_async()
# Example usage combining multiple dialogs
async def run_interactive_setup() -> Optional[dict]:
"""Run a series of interactive dialogs"""
# Step 1: Select options
file_options = [
("Process Documents", "docs"),
("Process Images", "images"),
("Process Audio", "audio"),
]
selected_types = await select_multiple_options(
"File Types",
"Which file types would you like to process?",
file_options
)
if not selected_types:
return None
# Step 2: Get input path
input_path = await get_text_input(
"Input Path",
"Enter the path to your files:",
default="./data"
)
if not input_path:
return None
# Step 3: Confirm settings
confirmed = await confirm_action(
"Confirm Settings",
f"Process {', '.join(selected_types)} from {input_path}?"
)
if not confirmed:
return None
return {
"types": selected_types,
"path": input_path,
"confirmed": True
}
Implement real-time streaming updates using Rich Live components:
"""Event system for streaming updates"""
from typing import Any, Optional
from pydantic import BaseModel
class StreamEvent:
"""Event for streaming response tokens and rich content"""
def __init__(
self,
delta: str = "",
rich_content: Any | None = None,
newline_after: bool = False,
is_code: bool = False,
):
self.delta = delta
self.rich_content = rich_content
self.newline_after = newline_after
self.is_code = is_code
"""Streaming handler with Rich Live updates"""
import asyncio
from rich.console import Console
from rich.live import Live
from rich.spinner import Spinner
from ..commons.formatter import CLIFormatter
from ..commons.events import StreamEvent
class StreamingHandler:
"""Handle streaming content with Rich Live updates"""
def __init__(self):
self.console = Console()
self.code_buffer = ""
self.live_panel = None
async def handle_stream(self, event_generator):
"""Process streaming events with live updates"""
try:
async for event in event_generator:
if isinstance(event, StreamEvent):
if event.is_code:
await self._handle_code_stream(event)
elif event.rich_content:
await self._handle_rich_content(event)
else:
await self._handle_text_stream(event)
except KeyboardInterrupt:
if self.live_panel:
self.live_panel.stop()
self.console.print("\nπ Goodbye!", style="bold yellow")
finally:
if self.live_panel:
self.live_panel.stop()
async def _handle_code_stream(self, event: StreamEvent):
"""Handle streaming code with live syntax highlighting"""
self.code_buffer += event.delta
# Implement tail-following for long code
max_lines = self.console.size.height - 15
lines = self.code_buffer.split("\n")
if len(lines) > max_lines:
visible_lines = lines[-max_lines:]
display_code = "\n".join(visible_lines)
hidden_lines = len(lines) - max_lines
display_code = f"# ... ({hidden_lines} lines above)\n" + display_code
title = f"π§ Generating Code... (showing last {max_lines}/{len(lines)} lines)"
else:
display_code = self.code_buffer
title = f"π§ Generating Code... ({len(lines)} lines)"
if not self.live_panel:
self.live_panel = Live(
CLIFormatter.code_output(display_code, title),
refresh_per_second=8,
console=self.console
)
self.live_panel.start()
else:
self.live_panel.update(
CLIFormatter.code_output(display_code, title)
)
async def _handle_rich_content(self, event: StreamEvent):
"""Handle rich content display"""
if self.live_panel:
self.live_panel.stop()
self.console.print()
self.live_panel = None
self.code_buffer = ""
self.console.print(event.rich_content)
if event.newline_after:
self.console.print()
async def _handle_text_stream(self, event: StreamEvent):
"""Handle plain text streaming"""
if self.live_panel:
self.live_panel.stop()
self.console.print()
self.live_panel = None
self.code_buffer = ""
print(event.delta, end="", flush=True)
# Example: Async generator that produces streaming events
async def simulate_code_generation():
"""Simulate streaming code generation"""
code_lines = [
"import asyncio\n",
"from rich.console import Console\n\n",
"async def main():\n",
" console = Console()\n",
" console.print('Hello World!')\n\n",
"if __name__ == '__main__':\n",
" asyncio.run(main())\n"
]
for line in code_lines:
# Simulate typing delay
for char in line:
yield StreamEvent(delta=char, is_code=True)
await asyncio.sleep(0.05)
# Send final rich content
yield StreamEvent(
rich_content=CLIFormatter.success("Code generation complete!"),
newline_after=True
)
Create an eye-catching logo with gradient effects:
"""Application logo and branding"""
from rich.console import Console
from rich_gradient.text import Text
# ASCII art logo
LOGO = """
βββ βββ βββββββ βββ ββββββββββ ββββββββββ βββ
ββββ ββββββββββββββββ βββββββββββ βββββββββββ βββ
βββββββ βββ ββββββ βββββββββββ βββ βββ βββ
βββββ βββ ββββββ βββββββββββ βββ βββ βββ
βββ βββββββββββββββββββββ βββ βββββββββββββββββββ
βββ βββββββ βββββββ βββ βββ ββββββββββββββββββ
"""
def print_logo() -> None:
"""Print the application logo with gradient colors"""
console = Console()
print("\n")
# Create gradient text - customize colors for your brand
gradient_logo = Text(
LOGO,
colors=["#F8E9D8", "#FFA6EA", "#45DFF8", "#BB8DEB"]
)
console.print(gradient_logo, justify="center")
print("\n")
console.print("-" * 50, style="gray bold", justify="center")
print("\n")
def print_app_info(version: str = "1.0.0") -> None:
"""Print application information"""
console = Console()
console.print(f"[bold cyan]Your CLI Tool v{version}[/bold cyan]", justify="center")
console.print("[dim]A professional CLI application built with Rich[/dim]", justify="center")
print()
Implement comprehensive error handling with user-friendly feedback:
"""Error handling and user feedback utilities"""
import os
import traceback
from typing import Tuple, List
from pathlib import Path
from rich.console import Console
from .formatter import CLIFormatter
console = Console()
class CLIError(Exception):
"""Base exception for CLI errors"""
pass
class ValidationError(CLIError):
"""Validation error with user-friendly message"""
def __init__(self, message: str, suggestions: List[str] = None):
self.message = message
self.suggestions = suggestions or []
super().__init__(message)
def handle_error(error: Exception, show_traceback: bool = False) -> None:
"""Handle and display errors with Rich formatting"""
if isinstance(error, ValidationError):
console.print(CLIFormatter.error(error.message))
for suggestion in error.suggestions:
console.print(CLIFormatter.indented_text(suggestion))
elif isinstance(error, FileNotFoundError):
console.print(CLIFormatter.error(f"File not found: {error.filename}"))
_suggest_similar_files(error.filename)
elif isinstance(error, PermissionError):
console.print(CLIFormatter.error(f"Permission denied: {error.filename}"))
console.print(CLIFormatter.indented_text("Check file permissions and try again"))
else:
console.print(CLIFormatter.error(f"Unexpected error: {str(error)}"))
if show_traceback:
console.print("\n[dim]Traceback:[/dim]")
console.print(f"[dim]{traceback.format_exc()}[/dim]")
def _suggest_similar_files(missing_file: str) -> None:
"""Suggest similar files in the directory"""
if not missing_file:
return
parent_dir = os.path.dirname(missing_file) or "."
if not os.path.exists(parent_dir):
return
try:
files = [f for f in os.listdir(parent_dir)
if not f.startswith('.')]
if files:
console.print(CLIFormatter.indented_text("Available files:"))
for file in files[:5]:
console.print(CLIFormatter.indented_text(f" π {file}"))
if len(files) > 5:
console.print(CLIFormatter.indented_text(f" ... and {len(files) - 5} more"))
except PermissionError:
pass
def validate_file_path(path: str) -> Tuple[bool, str]:
"""Validate file path and return status with message"""
if not path:
return False, "Path cannot be empty"
if not os.path.exists(path):
return False, f"Path does not exist: {path}"
if not os.path.isfile(path):
return False, f"Path is not a file: {path}"
if not os.access(path, os.R_OK):
return False, f"Cannot read file: {path}"
return True, "Valid file path"
def validate_directory_path(path: str) -> Tuple[bool, str]:
"""Validate directory path and return status with message"""
if not path:
return False, "Path cannot be empty"
if not os.path.exists(path):
return False, f"Directory does not exist: {path}"
if not os.path.isdir(path):
return False, f"Path is not a directory: {path}"
if not os.access(path, os.R_OK):
return False, f"Cannot access directory: {path}"
return True, "Valid directory path"
Add smart path completion and file operations:
"""Path completion for interactive input"""
import os
from typing import List
from prompt_toolkit.completion import Completer, Completion
class PathCompleter(Completer):
"""Smart path completer with @ symbol support"""
def get_completions(self, document, complete_event):
text = document.text
cursor_pos = document.cursor_position
# Find @ symbol for path completion
at_pos = text.rfind("@", 0, cursor_pos)
if at_pos == -1:
return
# Extract path after @
path_part = text[at_pos + 1:cursor_pos]
# Determine search directory and pattern
if "/" in path_part:
base_dir, pattern = path_part.rsplit("/", 1)
search_dir = base_dir if os.path.exists(base_dir) else "."
else:
search_dir = "."
pattern = path_part
try:
items = self._get_matching_items(search_dir, pattern)
completions = self._create_completions(items, pattern, at_pos + 1, cursor_pos)
return completions[:10] # Limit results
except (OSError, PermissionError):
return []
def _get_matching_items(self, search_dir: str, pattern: str) -> List[Completion]:
"""Get matching files and directories"""
items = []
for item in os.listdir(search_dir):
full_path = os.path.join(search_dir, item)
if item.lower().startswith(pattern.lower()):
is_dir = os.path.isdir(full_path)
if is_dir:
display_text = f"π {item}/"
insert_text = item + "/"
else:
display_text = f"π {item}"
insert_text = item
completion = Completion(
insert_text,
display=display_text,
)
completion._is_dir = is_dir
completion._sort_text = item.lower()
items.append(completion)
# Sort: directories first, then alphabetically
items.sort(key=lambda x: (not x._is_dir, x._sort_text))
return items
def _create_completions(self, items: List[Completion], pattern: str,
start_pos: int, cursor_pos: int) -> List[Completion]:
"""Create completion objects with correct positioning"""
completions = []
for item in items:
start_position = start_pos + len(pattern.split("/")[-1]) - cursor_pos
completion = Completion(
item.text,
start_position=start_position,
display=item.display
)
completions.append(completion)
return completions
Here's a complete example tying everything together:
#!/usr/bin/env python3
"""Complete example CLI application"""
import asyncio
import argparse
from pathlib import Path
from typing import List, Optional
from rich.console import Console
from rich.live import Live
from rich.progress import Progress, SpinnerColumn, TextColumn
from .logo import print_logo
from .commons.formatter import CLIFormatter
from .commons.events import StreamEvent
from .commons.errors import handle_error, validate_file_path
from .handlers.streaming import StreamingHandler
from .ui.dialogs import run_interactive_setup
console = Console()
class ExampleCLI:
"""Example CLI application class"""
def __init__(self):
self.formatter = CLIFormatter()
self.streaming_handler = StreamingHandler()
async def process_files(self, files: List[str], verbose: bool = False) -> None:
"""Process files with progress updates"""
console.print(self.formatter.tool_action("ProcessFiles", f"files={len(files)}"))
console.print(self.formatter.indented_text(f"Processing {len(files)} files..."))
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console
) as progress:
task = progress.add_task("Processing files...", total=len(files))
for i, file_path in enumerate(files):
# Validate file
is_valid, message = validate_file_path(file_path)
if not is_valid:
console.print(self.formatter.error(f"Skipping {file_path}: {message}"))
continue
# Simulate processing
progress.update(task, description=f"Processing {Path(file_path).name}...")
await asyncio.sleep(0.5) # Simulate work
if verbose:
console.print(self.formatter.indented_text(f"β Processed {file_path}"))
progress.update(task, advance=1)
console.print(self.formatter.success("All files processed successfully!"))
async def interactive_mode(self) -> None:
"""Run interactive mode with dialogs"""
console.print(self.formatter.info("Starting interactive mode..."))
# Run interactive setup
settings = await run_interactive_setup()
if not settings:
console.print(self.formatter.warning("Operation cancelled"))
return
console.print(self.formatter.success("Settings configured successfully!"))
# Display settings
console.print(self.formatter.indented_text("Configuration:"))
console.print(self.formatter.indented_text(f" Types: {', '.join(settings['types'])}"))
console.print(self.formatter.indented_text(f" Path: {settings['path']}"))
# Simulate processing with streaming updates
await self._simulate_processing()
async def _simulate_processing(self) -> None:
"""Simulate processing with streaming updates"""
async def generate_events():
# Initial status
yield StreamEvent(
rich_content=self.formatter.status_update("Starting processing..."),
newline_after=True
)
# Simulate code generation
code_lines = [
"# Generated processing script\n",
"import os\n",
"from pathlib import Path\n\n",
"def process_files(input_path):\n",
" files = list(Path(input_path).glob('*'))\n",
" for file in files:\n",
" print(f'Processing {file.name}...')\n",
" # Process file here\n",
" return len(files)\n\n",
"if __name__ == '__main__':\n",
" result = process_files('./data')\n",
" print(f'Processed {result} files')\n"
]
for line in code_lines:
yield StreamEvent(delta=line, is_code=True)
await asyncio.sleep(0.1)
# Final success message
yield StreamEvent(
rich_content=self.formatter.success("Processing complete!"),
newline_after=True
)
await self.streaming_handler.handle_stream(generate_events())
async def main() -> None:
"""Main CLI entry point"""
parser = argparse.ArgumentParser(
prog="example-cli",
description="Example CLI application with Rich interface"
)
subparsers = parser.add_subparsers(dest="command", required=True)
# Interactive command
subparsers.add_parser("interactive", help="Launch interactive mode")
# Process command
process_parser = subparsers.add_parser("process", help="Process files")
process_parser.add_argument("files", nargs="+", help="Files to process")
process_parser.add_argument("-v", "--verbose", action="store_true")
args = parser.parse_args()
# Show logo
print_logo()
cli = ExampleCLI()
try:
if args.command == "interactive":
await cli.interactive_mode()
elif args.command == "process":
await cli.process_files(args.files, args.verbose)
except Exception as error:
handle_error(error)
except KeyboardInterrupt:
console.print("\nπ Goodbye!", style="bold yellow")
if __name__ == "__main__":
asyncio.run(main())
"""Advanced Rich layout patterns"""
from rich.console import Console
from rich.layout import Layout
from rich.panel import Panel
from rich.align import Align
def create_dashboard_layout():
"""Create a dashboard-style layout"""
layout = Layout()
layout.split_column(
Layout(name="header", size=3),
Layout(name="body"),
Layout(name="footer", size=3)
)
layout["body"].split_row(
Layout(name="sidebar", ratio=1),
Layout(name="main", ratio=2)
)
# Add content to panels
layout["header"].update(
Panel(Align.center("π My CLI Dashboard"), style="bold blue")
)
layout["sidebar"].update(
Panel("Sidebar content here", title="Options")
)
layout["main"].update(
Panel("Main content area", title="Output")
)
layout["footer"].update(
Panel("Status: Ready", style="dim")
)
return layout
console = Console()
console.print(create_dashboard_layout())
"""Configuration management with rich display"""
import json
from pathlib import Path
from typing import Dict, Any
from rich.table import Table
from rich.console import Console
class CLIConfig:
"""CLI configuration management"""
def __init__(self, config_path: str = ".cli-config.json"):
self.config_path = Path(config_path)
self.config = self.load_config()
self.console = Console()
def load_config(self) -> Dict[str, Any]:
"""Load configuration from file"""
if self.config_path.exists():
return json.loads(self.config_path.read_text())
return self.get_default_config()
def save_config(self) -> None:
"""Save configuration to file"""
self.config_path.write_text(
json.dumps(self.config, indent=2)
)
def get_default_config(self) -> Dict[str, Any]:
"""Get default configuration"""
return {
"theme": "dark",
"verbose": False,
"max_workers": 4,
"output_format": "json"
}
def display_config(self) -> None:
"""Display configuration in a rich table"""
table = Table(title="Current Configuration")
table.add_column("Setting", style="cyan")
table.add_column("Value", style="green")
for key, value in self.config.items():
table.add_row(key, str(value))
self.console.print(table)
def update_setting(self, key: str, value: Any) -> None:
"""Update a configuration setting"""
self.config[key] = value
self.save_config()
self.console.print(f"β
Updated {key} = {value}")
"""Simple plugin system for extensibility"""
import importlib
from abc import ABC, abstractmethod
from typing import Dict, Type, List
class CLIPlugin(ABC):
"""Base class for CLI plugins"""
@property
@abstractmethod
def name(self) -> str:
"""Plugin name"""
pass
@abstractmethod
async def execute(self, args: Dict[str, Any]) -> None:
"""Execute plugin with arguments"""
pass
class PluginManager:
"""Manage CLI plugins"""
def __init__(self):
self.plugins: Dict[str, Type[CLIPlugin]] = {}
def register_plugin(self, plugin_class: Type[CLIPlugin]) -> None:
"""Register a plugin"""
plugin = plugin_class()
self.plugins[plugin.name] = plugin_class
def get_plugin(self, name: str) -> Optional[CLIPlugin]:
"""Get plugin by name"""
plugin_class = self.plugins.get(name)
return plugin_class() if plugin_class else None
def list_plugins(self) -> List[str]:
"""List available plugins"""
return list(self.plugins.keys())
# Example plugin
class ExamplePlugin(CLIPlugin):
@property
def name(self) -> str:
return "example"
async def execute(self, args: Dict[str, Any]) -> None:
console = Console()
console.print(f"[green]Example plugin executed with args: {args}[/green]")
"""Testing Rich CLI components"""
import pytest
from io import StringIO
from rich.console import Console
from your_cli.commons.formatter import CLIFormatter
def test_formatter_success_message():
"""Test success message formatting"""
console = Console(file=StringIO(), width=80)
message = CLIFormatter.success("Test message")
console.print(message)
output = console.file.getvalue()
assert "β
" in output
assert "Test message" in output
def test_formatter_error_message():
"""Test error message formatting"""
console = Console(file=StringIO(), width=80)
message = CLIFormatter.error("Test error")
console.print(message)
output = console.file.getvalue()
assert "β" in output
assert "Test error" in output
@pytest.mark.asyncio
async def test_interactive_dialog():
"""Test interactive dialog (mock user input)"""
# Mock prompt_toolkit dialog
from unittest.mock import patch, AsyncMock
with patch('your_cli.ui.dialogs.yes_no_dialog') as mock_dialog:
mock_dialog.return_value.run_async = AsyncMock(return_value=True)
from your_cli.ui.dialogs import confirm_action
result = await confirm_action("Test", "Confirm?")
assert result is True
"""Performance optimization patterns"""
import asyncio
from concurrent.futures import ThreadPoolExecutor
from typing import List, Callable, Any
async def run_concurrent_tasks(
tasks: List[Callable],
max_workers: int = 4
) -> List[Any]:
"""Run CPU-bound tasks concurrently"""
loop = asyncio.get_event_loop()
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [
loop.run_in_executor(executor, task)
for task in tasks
]
return await asyncio.gather(*futures)
# Lazy loading for large datasets
class LazyDataLoader:
"""Lazy load data to improve startup time"""
def __init__(self, data_source: str):
self.data_source = data_source
self._data = None
@property
def data(self):
if self._data is None:
self._data = self._load_data()
return self._data
def _load_data(self):
# Load expensive data only when needed
return {"loaded": True}
- Consistent Styling: Use a centralized
CLIFormatter
class - Async Operations: Use async/await for all I/O operations
- Error Handling: Provide helpful error messages with suggestions
- Progress Updates: Show progress for long-running operations
- Responsive UI: Use Rich Live components for real-time updates
- Path Completion: Implement smart path completion for better UX
- Configuration: Store user preferences in config files
- Testing: Write tests for CLI components and user interactions
- Performance: Use lazy loading and concurrent execution when appropriate
- Documentation: Document all CLI commands and options
This guide provides a comprehensive foundation for building professional CLI applications with Rich. The patterns used here mirror those in vibe-llama and can be adapted for any command-line tool that needs beautiful, interactive interfaces.