GRM-15: feat: add colorized output for better visual feedback
- Extend ui.say() with optional color parameter using click.style() - Console output gets tinted; log file always stores plain text (no ANSI) - executor.py: cyan for start, yellow for status, green for done, red for errors - report.py: bright_cyan header, green completed, red failed, yellow in-progress, white pending - Update README with colorized output documentation - 117 tests, 100% coverage, pyright clean, ruff clean
This commit is contained in:
@@ -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.
|
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
|
## Architecture
|
||||||
|
|
||||||
GRM consists of two layers:
|
GRM consists of two layers:
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class AnsibleExecutor:
|
|||||||
log_file = self._prepare_log(cmd)
|
log_file = self._prepare_log(cmd)
|
||||||
desc = description or _("Running Ansible playbook")
|
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)}")
|
say(f"[GRM] {_('Full log: {log_file}', log_file=log_file)}")
|
||||||
|
|
||||||
returncode = self._stream(cmd, log_file)
|
returncode = self._stream(cmd, log_file)
|
||||||
@@ -38,14 +38,14 @@ class AnsibleExecutor:
|
|||||||
code=returncode,
|
code=returncode,
|
||||||
log_file=log_file,
|
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)
|
raise AnsibleError(msg)
|
||||||
|
|
||||||
status = self._extract_status(log_file)
|
status = self._extract_status(log_file)
|
||||||
if status:
|
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:
|
def _prepare_log(self, cmd: list[str]) -> Path:
|
||||||
"""Create a log file with header."""
|
"""Create a log file with header."""
|
||||||
|
|||||||
@@ -63,8 +63,14 @@ def _print_report(steps: list[Step]) -> None:
|
|||||||
"pending": "○",
|
"pending": "○",
|
||||||
"in_progress": "◌",
|
"in_progress": "◌",
|
||||||
}
|
}
|
||||||
say(f"[GRM] {_('=== Operation Report ===')}")
|
say(f"[GRM] {_('=== Operation Report ===')}", color="bright_cyan")
|
||||||
for step in steps:
|
for step in steps:
|
||||||
icon = icons.get(step.status, "?")
|
icon = icons.get(step.status, "?")
|
||||||
status_label = _(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)
|
||||||
|
|||||||
@@ -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
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -7,12 +11,17 @@ import logging
|
|||||||
import click
|
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.
|
"""Output a message to the user and also log it for auditing.
|
||||||
|
|
||||||
Console output goes via ``click.echo`` (handles encoding, CliRunner,
|
Console output goes via ``click.echo`` (handles encoding, CliRunner,
|
||||||
Windows colorama). The same message is also sent to the ``grm`` logger
|
Windows colorama). The same message is also sent to the ``grm`` logger
|
||||||
so it appears in the persistent log file.
|
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)
|
logging.getLogger("grm").log(level, msg)
|
||||||
|
|||||||
Reference in New Issue
Block a user