diff --git a/README.md b/README.md index d6d7a93..6839c8c 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,8 @@ The GRM application writes two parallel log streams: Set `GRM_LOG_LEVEL` to one of `DEBUG`, `INFO`, `WARNING`, `ERROR`, or `CRITICAL` to control console verbosity. The log file always captures everything at DEBUG level. +Console output is automatically colorised: operation headers in bright cyan, completed steps in green, failures in red, and status updates in yellow. + ## Architecture GRM consists of two layers: diff --git a/src/gitea_runner_manager/executor.py b/src/gitea_runner_manager/executor.py index dbf5a6e..5f2d00f 100644 --- a/src/gitea_runner_manager/executor.py +++ b/src/gitea_runner_manager/executor.py @@ -27,7 +27,7 @@ class AnsibleExecutor: log_file = self._prepare_log(cmd) desc = description or _("Running Ansible playbook") - say(f"[GRM] {desc}") + say(f"[GRM] {desc}", color="cyan") say(f"[GRM] {_('Full log: {log_file}', log_file=log_file)}") returncode = self._stream(cmd, log_file) @@ -38,14 +38,14 @@ class AnsibleExecutor: code=returncode, log_file=log_file, ) - say(f"[GRM] {msg}", level=logging.ERROR, err=True) + say(f"[GRM] {msg}", level=logging.ERROR, err=True, color="red") raise AnsibleError(msg) status = self._extract_status(log_file) if status: - say(f"[GRM] {status}") + say(f"[GRM] {status}", color="yellow") - say(f"[GRM] {_('Done. See full log: {log_file}', log_file=log_file)}") + say(f"[GRM] {_('Done. See full log: {log_file}', log_file=log_file)}", color="green") def _prepare_log(self, cmd: list[str]) -> Path: """Create a log file with header.""" diff --git a/src/gitea_runner_manager/report.py b/src/gitea_runner_manager/report.py index 2be05eb..48e40d1 100644 --- a/src/gitea_runner_manager/report.py +++ b/src/gitea_runner_manager/report.py @@ -63,8 +63,14 @@ def _print_report(steps: list[Step]) -> None: "pending": "○", "in_progress": "◌", } - say(f"[GRM] {_('=== Operation Report ===')}") + say(f"[GRM] {_('=== Operation Report ===')}", color="bright_cyan") for step in steps: icon = icons.get(step.status, "?") status_label = _(step.status) - say(f"[GRM] {icon} {step.name} ({status_label})") + color = { + "completed": "green", + "failed": "red", + "in_progress": "yellow", + "pending": "white", + }.get(step.status) + say(f"[GRM] {icon} {step.name} ({status_label})", color=color) diff --git a/src/gitea_runner_manager/ui.py b/src/gitea_runner_manager/ui.py index b39661a..3f76f92 100644 --- a/src/gitea_runner_manager/ui.py +++ b/src/gitea_runner_manager/ui.py @@ -1,4 +1,8 @@ -"""User-facing output utilities for GRM.""" +"""User-facing output utilities for GRM. + +Console messages are colorised via ``click.style`` for visual feedback. +The persistent log file always receives plain text (no ANSI codes). +""" from __future__ import annotations @@ -7,12 +11,17 @@ import logging import click -def say(msg: str, level: int = logging.INFO, err: bool = False) -> None: +def say(msg: str, level: int = logging.INFO, err: bool = False, color: str | None = None) -> None: """Output a message to the user and also log it for auditing. Console output goes via ``click.echo`` (handles encoding, CliRunner, Windows colorama). The same message is also sent to the ``grm`` logger so it appears in the persistent log file. + + The optional *color* parameter is passed to ``click.style`` so the + console line is tinted (e.g. ``color="green"``). The log file always + stores the raw plain text. """ - click.echo(msg, err=err) + styled = click.style(msg, fg=color) if color else msg + click.echo(styled, err=err) logging.getLogger("grm").log(level, msg)