Skip to content

Commit

Permalink
cli: add support for rendered links
Browse files Browse the repository at this point in the history
Add a function to create rendered links for the terminal, and logic
to ignore anything but the text when calculating the length or
printing to a non-tty stdout

Signed-off-by: Renan Rodrigo <[email protected]>
  • Loading branch information
renanrodrigo committed Sep 30, 2024
1 parent b5da708 commit f02b33e
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 14 deletions.
32 changes: 24 additions & 8 deletions uaclient/cli/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from uaclient.messages import TxtColor

COLOR_FORMATTING_PATTERN = r"\033\[.*?m"
LINK_START_PATTERN = r"\033]8;;.+?\033\\+"
LINK_END = "\033]8;;\033\\"
UTF8_ALTERNATIVES = {
"—": "-",
"✘": "x",
Expand Down Expand Up @@ -56,8 +58,18 @@ def disable_suggestions(cls) -> None:
ProOutputFormatterConfig.init(cfg=UAConfig())


def len_no_color(text: str) -> int:
return len(re.sub(COLOR_FORMATTING_PATTERN, "", text))
def create_link(text: str, url: str) -> str:
return "\033]8;;{url}\033\\{text}\033]8;;\033\\".format(url=url, text=text)


def real_len(text: str) -> int:
# ignore colors if existing
result = re.sub(COLOR_FORMATTING_PATTERN, "", text)
# Ignore link control characters and metadata
result = re.sub(LINK_START_PATTERN, "", result)
result = result.replace(LINK_END, "")

return len(result)


def _get_default_length():
Expand All @@ -78,21 +90,25 @@ def process_formatter_config(text: str) -> str:
output = output.replace(char, alternative)
output = output.encode("ascii", "ignore").decode()

if not sys.stdout.isatty():
output = re.sub(LINK_START_PATTERN, "", output)
output = output.replace(LINK_END, "")

return output


# We can't rely on textwrap because of the len_no_color function
# We can't rely on textwrap because of the real_len function
# Textwrap is using a magic regex instead
def wrap_text(text: str, max_width: int) -> List[str]:
if len_no_color(text) <= max_width:
if real_len(text) <= max_width:
return [text]

words = text.split()
wrapped_lines = []
current_line = ""

for word in words:
if len_no_color(current_line) + len_no_color(word) >= max_width:
if real_len(current_line) + real_len(word) >= max_width:
wrapped_lines.append(current_line.strip())
current_line = word
else:
Expand Down Expand Up @@ -140,14 +156,14 @@ def __init__(

@staticmethod
def ljust(string: str, total_length: int) -> str:
str_length = len_no_color(string)
str_length = real_len(string)
if str_length >= total_length:
return string
return string + " " * (total_length - str_length)

@staticmethod
def rjust(string: str, total_length: int) -> str:
str_length = len_no_color(string)
str_length = real_len(string)
if str_length >= total_length:
return string
return " " * (total_length - str_length) + string
Expand Down Expand Up @@ -179,7 +195,7 @@ def _get_column_sizes(self) -> List[int]:
column_sizes = []
for i in range(len(all_content[0])):
column_sizes.append(
max(len_no_color(str(item[i])) for item in all_content)
max(real_len(str(item[i])) for item in all_content)
)

return column_sizes
Expand Down
42 changes: 36 additions & 6 deletions uaclient/cli/tests/test_cli_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
from uaclient.cli.formatter import (
SuggestionBlock,
Table,
len_no_color,
create_link,
real_len,
wrap_text,
)

Expand Down Expand Up @@ -74,7 +75,15 @@ def test_suggestions_config(self, config_value, FakeConfig):
assert POFC.show_suggestions is False


class TestLenNoColor:
class TestCreateLink:
def test_creates_link(self):
assert (
"\x1b]8;;http://example.test\x1b\\some link\x1b]8;;\x1b\\"
== create_link(text="some link", url="http://example.test")
)


class TestRealLen:
@pytest.mark.parametrize(
"input_string,expected_length",
(
Expand All @@ -87,7 +96,15 @@ class TestLenNoColor:
),
)
def test_length_ignores_color(self, input_string, expected_length):
assert expected_length == len_no_color(input_string)
assert expected_length == real_len(input_string)

def test_length_ignores_links(self):
str_with_link = (
"There is an "
"\x1b]8;;https://example.com\x1b\\example link here\x1b]8;;\x1b\\"
" to be clicked"
)
assert 43 == real_len(str_with_link)


class TestWrapText:
Expand Down Expand Up @@ -119,6 +136,19 @@ def test_single_line_wrapped(self):
"too",
] == wrap_text(colored_string, 20)

string_with_link = (
"There is an "
"\x1b]8;;https://example.com\x1b\\example link here\x1b]8;;\x1b\\"
" to be clicked"
)
assert len(string_with_link) == 76
assert [string_with_link] == wrap_text(string_with_link, 45)
assert [
"There is an \x1b]8;;https://example.com\x1b\\example",
"link here\x1b]8;;\x1b\\ to be",
"clicked",
] == wrap_text(string_with_link, 20)


class TestTable:
@pytest.mark.parametrize(
Expand Down Expand Up @@ -290,15 +320,15 @@ def test_to_string_wraps_to_terminal_size(
table = Table(
["header1", "h2", "h3", "h4"],
[
["a", "bc", "de", "f"],
[create_link(text="a", url="example.com"), "bc", "de", "f"],
["b", "de", "fg", "wow this is a really big string of data"],
["c", "fg", "hijkl", "m"],
],
)
assert table.to_string() == textwrap.dedent(
"""\
\x1b[1mheader1 h2 h3 h4\x1b[0m
a bc de f
\x1b]8;;example.com\x1b\\a\x1b]8;;\x1b\\ bc de f
b de fg wow this is a
really big string of
data
Expand All @@ -319,7 +349,7 @@ def test_to_string_no_wrap_if_no_tty(
table = Table(
["header1", "h2", "h3", "h4"],
[
["a", "bc", "de", "f"],
[create_link(text="a", url="example.com"), "bc", "de", "f"],
[
"b",
"de",
Expand Down

0 comments on commit f02b33e

Please sign in to comment.