import textwrap
from datetime import datetime
from pathlib import Path
from typing import Literal
from framed_text.utils import (
_format_col_text,
_format_big_number,
_human_readable_bytes,
_remove_ansi,
_truncate_text_middle,
_validate_attrs,
_validate_color,
)
[docs]
class LabeledData:
"""
Class for creating a string with a label and different types of data.
Each item can be configured to include colored text.
"""
@staticmethod
def _format_text(label: str,
value: str,
suffix: str,
quotes: bool,
colon_match: bool = True,
no_colon: bool = False,
val_color: str | tuple[int, int, int] | None = None,
label_color: str | tuple[int, int, int] | None = None,
suffix_color: str | tuple[int, int, int] | None = None,
val_attrs: list[str] | None = None,
label_attrs: list[str] | None = None,
suffix_attrs: list[str] | None = None) -> str:
"""
Compile each part of the LabeledData string into a single string
:param label: Label
:param value: Value
:param suffix: Suffix
:param quotes: If true, wrap text in single quotes (')
:param colon_match: If true, the colon will match the style of the label
:param no_colon: If true, the colon will be removed. Overrides ``colon_match``
:param val_color: Color of the value
:param label_color: Color of the label
:param suffix_color: Color of the suffix
:param val_attrs: Attributes to apply to the value
:param label_attrs: Attributes to apply to the label
:param suffix_attrs: Attributes to apply to the suffix
:return: LabeledData text string
"""
# Label
_label: str = _format_col_text(text=label, color=label_color, attrs=label_attrs)
# Colon formatting if needed
if colon_match and not no_colon:
_col: str = _format_col_text(text=':', color=label_color)
elif not no_colon:
_col: str = ':'
else:
_col: str = ''
# Value
if quotes:
_value: str = _format_col_text(text=f"'{value}'", color=val_color, attrs=val_attrs)
else:
_value: str = _format_col_text(text=value, color=val_color, attrs=val_attrs)
# Suffix
_suffix: str = _format_col_text(text=suffix, color=suffix_color, attrs=suffix_attrs)
if suffix:
return f"{_label}{_col} {_value} {_suffix}"
else:
return f"{_label}{_col} {_value}"
@staticmethod
def _format_bytes(label: str,
value: int,
bytes_hr: float,
unit: str,
suffix: str,
quotes: bool,
colon_match: bool = True,
unit_match: bool = True,
no_colon: bool = False,
show_unit: bool = True,
val_color: str | tuple[int, int, int] | None = None,
label_color: str | tuple[int, int, int] | None = None,
suffix_color: str | tuple[int, int, int] | None = None,
unit_color: str | tuple[int, int, int] | None = None,
val_attrs: list[str] | None = None,
label_attrs: list[str] | None = None,
suffix_attrs: list[str] | None = None,
unit_attrs: list[str] | None = None) -> str:
"""
Compile each part of the LabeledData string into a single string
:param label: Label
:param value: Value
:param bytes_hr: Bytes value in a human-readable format.
:param unit: Unit
:param suffix: Suffix
:param quotes: If true, wrap text in single quotes (')
:param colon_match: If true, the colon will match the style of the label
:param unit_match: If true, the unit will match the style of the value
:param no_colon: If true, the colon will be removed. Overrides ``colon_match``
:param show_unit: If true, the unit will be shown
:param val_color: Color of the value
:param label_color: Color of the label
:param suffix_color: Color of the suffix
:param unit_color: Color of the unit
:param val_attrs: Attributes to apply to the value
:param label_attrs: Attributes to apply to the label
:param suffix_attrs: Attributes to apply to the suffix
:param unit_attrs: Attributes to apply to the unit
:return: LabeledData text string
"""
# Label
_label: str = _format_col_text(text=label, color=label_color, attrs=label_attrs)
# Colon formatting if needed
if colon_match and not no_colon:
_col: str = _format_col_text(text=':', color=label_color)
elif not no_colon:
_col: str = ':'
else:
_col: str = ''
# Unit & Value
_unit: str = ''
_unit_color: str | tuple[int, int, int] | None = None
_unit_attrs: list[str] | None = None
# Override unit_match if a unit color/attr is passed
if unit_match:
_unit_color = val_color if not unit_color else unit_color
_unit_attrs = val_attrs if not unit_attrs else unit_attrs
else:
_unit_color = unit_color
_unit_attrs = unit_attrs
if show_unit:
# If the value is large (Over 32 bits), format it to E-Notation
if bytes_hr and abs(bytes_hr) > 0xffffffff:
_value: str = _format_big_number(num=bytes_hr)
elif bytes_hr:
_value: str = f"{bytes_hr:,.2f}"
elif abs(value) > 0xffffffff:
_value: str = _format_big_number(num=value)
else:
_value: str = str(value)
_unit = _format_col_text(text=unit, color=_unit_color, attrs=_unit_attrs)
else:
_value: str = str(value)
_value = _format_col_text(text=_value, color=val_color, attrs=val_attrs)
# Suffix
_suffix: str = _format_col_text(text=suffix, color=suffix_color, attrs=suffix_attrs)
_val_unit: str = f"'{_value} {_unit}'" if quotes else f"{_value} {_unit}"
if suffix:
return f"{_label}{_col} {_val_unit} {_suffix}"
else:
return f"{_label}{_col} {_val_unit}"
@staticmethod
def _shorten_string(label: str,
value: str,
suffix: str,
text: str,
limit: int,
quotes: bool,
mode: Literal["char", "word", "middle"],
val_color: str | tuple[int, int, int] | None = "cyan",
label_color: str | tuple[int, int, int] | None = None,
suffix_color: str | tuple[int, int, int] | None = None,
colon_match: bool = True,
no_colon: bool = False,
val_attrs: list[str] | None = None,
label_attrs: list[str] | None = None,
suffix_attrs: list[str] | None = None) -> str:
"""
For use with LabeledData.String. Internal only
:param label: Label
:param value: Value
:param suffix: Suffix
:param text: "Label: Value" formatted string
:param limit: Maximum length of text
:param quotes: If true, wrap text in single quotes (')
:param mode: Mode passed from LabeledData.String. Same as LabeledData.String.cutoff_text
:param val_color: Color of text.
:param label_color: Color of label.
:param suffix_color: Color of suffix.
:param colon_match: If true, the colon will match the style of the label.
:param no_colon: If true, the colon will be removed. Overrides ``colon_match``
:param val_attrs: Attributes to apply to the value.
:param label_attrs: Attributes to apply to the label.
:param suffix_attrs: Attributes to apply to the suffix.
:return: Shortened text
"""
# Remove ANSI
label_no_ansi: str = _remove_ansi(text=label)
text_no_ansi: str = _remove_ansi(text=value)
suffix_no_ansi: str = _remove_ansi(text=suffix)
suffix_short: str = ''
if len(text) <= limit:
# Under/at limit
return text
# Calculate limit
text_limit: int = limit - len(label_no_ansi)
text_limit -= 2 if quotes else 0 # Quotes
text_limit -= 2 # Colon and space
text_limit -= 1 # Ellipsis
if len(label_no_ansi) <= limit // 2:
label: str = str(label_no_ansi)
else:
# Label needs to be shortened
label_old: str = str(label_no_ansi)
label: str = textwrap.shorten(text=label_no_ansi, width=limit // 2, placeholder="…")
# Add back to limit
text_limit += len(label_old) - len(label)
if suffix:
text_limit -= 1 # Space between value and suffix
text_limit -= 1 # Space at end
# With suffix, value and suffix share limit
if len(value) + len(suffix) <= text_limit:
text_short: str = text_no_ansi
else:
text_short: str = ''
# Calculate the ratio of text between the value and suffix
_ratio: float = len(value) / (len(value) + len(suffix))
_limit_val: int = round(text_limit * _ratio)
_limit_suf: int = text_limit - _limit_val
if len(value) > _limit_val:
# Value
match mode.lower():
case "char":
# Shorten by char
text_short = f"{text_no_ansi[:_limit_val]}…"
case "word":
# Shorten by word
text_short = textwrap.shorten(text=text_no_ansi, width=_limit_val, placeholder="…")
case "middle":
# Shorten by middle characters
text_short = _truncate_text_middle(text=text_no_ansi, limit=_limit_val)
else:
text_short = text_no_ansi
if len(suffix) > _limit_suf:
# Suffix will always be shortened by word
suffix_short = textwrap.shorten(text=suffix_no_ansi, width=_limit_suf, placeholder="…")
else:
suffix_short = suffix_no_ansi
elif len(value) > text_limit:
match mode.lower():
case "char":
# Shorten by char
text_short: str = f"{text_no_ansi[:text_limit]}…"
case "word":
# Shorten by word
text_short: str = textwrap.shorten(text=text_no_ansi, width=text_limit, placeholder="…")
case "middle":
# Shorten by middle characters
text_short: str = _truncate_text_middle(text=text_no_ansi, limit=text_limit)
else:
text_short: str = text_no_ansi
# Format Text
return LabeledData._format_text(label=label,
value=text_short,
suffix=suffix_short,
quotes=quotes,
colon_match=colon_match,
no_colon=no_colon,
val_color=val_color,
label_color=label_color,
suffix_color=suffix_color,
val_attrs=val_attrs,
label_attrs=label_attrs,
suffix_attrs=suffix_attrs
)
@staticmethod
def _shorten_number(label: str,
value: int | float,
suffix: str,
text: str,
limit: int,
quotes: bool,
val_color: str | tuple[int, int, int] | None = "yellow",
label_color: str | tuple[int, int, int] | None = None,
suffix_color: str | tuple[int, int, int] | None = None,
colon_match: bool = True,
no_colon: bool = False,
leading_zeros: int | None = None,
int_no_round: bool = False,
round_to: int | None = None,
val_attrs: list[str] | None = None,
label_attrs: list[str] | None = None,
suffix_attrs: list[str] | None = None) -> str:
"""
For use with LabeledData.Number. Internal only
:param label: Label
:param value: Value
:param suffix: Suffix
:param text: "Label: Value" formatted string
:param limit: Maximum length of text
:param quotes: If true, wrap text in single quotes (')
:param val_color: Color of text.
:param label_color: Color of label.
:param suffix_color: Color of suffix.
:param colon_match: If true, the colon will match the style of the label.
:param no_colon: If true, the colon will be removed. Overrides ``colon_match``
:param round_to: Number of decimal places to round to. Leave blank to not round.
:param leading_zeros: Number of leading zeros to add to the number. Leave blank to not add leading zeros.
:param int_no_round: If true, displays number as integer without rounding. Overrides ``round_to``
:param val_attrs: Attributes to apply to the value.
:param label_attrs: Attributes to apply to the label.
:param suffix_attrs: Attributes to apply to the suffix.
:return: Shortened text
"""
# If the value is large (Over 32 bits), format it to E-Notation
# Will override rounding and leading zeros
if abs(value) > 0xffffffff:
_value: str = _format_big_number(num=value)
else:
# Rounding
if int_no_round:
# Display num as integer
_value: str = f"{int(value):,}"
elif round_to is not None and round_to > 0:
# Round
_value: str = f"{round(value, round_to):,}"
elif round_to is not None and round_to == 0:
# Round to Integer. Don't display trailing 0
_value: str = f"{round(value, round_to):,.0f}"
elif round_to:
# Round error
raise ValueError("'round' must be an integer greater than 0")
else:
# No rounding
_value: str = f"{value:,}"
# Leading Zeros
if leading_zeros and leading_zeros >= 0:
_value = f"{_value:0{leading_zeros}d}"
elif leading_zeros:
# Leading zeros error
raise ValueError("'leading_zeros' must be an integer greater than 0")
# Remove ANSI
label_no_ansi: str = _remove_ansi(text=label)
suffix_no_ansi: str = _remove_ansi(text=suffix)
suffix_short: str = ''
if len(text) <= limit:
# Under/at limit
return text
# Calculate limit
text_limit: int = limit - len(label_no_ansi)
text_limit -= 2 if quotes else 0 # Quotes
text_limit -= 2 # Colon and space
text_limit -= 1 # Ellipsis
if len(label_no_ansi) <= limit // 2:
label: str = str(label_no_ansi)
else:
# Label needs to be shortened
label_old: str = str(label_no_ansi)
label: str = textwrap.shorten(text=label_no_ansi, width=limit // 2, placeholder="…")
# Add back to limit
text_limit += len(label_old) - len(label)
if suffix:
text_limit -= 1 # Space between value and suffix
text_limit -= 1 # Space at end
# With suffix, value and suffix share limit
if len(_value) + len(suffix) > text_limit:
# Calculate the ratio of text between the value and suffix
_ratio: float = len(_value) / (len(_value) + len(suffix))
_limit_val: int = round(text_limit * _ratio)
_limit_suf: int = text_limit - _limit_val
if len(suffix) > _limit_suf:
# Suffix will always be shortened by word
suffix_short = textwrap.shorten(text=suffix_no_ansi, width=_limit_suf, placeholder="…")
else:
suffix_short = suffix_no_ansi
# Format Text
return LabeledData._format_text(label=label,
value=_value,
suffix=suffix_short,
quotes=quotes,
colon_match=colon_match,
no_colon=no_colon,
val_color=val_color,
label_color=label_color,
suffix_color=suffix_color,
val_attrs=val_attrs,
label_attrs=label_attrs,
suffix_attrs=suffix_attrs,
)
@staticmethod
def _shorten_path(label: str,
value: str,
text: str,
suffix: str,
limit: int,
quotes: bool,
mode: Literal["char", "word", "middle"],
val_color: str | tuple[int, int, int] | None = "cyan",
label_color: str | tuple[int, int, int] | None = None,
suffix_color: str | tuple[int, int, int] | None = None,
colon_match: bool = True,
no_colon: bool = False,
val_attrs: list[str] | None = None,
label_attrs: list[str] | None = None,
suffix_attrs: list[str] | None = None) -> str:
"""
For use with LabeledData.Path. Internal only
:param label: Label
:param value: Value
:param suffix: Suffix
:param text: "Label: Value" formatted string
:param limit: Maximum length of text
:param quotes: If true, wrap text in single quotes (')
:param mode: Mode passed from LabeledData.Path. Same as LabeledData.Path.cutoff_text
:param val_color: Color of text.
:param label_color: Color of label.
:param suffix_color: Color of suffix.
:param colon_match: If true, the colon will match the style of the label.
:param no_colon: If true, the colon will be removed. Overrides ``colon_match``
:param val_attrs: Attributes to apply to the value.
:param label_attrs: Attributes to apply to the label.
:param suffix_attrs: Attributes to apply to the suffix.
:return: Shortened text
"""
# Remove ANSI
label_no_ansi: str = _remove_ansi(text=label)
text_no_ansi: str = _remove_ansi(text=value)
suffix_no_ansi: str = _remove_ansi(text=suffix)
suffix_short: str = ''
if len(text) <= limit:
return text
# Calculate limit
text_limit: int = limit - len(label_no_ansi)
text_limit -= 2 if quotes else 0 # Quotes
text_limit -= 2 # Colon and space
text_limit -= 1 # Ellipsis
if len(label_no_ansi) > limit // 2:
# Label needs to be shortened
label_old: str = str(label_no_ansi)
label: str = textwrap.shorten(text=label_no_ansi, width=limit // 2, placeholder="…")
# Add back to limit
text_limit += len(label_old) - len(label)
else:
label: str = str(label_no_ansi)
if suffix:
text_limit -= 1 # Space between value and suffix
text_limit -= 1 # Space at end
# With suffix, vlaue and suffix share limit
if len(value) + len(suffix) <= text_limit:
text_short: str = text_no_ansi
else:
text_short: str = ''
# Calculate the ratio of text between the value and suffix
_ratio: float = len(value) / (len(value) + len(suffix))
_limit_val: int = round(text_limit * _ratio)
_limit_suf: int = text_limit - _limit_val
if len(value) <= _limit_val:
text_short = text_no_ansi
else:
# Value
match mode.lower():
case "char":
# Cut char at end
text_short: str = f"{text_no_ansi[:_limit_val]}…"
case "word":
# Cut path at end
# Removing leading/trailing slash
if text_no_ansi[-1] == "/":
text_no_ansi = text_no_ansi[:-1]
if text_no_ansi[0] == "/":
text_no_ansi = text_no_ansi[1:]
# Split path to count
text_parts: list[str] = text_no_ansi.split("/")
# Remove last part and slashes from limit
_limit_val -= len(text_parts[-1])
_limit_val -= value.count('/')
char_cnt: int = 0
part_at_limit: int = -1
# Find which part exceeds limit
for i, part in enumerate(text_parts):
char_cnt += len(part)
if char_cnt > _limit_val:
part_at_limit = i
break
if part_at_limit != -1:
# Cut of part, replace with ellipsis
part_end: str = text_parts[-1]
text_parts = text_parts[:part_at_limit]
text_parts.insert(part_at_limit, "…")
text_short: str = f"/{'/'.join(text_parts)}/{part_end}"
else:
# No part exceeds limit
text_short: str = text_no_ansi
case "middle":
# Cut char at middle
text_short: str = _truncate_text_middle(text=text_no_ansi, limit=_limit_val)
if len(suffix) > _limit_suf:
# Suffix will always be shortened by word
suffix_short = textwrap.shorten(text=suffix_no_ansi, width=_limit_suf, placeholder="…")
else:
suffix_short = suffix_no_ansi
elif len(value) > text_limit:
# Shorten value
match mode.lower():
case "char":
# Cut char at end
text_short: str = f"{text_no_ansi[:text_limit]}…"
case "word":
# Cut path at end
# Removing leading/trailing slash
if text_no_ansi[-1] == "/":
text_no_ansi = text_no_ansi[:-1]
if text_no_ansi[0] == "/":
text_no_ansi = text_no_ansi[1:]
# Split path to count
text_parts: list[str] = text_no_ansi.split("/")
# Remove last part and slashes from limit
text_limit -= len(text_parts[-1])
text_limit -= value.count('/')
char_cnt: int = 0
part_at_limit: int = -1
# Find which part exceeds limit
for i, part in enumerate(text_parts):
char_cnt += len(part)
if char_cnt > text_limit:
part_at_limit = i
break
if part_at_limit == -1:
# No part exceeds limit
text_short: str = text_no_ansi
else:
# Cut of part, replace with ellipsis
part_end: str = text_parts[-1]
text_parts = text_parts[:part_at_limit]
text_parts.insert(part_at_limit, "…")
text_short: str = f"/{'/'.join(text_parts)}/{part_end}"
case "middle":
# Cut char at middle
text_short: str = _truncate_text_middle(text=text_no_ansi, limit=text_limit)
else:
text_short: str = text_no_ansi
# Format Text
return LabeledData._format_text(label=label,
value=text_short,
suffix=suffix_short,
quotes=quotes,
colon_match=colon_match,
no_colon=no_colon,
val_color=val_color,
label_color=label_color,
suffix_color=suffix_color,
val_attrs=val_attrs,
label_attrs=label_attrs,
suffix_attrs=suffix_attrs
)
@staticmethod
def _shorten_text(label: str,
value: str,
suffix: str,
text: str,
limit: int,
quotes: bool,
val_color: str | tuple[int, int, int] | None = "cyan",
label_color: str | tuple[int, int, int] | None = None,
suffix_color: str | tuple[int, int, int] | None = None,
colon_match: bool = True,
no_colon: bool = False,
val_attrs: list[str] | None = None,
label_attrs: list[str] | None = None,
suffix_attrs: list[str] | None = None) -> str:
"""
Generic text shortener for all LabeledData types without a mode.
Shortens by char. Internal only.
:param label: Label
:param value: Value
:param suffix: Suffix
:param text: "Label: Value" formatted string
:param limit: Maximum length of text
:param quotes: If true, wrap text in single quotes (')
:param val_color: Color of text.
:param label_color: Color of label.
:param suffix_color: Color of suffix.
:param colon_match: If true, the colon will match the style of the label.
:param no_colon: If true, the colon will be removed. Overrides ``colon_match``
:param val_attrs: Attributes to apply to the value.
:param label_attrs: Attributes to apply to the label.
:param suffix_attrs: Attributes to apply to the suffix.
:return: Shortened text
"""
# Remove ANSI
label_no_ansi: str = _remove_ansi(text=label)
text_no_ansi: str = _remove_ansi(text=value)
suffix_no_ansi: str = _remove_ansi(text=suffix)
suffix_short: str = ''
if len(text) <= limit:
return text
# Calculate limit
text_limit: int = limit - len(label_no_ansi)
text_limit -= 2 if quotes else 0 # Quotes
text_limit -= 2 # Colon and space
text_limit -= 1 # Ellipsis
if len(label_no_ansi) <= limit // 2:
label: str = str(label_no_ansi)
else:
# Label needs to be shortened
label_old: str = str(label_no_ansi)
label: str = textwrap.shorten(text=label_no_ansi, width=limit // 2, placeholder="…")
# Add back to limit
text_limit += len(label_old) - len(label)
if suffix:
text_limit -= 1 # Space between value and suffix
text_limit -= 1 # Space at end
# With suffix, value and suffix share limit
if len(value) + len(suffix) <= text_limit:
text_short: str = text_no_ansi
else:
text_short: str = ''
# Calculate the ratio of text between the value and suffix
_ratio: float = len(value) / (len(value) + len(suffix))
_limit_val: int = round(text_limit * _ratio)
_limit_suf: int = text_limit - _limit_val
if len(value) > _limit_val:
# Value
text_short = f"{text_no_ansi[:_limit_val]}…"
else:
text_short = text_no_ansi
if len(suffix) > _limit_suf:
# Suffix will always be shortened by word
suffix_short = textwrap.shorten(text=suffix_no_ansi, width=_limit_suf, placeholder="…")
else:
suffix_short = suffix_no_ansi
elif len(value) > text_limit:
# Shorten value
text_short: str = f"{text_no_ansi[:text_limit]}…"
else:
text_short: str = text_no_ansi
# Format Text
return LabeledData._format_text(label=label,
value=text_short,
suffix=suffix_short,
quotes=quotes,
colon_match=colon_match,
no_colon=no_colon,
val_color=val_color,
label_color=label_color,
suffix_color=suffix_color,
val_attrs=val_attrs,
label_attrs=label_attrs,
suffix_attrs=suffix_attrs
)
@staticmethod
def _shorten_datetime(label: str,
value: str,
suffix: str,
text: str,
limit: int,
quotes: bool,
val_color: str | tuple[int, int, int] | None = "blue",
label_color: str | tuple[int, int, int] | None = None,
suffix_color: str | tuple[int, int, int] | None = None,
colon_match: bool = True,
no_colon: bool = False,
val_attrs: list[str] | None = None,
label_attrs: list[str] | None = None,
suffix_attrs: list[str] | None = None) -> str:
"""
For use with LabeledData.Date. Internal only
:param label: Label
:param value: Value
:param suffix: Suffix
:param text: "Label: Value" formatted string
:param limit: Maximum length of text
:param quotes: If true, wrap text in single quotes (')
:param val_color: Color of text.
:param label_color: Color of label.
:param suffix_color: Color of suffix.
:param colon_match: If true, the colon will match the style of the label.
:param no_colon: If true, the colon will be removed. Overrides ``colon_match``
:param val_attrs: Attributes to apply to the value.
:param label_attrs: Attributes to apply to the label.
:param suffix_attrs: Attributes to apply to the suffix.
:return: Shortened text
"""
# Remove ANSI
label_no_ansi: str = _remove_ansi(text=label)
text_no_ansi: str = _remove_ansi(text=value)
suffix_no_ansi: str = _remove_ansi(text=suffix)
suffix_short: str = ''
# Known datetime dividers
KNOWN_DIVIDERS: list[str] = [':', '-', '_', '.', ',', ';', '+', '/']
if len(text) <= limit:
# Under/at limit
return text
# Calculate limit
text_limit: int = limit - len(label_no_ansi)
text_limit -= 2 if quotes else 0 # Quotes
text_limit -= 2 # Colon and space
text_limit -= 1 # Ellipsis
if len(label_no_ansi) <= limit // 2:
label: str = str(label_no_ansi)
else:
# Label needs to be shortened
label_old: str = str(label_no_ansi)
label: str = textwrap.shorten(text=label_no_ansi, width=limit // 2, placeholder="…")
# Add back to limit
text_limit += len(label_old) - len(label)
if suffix:
text_limit -= 1 # Space between value and suffix
text_limit -= 1 # Space at end
# With suffix, value and suffix share limit
if len(value) + len(suffix) <= text_limit:
text_short: str = text_no_ansi
else:
text_short: str = ''
# Calculate the ratio of text between the value and suffix
_ratio: float = len(value) / (len(value) + len(suffix))
_limit_val: int = round(text_limit * _ratio)
_limit_suf: int = text_limit - _limit_val
if len(value) > _limit_val:
# Value
# If the value has a known divider, shorten by word
# Otherwise, fallback to char
if any(div in text_no_ansi for div in KNOWN_DIVIDERS):
text_short = textwrap.shorten(text=text_no_ansi, width=_limit_val, placeholder="…")
else:
text_short = f"{text_no_ansi[:_limit_val]}…"
else:
text_short = text_no_ansi
if len(suffix) > _limit_suf:
# Suffix will always be shortened by word
suffix_short = textwrap.shorten(text=suffix_no_ansi, width=_limit_suf, placeholder="…")
else:
suffix_short = suffix_no_ansi
elif len(value) > text_limit:
# If the value has a known divider, shorten by word
# Otherwise, fallback to char
if any(div in text_no_ansi for div in KNOWN_DIVIDERS):
text_short = textwrap.shorten(text=text_no_ansi, width=text_limit, placeholder="…")
else:
text_short = f"{text_no_ansi[:text_limit]}…"
else:
text_short: str = text_no_ansi
# Format Text
return LabeledData._format_text(label=label,
value=text_short,
suffix=suffix_short,
quotes=quotes,
colon_match=colon_match,
no_colon=no_colon,
val_color=val_color,
label_color=label_color,
suffix_color=suffix_color,
val_attrs=val_attrs,
label_attrs=label_attrs,
suffix_attrs=suffix_attrs
)
@staticmethod
def _shorten_bytes(label: str,
value: int,
bytes_hr: float,
unit: str,
suffix: str,
text: str,
limit: int,
quotes: bool,
val_color: str | tuple[int, int, int] | None = "yellow",
label_color: str | tuple[int, int, int] | None = None,
suffix_color: str | tuple[int, int, int] | None = None,
unit_color: str | tuple[int, int, int] | None = None,
colon_match: bool = True,
unit_match: bool = True,
no_colon: bool = False,
show_unit: bool = True,
val_attrs: list[str] | None = None,
label_attrs: list[str] | None = None,
suffix_attrs: list[str] | None = None,
unit_attrs: list[str] | None = None) -> str:
"""
For use with LabeledData.Bytes. Internal only
:param label: Label
:param value: Value
:param bytes_hr: Bytes in human-readable format
:param unit: Unit of measurement
:param suffix: Suffix
:param text: "Label: Value" formatted string
:param limit: Maximum length of text
:param quotes: If true, wrap text in single quotes (')
:param val_color: Color of text.
:param label_color: Color of label.
:param suffix_color: Color of suffix.
:param unit_color: Color of unit.
:param colon_match: If true, the colon will match the style of the label.
:param unit_match: If true, the unit will match the style of the value.
:param no_colon: If true, the colon will be removed. Overrides ``colon_match``
:param show_unit: If true, the unit will be displayed.
:param val_attrs: Attributes to apply to the value.
:param label_attrs: Attributes to apply to the label.
:param suffix_attrs: Attributes to apply to the suffix.
:param unit_attrs: Attributes to apply to the unit.
:return: Shortened text
"""
# If the value is large (Over 32 bits), format it to E-Notation
if bytes_hr and abs(bytes_hr) > 0xffffffff:
_value: str = _format_big_number(num=bytes_hr)
elif bytes_hr:
_value: str = f"{bytes_hr:,.2f}"
elif abs(value) > 0xffffffff:
_value: str = _format_big_number(num=value)
else:
_value: str = str(value)
# Remove ANSI
label_no_ansi: str = _remove_ansi(text=label)
unit_no_ansi: str = _remove_ansi(text=unit)
suffix_no_ansi: str = _remove_ansi(text=suffix)
suffix_short: str = ''
if len(text) <= limit:
# Under/at limit
return text
# Calculate limit
text_limit: int = limit - len(label_no_ansi)
text_limit -= 2 if quotes else 0 # Quotes
text_limit -= 2 # Colon and space
text_limit -= 1 # Ellipsis
# Unit
if show_unit:
text_limit -= len(unit_no_ansi) + 1 # Unit & space
if len(label_no_ansi) <= limit // 2:
label: str = str(label_no_ansi)
else:
# Label needs to be shortened
label_old: str = str(label_no_ansi)
label: str = textwrap.shorten(text=label_no_ansi, width=limit // 2, placeholder="…")
# Add back to limit
text_limit += len(label_old) - len(label)
if suffix:
text_limit -= 1 # Space between value and suffix
text_limit -= 1 # Space at end
# With suffix, value and suffix share limit
if len(_value) + len(suffix) > text_limit:
# Calculate the ratio of text between the value and suffix
_ratio: float = len(_value) / (len(_value) + len(suffix))
_limit_val: int = round(text_limit * _ratio)
_limit_suf: int = text_limit - _limit_val
if len(suffix) > _limit_suf:
# Suffix will always be shortened by word
suffix_short = textwrap.shorten(text=suffix_no_ansi, width=_limit_suf, placeholder="…")
else:
suffix_short = suffix_no_ansi
# Format Text
return LabeledData._format_bytes(label=label,
value=value,
bytes_hr=bytes_hr,
unit=unit_no_ansi,
suffix=suffix_short,
quotes=quotes,
colon_match=colon_match,
unit_match=unit_match,
no_colon=no_colon,
show_unit=show_unit,
val_color=val_color,
label_color=label_color,
suffix_color=suffix_color,
unit_color=unit_color,
val_attrs=val_attrs,
label_attrs=label_attrs,
suffix_attrs=suffix_attrs,
unit_attrs=unit_attrs
)
[docs]
class String:
[docs]
def __init__(self, label: str,
value: str,
suffix: str = '',
suffix_color: str | tuple[int, int, int] | None = None,
val_color: str | tuple[int, int, int] | None = "cyan",
label_color: str | tuple[int, int, int] | None = None,
quotes: bool = True,
colon_match: bool = True,
no_colon: bool = False,
val_attrs: list[str] | None = None,
label_attrs: list[str] | None = None,
suffix_attrs: list[str] | None = None):
"""
Creates a string with a label and string value
:param label: Label for string value
:param value: String value
:param suffix: Optional text to appear after the value
:param val_color: Color of string value.
:param label_color: Color of label.
:param quotes: If true, wrap text in single quotes (')
:param colon_match: If true, the colon will match the style of the label.
:param no_colon: If true, the colon will be removed. Overrides ``colon_match``
:param val_attrs: Attributes to apply to the value.
:param label_attrs: Attributes to apply to the label.
"""
self.label: str = label
self.value: str = value
self.suffix: str = suffix
self.val_color: str | tuple[int, int, int] | None = None
self.label_color: str | tuple[int, int, int] | None = None
self.suffix_color: str | tuple[int, int, int] | None = None
self.val_attrs: list[str] | None = None
self.label_attrs: list[str] | None = None
self.suffix_attrs: list[str] | None = None
self.colon_match: bool = colon_match
self.no_colon: bool = no_colon
self.quotes: bool = quotes
self.text_shorten: str = ""
# Validate Colors
if val_color:
_validate_color(color=val_color)
self.val_color = val_color.lower() if isinstance(val_color, str) else val_color
if label_color:
_validate_color(color=label_color)
self.label_color = label_color.lower() if isinstance(label_color, str) else label_color
if suffix_color:
_validate_color(color=suffix_color)
self.suffix_color = suffix_color.lower() if isinstance(suffix_color, str) else suffix_color
# Validate Attributes
if val_attrs:
_validate_attrs(attrs=val_attrs)
self.val_attrs = val_attrs
if label_attrs:
_validate_attrs(attrs=label_attrs)
self.label_attrs = label_attrs
if suffix_attrs:
_validate_attrs(attrs=suffix_attrs)
self.suffix_attrs = suffix_attrs
# Remove redundant colon from label
if self.label[-1] == ":":
self.label = self.label[:-1]
# Format Text
self.text: str = LabeledData._format_text(label=self.label,
value=self.value,
suffix=self.suffix,
quotes=self.quotes,
colon_match=self.colon_match,
no_colon=self.no_colon,
val_color=self.val_color,
label_color=self.label_color,
suffix_color=self.suffix_color,
val_attrs=self.val_attrs,
label_attrs=self.label_attrs,
suffix_attrs=self.suffix_attrs
)
def __str__(self):
return self.text
[docs]
def cutoff_text(self, limit: int, mode: Literal["char", "word", "middle"] = "char") -> str:
"""
Cuts off text if it exceeds limit. Will first try to shorten string.
If there isn't enough space for just an ellipsis, it will try to shorten the label,
so that the output will be 50/50 (label: value).
Modes:
- char: Cutoff char at end of string (The quick brown fox jumped ove…)
- word: Cutoff word at end of string (The quick brown fox jumped…)
- middle: Cutoff words at middle of string (The quick brown…the lazy dog)
:param limit: Maximum length of text
:param mode: How to cut off value text
:return: Shortened text
"""
self.text_shorten = LabeledData._shorten_string(
label=self.label,
value=self.value,
suffix=self.suffix,
text=self.text,
limit=limit,
mode=mode,
val_color=self.val_color,
label_color=self.label_color,
suffix_color=self.suffix_color,
colon_match=self.colon_match,
no_colon=self.no_colon,
val_attrs=self.val_attrs,
label_attrs=self.label_attrs,
suffix_attrs=self.suffix_attrs,
quotes=self.quotes
)
return self.text_shorten
[docs]
class Number:
[docs]
def __init__(self, label: str,
value: int | float,
suffix: str = '',
round_to: int | None = None,
leading_zeros: int | None = None,
int_no_round: bool = False,
val_color: str | tuple[int, int, int] | None = "yellow",
label_color: str | tuple[int, int, int] | None = None,
suffix_color: str | tuple[int, int, int] | None = None,
quotes: bool = False,
colon_match: bool = True,
no_colon: bool = False,
val_attrs: list[str] | None = None,
label_attrs: list[str] | None = None,
suffix_attrs: list[str] | None = None):
"""
Creates a string with a label and number value
:param label: Label for number value
:param value: Number value
:param suffix: Optional text to appear after the value
:param round_to: Number of decimal places to round to. Leave blank to not round.
:param leading_zeros: Number of leading zeros to add to the number. Leave blank to not add leading zeros.
:param int_no_round: If true, displays number as integer without rounding. Overrides ``round_to``
:param val_color: Color of number value.
:param label_color: Color of label.
:param suffix_color: Color of suffix.
:param quotes: If true, wrap text in single quotes (')
:param colon_match: If true, the colon will match the style of the label.
:param no_colon: If true, the colon will be removed. Overrides ``colon_match``
:param val_attrs: Attributes to apply to the value.
:param label_attrs: Attributes to apply to the label.
"""
self.label: str = label
self.value: int | float = value
self.suffix: str = suffix
self.round_to: int | None = round_to
self.int_no_round: bool = int_no_round
self.leading_zeros: int | None = leading_zeros
self.val_color: str | tuple[int, int, int] | None = None
self.label_color: str | tuple[int, int, int] | None = None
self.suffix_color: str | tuple[int, int, int] | None = None
self.colon_match: bool = colon_match
self.no_colon: bool = no_colon
self.val_attrs: list[str] | None = None
self.label_attrs: list[str] | None = None
self.suffix_attrs: list[str] | None = None
self.quotes: bool = quotes
self.text_shorten: str = ""
# Validate Colors
if val_color:
_validate_color(color=val_color)
self.val_color = val_color.lower() if isinstance(val_color, str) else val_color
if label_color:
_validate_color(color=label_color)
self.label_color = label_color.lower() if isinstance(label_color, str) else label_color
if suffix_color:
_validate_color(color=suffix_color)
self.suffix_color = suffix_color.lower() if isinstance(suffix_color, str) else suffix_color
# Validate Attributes
if val_attrs:
_validate_attrs(attrs=val_attrs)
self.val_attrs = val_attrs
if label_attrs:
_validate_attrs(attrs=label_attrs)
self.label_attrs = label_attrs
if suffix_attrs:
_validate_attrs(attrs=suffix_attrs)
self.suffix_attrs = suffix_attrs
# Remove redundant colon from label
if self.label[-1] == ":":
self.label = self.label[:-1]
# If number is large (>32-bit integer), format to E-Notation
# Will override rounding and leading zeros
if abs(self.value) > 0xffffffff:
num_text: str = _format_big_number(num=self.value)
else:
# Rounding
if self.int_no_round:
# Display num as integer
num_text: str = f"{int(self.value):,}"
elif self.round_to is not None and self.round_to > 0:
# Round
num_text: str = f"{round(self.value, self.round_to):,}"
elif self.round_to is not None and self.round_to == 0:
# Round to Integer. Don't display trailing 0
num_text: str = f"{round(self.value, self.round_to):,.0f}"
elif self.round_to:
# Round error
raise ValueError("'round' must be an integer greater than 0")
else:
# No rounding
num_text: str = f"{self.value:,}"
# Leading Zeros
if self.leading_zeros and self.leading_zeros >= 0:
num_text = f"{num_text:0{self.leading_zeros}d}"
elif self.leading_zeros:
# Leading zeros error
raise ValueError("'leading_zeros' must be an integer greater than 0")
# Format Text
self.text: str = LabeledData._format_text(label=self.label,
value=num_text,
suffix=self.suffix,
quotes=self.quotes,
colon_match=self.colon_match,
no_colon=self.no_colon,
val_color=self.val_color,
label_color=self.label_color,
suffix_color=self.suffix_color,
val_attrs=self.val_attrs,
label_attrs=self.label_attrs,
suffix_attrs=self.suffix_attrs
)
def __str__(self) -> str:
return self.text
[docs]
def cutoff_text(self, limit: int) -> str:
"""
Cuts off text if it exceeds limit. Will NOT shorten the number, only the suffix (if there is one).
If there isn't enough space for just an ellipsis, it will try to shorten the label,
so that the output will be 50/50 (label: value).
:param limit: Maximum length of text
:return: Shortened text
"""
self.text_shorten = LabeledData._shorten_number(
label=self.label,
value=self.value,
suffix=self.suffix,
text=self.text,
limit=limit,
quotes=self.quotes,
val_color=self.val_color,
label_color=self.label_color,
suffix_color=self.suffix_color,
colon_match=self.colon_match,
no_colon=self.no_colon,
leading_zeros=self.leading_zeros,
int_no_round=self.int_no_round,
round_to=self.round_to,
val_attrs=self.val_attrs,
label_attrs=self.label_attrs,
suffix_attrs=self.suffix_attrs,
)
return self.text_shorten
[docs]
class Boolean:
[docs]
def __init__(self, label: str,
value: bool,
suffix: str = '',
t_color: str | tuple[int, int, int] | None = "green",
f_color: str | tuple[int, int, int] | None = "red",
label_color: str | tuple[int, int, int] | None = None,
suffix_color: str | tuple[int, int, int] | None = None,
t_text: str = "True",
f_text: str = "False",
quotes: bool = False,
colon_match: bool = True,
no_colon: bool = False,
val_attrs: list[str] | None = None,
label_attrs: list[str] | None = None,
suffix_attrs: list[str] | None = None):
"""
Creates a string with a label and boolean value
:param label: Label for boolean value
:param value: Boolean value
:param suffix: Optional text to appear after the value
:param t_color: Color of true value.
:param f_color: Color of false value.
:param label_color: Color of label.
:param suffix_color: Color of suffix.
:param t_text: Text to display for true value
:param f_text: Text to display for false value
:param quotes: If true, wrap text in single quotes (')
:param colon_match: If true, the colon will match the style of the label.
:param no_colon: If true, the colon will be removed. Overrides ``colon_match``
:param val_attrs: Attributes to apply to the value.
:param label_attrs: Attributes to apply to the label.
:param suffix_attrs: Attributes to apply to the suffix.
"""
self.label: str = label
self.value: bool = value
self.suffix: str = suffix
self.val_color: str | tuple[int, int, int] | None = None
self.label_color: str | tuple[int, int, int] | None = None
self.suffix_color: str | tuple[int, int, int] | None = None
self.val_text: str = t_text if value else f_text
self.quotes: bool = quotes
self.text_shorten: str = ""
self.colon_match: bool = colon_match
self.no_colon: bool = no_colon
self.val_attrs: list[str] | None = None
self.label_attrs: list[str] | None = None
self.suffix_attrs: list[str] | None = None
# Validate Colors
if value and t_color:
_validate_color(color=t_color)
self.val_color = t_color
elif not value and f_color:
_validate_color(color=f_color)
self.val_color = f_color
if label_color:
_validate_color(color=label_color)
self.label_color = label_color.lower() if isinstance(label_color, str) else label_color
if suffix_color:
_validate_color(color=suffix_color)
self.suffix_color = suffix_color.lower() if isinstance(suffix_color, str) else suffix_color
# Validate Attributes
if val_attrs:
_validate_attrs(attrs=val_attrs)
self.val_attrs = val_attrs
if label_attrs:
_validate_attrs(attrs=label_attrs)
self.label_attrs = label_attrs
if suffix_attrs:
_validate_attrs(attrs=suffix_attrs)
self.suffix_attrs = suffix_attrs
# Remove redundant colon from label
if self.label[-1] == ":":
self.label = self.label[:-1]
# Format Text
self.text: str = LabeledData._format_text(label=self.label,
value=self.val_text,
suffix=self.suffix,
quotes=self.quotes,
colon_match=self.colon_match,
no_colon=self.no_colon,
val_color=self.val_color,
label_color=self.label_color,
suffix_color=self.suffix_color,
val_attrs=self.val_attrs,
label_attrs=self.label_attrs,
suffix_attrs=self.suffix_attrs,
)
def __str__(self):
return self.text
[docs]
def cutoff_text(self, limit: int) -> str:
"""
Cuts off text if it exceeds limit. Will first try to shorten bool value.
If there isn't enough space for just an ellipsis, it will try to shorten the label,
so that the output will be 50/50 (label: value).
:param limit: Maximum length of text
:return: Shortened text
"""
self.text_shorten = LabeledData._shorten_text(
label=self.label,
value=self.val_text,
suffix=self.suffix,
text=self.text,
limit=limit,
val_color=self.val_color,
label_color=self.label_color,
suffix_color=self.suffix_color,
colon_match=self.colon_match,
no_colon=self.no_colon,
val_attrs=self.val_attrs,
label_attrs=self.label_attrs,
suffix_attrs=self.suffix_attrs,
quotes=self.quotes
)
return self.text_shorten
[docs]
class Path:
[docs]
def __init__(self, label: str,
value: Path | str,
suffix: str = '',
quotes: bool = True,
val_color: str | tuple[int, int, int] | None = "cyan",
label_color: str | tuple[int, int, int] | None = None,
suffix_color: str | tuple[int, int, int] | None = None,
colon_match: bool = True,
no_colon: bool = False,
val_attrs: list[str] | None = None,
label_attrs: list[str] | None = None,
suffix_attrs: list[str] | None = None):
"""
Creates a string with a label and path to a file or directory
:param label: Label for path
:param value: Path to file or directory
:param suffix: Optional text to appear after the value
:param quotes: If true, wrap path in single quotes (')
:param val_color: Color of path value.
:param label_color: Color of label.
:param suffix_color: Color of suffix.
:param colon_match: If true, the colon will match the style of the label.
:param no_colon: If true, the colon will be removed. Overrides ``colon_match``
:param val_attrs: Attributes to apply to the value.
:param label_attrs: Attributes to apply to the label.
:param suffix_attrs: Attributes to apply to the suffix.
"""
self.label: str = label
self.quotes: bool = quotes
self.suffix: str = suffix
self.val_color: str | tuple[int, int, int] | None = None
self.label_color: str | tuple[int, int, int] | None = None
self.suffix_color: str | tuple[int, int, int] | None = None
self.colon_match: bool = colon_match
self.no_colon: bool = no_colon
self.val_attrs: list[str] | None = None
self.label_attrs: list[str] | None = None
self.suffix_attrs: list[str] | None = None
# Remove redundant quotes from path
if self.quotes and isinstance(value, str) and value[0] == "'" and value[-1] == "'":
value = value[1:-1]
self.value: Path = Path(value) if isinstance(value, str) else value
self.text_shorten: str = ""
self.value.resolve()
# Validate Color
if val_color:
_validate_color(color=val_color)
self.val_color = val_color.lower() if isinstance(val_color, str) else val_color
if label_color:
_validate_color(color=label_color)
self.label_color = label_color.lower() if isinstance(label_color, str) else label_color
if suffix_color:
_validate_color(color=suffix_color)
self.suffix_color = suffix_color.lower() if isinstance(suffix_color, str) else suffix_color
# Validate Attributes
if val_attrs:
_validate_attrs(attrs=val_attrs)
self.val_attrs = val_attrs
if label_attrs:
_validate_attrs(attrs=label_attrs)
self.label_attrs = label_attrs
if suffix_attrs:
_validate_attrs(attrs=suffix_attrs)
self.suffix_attrs = suffix_attrs
# Remove redundant colon from label
if self.label[-1] == ":":
self.label = self.label[:-1]
# Format Text
self.text: str = LabeledData._format_text(label=self.label,
value=str(self.value),
suffix=self.suffix,
quotes=self.quotes,
colon_match=self.colon_match,
no_colon=self.no_colon,
val_color=self.val_color,
label_color=self.label_color,
suffix_color=self.suffix_color,
val_attrs=self.val_attrs,
label_attrs=self.label_attrs,
suffix_attrs=self.suffix_attrs,
)
def __str__(self):
return self.text
[docs]
def cutoff_text(self, limit: int, mode: Literal["char", "word", "middle"] = "char") -> str:
"""
Shortens path to limit.
Modes:
- char: Cutoff char at end of string (/path/to/a/f…)
- word: Cutoff path at end, preserve the deepest directory/file (/path/to/…/file.txt)
- middle: Cutoff chars at middle of string (/long/path/t…/an/file.txt)
:param limit: Character limit for full labeled path string
:param mode: How to cut off path.
:return: LabeledData.Path string with shortened path if it exceeds limit, else returns original string
"""
self.text_shorten = LabeledData._shorten_path(
label=self.label,
value=str(self.value),
suffix=self.suffix,
text=self.text,
limit=limit,
quotes=self.quotes,
mode=mode,
val_color=self.val_color,
label_color=self.label_color,
suffix_color=self.suffix_color,
colon_match=self.colon_match,
no_colon=self.no_colon,
val_attrs=self.val_attrs,
label_attrs=self.label_attrs,
suffix_attrs=self.suffix_attrs,
)
return self.text_shorten
[docs]
class Date:
[docs]
def __init__(self, label: str,
value: str | datetime = '',
suffix: str = '',
strftime: str = '%Y-%m-%d %H:%M:%S',
suffix_color: str | tuple[int, int, int] | None = None,
val_color: str | tuple[int, int, int] | None = "blue",
label_color: str | tuple[int, int, int] | None = None,
quotes: bool = True,
colon_match: bool = True,
no_colon: bool = False,
val_attrs: list[str] | None = None,
label_attrs: list[str] | None = None,
suffix_attrs: list[str] | None = None):
"""
Creates a string with a label and a date value
If no value is passed, will print current time using ``strftime`` parameter
:param label: Label for string value
:param value: Value, either as a datetime object or string
:param suffix: Optional text to appear after the value
:param strftime: Formatter for datetime value
:param val_color: Color of string value.
:param label_color: Color of label.
:param quotes: If true, wrap text in single quotes (')
:param colon_match: If true, the colon will match the style of the label.
:param no_colon: If true, the colon will be removed. Overrides ``colon_match``
:param val_attrs: Attributes to apply to the value.
:param label_attrs: Attributes to apply to the label.
"""
self.label: str = label
self.value: str = value.__str__()
self.suffix: str = suffix
self.strftime: str = strftime
self.val_color: str | tuple[int, int, int] | None = None
self.label_color: str | tuple[int, int, int] | None = None
self.suffix_color: str | tuple[int, int, int] | None = None
self.val_attrs: list[str] | None = None
self.label_attrs: list[str] | None = None
self.suffix_attrs: list[str] | None = None
self.colon_match: bool = colon_match
self.no_colon: bool = no_colon
self.quotes: bool = quotes
self.text_shorten: str = ""
# Validate Colors
if val_color:
_validate_color(color=val_color)
self.val_color = val_color.lower() if isinstance(val_color, str) else val_color
if label_color:
_validate_color(color=label_color)
self.label_color = label_color.lower() if isinstance(label_color, str) else label_color
if suffix_color:
_validate_color(color=suffix_color)
self.suffix_color = suffix_color.lower() if isinstance(suffix_color, str) else suffix_color
# Validate Attributes
if val_attrs:
_validate_attrs(attrs=val_attrs)
self.val_attrs = val_attrs
if label_attrs:
_validate_attrs(attrs=label_attrs)
self.label_attrs = label_attrs
if suffix_attrs:
_validate_attrs(attrs=suffix_attrs)
self.suffix_attrs = suffix_attrs
# Remove redundant colon from label
if self.label[-1] == ":":
self.label = self.label[:-1]
# If strftime was passed, format time
if self.strftime and not self.value:
# No value, use current time
self.value = datetime.now().strftime(self.strftime)
elif self.strftime and isinstance(value, datetime):
# Format value
self.value = value.strftime(self.strftime)
# Format Text
self.text: str = LabeledData._format_text(label=self.label,
value=self.value,
suffix=self.suffix,
quotes=self.quotes,
colon_match=self.colon_match,
no_colon=self.no_colon,
val_color=self.val_color,
label_color=self.label_color,
suffix_color=self.suffix_color,
val_attrs=self.val_attrs,
label_attrs=self.label_attrs,
suffix_attrs=self.suffix_attrs
)
def __str__(self):
return self.text
[docs]
def cutoff_text(self, limit: int) -> str:
"""
Cuts off text if it exceeds limit. Will first try to shorten string.
If there isn't enough space for just an ellipsis, it will try to shorten the label,
so that the output will be 50/50 (label: value).
:param limit: Maximum length of text
:return: Shortened text
"""
self.text_shorten = LabeledData._shorten_datetime(
label=self.label,
value=self.value,
suffix=self.suffix,
text=self.text,
limit=limit,
val_color=self.val_color,
label_color=self.label_color,
suffix_color=self.suffix_color,
colon_match=self.colon_match,
no_colon=self.no_colon,
val_attrs=self.val_attrs,
label_attrs=self.label_attrs,
suffix_attrs=self.suffix_attrs,
quotes=self.quotes
)
return self.text_shorten
[docs]
class Bytes:
[docs]
def __init__(self, label: str,
value: int,
suffix: str = '',
suffix_color: str | tuple[int, int, int] | None = None,
val_color: str | tuple[int, int, int] | None = "yellow",
label_color: str | tuple[int, int, int] | None = None,
unit_color: str | tuple[int, int, int] | None = None,
quotes: bool = False,
colon_match: bool = True,
unit_match: bool = True,
no_colon: bool = False,
show_unit: bool = True,
unit_type: Literal["iec", "si"] = "iec",
val_attrs: list[str] | None = None,
label_attrs: list[str] | None = None,
suffix_attrs: list[str] | None = None,
unit_attrs: list[str] | None = None):
"""
Creates a string with a label and bytes value
:param label: Label for string value
:param value: Integer value. Value MUST be in bytes
:param suffix: Optional text to appear after the value
:param val_color: Color of string value.
:param label_color: Color of label.
:param unit_color: Color of unit. Overrides ``unit_match``.
:param quotes: If true, wrap text in single quotes (')
:param colon_match: If true, the colon will match the style of the label.
:param unit_match: If true, the unit will match the style of the value.
:param no_colon: If true, the colon will be removed. Overrides ``colon_match``
:param show_unit: If true, the unit will be shown.
:param unit_type: Unit type: IEC is base 2, SI is base 10
:param val_attrs: Attributes to apply to the value.
:param label_attrs: Attributes to apply to the label.
:param unit_attrs: Attributes to apply to the unit. Overrides ``unit_match``.
"""
self.label: str = label
self.value: int = value
self.suffix: str = suffix
self.val_color: str | tuple[int, int, int] | None = None
self.label_color: str | tuple[int, int, int] | None = None
self.suffix_color: str | tuple[int, int, int] | None = None
self.unit_color: str | tuple[int, int, int] | None = None
self.val_attrs: list[str] | None = None
self.label_attrs: list[str] | None = None
self.suffix_attrs: list[str] | None = None
self.unit_attrs: list[str] | None = None
self.colon_match: bool = colon_match
self.unit_match: bool = unit_match
self.no_colon: bool = no_colon
self.show_unit: bool = show_unit
self.unit_type: Literal["iec", "si"] = unit_type
self.quotes: bool = quotes
self.text_shorten: str = ""
self.unit: str = ''
self.byte_val: float = 0
# Validate Colors
if val_color:
_validate_color(color=val_color)
self.val_color = val_color.lower() if isinstance(val_color, str) else val_color
if label_color:
_validate_color(color=label_color)
self.label_color = label_color.lower() if isinstance(label_color, str) else label_color
if suffix_color:
_validate_color(color=suffix_color)
self.suffix_color = suffix_color.lower() if isinstance(suffix_color, str) else suffix_color
if unit_color:
_validate_color(color=unit_color)
self.unit_color = unit_color.lower() if isinstance(unit_color, str) else unit_color
# Validate Attributes
if val_attrs:
_validate_attrs(attrs=val_attrs)
self.val_attrs = val_attrs
if label_attrs:
_validate_attrs(attrs=label_attrs)
self.label_attrs = label_attrs
if suffix_attrs:
_validate_attrs(attrs=suffix_attrs)
self.suffix_attrs = suffix_attrs
if unit_attrs:
_validate_attrs(attrs=unit_attrs)
self.unit_attrs = unit_attrs
# Remove redundant colon from label
if self.label[-1] == ":":
self.label = self.label[:-1]
# Get unit
if self.show_unit:
self.byte_val, self.unit = _human_readable_bytes(value=self.value, unit_type=self.unit_type)
# Format Text
self.text: str = LabeledData._format_bytes(label=self.label,
value=self.value,
bytes_hr=self.byte_val,
unit=self.unit,
suffix=self.suffix,
quotes=self.quotes,
colon_match=self.colon_match,
unit_match=self.unit_match,
no_colon=self.no_colon,
show_unit=self.show_unit,
val_color=self.val_color,
label_color=self.label_color,
suffix_color=self.suffix_color,
unit_color=self.unit_color,
val_attrs=self.val_attrs,
label_attrs=self.label_attrs,
suffix_attrs=self.suffix_attrs,
unit_attrs=self.unit_attrs
)
def __str__(self):
return self.text
[docs]
def cutoff_text(self, limit: int) -> str:
"""
Cuts off text if it exceeds limit. Will NOT shorten the value, only suffix (if there is one).
If there isn't enough space for just an ellipsis, it will try to shorten the label,
so that the output will be 50/50 (label: value).
:param limit: Maximum length of text
:return: Shortened text
"""
self.text_shorten = LabeledData._shorten_bytes(
label=self.label,
value=self.value,
bytes_hr=self.byte_val,
unit=self.unit,
suffix=self.suffix,
text=self.text,
limit=limit,
val_color=self.val_color,
label_color=self.label_color,
suffix_color=self.suffix_color,
unit_color=self.unit_color,
colon_match=self.colon_match,
unit_match=self.unit_match,
no_colon=self.no_colon,
show_unit=self.show_unit,
val_attrs=self.val_attrs,
label_attrs=self.label_attrs,
suffix_attrs=self.suffix_attrs,
unit_attrs=self.unit_attrs,
quotes=self.quotes
)
return self.text_shorten
# Typing alias
AnyLabeledData = LabeledData.String | LabeledData.Number | LabeledData.Boolean | LabeledData.Path | LabeledData.Date \
| LabeledData.Bytes