Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion craft_cli/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -602,14 +602,22 @@ 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, # 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.

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).
"""
Expand All @@ -625,6 +633,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
Expand Down
10 changes: 10 additions & 0 deletions craft_cli/printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import math
import queue
import shutil
import sys
import threading
import time
from dataclasses import dataclass, field
Expand Down Expand Up @@ -371,6 +372,15 @@ def show( # noqa: PLR0913 (too many parameters)
if not avoid_logging:
self._log(msg)

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
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,
Expand Down
15 changes: 15 additions & 0 deletions examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,21 @@ def _call_lib(logger, index):
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"""
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:
Expand Down
26 changes: 26 additions & 0 deletions tests/integration/test_messages_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_no_tty(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_in_tty(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"
assert err.find("\x1b]2;The meaning of life is 42.\x07") is -1


@pytest.mark.parametrize(
"mode",
[
Expand Down
55 changes: 55 additions & 0 deletions tests/unit/test_printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

"""Tests that check the whole Printer machinery."""

import io
import re
import shutil
import sys
Expand Down Expand Up @@ -116,6 +117,60 @@ 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(io.StringIO):
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 == ""
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"""

class FakeStream(io.StringIO):
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"
assert stream.flushed == 1


# -- tests for the writing line (terminal version) function


Expand Down