import textwrap
from typing import Literal
from framed_text.labeled_data import AnyLabeledData, LabeledData
from framed_text.status import AnyStatus
from framed_text.utils import (
_remove_ansi,
_truncate_text_middle,
_get_terminal_width,
)
[docs]
class ShortenText:
[docs]
def __init__(self, text: str | AnyStatus | AnyLabeledData,
limit: int = 0,
allow_ansi: bool = False,
mode: Literal["char", "word", "middle"] = "char"):
"""
Shorten text to a specified limit. Supports ``LabeledData``, ``Status`` and regular strings.
**Modes**
- ``char``: Cutoff char at end of text (The quick brown fox jumped ove…)
- ``word``: Cutoff word at end of text (The quick brown fox jumped…)
- ``middle``: Cutoff chars at middle of text (The quick brown…the lazy dog)
:param text: Text to shorten
:param limit: Limit. Text will be shortened to this length. If no limit is passed, will default to the width of the terminal.
:param allow_ansi: If true, allow ANSI codes in the text. This parameter only applies to regular strings.
:param mode: Mode to use to shorten text. See above for more info.
"""
self.text: str | AnyStatus | AnyLabeledData = text
self._limit: int = limit if limit > 0 else _get_terminal_width()
self._allow_ansi: bool = allow_ansi
self._mode: Literal["char", "word", "middle"] = mode
self.text_shorten: str = ''
self._text_no_ansi: str = ''
if self._limit < 0:
raise ValueError("Limit must be a positive integer.")
# Handle regular strings
if isinstance(self.text, str):
self._text_no_ansi = _remove_ansi(text=self.text.rstrip())
if len(self._text_no_ansi) > self._limit:
text: str = self.text if self._allow_ansi else self._text_no_ansi
match self._mode.lower():
case "char":
# Shorten by char
self.text_shorten = f"{text[:self._limit - 1]}…"
case "word":
# Shorten by word
self.text_shorten = textwrap.shorten(text=text, width=self._limit, placeholder="…")
case "middle":
# Shorten by middle characters
self.text_shorten = _truncate_text_middle(text=text, limit=self._limit)
else:
self.text_shorten = self.text
else:
# Handle special framed-text types
self._text_no_ansi = _remove_ansi(text=self.text.__str__().rstrip())
if isinstance(self.text, AnyStatus):
# Status object
if len(self._text_no_ansi) > self._limit:
self.text.cutoff_text(limit=self._limit, mode=self._mode)
self.text_shorten = self.text.text_shorten
else:
self.text_shorten = self.text.text
elif isinstance(self.text, AnyLabeledData):
# LabeledData object
if len(self._text_no_ansi) > self._limit:
if (isinstance(self.text, LabeledData.String) or
isinstance(self.text, LabeledData.Path)):
# These types use mode
self.text.cutoff_text(limit=self._limit, mode=self._mode)
self.text_shorten = self.text.text_shorten
elif isinstance(self.text, AnyLabeledData):
# These types don't use mode
self.text.cutoff_text(limit=self._limit)
self.text_shorten = self.text.text_shorten
else:
# Text does not need to be shortened
self.text_shorten = self.text.text
else:
# Not a valid type
raise ValueError("Text must be a string, Status, or LabeledData.")
def __str__(self) -> str:
return self.text_shorten