Skip to content

Terminal UI API

ui

Professional, lightly pirate-themed terminal presentation for Maraudarr.

UserCancelled

Bases: RuntimeError

Represent an intentional cancellation rather than a generator failure.

UI

UI(plain: bool = False)

Render Maraudarr output consistently in rich and plain terminals.

Rich presentation is used only when optional dependencies are available, styled output is allowed, and the caller did not request plain mode.

Attributes:

Name Type Description
plain

Whether output uses dependency-free text presentation.

console

Rich console instance, or None in plain mode.

Create a terminal presenter.

Parameters:

Name Type Description Default
plain bool

Force dependency-free output even when Rich is available. The NO_COLOR environment variable also enables this mode.

False
Source code in docker/src/maraudarr/ui.py
def __init__(self, plain: bool = False) -> None:
    """Create a terminal presenter.

    Args:
        plain: Force dependency-free output even when Rich is available.
            The ``NO_COLOR`` environment variable also enables this mode.
    """
    self.plain = plain or not RICH_AVAILABLE or bool(os.environ.get("NO_COLOR"))
    self.console = None if self.plain else Console()

welcome

welcome() -> None

Display Maraudarr's purpose before listing available choices.

Source code in docker/src/maraudarr/ui.py
def welcome(self) -> None:
    """Display Maraudarr's purpose before listing available choices."""
    message = (
        "[bold]Choose a voyage, load the services, and chart one reusable "
        "Plundarr Docker Compose stack.[/bold]"
    )
    if self.console:
        self.console.print(
            Panel(
                message,
                title="🏴‍☠️  Maraudarr",
                border_style="#f2c14e",
                padding=(1, 3),
            )
        )
    else:
        print("Maraudarr")
        print("Generate one reusable Plundarr Docker Compose stack.\n")

show_presets

show_presets(
    presets: list[Preset],
    service_lookup: dict[str, Service],
) -> None

List presets with their exact default services.

Parameters:

Name Type Description Default
presets list[Preset]

Presets to display in caller-supplied order.

required
service_lookup dict[str, Service]

Service metadata keyed by catalog identifier.

required
Source code in docker/src/maraudarr/ui.py
def show_presets(
    self,
    presets: list[Preset],
    service_lookup: dict[str, Service],
) -> None:
    """List presets with their exact default services.

    Args:
        presets: Presets to display in caller-supplied order.
        service_lookup: Service metadata keyed by catalog identifier.
    """
    if self.console:
        table = Table(
            title="🗺️  Available Voyages",
            box=box.ROUNDED,
            header_style="bold",
        )
        table.add_column("Preset", style="#f2c14e", no_wrap=True)
        table.add_column("Purpose")
        table.add_column("Default cargo")
        for preset in presets:
            ordered_services = sorted(
                (service_lookup[item] for item in preset.services),
                key=lambda service: service.order,
            )
            cargo = ", ".join(service.title for service in ordered_services)
            table.add_row(preset.title, preset.description, cargo or "Choose your own")
        self.console.print(table)
        return

    for preset in presets:
        ordered_services = sorted(
            (service_lookup[item] for item in preset.services),
            key=lambda service: service.order,
        )
        cargo = ", ".join(service.title for service in ordered_services)
        print(f"{preset.id}: {preset.description}\n  Services: {cargo or 'Choose your own'}")

choose_preset

choose_preset(presets: list[Preset]) -> str

Prompt for one preset in an interactive terminal.

Parameters:

Name Type Description Default
presets list[Preset]

Ordered choices presented to the user.

required

Returns:

Type Description
str

Stable identifier of the selected preset.

Raises:

Type Description
UserCancelled

If no interactive terminal is available or the user exits without choosing a preset.

Source code in docker/src/maraudarr/ui.py
def choose_preset(self, presets: list[Preset]) -> str:
    """Prompt for one preset in an interactive terminal.

    Args:
        presets: Ordered choices presented to the user.

    Returns:
        Stable identifier of the selected preset.

    Raises:
        UserCancelled: If no interactive terminal is available or the user
            exits without choosing a preset.
    """
    if self.plain or not sys.stdin.isatty():
        raise UserCancelled("Interactive configuration requires a terminal.")
    answer = questionary.select(
        "🗺️  Choose a voyage",
        choices=[Choice(preset.title, preset.id) for preset in presets],
        style=PIRATE_STYLE,
        use_shortcuts=True,
    ).ask()
    if answer is None:
        raise UserCancelled("Voyage cancelled before leaving port.")
    return str(answer)

