python/utils: add add_visual_margin() text decoration utility

>>> print(add_visual_margin(msg, width=72, name="Commit Message"))
┏━ Commit Message ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┃ add_visual_margin() takes a chunk of text and wraps it in a visual
┃ container that force-wraps to a specified width. An optional title
┃ label may be given, and any of the individual glyphs used to draw the
┃ box may be replaced or specified as well.
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Eric Blake <eblake@redhat.com>
Acked-by: Hanna Reitz <hreitz@redhat.com>
Message-Id: <20220321201618.903471-2-jsnow@redhat.com>
Signed-off-by: Hanna Reitz <hreitz@redhat.com>
This commit is contained in:
John Snow 2022-03-21 16:16:01 -04:00 committed by Hanna Reitz
parent e48093a6c0
commit be73231ba8

View File

@ -15,7 +15,10 @@ various tasks not directly related to the launching of a VM.
# the COPYING file in the top-level directory.
#
import os
import re
import shutil
import textwrap
from typing import Optional
# pylint: disable=import-error
@ -23,6 +26,7 @@ from .accel import kvm_available, list_accel, tcg_available
__all__ = (
'add_visual_margin',
'get_info_usernet_hostfwd_port',
'kvm_available',
'list_accel',
@ -43,3 +47,77 @@ def get_info_usernet_hostfwd_port(info_usernet_output: str) -> Optional[int]:
if match is not None:
return int(match[1])
return None
# pylint: disable=too-many-arguments
def add_visual_margin(
content: str = '',
width: Optional[int] = None,
name: Optional[str] = None,
padding: int = 1,
upper_left: str = '',
lower_left: str = '',
horizontal: str = '',
vertical: str = '',
) -> str:
"""
Decorate and wrap some text with a visual decoration around it.
This function assumes that the text decoration characters are single
characters that display using a single monospace column.
Example
This is what this function looks like with text content that's
wrapped to 66 characters. The right-hand margin is left open to
accommodate the occasional unicode character that might make
predicting the total "visual" width of a line difficult. This
provides a visual distinction that's good-enough, though.
:param content: The text to wrap and decorate.
:param width:
The number of columns to use, including for the decoration
itself. The default (None) uses the the available width of the
current terminal, or a fallback of 72 lines. A negative number
subtracts a fixed-width from the default size. The default obeys
the COLUMNS environment variable, if set.
:param name: A label to apply to the upper-left of the box.
:param padding: How many columns of padding to apply inside.
:param upper_left: Upper-left single-width text decoration character.
:param lower_left: Lower-left single-width text decoration character.
:param horizontal: Horizontal single-width text decoration character.
:param vertical: Vertical single-width text decoration character.
"""
if width is None or width < 0:
avail = shutil.get_terminal_size(fallback=(72, 24))[0]
if width is None:
_width = avail
else:
_width = avail + width
else:
_width = width
prefix = vertical + (' ' * padding)
def _bar(name: Optional[str], top: bool = True) -> str:
ret = upper_left if top else lower_left
if name is not None:
ret += f"{horizontal} {name} "
filler_len = _width - len(ret)
ret += f"{horizontal * filler_len}"
return ret
def _wrap(line: str) -> str:
return os.linesep.join(
textwrap.wrap(
line, width=_width - padding, initial_indent=prefix,
subsequent_indent=prefix, replace_whitespace=False,
drop_whitespace=True, break_on_hyphens=False)
)
return os.linesep.join((
_bar(name, top=True),
os.linesep.join(_wrap(line) for line in content.splitlines()),
_bar(None, top=False),
))