From 164ba6e659f0bd184b641d5fecbc80e290e43460 Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Tue, 10 Sep 2024 17:52:18 -0400 Subject: [PATCH] feat(printer): hide cursor when supported --- craft_cli/printer.py | 12 ++++++++-- .../integration/test_messages_integration.py | 11 +++++++++- tests/unit/test_printer.py | 22 +++++++++++++++---- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/craft_cli/printer.py b/craft_cli/printer.py index 1448fa0..5ffe267 100644 --- a/craft_cli/printer.py +++ b/craft_cli/printer.py @@ -23,6 +23,7 @@ import platform import queue import shutil +import sys import threading import time from dataclasses import dataclass, field @@ -46,7 +47,9 @@ # craft_cli/pytest_plugin.py ) TESTMODE = False -ANSI_CLEAR_LINE_TO_END = "\033[K" # ANSI escape code to clear the rest of the line. +ANSI_CLEAR_LINE_TO_END = "\x1b[K" # ANSI escape code to clear the rest of the line. +ANSI_HIDE_CURSOR = "\x1b[?25l" +ANSI_SHOW_CURSOR = "\x1b[?25h" @dataclass @@ -224,6 +227,9 @@ def __init__(self, log_filepath: pathlib.Path) -> None: self.spinner = _Spinner(self) if not TESTMODE: self.spinner.start() + if _supports_ansi_escape_sequences() and _stream_is_terminal(sys.stderr): + # pass + print(ANSI_HIDE_CURSOR, end="", file=sys.stderr, flush=True) def set_terminal_prefix(self, prefix: str) -> None: """Set the string to be prepended to every message shown to the terminal.""" @@ -466,11 +472,14 @@ def stop(self) -> None: In detail: - stop the spinner + - show the cursor - add a new line to the screen (if needed) - close the log file """ if not TESTMODE: self.spinner.stop() + if _supports_ansi_escape_sequences() and _stream_is_terminal(sys.stderr): + print(ANSI_SHOW_CURSOR, end="", file=sys.stderr, flush=True) if self.unfinished_stream is not None: # With unfinished_stream set, the prv_msg object is valid. if self.prv_msg is not None and self.prv_msg.ephemeral: @@ -483,7 +492,6 @@ def stop(self) -> None: # The last printed message is permanent. Leave the cursor on # the next clean line. print(flush=True, file=self.unfinished_stream) - self.log.close() self.stopped = True diff --git a/tests/integration/test_messages_integration.py b/tests/integration/test_messages_integration.py index 88a1773..62b8f95 100644 --- a/tests/integration/test_messages_integration.py +++ b/tests/integration/test_messages_integration.py @@ -76,6 +76,15 @@ def logger(): return logger +def remove_control_characters(string: str) -> str: + """Strip the non-printing characters from an output string.""" + return ( + string.replace(printer.ANSI_CLEAR_LINE_TO_END, "") + .replace(printer.ANSI_HIDE_CURSOR, "") + .replace(printer.ANSI_SHOW_CURSOR, "") + ) + + @dataclass class Line: """A line that is expected to be in the result.""" @@ -95,7 +104,7 @@ def compare_lines(expected_lines: Collection[Line], raw_stream: str, std_stream) if terminal: if printer._supports_ansi_escape_sequences(): - lines = raw_stream.replace("\033[K", "").splitlines(keepends=True) + lines = remove_control_characters(raw_stream).splitlines(keepends=True) else: # If the terminal doesn't support ANSI escape sequences, we fill the screen # width and don't terminate lines, so we split lines according to that length diff --git a/tests/unit/test_printer.py b/tests/unit/test_printer.py index a292e07..69ed9b4 100644 --- a/tests/unit/test_printer.py +++ b/tests/unit/test_printer.py @@ -29,6 +29,13 @@ from craft_cli import printer as printermod from craft_cli.printer import Printer, _MessageInfo, _Spinner +pytestmark = [ + # Always use capsys, giving the printer its own stdout and stderr. + # This is useful because the printer will print control characters + # directly to stderr. + pytest.mark.usefixtures("capsys"), +] + @pytest.fixture(autouse=True) def init_emitter(): @@ -77,6 +84,15 @@ def _supports_ansi_escape_sequences(): return request.param +def remove_control_characters(string: str) -> str: + """Strip the non-printing characters from an output string.""" + return ( + string.replace(printermod.ANSI_CLEAR_LINE_TO_END, "") + .replace(printermod.ANSI_HIDE_CURSOR, "") + .replace(printermod.ANSI_SHOW_CURSOR, "") + ) + + # -- simple helpers @@ -1423,7 +1439,7 @@ def test_secrets_progress_bar(capsys, log_filepath, monkeypatch): printer.progress_bar(stream, message, progress=0.0, total=1.0, use_timestamp=False) _, stderr = capsys.readouterr() - assert stderr.startswith(expected) + assert remove_control_characters(stderr).startswith(expected) def test_secrets_terminal_prefix(capsys, log_filepath, monkeypatch): @@ -1449,7 +1465,5 @@ def test_secrets_terminal_prefix(capsys, log_filepath, monkeypatch): ] _, stderr = capsys.readouterr() - obtained = [ - l.replace(printermod.ANSI_CLEAR_LINE_TO_END, "").strip() for l in stderr.splitlines() - ] + obtained = [remove_control_characters(l).rstrip() for l in stderr.splitlines()] assert obtained == expected