diff --git a/src/repoman/_version.py b/src/repoman/_version.py index bf15195..0817354 100644 --- a/src/repoman/_version.py +++ b/src/repoman/_version.py @@ -11,17 +11,31 @@ import sys from dataclasses import dataclass from importlib import metadata +from pathlib import Path from typing import TYPE_CHECKING +from rich.box import HEAVY_EDGE, Box +from rich.columns import Columns from rich.layout import Layout from rich.panel import Panel from rich.table import Table from rich.text import Text +from repoman.utils.paths import get_os_config_path from repoman.utils.ui import get_console if TYPE_CHECKING: - from rich.console import Console + from rich.console import Console, RenderableType + + +def section(title: str, renderable: RenderableType, style: str = "blue", box: Box = HEAVY_EDGE) -> Panel: + return Panel( + renderable, + title=f"[bold {style}]{title}[/]", + border_style=style, + box=box, + padding=(1, 2), + ) @dataclass @@ -66,6 +80,8 @@ class Environment: """Installed packages.""" variables: list[Variable] """Environment variables.""" + platform_version: str = "" + """Operating System version.""" def _interpreter_name_version() -> tuple[str, str]: @@ -137,18 +153,72 @@ def get_debug_info() -> Environment: "PYTHONPATH", *[var for var in os.environ if var.upper().startswith(repoman_prefix)], ] + return Environment( interpreter_name=py_name, interpreter_version=py_version, interpreter_path=sys.executable, platform=platform.platform(), + platform_version=platform.version(), variables=[Variable(var, val) for var in variables if (val := os.getenv(var))], packages=[Package(name, version) for name, version in packages], ) -def _make_debug_layout(env: Environment) -> Layout: - """Build a Layout for debug info: header + packages | env vars.""" +def working_paths() -> list[Path]: + """Get the working paths for the project. + + Returns: + A list of Path objects. Starting with the config path and ending with the working directory. + """ + config_path = get_os_config_path() + working_dir = Path.cwd() + return [config_path, working_dir] + + +def package_inventory(packages: list[Package]) -> Panel: + """Build a Columns object for the package inventory, as tightly packed as possible. + + Args: + packages: A list of Package objects. + + Returns: + A Columns object. + """ + items = [ + Text.assemble( + (pkg.name, "bold"), + (" ", "dim"), + (pkg.version, "cyan"), + ) + for pkg in packages + ] + + return section( + "Package Inventory", + Columns(items, equal=True, expand=True, column_first=True), + style="lavender", + ) + + +def _make_project_panel(paths: list[Path]) -> Panel: + project_table = Table(highlight=True, box=None, show_header=False) + project_table.add_row( + Text("Project", style="rosewater"), + Text(str(paths[1]), style="bold"), + ) + project_table.add_row( + Text("Config Dir", style="rosewater"), + Text(str(paths[0]), style="bold"), + ) + project_table.add_row( + Text("Working Dir", style="rosewater"), + Text(str(paths[1]), style="bold"), + ) + return section("Project", project_table, style="bright_blue") + + +def _make_header_panel(env: Environment) -> Panel: header_table = Table(highlight=True, box=None, show_header=False) header_table.add_row( Text("Interpreter Name", style="rosewater"), @@ -163,44 +233,23 @@ def _make_debug_layout(env: Environment) -> Layout: Text(env.interpreter_path, style="bold"), ) header_table.add_row(Text("Platform", style="rosewater"), Text(env.platform, style="bold")) - header = Panel( - header_table, - title="Debug Information", - title_align="left", - border_style="bright_blue", - ) - - packages_table = Table( - highlight=True, - box=None, - show_header=True, - title=f"Packages ({len(env.packages)})", - ) - packages_table.add_column("Package", style="rosewater") - packages_table.add_column("Version", style="bold") - for pkg in env.packages: - packages_table.add_row(pkg.name, pkg.version) - - env_table = Table(highlight=True, box=None, show_header=True, title="Environment Variables") - env_table.add_column("Variable", style="rosewater") - env_table.add_column("Value", style="bold") - for var in env.variables: - env_table.add_row(var.name, var.value) + return section("Debug Information", header_table, style="bright_blue") + + +def _make_debug_layout(env: Environment) -> Layout: + """Build a Layout for debug info: header + packages | env vars.""" + header = _make_header_panel(env) layout = Layout() layout.split_column( - Layout(header, name="header", size=7), - Layout(name="main", ratio=1), + Layout(header, name="header", size=9), + Layout(name="main"), ) layout["main"].split_row( - Layout(Panel(packages_table, border_style="bright_blue"), name="packages", ratio=1), - Layout( - Panel(env_table, border_style="bright_blue"), - name="vars", - ratio=1, - minimum_size=30, - ), + Layout(_make_project_panel(working_paths()), name="paths", ratio=1), + Layout(package_inventory(env.packages), name="packages", ratio=2), ) + return layout @@ -228,7 +277,7 @@ def _make_debug_panel(env: Environment) -> Panel: Text("Environment Variables", style="rosewater"), Text.assemble(*[Text(str(var), style="bold") for var in env.variables]), ) - return Panel(table, title="Debug Information", title_align="left") + return section("Debug Information", table, style="bright_blue") def debug_info(console: Console | None = None) -> None: diff --git a/src/repoman/cli/main_cli.py b/src/repoman/cli/main_cli.py index 0785462..bb29668 100644 --- a/src/repoman/cli/main_cli.py +++ b/src/repoman/cli/main_cli.py @@ -134,6 +134,7 @@ def main( # Clear screen for CRT boot — starts from top console.file.write("\033[2J\033[H") console.file.flush() + config: Config | None = None try: config = Config.load(custom_path=Path(config_path) if config_path else None) diff --git a/src/repoman/cli/messages/layout.py b/src/repoman/cli/messages/layout.py deleted file mode 100644 index a0ec828..0000000 --- a/src/repoman/cli/messages/layout.py +++ /dev/null @@ -1,361 +0,0 @@ -"""CLI layout utilities for Rich multi-panel output. - -Provides helpers to build Layout-based output for wide terminals, with -fallback to single-panel output for narrow terminals or piped output. -""" - -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from repoman.copier import ValidationReport - -from rich.console import Console -from rich.json import JSON -from rich.layout import Layout -from rich.panel import Panel -from rich.syntax import Syntax -from rich.text import Text - - -def use_layout(console: Console | None, min_width: int = 100) -> bool: - """Return True when Layout should be used instead of single-panel output. - - Uses Layout when the console is wide enough; otherwise falls back to - single-panel layout to avoid cramped output. - - Args: - console: The Rich Console instance, or None. - min_width: Minimum console width (in characters) to use Layout. - Defaults to 100. - - Returns: - True if Layout should be used; False for single-panel fallback. - """ - if console is None: - return False - width = getattr(console, "width", None) - return not (width is None or width < min_width) - - -def _make_two_panel_layout( - left_content: Any, - right_content: Any, - *, - left_title: str = "Summary", - right_title: str = "Details", - left_style: str = "green", - right_style: str = "green", -) -> Layout: - """Build a two-panel horizontal Layout. - - Args: - left_content: Rich renderable for the left panel. - right_content: Rich renderable for the right panel. - left_title: Panel title for the left side. - right_title: Panel title for the right side. - left_style: Border style for the left panel. - right_style: Border style for the right panel. - - Returns: - A Layout with two side-by-side panels. - """ - layout = Layout() - - left_panel = Panel(left_content, title=left_title, border_style=left_style) - right_panel = Panel(right_content, title=right_title, border_style=right_style) - - layout.split_row( - Layout(left_panel, name="left", ratio=1), - Layout(right_panel, name="right", ratio=1, minimum_size=40), - ) - return layout - - -def layout_project_created( - project_name: str, - output_dir: str, - copier_options_serializable: dict[str, Any], - next_steps_text: str, - _console: Console | None, - *, - use_unicode: bool, -) -> Layout: - """Build a two-panel Layout for project creation success. - - Returns: - Layout with summary + next steps (left) and copier options JSON (right). - - Examples: - Layout (wide terminal):: - - ╭─ Success — Summary & Next Steps ───╮ ╭─ Copier Options ──────╮ - │ ✓ Project 'my-project' created │ │ { "src_path": ... } │ - │ in ./out │ │ { "dst_path": ... } │ - │ Next steps: │ │ │ - │ • cd my-project │ │ │ - ╰────────────────────────────────────╯ ╰───────────────────────╯ - """ - prefix = "✓ " if use_unicode else "" - summary = Text( - f"{prefix}Project '{project_name}' created successfully in {output_dir}\n\nNext steps:\n{next_steps_text}", - style="green", - ) - json_content = JSON.from_data(copier_options_serializable, indent=2) - return _make_two_panel_layout( - summary, - json_content, - left_title="Success — Summary & Next Steps", - right_title="Copier Options", - left_style="green", - right_style="green", - ) - - -def layout_project_updated( - project_dir: str, - copier_options_serializable: dict[str, Any], - next_steps_text: str, - _console: Console | None, - *, - use_unicode: bool, -) -> Layout: - """Build a two-panel Layout for project update success. - - Returns: - Layout with summary + next steps (left) and copier options JSON (right). - """ - prefix = "✓ " if use_unicode else "" - summary = Text( - f"{prefix}Project updated successfully in {project_dir}\n\nNext steps:\n{next_steps_text}", - style="green", - ) - json_content = JSON.from_data(copier_options_serializable, indent=2) - return _make_two_panel_layout( - summary, - json_content, - left_title="Success — Summary & Next Steps", - right_title="Copier Options", - left_style="green", - right_style="green", - ) - - -def layout_dry_run_create( - project_name: str, - output_dir: str, - template_path: str, - copier_options_serializable: dict[str, Any], - next_steps_text: str, -) -> Layout: - """Build a two-panel Layout for create dry-run. - - Returns: - Layout with summary + next steps (left) and copier options JSON (right). - """ - summary = Text( - f"Would create project '{project_name}' in {output_dir}\n" - f"Using template: {template_path}\n\n" - f"Next steps:\n{next_steps_text}", - style="blue", - ) - json_content = JSON.from_data(copier_options_serializable, indent=2) - return _make_two_panel_layout( - summary, - json_content, - left_title="Dry Run — Summary & Next Steps", - right_title="Copier Options", - left_style="blue", - right_style="blue", - ) - - -def layout_dry_run_update( - project_dir: str, - answers_path: str, - template_path: str | None, - vcs_ref: str | None, - copier_options_serializable: dict[str, Any], - next_steps_text: str, -) -> Layout: - """Build a two-panel Layout for update dry-run. - - Returns: - Layout with summary + next steps (left) and copier options JSON (right). - """ - template_line = f"Using template: {template_path}\n" if template_path else "Template: (from answers file)\n" - vcs_line = f"VCS ref: {vcs_ref}\n" if vcs_ref else "" - summary = Text( - f"Would update project in {project_dir}\n" - f"Using answers file: {answers_path}\n" - f"{template_line}{vcs_line}\n" - f"Next steps:\n{next_steps_text}", - style="blue", - ) - json_content = JSON.from_data(copier_options_serializable, indent=2) - return _make_two_panel_layout( - summary, - json_content, - left_title="Dry Run — Summary & Next Steps", - right_title="Copier Options", - left_style="blue", - right_style="blue", - ) - - -def layout_validation_failed(report: "ValidationReport") -> tuple[Layout, int]: - """Build a three-column Layout for config validate failure. - - Args: - report: ValidationReport with missing_keys, extra_keys, type_errors. - - Returns: - (Layout, height): Layout with Missing Keys (left), Extra Keys (center), - Type Errors (right), and its height in lines (for console.print(..., height=...)). - - Examples: - Layout (wide terminal):: - - ╭─ Missing Keys ────────╮ ╭─ Extra Keys ───╮ ╭─ Type Errors ───────────╮ - │ • project_name │ │ • debug_mode │ │ • project_name: ... │ - │ • ci_provider │ │ │ │ • ci: not in choices │ - ╰──────────────────────╯ ╰────────────────╯ ╰─────────────────────────╯ - """ - missing_text = Text( - "\n".join(f" • {k}" for k in report.missing_keys) if report.missing_keys else " (none)", - style="red", - ) - extra_text = Text( - "\n".join(f" • {k}" for k in report.extra_keys) if report.extra_keys else " (none)", - style="red", - ) - type_errors_text = Text( - "\n".join(f" • {e}" for e in report.type_errors) if report.type_errors else " (none)", - style="red", - ) - - # Height = panel top border + content lines + panel bottom border. - # Rich Layout defaults to full terminal height; fix size so there's no extra whitespace. - content_lines = max( - len(report.missing_keys) or 1, - len(report.extra_keys) or 1, - len(report.type_errors) or 1, - ) - row_height = 2 + content_lines - - row_layout = Layout() - row_layout.split_row( - Layout( - Panel(missing_text, title="Missing Keys", border_style="red"), - name="missing", - ratio=1, - minimum_size=20, - ), - Layout( - Panel(extra_text, title="Extra Keys", border_style="red"), - name="extra", - ratio=1, - minimum_size=20, - ), - Layout( - Panel(type_errors_text, title="Type Errors", border_style="red"), - name="type_errors", - ratio=1, - minimum_size=25, - ), - ) - root = Layout() - root.split_column(Layout(row_layout, name="row", size=row_height)) - return root, row_height - - -def layout_command_created( - summary_section: str, - next_steps_section: str, - *, - use_unicode: bool, -) -> Layout: - """Build a two-panel Layout for command creation success. - - Args: - summary_section: Summary + created files for left panel. - next_steps_section: Next steps for right panel. - use_unicode: Whether to use Unicode prefix (✓) in summary. - - Returns: - Layout with Created Files (left) and Next Steps (right). - """ - prefix = "✓ " if use_unicode else "" - left_content = Text(f"{prefix}{summary_section}", style="green") - right_content = Text(next_steps_section, style="green") - return _make_two_panel_layout( - left_content, - right_content, - left_title="Success — Created Files", - right_title="Next Steps", - left_style="green", - right_style="green", - ) - - -def layout_config_show_template(raw_yaml: str, key_count: int) -> tuple[Layout, int]: - """Build a Layout for config show full template: summary header + YAML content. - - Args: - raw_yaml: Raw YAML string of template answers. - key_count: Number of keys in the template. - - Returns: - (Layout, height): Layout with header (top) and YAML syntax panel (bottom), - and its height in lines (for console.print(..., height=...)). - - Examples: - Layout (wide terminal, top header + bottom YAML panel):: - - ╭──────────────────────────────────────────────────╮ - │ Template answers (12 keys) │ - ╰──────────────────────────────────────────────────╯ - ╭──────────────────────────────────────────────────╮ - │ project_name: my-awesome-project │ - │ ci: github │ - │ ... │ - ╰──────────────────────────────────────────────────╯ - """ - header_text = Text(f"Template answers ({key_count} keys)", style="bold") - header = Panel(header_text, border_style="bright_blue") - syntax = Syntax(raw_yaml, "yaml", line_numbers=False) - content_panel = Panel(syntax, border_style="bright_blue") - # Fix content height so layout doesn't fill terminal (no trailing whitespace). - yaml_lines = len(raw_yaml.splitlines()) or 1 - content_height = 2 + yaml_lines - total_height = 3 + content_height # header size=3 + content panel - layout = Layout() - layout.split_column( - Layout(header, name="header", size=3), - Layout(content_panel, name="content", size=content_height), - ) - return layout, total_height - - -def layout_dry_run_command_add( - summary_section: str, - context_section: str, -) -> Layout: - """Build a two-panel Layout for generator add dry-run. - - Args: - summary_section: Summary + command/test paths for left panel. - context_section: Template context for right panel. - - Returns: - Layout with Summary (left) and Template Context (right). - """ - left_content = Text(summary_section, style="blue") - right_content = Text(context_section, style="blue") - return _make_two_panel_layout( - left_content, - right_content, - left_title="Dry Run — Summary", - right_title="Template Context", - left_style="blue", - right_style="blue", - ) diff --git a/src/repoman/utils/theme/terminal_colors.py b/src/repoman/utils/theme/terminal_colors.py deleted file mode 100644 index d48f380..0000000 --- a/src/repoman/utils/theme/terminal_colors.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Terminal color utilities for Rich visualization.""" - - -def get_rich_color(label: str) -> str: - """Map class labels to Catppuccin colors for Rich visualization.""" - color_map = { - "TP": "green", - "FP": "maroon", - "FN": "red", - "TN": "peach", - "header": "subtext1", - "border": "overlay1", - } - return color_map.get(label, "text")