UI Utilities¶
The UI utilities module provides a comprehensive set of tools for building consistent, type-safe, and functional command-line interfaces using Rich and Expression.
Core Concepts¶
Type Aliases¶
# Basic UI Results
DisplayResult: TypeAlias = Result[None, DisplayError]
"""Result type for display operations that don't return a value."""
TableResult: TypeAlias = Result[Table, DisplayError]
"""Result type for operations that return a Rich Table."""
PanelResult: TypeAlias = Result[Panel, DisplayError]
"""Result type for operations that return a Rich Panel."""
ValidationResult: TypeAlias = Result[str, DisplayError]
"""Result type for string validation operations."""
# Data Structures
TableRow: TypeAlias = tuple[str, str]
"""Type for representing a table row with two columns."""
TableData: TypeAlias = list[TableRow]
"""Type for representing table data as a list of rows."""
# Function Types
StyleValidator: TypeAlias = Callable[[str], ValidationResult]
"""Type for style validation functions."""
DisplayFunction[T]: TypeAlias = Callable[[T], DisplayResult]
"""Type for display functions that return DisplayResult."""
RecoveryStrategy[T]: TypeAlias = dict[str, Callable[[], Result[T, DisplayError]]]
"""Type for mapping error types to recovery functions."""
ProgressProcessor[T, U]: TypeAlias = Callable[[T], Result[U, str]]
"""Type for processing items in a progress operation."""
DisplayError¶
A tagged union type representing all possible UI-related errors:
@tagged_union
class DisplayError:
"""Represents display-related errors."""
tag: Literal["validation", "rendering", "interaction", "timeout", "execution", "input"]
validation: str | None = None
rendering: tuple[str, Exception] | None = None
interaction: tuple[str, Exception] | None = None
timeout: tuple[str, Exception] | None = None
execution: tuple[str, str] | None = None
input: tuple[str, str] | None = None
# Static constructors for each error type
@staticmethod
def Validation(message: str) -> "DisplayError":
return DisplayError(tag="validation", validation=message)
# Other constructors omitted for brevity
Result-based Error Handling¶
All UI functions return Result[T, DisplayError] for type-safe error handling:
Result[None, DisplayError] # For display operations
Result[Table, DisplayError] # For table creation
Result[Panel, DisplayError] # For panel creation
Basic Display Functions¶
Messages¶
success_message(message: str) -> DisplayResult
error_message(message: str, details: str | None = None) -> DisplayResult
warning_message(message: str) -> DisplayResult
display_message(message: str, style: str) -> DisplayResult
Panels and Rules¶
create_panel(title: str, content: str, style: str) -> Result[Panel, DisplayError]
display_rule(message: str, style: str = "blue") -> DisplayResult
Tables¶
Basic Tables¶
create_table_row(name_result: tuple[str, Result[str, T]]) -> Result[tuple[str, str], DisplayError]
add_row_to_table(table: Table, row: tuple[str, str]) -> Result[Table, DisplayError]
create_summary_table(results: Block[tuple[str, Result[str, T]]]) -> Result[Table, DisplayError]
Advanced Tables¶
create_multi_column_table(
columns: list[tuple[str, str | None]],
rows: list[list[str]],
title: str | None = None
) -> Result[Table, DisplayError]
Input Handling¶
prompt_for_input(
prompt: str,
validator: Callable[[str], Result[str, str]] | None = None
) -> Result[str, DisplayError]
confirm_action(prompt: str) -> Result[bool, DisplayError]
Progress Display¶
display_progress[T](
items: Iterable[T],
process_fn: Callable[[T], Result[str, str]],
description: str = "Processing"
) -> DisplayResult
Error Handling Utilities¶
Basic Error Handling¶
handle_ui_error(error: DisplayError) -> DisplayResult
aggregate_errors(errors: Block[DisplayError]) -> DisplayError
Advanced Error Recovery¶
with_fallback[T](
operation: Callable[[], Result[T, DisplayError]],
fallback: T,
error_message: str | None = None
) -> T
with_retry[T](
operation: Callable[[], Result[T, DisplayError]],
max_attempts: int = 3,
delay: float = 1.0
) -> Result[T, DisplayError]
recover_ui[T](
operation: Callable[[], Result[T, DisplayError]],
recovery_strategies: dict[str, Callable[[], Result[T, DisplayError]]],
max_attempts: int = 3
) -> Result[T, DisplayError]
Context Management¶
with_ui_context[T](
operation: Callable[[], Result[T, DisplayError]],
setup: Callable[[], Result[None, DisplayError]] | None = None,
cleanup: Callable[[], Result[None, DisplayError]] | None = None
) -> Result[T, DisplayError]
Safe Display Operations¶
safe_display[T](
content: T,
display_fn: Callable[[T], Result[None, DisplayError]],
fallback_fn: Callable[[T], Result[None, DisplayError]] | None = None
) -> DisplayResult
Validation¶
def validate_input(value: str | None, name: str) -> Result[str, DisplayError]
def validate_style(style: str) -> Result[str, DisplayError]
def validate_table_data(headers: list[str], rows: list[list[str]]) -> Result[None, DisplayError]
Best Practices¶
- Always Handle Errors: Use
.map_error(handle_ui_error)or proper error handling for all UI operations - Validate Inputs: Use validation functions before processing
- Use Type Safety: Leverage generic types and Result for type-safe operations
- Compose Operations: Use pipe and functional composition for complex operations
- Provide Context: Use descriptive error messages and proper error types
Examples¶
Creating a Summary Display¶
def display_summary(results: Block[tuple[str, Result[str, Error]]]) -> DisplayResult:
return (
create_summary_table(results)
.bind(lambda table:
display_rule("Summary")
.bind(lambda _: Ok(console.print(table)))
)
)
Input with Validation¶
def get_verified_input(prompt: str) -> Result[str, DisplayError]:
return prompt_for_input(
prompt,
lambda value: Ok(value) if value.strip() else Error("Input cannot be empty")
)
Error Recovery¶
def safe_display_operation() -> DisplayResult:
return recover_ui(
lambda: display_complex_ui(),
{
"rendering": lambda: display_fallback_ui(),
"validation": lambda: display_error_message()
}
)
Progress with Context¶
def process_with_progress(items: list[str]) -> DisplayResult:
return with_ui_context(
lambda: display_progress(
items,
process_item,
"Processing items"
),
setup=lambda: display_rule("Starting Process"),
cleanup=lambda: display_rule("Process Complete")
)
Common Usage Patterns¶
Composing Multiple UI Operations¶
def complex_ui_operation() -> DisplayResult:
return (
display_rule("Starting Operation")
.bind(lambda _: prompt_for_input("Enter value: "))
.bind(lambda value:
create_panel("Input", value, "cyan")
.bind(lambda panel: Ok(console.print(panel)))
)
.bind(lambda _: success_message("Operation complete"))
)
Safe Error Recovery¶
When dealing with potentially failing UI operations:
def display_with_recovery() -> None:
result = with_retry(
lambda: display_complex_data(),
max_attempts=3,
delay=1.0
)
if result.is_error():
with_fallback(
lambda: display_simple_fallback(),
fallback=None,
error_message="Failed to display data"
)
Progress with Error Handling¶
Processing items with progress and proper error handling:
def process_items(items: list[str]) -> DisplayResult:
return with_ui_context(
lambda: display_progress(
items,
lambda item: Try.apply(lambda: process_single_item(item))
.map_error(lambda e: f"Failed to process {item}: {str(e)}"),
"Processing items"
),
setup=lambda: display_rule("Starting batch process"),
cleanup=lambda: display_rule("Process complete")
)
Terminal UI (TUI)¶
The Fast-Craftsmanship CLI includes an interactive Terminal User Interface (TUI) that provides a user-friendly way to navigate and use all available commands.
Launching the TUI¶
The TUI can be launched with:
TUI Features¶
- Category-based Navigation: Commands are organized into logical categories for easy navigation
- Interactive Selection: Select commands and options with simple keyboard input
- Command Help: View detailed help for any available command
- Command Execution: Run commands directly from the TUI with interactive input
- Keyboard Navigation: Use keyboard shortcuts for quick navigation (b for back, q for quit)
- Context-aware Display: See relevant information for each command
- Error Handling: Graceful error handling for command execution failures
TUI Structure¶
The TUI provides a three-level navigation structure:
- Category Selection: Choose from available command categories (scaffold, vcs, quality, db)
- Command Selection: Select a specific command within the chosen category
- Command Options: Run the command or view its help information
Implementation Details¶
The TUI is implemented using the Rich library for Python:
# Main TUI components
def run_tui() -> None:
"""Run the Terminal UI application."""
try:
while True:
# Display categories and get selection
valid_categories = display_categories()
choice = Prompt.ask("> ", choices=["q"] + [str(i) for i in range(1, len(valid_categories) + 1)])
if choice.lower() == 'q':
break
# Handle category, command, and option selection
# [implementation details]
except KeyboardInterrupt:
console.print("\n[bold yellow]Menu interrupted. Exiting...[/bold yellow]")
finally:
clear_screen()
console.print("[bold green]Thanks for using Fast-Craftsmanship CLI![/bold green]")
Menu Navigation Functions¶
The TUI implementation includes several key functions:
# Display available categories
def display_categories():
"""Display the available command categories."""
# Implementation using Rich tables and panels
# Display commands within a category
def display_commands(category_id: str):
"""Display the commands for a specific category."""
# Implementation using Rich tables
# Display options for a command
def display_command_options(category_id: str, command_name: str):
"""Display options for a specific command."""
# Implementation using Rich panels and tables
# Execute commands
def run_command(command_name: str, show_help: bool = False):
"""Run a command or show its help."""
# Implementation using subprocess
Command Execution¶
Commands are executed using Python's subprocess module to maintain interactive capabilities:
def run_command(command_name: str, show_help: bool = False):
"""Run a command or show its help."""
cmd = ["python", "-m", "fcship.cli", command_name, "--help" if show_help else ""]
try:
# Use subprocess.Popen to maintain interactive capabilities
process = subprocess.Popen(cmd)
process.wait()
# Handle command completion or failure
if process.returncode == 0:
console.print("\n[bold green]Command completed successfully.[/bold green]")
else:
console.print(f"\n[bold red]Command failed with exit code {process.returncode}[/bold red]")
except Exception as e:
console.print(f"\n[bold red]Error: {e}[/bold red]")
Example Usage Flow¶
A typical user interaction flow:
- Launch the TUI with
python -m fcship.cli menu - View the available categories and select one (e.g., "scaffold")
- View commands in the selected category and choose one (e.g., "project")
- Choose to either run the command or view its help
- If running the command, interact with it directly
- After command execution, return to the command options menu
- Navigate back to previous menus or quit the TUI
Batch Operations with Progress¶
For commands that process multiple items with progress tracking:
def batch_process[T, U](
items: Block[T],
process_fn: Callable[[T], Result[U, DisplayError]],
batch_size: int = 10
) -> Result[Block[U], DisplayError]:
def process_batch(batch: Block[T]) -> DisplayResult:
return with_ui_context(
lambda: display_progress(
batch,
process_fn,
f"Processing batch of {len(batch)} items"
),
setup=lambda: display_rule("Starting batch"),
cleanup=lambda: success_message("Batch complete")
)
return pipe(
items.chunk(batch_size),
seq.traverse(process_batch),
Result.map(seq.concat)
)
Form Input Collection¶
Collecting multiple inputs with validation:
def collect_form_data(
fields: list[tuple[str, Callable[[str], Result[str, str]]]]
) -> Result[dict[str, str], DisplayError]:
def collect_field(field: tuple[str, Callable[[str], Result[str, str]]]) -> Result[tuple[str, str], DisplayError]:
prompt, validator = field
return prompt_for_input(f"{prompt}: ", validator).map(lambda v: (prompt, v))
return pipe(
fields,
Block.of_seq,
seq.traverse(collect_field),
Result.map(dict)
)
Integration with Other Modules¶
With Error Handling Module¶
from fcship.utils.error_handling import handle_command_errors
@handle_command_errors
def safe_ui_operation() -> None:
result = complex_ui_operation()
if result.is_error():
handle_ui_error(result.error)
With Validation Module¶
from fcship.utils.validation import validate_path
def prompt_for_file() -> Result[str, DisplayError]:
return prompt_for_input(
"Enter file path: ",
lambda p: validate_path(p).map_error(str)
)
With Type Utils¶
from fcship.utils.type_utils import ensure_type
def display_typed_data[T](data: Any, expected_type: type[T]) -> DisplayResult:
return (
ensure_type(data, expected_type)
.map_error(lambda e: DisplayError.Validation(str(e)))
.bind(lambda typed_data: display_message(str(typed_data), "cyan"))
)
Testing UI Components¶
Mock Console Example¶
from unittest.mock import Mock
from rich.console import Console
def test_ui_component():
mock_console = Mock(spec=Console)
result = with_ui_context(
lambda: display_complex_ui(),
console=mock_console
)
assert result.is_ok()
mock_console.print.assert_called()
Testing Error Handling¶
def test_error_recovery():
failing_op = lambda: Error(DisplayError.Rendering("Test error", Exception()))
result = with_retry(failing_op, max_attempts=2)
assert result.is_error()
assert result.error.tag == "rendering"
Performance Considerations¶
- Batch Processing: Use
batch_processfor large datasets - Progress Indicators: Only use for operations taking >1 second
- Error Recovery: Set reasonable retry attempts and delays
- Context Management: Clean up resources properly with
with_ui_context
Extending the UI Utilities¶
Creating Custom Display Types¶
@dataclass
class CustomDisplay:
title: str
content: str
style: str
def display_custom(
custom: CustomDisplay
) -> DisplayResult:
return create_panel(
custom.title,
custom.content,
custom.style
).bind(lambda panel: Ok(console.print(panel)))
Adding New Error Types¶
@tagged_union
class CustomDisplayError(DisplayError):
tag: Literal["validation", "rendering", "interaction", "timeout", "execution", "input", "custom"]
custom: str | None = None
@staticmethod
def Custom(message: str) -> "CustomDisplayError":
return CustomDisplayError(tag="custom", custom=message)
Usage Examples¶
# Using TableResult
def create_status_table(items: list[str]) -> TableResult:
table = Table()
table.add_column("Item")
table.add_column("Status")
for item in items:
table.add_row(item, "✓")
return Ok(table)
# Using DisplayFunction
def with_fallback_display[T](
content: T,
primary: DisplayFunction[T],
fallback: DisplayFunction[T]
) -> DisplayResult:
result = primary(content)
return result if result.is_ok() else fallback(content)
# Using RecoveryStrategy
def with_recovery[T](operation: Callable[[], Result[T, DisplayError]]) -> Result[T, DisplayError]:
strategies: RecoveryStrategy[T] = {
"validation": lambda: retry_with_default(),
"rendering": lambda: retry_with_simple_output(),
"interaction": lambda: retry_with_alternate_input()
}
return recover_ui(operation, strategies)
Future Improvements¶
- Keyboard Shortcuts: Add more keyboard shortcuts for faster navigation
- Theming Support: Allow users to customize the TUI appearance
- Command History: Implement command history tracking
- Parameter Input: Add interactive parameter input for commands
- Persistent Settings: Save user preferences between sessions
- Help Context: Show command help inline for better usability
- Auto-complete: Add tab completion for command names and parameters
- Filter Options: Allow filtering command lists by keyword