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 | None = None,
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 mode: Mode to use to shorten text. See above for more info.
"""
self._text: str | AnyStatus | AnyLabeledData = text
self._limit: int = limit if limit and limit > 0 else _get_terminal_width()
self._mode: Literal["char", "word", "middle"] = mode
self._text_no_ansi: str = ''
self._ph_char: str = '…' # Placeholder character
self.text_shorten: str = ''
if self._limit <= 0:
# Error in case limit is ever below 1 (Likely won't happen)
raise ValueError("Limit must be a positive integer.")
if not isinstance(self._text, (str, AnyStatus, AnyLabeledData)):
raise ValueError("Text must be a string, Status, or LabeledData.")
self._text_no_ansi = _remove_ansi(text=self._text.__str__()).rstrip()
if isinstance(self._text, str):
# Handle regular strings
self.text_shorten = self._handle_str(text=self._text_no_ansi)
else:
# Handle special framed-text types
self.text_shorten = self._handle_ft(text=self._text)
def __str__(self) -> str:
return self.text_shorten
def _handle_str(self, text: str) -> str:
"""
Handle string logic
:return: Shortened text
"""
if len(text) > self._limit:
match self._mode.lower():
case "char":
# Shorten by char
return f"{text[:self._limit - 1]}{self._ph_char}"
case "word":
# Shorten by word
return textwrap.shorten(text=text, width=self._limit, placeholder=self._ph_char)
case "middle":
# Shorten by middle characters
return _truncate_text_middle(text=text, limit=self._limit)
else:
return text
def _handle_ft(self, text: AnyStatus | AnyLabeledData) -> str:
"""
Handle framed-text specific logic.
:return: Shortened text
"""
if isinstance(text, AnyStatus):
# Status object
if len(self._text_no_ansi) > self._limit:
text.cutoff_text(limit=self._limit, mode=self._mode)
return text.text_shorten
else:
return text.text
else:
# LabeledData object
if len(self._text_no_ansi) > self._limit:
if isinstance(text, (
LabeledData.String,
LabeledData.Path
)):
# These types use mode
text.cutoff_text(limit=self._limit, mode=self._mode)
return text.text_shorten
else:
# These types don't use mode
text.cutoff_text(limit=self._limit)
return text.text_shorten
else:
# Text does not need to be shortened
return text.text