show_service_choices

show_service_choices(services: list[Service]) -> None

Explain selectable services before interactive checkbox prompts.

Parameters:

Name Type Description Default
services list[Service]

Selectable services in desired presentation order.

required
Source code in docker/src/maraudarr/ui.py
def show_service_choices(self, services: list[Service]) -> None:
    """Explain selectable services before interactive checkbox prompts.

    Args:
        services: Selectable services in desired presentation order.
    """
    if self.console:
        table = Table(
            title="🧩 Available Cargo",
            box=box.ROUNDED,
            header_style="bold",
        )
        table.add_column("Category", style="#f2c14e", no_wrap=True)
        table.add_column("Service", style="bold", no_wrap=True)
        table.add_column("What it adds")
        for service in services:
            table.add_row(service.category, service.title, service.description)
        self.console.print(table)
        return

    print("Available services:")
    for service in services:
        print(f"  {service.id:<18} {service.description}")

choose_services

choose_services(
    services: list[Service], selected: set[str]
) -> set[str]

Collect service choices grouped by category.

Parameters:

Name Type Description Default
services list[Service]

Selectable services in category and presentation order.

required
selected set[str]

Service IDs checked when each category prompt opens.

required

Returns:

Type Description
set[str]

Service IDs checked across every category.

Raises:

Type Description
UserCancelled

If no interactive terminal is available or the user exits any category prompt.

Source code in docker/src/maraudarr/ui.py
def choose_services(
    self,
    services: list[Service],
    selected: set[str],
) -> set[str]:
    """Collect service choices grouped by category.

    Args:
        services: Selectable services in category and presentation order.
        selected: Service IDs checked when each category prompt opens.

    Returns:
        Service IDs checked across every category.

    Raises:
        UserCancelled: If no interactive terminal is available or the user
            exits any category prompt.
    """
    if self.plain or not sys.stdin.isatty():
        raise UserCancelled("Interactive configuration requires a terminal.")

    grouped_services: dict[str, list[Service]] = defaultdict(list)
    for service in services:
        grouped_services[service.category].append(service)

    chosen: set[str] = set()
    for category, category_services in grouped_services.items():
        answers = questionary.checkbox(
            f"🧩 {category}",
            choices=[
                Choice(
                    service.title,
                    service.id,
                    checked=service.id in selected,
                )
                for service in category_services
            ],
            style=PIRATE_STYLE,
            instruction="Space selects cargo; Enter continues",
        ).ask()
        if answers is None:
            raise UserCancelled("Voyage cancelled while loading cargo.")
        chosen.update(str(answer) for answer in answers)
    return chosen

show_plan

show_plan(plan: StackPlan) -> None

Present the resolved service manifest before writing files.

Parameters:

Name Type Description Default
plan StackPlan

Fully resolved stack plan, including automatic dependencies.

required
Source code in docker/src/maraudarr/ui.py
def show_plan(self, plan: StackPlan) -> None:
    """Present the resolved service manifest before writing files.

    Args:
        plan: Fully resolved stack plan, including automatic dependencies.
    """
    if self.console:
        summary = Table.grid(padding=(0, 2))
        summary.add_column(style="bold")
        summary.add_column()
        summary.add_row("Preset", plan.preset.title)
        summary.add_row("Services", str(len(plan.services)))
        summary.add_row(
            "Cargo",
            ", ".join(service.title for service in plan.services),
        )
        self.console.print(
            Panel(summary, title="⚓ Stack Manifest", border_style="#2a9d8f")
        )
        if plan.auto_added:
            self.console.print(
                "[bold #f2c14e]Dependency check:[/] "
                + ", ".join(plan.auto_added)
                + " joined the fleet automatically."
            )
        return

    print(f"Preset: {plan.preset.title}")
    print("Services: " + ", ".join(service.id for service in plan.services))

confirm

confirm() -> bool

Confirm a plan when attached to an interactive terminal.

Returns:

Type Description
bool

True for a confirmed prompt or any non-interactive plain run.

Raises:

Type Description
UserCancelled

If the user dismisses the confirmation prompt.

