From affefd0b31bb044f11b61c6f89c151ed292eb3cf Mon Sep 17 00:00:00 2001 From: Sergio Costas Rodriguez Date: Wed, 21 Jun 2023 19:29:57 +0200 Subject: [PATCH 01/15] messages: allow to set the terminal window title This patch allows to set the text in the title of a terminal window. The idea is to allow snapcraft to set in the title which step is currently doing. --- craft_cli/messages.py | 8 +++- craft_cli/printer.py | 12 ++++- .../integration/test_messages_integration.py | 26 +++++++++++ tests/unit/test_printer.py | 46 +++++++++++++++++++ 4 files changed, 90 insertions(+), 2 deletions(-) diff --git a/craft_cli/messages.py b/craft_cli/messages.py index af0a8b3f..d43088e5 100644 --- a/craft_cli/messages.py +++ b/craft_cli/messages.py @@ -602,7 +602,7 @@ def _get_progress_params( return stream, use_timestamp, ephemeral @_active_guard() - def progress(self, text: str, permanent: bool = False) -> None: # noqa: FBT001, FBT002 + def progress(self, text: str, permanent: bool = False, update_titlebar: bool = False) -> None: # noqa: FBT001, FBT002 """Progress information for a multi-step command. This is normally used to present several separated text messages. @@ -610,6 +610,9 @@ def progress(self, text: str, permanent: bool = False) -> None: # noqa: FBT001, If a progress message is important enough that it should not be overwritten by the next ones, use 'permanent=True'. + If a progress message describes an important step that you want to be visible, use + 'update_titlebar=True' to also set it as the titlebar text in the terminal window. + These messages will be truncated to the terminal's width, and overwritten by the next line (unless verbose/trace mode). """ @@ -625,6 +628,9 @@ def progress(self, text: str, permanent: bool = False) -> None: # noqa: FBT001, # Set the "progress prefix" for upcoming non-permanent messages. self._printer.set_terminal_prefix(text) + if update_titlebar: + self._printer.set_titlebar(stream, text) + @_active_guard() def progress_bar( self, text: str, total: float, delta: bool = True # noqa: FBT001, FBT002 diff --git a/craft_cli/printer.py b/craft_cli/printer.py index 1a230d80..757f2d9b 100644 --- a/craft_cli/printer.py +++ b/craft_cli/printer.py @@ -21,12 +21,13 @@ import math import queue import shutil +import sys import threading import time from dataclasses import dataclass, field from datetime import datetime from functools import lru_cache -from typing import TYPE_CHECKING, Any, Callable, TextIO +from typing import TYPE_CHECKING, Any, Callable, TextIO, Optional if TYPE_CHECKING: import pathlib @@ -371,6 +372,15 @@ def show( # noqa: PLR0913 (too many parameters) if not avoid_logging: self._log(msg) + def set_titlebar(self, stream: Optional[TextIO], text: str) -> None: + """Sets 'text' as the window titlebar content""" + if _stream_is_terminal(stream): + # Sends the text with the right ANSI codes: + # ESC]2;textoBEL + if stream==sys.stderr: + stream=sys.stdout + print(f"\033]2;{text}\007", flush=True, file=stream, end="") + def progress_bar( # noqa: PLR0913 self, stream: TextIO | None, diff --git a/tests/integration/test_messages_integration.py b/tests/integration/test_messages_integration.py index 658f6d25..a47e9531 100644 --- a/tests/integration/test_messages_integration.py +++ b/tests/integration/test_messages_integration.py @@ -301,6 +301,32 @@ def test_progress_verbose(capsys, permanent): assert_outputs(capsys, emit, expected_err=expected, expected_log=expected) +@pytest.mark.parametrize("output_is_terminal", [False]) +def test_title_set(capsys, monkeypatch): + """Show a progress message with update_title flag.""" + emit = Emitter() + emit.init(EmitterMode.BRIEF, "testapp", GREETING) + emit.progress("The meaning of life is 42.", update_titlebar=True) + emit.ended_ok() + + out, err = capsys.readouterr() + assert out.find("\x1b]2;The meaning of life is 42.\x07") is -1 + assert err.find("\x1b]2;The meaning of life is 42.\x07") is -1 + + +@pytest.mark.parametrize("output_is_terminal", [True]) +def test_title_set(capsys, monkeypatch): + """Show a progress message with update_title flag.""" + emit = Emitter() + emit.init(EmitterMode.BRIEF, "testapp", GREETING) + emit.progress("The meaning of life is 42.", update_titlebar=True) + emit.ended_ok() + + out, err = capsys.readouterr() + assert (out=="\x1b]2;The meaning of life is 42.\x07") is True + assert err.find("\x1b]2;The meaning of life is 42.\x07") is -1 + + @pytest.mark.parametrize( "mode", [ diff --git a/tests/unit/test_printer.py b/tests/unit/test_printer.py index 02d838e2..7421ca15 100644 --- a/tests/unit/test_printer.py +++ b/tests/unit/test_printer.py @@ -116,6 +116,52 @@ def isatty(self): assert result is False +# -- tests for terminal titlebar + + +def test_titlebar_no_tty(log_filepath): + """Setting the titlebar to a no-tty stream does nothing""" + class FakeStream: + def __init__(self): + self.output = "" + self.flushed = 0 + def isatty(self): + return False + def write(self, data): + self.output += data + def flush(self): + self.flushed += 1 + + stream = FakeStream() + text = "test text" + printer = Printer(log_filepath) + printer.set_titlebar(stream, text) + assert stream.output is "" + assert stream.flushed is 0 + + +def test_titlebar_true_tty(log_filepath): + """Setting the titlebar to a true-tty stream sends the text and + the corresponding ANSI escape codes to set the title""" + class FakeStream: + def __init__(self): + self.output = "" + self.flushed = 0 + def isatty(self): + return True + def write(self, data): + self.output += data + def flush(self): + self.flushed += 1 + + stream = FakeStream() + text = "test text" + printer = Printer(log_filepath) + printer.set_titlebar(stream, text) + assert (stream.output == f"\033]2;{text}\007") is True + assert stream.flushed is 1 + + # -- tests for the writing line (terminal version) function From a646494e947f82c315bc432217ee18e5a8f88733 Mon Sep 17 00:00:00 2001 From: Sergio Costas Rodriguez Date: Wed, 21 Jun 2023 20:16:01 +0200 Subject: [PATCH 02/15] Fix testx --- craft_cli/printer.py | 4 ++-- tests/integration/test_messages_integration.py | 6 +++--- tests/unit/test_printer.py | 18 +++++++++++++----- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/craft_cli/printer.py b/craft_cli/printer.py index 757f2d9b..e291d310 100644 --- a/craft_cli/printer.py +++ b/craft_cli/printer.py @@ -377,8 +377,8 @@ def set_titlebar(self, stream: Optional[TextIO], text: str) -> None: if _stream_is_terminal(stream): # Sends the text with the right ANSI codes: # ESC]2;textoBEL - if stream==sys.stderr: - stream=sys.stdout + if stream == sys.stderr: + stream = sys.stdout print(f"\033]2;{text}\007", flush=True, file=stream, end="") def progress_bar( # noqa: PLR0913 diff --git a/tests/integration/test_messages_integration.py b/tests/integration/test_messages_integration.py index a47e9531..3adddcb3 100644 --- a/tests/integration/test_messages_integration.py +++ b/tests/integration/test_messages_integration.py @@ -302,7 +302,7 @@ def test_progress_verbose(capsys, permanent): @pytest.mark.parametrize("output_is_terminal", [False]) -def test_title_set(capsys, monkeypatch): +def test_title_set_no_tty(capsys, monkeypatch): """Show a progress message with update_title flag.""" emit = Emitter() emit.init(EmitterMode.BRIEF, "testapp", GREETING) @@ -315,7 +315,7 @@ def test_title_set(capsys, monkeypatch): @pytest.mark.parametrize("output_is_terminal", [True]) -def test_title_set(capsys, monkeypatch): +def test_title_set_in_tty(capsys, monkeypatch): """Show a progress message with update_title flag.""" emit = Emitter() emit.init(EmitterMode.BRIEF, "testapp", GREETING) @@ -323,7 +323,7 @@ def test_title_set(capsys, monkeypatch): emit.ended_ok() out, err = capsys.readouterr() - assert (out=="\x1b]2;The meaning of life is 42.\x07") is True + assert (out == "\x1b]2;The meaning of life is 42.\x07") is True assert err.find("\x1b]2;The meaning of life is 42.\x07") is -1 diff --git a/tests/unit/test_printer.py b/tests/unit/test_printer.py index 7421ca15..80f5f980 100644 --- a/tests/unit/test_printer.py +++ b/tests/unit/test_printer.py @@ -121,14 +121,18 @@ def isatty(self): def test_titlebar_no_tty(log_filepath): """Setting the titlebar to a no-tty stream does nothing""" + class FakeStream: def __init__(self): self.output = "" self.flushed = 0 + def isatty(self): return False + def write(self, data): self.output += data + def flush(self): self.flushed += 1 @@ -136,21 +140,25 @@ def flush(self): text = "test text" printer = Printer(log_filepath) printer.set_titlebar(stream, text) - assert stream.output is "" - assert stream.flushed is 0 + assert stream.output == "" + assert stream.flushed == 0 def test_titlebar_true_tty(log_filepath): """Setting the titlebar to a true-tty stream sends the text and - the corresponding ANSI escape codes to set the title""" + the corresponding ANSI escape codes to set the title""" + class FakeStream: def __init__(self): self.output = "" self.flushed = 0 + def isatty(self): return True + def write(self, data): self.output += data + def flush(self): self.flushed += 1 @@ -158,8 +166,8 @@ def flush(self): text = "test text" printer = Printer(log_filepath) printer.set_titlebar(stream, text) - assert (stream.output == f"\033]2;{text}\007") is True - assert stream.flushed is 1 + assert stream.output == f"\033]2;{text}\007" + assert stream.flushed == 1 # -- tests for the writing line (terminal version) function From adfa4d34851fd9925425eceba886108a3ac55b2e Mon Sep 17 00:00:00 2001 From: Sergio Costas Rodriguez Date: Wed, 21 Jun 2023 20:25:26 +0200 Subject: [PATCH 03/15] Try to fix lint test --- tests/unit/test_printer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_printer.py b/tests/unit/test_printer.py index 80f5f980..6211bbff 100644 --- a/tests/unit/test_printer.py +++ b/tests/unit/test_printer.py @@ -16,6 +16,7 @@ """Tests that check the whole Printer machinery.""" +import io import re import shutil import sys @@ -122,7 +123,7 @@ def isatty(self): def test_titlebar_no_tty(log_filepath): """Setting the titlebar to a no-tty stream does nothing""" - class FakeStream: + class FakeStream(io.TextIOBase): def __init__(self): self.output = "" self.flushed = 0 @@ -148,7 +149,7 @@ def test_titlebar_true_tty(log_filepath): """Setting the titlebar to a true-tty stream sends the text and the corresponding ANSI escape codes to set the title""" - class FakeStream: + class FakeStream(io.TextIOBase): def __init__(self): self.output = "" self.flushed = 0 From e2c82d8e34ee99ac0ad9c508fc2b0583e2f4ff7a Mon Sep 17 00:00:00 2001 From: Sergio Costas Rodriguez Date: Wed, 21 Jun 2023 20:31:39 +0200 Subject: [PATCH 04/15] Fix final error in tox --- tests/unit/test_printer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_printer.py b/tests/unit/test_printer.py index 6211bbff..fddf460c 100644 --- a/tests/unit/test_printer.py +++ b/tests/unit/test_printer.py @@ -123,7 +123,7 @@ def isatty(self): def test_titlebar_no_tty(log_filepath): """Setting the titlebar to a no-tty stream does nothing""" - class FakeStream(io.TextIOBase): + class FakeStream(io.StringIO): def __init__(self): self.output = "" self.flushed = 0 @@ -149,7 +149,7 @@ def test_titlebar_true_tty(log_filepath): """Setting the titlebar to a true-tty stream sends the text and the corresponding ANSI escape codes to set the title""" - class FakeStream(io.TextIOBase): + class FakeStream(io.StringIO): def __init__(self): self.output = "" self.flushed = 0 From 78d3cfdf9a61d54beca06625084b9d69c741ec0b Mon Sep 17 00:00:00 2001 From: Sergio Costas Rodriguez Date: Wed, 21 Jun 2023 20:38:11 +0200 Subject: [PATCH 05/15] Fix "set_titlebar" is not a known member of "None" --- craft_cli/messages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/craft_cli/messages.py b/craft_cli/messages.py index d43088e5..17336058 100644 --- a/craft_cli/messages.py +++ b/craft_cli/messages.py @@ -629,7 +629,7 @@ def progress(self, text: str, permanent: bool = False, update_titlebar: bool = F self._printer.set_terminal_prefix(text) if update_titlebar: - self._printer.set_titlebar(stream, text) + self._printer.set_titlebar(stream, text) # type: ignore @_active_guard() def progress_bar( From 4900e1698705b158365ac1464eeb3f9ad48def63 Mon Sep 17 00:00:00 2001 From: Sergio Costas Date: Wed, 21 Jun 2023 20:58:46 +0200 Subject: [PATCH 06/15] Update tests/unit/test_messages_integration.py Co-authored-by: Tiago Nobrega --- tests/integration/test_messages_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_messages_integration.py b/tests/integration/test_messages_integration.py index 3adddcb3..49ff01e8 100644 --- a/tests/integration/test_messages_integration.py +++ b/tests/integration/test_messages_integration.py @@ -323,7 +323,7 @@ def test_title_set_in_tty(capsys, monkeypatch): emit.ended_ok() out, err = capsys.readouterr() - assert (out == "\x1b]2;The meaning of life is 42.\x07") is True + assert out == "\x1b]2;The meaning of life is 42.\x07" assert err.find("\x1b]2;The meaning of life is 42.\x07") is -1 From 442af45a255fac2e62579e873a52217e87148b5c Mon Sep 17 00:00:00 2001 From: Sergio Costas Rodriguez Date: Thu, 22 Jun 2023 20:04:48 +0200 Subject: [PATCH 07/15] Use command 0 instead of 2 to set the titlebar --- craft_cli/printer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/craft_cli/printer.py b/craft_cli/printer.py index e291d310..1cb73b5c 100644 --- a/craft_cli/printer.py +++ b/craft_cli/printer.py @@ -379,7 +379,7 @@ def set_titlebar(self, stream: Optional[TextIO], text: str) -> None: # ESC]2;textoBEL if stream == sys.stderr: stream = sys.stdout - print(f"\033]2;{text}\007", flush=True, file=stream, end="") + print(f"\033]0;{text}\007", flush=True, file=stream, end="") def progress_bar( # noqa: PLR0913 self, From 43c1da3e3728ddc604ddde1b1a8f683e61a51eaa Mon Sep 17 00:00:00 2001 From: Sergio Costas Rodriguez Date: Thu, 22 Jun 2023 20:15:42 +0200 Subject: [PATCH 08/15] Test with command 30 instead of command 2 --- craft_cli/printer.py | 2 +- tests/unit/test_printer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/craft_cli/printer.py b/craft_cli/printer.py index 1cb73b5c..3eebeb43 100644 --- a/craft_cli/printer.py +++ b/craft_cli/printer.py @@ -379,7 +379,7 @@ def set_titlebar(self, stream: Optional[TextIO], text: str) -> None: # ESC]2;textoBEL if stream == sys.stderr: stream = sys.stdout - print(f"\033]0;{text}\007", flush=True, file=stream, end="") + print(f"\033]30;{text}\007", flush=True, file=stream, end="") def progress_bar( # noqa: PLR0913 self, diff --git a/tests/unit/test_printer.py b/tests/unit/test_printer.py index fddf460c..275fc6b6 100644 --- a/tests/unit/test_printer.py +++ b/tests/unit/test_printer.py @@ -167,7 +167,7 @@ def flush(self): text = "test text" printer = Printer(log_filepath) printer.set_titlebar(stream, text) - assert stream.output == f"\033]2;{text}\007" + assert stream.output == f"\033]30;{text}\007" assert stream.flushed == 1 From 8ece8a5c5d496cf12776d01eeee160c3435ab982 Mon Sep 17 00:00:00 2001 From: Sergio Costas Rodriguez Date: Thu, 22 Jun 2023 20:30:13 +0200 Subject: [PATCH 09/15] Restore the right command and add an example --- craft_cli/printer.py | 2 +- examples.py | 4 ++++ tests/unit/test_printer.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/craft_cli/printer.py b/craft_cli/printer.py index 3eebeb43..e291d310 100644 --- a/craft_cli/printer.py +++ b/craft_cli/printer.py @@ -379,7 +379,7 @@ def set_titlebar(self, stream: Optional[TextIO], text: str) -> None: # ESC]2;textoBEL if stream == sys.stderr: stream = sys.stdout - print(f"\033]30;{text}\007", flush=True, file=stream, end="") + print(f"\033]2;{text}\007", flush=True, file=stream, end="") def progress_bar( # noqa: PLR0913 self, diff --git a/examples.py b/examples.py index ebd95709..349c9f5b 100755 --- a/examples.py +++ b/examples.py @@ -485,6 +485,10 @@ def _call_lib(logger, index): logger.debug(f" {lib} DEBUG 2") time.sleep(2) +def example_30(new_title): + """Set the window title""" + emit.progress(new_title, update_titlebar=True) + time.sleep(1.5) # -- end of test cases diff --git a/tests/unit/test_printer.py b/tests/unit/test_printer.py index 275fc6b6..fddf460c 100644 --- a/tests/unit/test_printer.py +++ b/tests/unit/test_printer.py @@ -167,7 +167,7 @@ def flush(self): text = "test text" printer = Printer(log_filepath) printer.set_titlebar(stream, text) - assert stream.output == f"\033]30;{text}\007" + assert stream.output == f"\033]2;{text}\007" assert stream.flushed == 1 From 2876550b6780971c568f253fbbcc477dffd284ab Mon Sep 17 00:00:00 2001 From: Sergio Costas Rodriguez Date: Thu, 22 Jun 2023 20:33:51 +0200 Subject: [PATCH 10/15] Added extra example --- examples.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/examples.py b/examples.py index 349c9f5b..96a61fc4 100755 --- a/examples.py +++ b/examples.py @@ -490,6 +490,13 @@ def example_30(new_title): emit.progress(new_title, update_titlebar=True) time.sleep(1.5) +def example_31(): + """Set the window title twice""" + emit.progress("Changed the title once", update_titlebar=True) + time.sleep(2) + emit.progress("Changed the title twice", update_titlebar=True) + time.sleep(2) + # -- end of test cases if len(sys.argv) < 2: From 06fb78115b50b67736d9c13ef43e8fb773f40ada Mon Sep 17 00:00:00 2001 From: Sergio Costas Rodriguez Date: Thu, 22 Jun 2023 20:34:16 +0200 Subject: [PATCH 11/15] Extra documentation of the new example --- examples.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples.py b/examples.py index 96a61fc4..ba3800a3 100755 --- a/examples.py +++ b/examples.py @@ -491,7 +491,8 @@ def example_30(new_title): time.sleep(1.5) def example_31(): - """Set the window title twice""" + """Set the window title twice, to test if there is a delay when + changing the title""" emit.progress("Changed the title once", update_titlebar=True) time.sleep(2) emit.progress("Changed the title twice", update_titlebar=True) From 7243ea24d2b22eb9e40be211924d23f446d3a12f Mon Sep 17 00:00:00 2001 From: Sergio Costas Rodriguez Date: Mon, 25 Sep 2023 12:28:15 +0200 Subject: [PATCH 12/15] Fix lint problem --- examples.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples.py b/examples.py index ba3800a3..0cbfd303 100755 --- a/examples.py +++ b/examples.py @@ -485,11 +485,13 @@ def _call_lib(logger, index): logger.debug(f" {lib} DEBUG 2") time.sleep(2) + def example_30(new_title): """Set the window title""" emit.progress(new_title, update_titlebar=True) time.sleep(1.5) + def example_31(): """Set the window title twice, to test if there is a delay when changing the title""" @@ -498,6 +500,7 @@ def example_31(): emit.progress("Changed the title twice", update_titlebar=True) time.sleep(2) + # -- end of test cases if len(sys.argv) < 2: From 16e7498b7cb7479020ae9537980fdec0d0f75904 Mon Sep 17 00:00:00 2001 From: Sergio Costas Rodriguez Date: Mon, 25 Sep 2023 12:33:26 +0200 Subject: [PATCH 13/15] Fix other lint problem --- craft_cli/messages.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/craft_cli/messages.py b/craft_cli/messages.py index 17336058..417753db 100644 --- a/craft_cli/messages.py +++ b/craft_cli/messages.py @@ -602,7 +602,9 @@ def _get_progress_params( return stream, use_timestamp, ephemeral @_active_guard() - def progress(self, text: str, permanent: bool = False, update_titlebar: bool = False) -> None: # noqa: FBT001, FBT002 + def progress( + self, text: str, permanent: bool = False, update_titlebar: bool = False + ) -> None: # noqa: FBT001, FBT002 """Progress information for a multi-step command. This is normally used to present several separated text messages. From fa92f7c6db85985e1b0b50365bf52ef1b294bed3 Mon Sep 17 00:00:00 2001 From: Sergio Costas Rodriguez Date: Mon, 25 Sep 2023 12:42:56 +0200 Subject: [PATCH 14/15] More fixes --- craft_cli/printer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/craft_cli/printer.py b/craft_cli/printer.py index e291d310..55d0c0d4 100644 --- a/craft_cli/printer.py +++ b/craft_cli/printer.py @@ -27,7 +27,7 @@ from dataclasses import dataclass, field from datetime import datetime from functools import lru_cache -from typing import TYPE_CHECKING, Any, Callable, TextIO, Optional +from typing import TYPE_CHECKING, Any, Callable, TextIO if TYPE_CHECKING: import pathlib @@ -372,8 +372,8 @@ def show( # noqa: PLR0913 (too many parameters) if not avoid_logging: self._log(msg) - def set_titlebar(self, stream: Optional[TextIO], text: str) -> None: - """Sets 'text' as the window titlebar content""" + def set_titlebar(self, stream: TextIO | None, text: str) -> None: + """Set 'text' as the window titlebar content.""" if _stream_is_terminal(stream): # Sends the text with the right ANSI codes: # ESC]2;textoBEL From 04c2c41ee8ca9223a3d01e7427b574a34ddf5e3b Mon Sep 17 00:00:00 2001 From: Sergio Costas Rodriguez Date: Mon, 25 Sep 2023 12:48:56 +0200 Subject: [PATCH 15/15] Complete lint fixes --- craft_cli/messages.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/craft_cli/messages.py b/craft_cli/messages.py index 417753db..e1cf5668 100644 --- a/craft_cli/messages.py +++ b/craft_cli/messages.py @@ -603,8 +603,11 @@ def _get_progress_params( @_active_guard() def progress( - self, text: str, permanent: bool = False, update_titlebar: bool = False - ) -> None: # noqa: FBT001, FBT002 + self, + text: str, + permanent: bool = False, # noqa: FBT001, FBT002 + update_titlebar: bool = False, # noqa: FBT001, FBT002 + ) -> None: """Progress information for a multi-step command. This is normally used to present several separated text messages. @@ -631,7 +634,7 @@ def progress( self._printer.set_terminal_prefix(text) if update_titlebar: - self._printer.set_titlebar(stream, text) # type: ignore + self._printer.set_titlebar(stream, text) @_active_guard() def progress_bar(