Source code in docker/src/maraudarr/ui.py
def confirm(self) -> bool:
    """Confirm a plan when attached to an interactive terminal.

    Returns:
        ``True`` for a confirmed prompt or any non-interactive plain run.

    Raises:
        UserCancelled: If the user dismisses the confirmation prompt.
    """
    if self.plain or not sys.stdin.isatty():
        return True
    answer = questionary.confirm(
        "⚒️  Build this Plundarr stack?",
        default=True,
        style=PIRATE_STYLE,
    ).ask()
    if answer is None:
        raise UserCancelled("Voyage cancelled before the stack was built.")
    return bool(answer)

progress

progress(message: str) -> None

Print one generation progress message.

Parameters:

Name Type Description Default
message str

User-facing status text to display unchanged.

required
Source code in docker/src/maraudarr/ui.py
def progress(self, message: str) -> None:
    """Print one generation progress message.

    Args:
        message: User-facing status text to display unchanged.
    """
    if self.console:
        self.console.print(message)
    else:
        print(message)

success

success(
    plan: StackPlan,
    compose_path: str,
    env_path: str,
    config_path: str,
) -> None

Report generated artifacts and context-aware launch instructions.

Parameters:

Name Type Description Default
plan StackPlan

Generated plan used to select relevant follow-up steps.

required
compose_path str

Display path to the generated Compose chart.

required
env_path str

Display path to the generated environment file.

required
config_path str

Display path to the generated config root.

required
Source code in docker/src/maraudarr/ui.py
def success(
    self,
    plan: StackPlan,
    compose_path: str,
    env_path: str,
    config_path: str,
) -> None:
    """Report generated artifacts and context-aware launch instructions.

    Args:
        plan: Generated plan used to select relevant follow-up steps.
        compose_path: Display path to the generated Compose chart.
        env_path: Display path to the generated environment file.
        config_path: Display path to the generated config root.
    """
    selected = set(plan.service_ids)
    steps = []
    if "privateerr" in selected:
        steps.append("Set PIA_USER and PIA_PASS in .env.")
    if selected.intersection(
        {
            "bazarr",
            "duplicati",
            "jellyfin",
            "nzbget",
            "plex",
            "qbittorrent",
            "radarr",
            "sabnzbd",
            "sonarr",
            "sonarr-anime",
            "whisparr",
        }
    ):
        steps.append("Check download, media, and config paths in .env.")
    steps.append("Start the stack with make up.")

    if self.console:
        table = Table.grid(padding=(0, 2))
        table.add_column(style="bold")
        table.add_column()
        table.add_row("Compose chart", compose_path)
        table.add_row("Environment", env_path)
        table.add_row("Config cargo", config_path)
        table.add_row("Services", str(len(plan.services)))
        self.console.print(
            Panel(table, title="✅ Plundarr Ready to Sail", border_style="#52b788")
        )
        self.console.print("\n[bold]Before launch:[/]")
        for number, step in enumerate(steps, start=1):
            self.console.print(f"  {number}. {step}")
        return

    print(f"Plundarr ready: {compose_path}, {env_path}, and {config_path}")
    for number, step in enumerate(steps, start=1):
        print(f"{number}. {step}")

error

error(message: str, fix: str | None = None) -> None

Display a generator failure and optional corrective action.

Parameters:

Name Type Description Default
message str

Concrete failure explanation.

required
fix str | None

Optional next action that may correct the failure.

None
Source code in docker/src/maraudarr/ui.py
def error(self, message: str, fix: str | None = None) -> None:
    """Display a generator failure and optional corrective action.

    Args:
        message: Concrete failure explanation.
        fix: Optional next action that may correct the failure.
    """
    body = message
    if fix:
        body += f"\n\n[bold]Fix:[/] {fix}"
    if self.console:
        self.console.print(
            Panel(body, title="☠️ Maraudarr could not finish", border_style="red")
        )
    else:
        print(f"Error: {message}", file=sys.stderr)
        if fix:
            print(f"Fix: {fix}", file=sys.stderr)

cancelled

cancelled(message: str) -> None

Display an intentional cancellation without reporting failure.

Parameters:

Name Type Description Default
message str

Cancellation reason to present to the user.

required
Source code in docker/src/maraudarr/ui.py
def cancelled(self, message: str) -> None:
    """Display an intentional cancellation without reporting failure.

    Args:
        message: Cancellation reason to present to the user.
    """
    if self.console:
        self.console.print(
            Panel(message, title="⚓ Voyage Cancelled", border_style="#f2c14e")
        )
    else:
        print(message)