import textwrap
from sys import stdout
from typing import Literal
from termcolor import colored as col
from framed_text.labeled_data import AnyLabeledData
from framed_text.utils import (
_validate_color,
_remove_ansi,
_validate_attrs,
_format_col_text,
_truncate_text_middle,
ANSIEscape,
_ansi_pipe_clean,
_get_terminal_width
)
[docs]
class StatusColors:
"""
A collection of the default colors used by the Status classes. Pre-formatted
Available Icons
---------------
- **INFO**: ``'?'``
- **ACTION**: ``'➤'``
- **SUCCESS**: ``'✔'``
- **FAIL**: ``'✗'``
- **WARN**: ``'⚠️'``
- **HIDDEN**: ``'➤'``
"""
INFO: str = "yellow"
ACTION: str = "blue"
SUCCESS: str = "green"
FAIL: str = "red"
WARN: tuple[int, int, int] = (239, 202, 19)
HIDDEN: tuple[int, int, int] = (125, 125, 125)
[docs]
class StatusIcons:
"""
A collection of the default icons used by the Status classes. Pre-formatted
Available Icons
---------------
- **INFO**: ``'?'``
- **ACTION**: ``'➤'``
- **SUCCESS**: ``'✔'``
- **FAIL**: ``'✗'``
- **WARN**: ``'⚠️'``
- **HIDDEN**: ``'➤'``
"""
INFO: str = _ansi_pipe_clean(text=col('?', StatusColors.INFO))
ACTION: str = _ansi_pipe_clean(text=col('➤', StatusColors.ACTION))
SUCCESS: str = _ansi_pipe_clean(text=col('✔', StatusColors.SUCCESS))
FAIL: str = _ansi_pipe_clean(text=col('✗', StatusColors.FAIL))
WARN: str = _ansi_pipe_clean(text=col('⚠️', StatusColors.WARN))
HIDDEN: str = _ansi_pipe_clean(text=col('➤', StatusColors.HIDDEN))
[docs]
class BaseStatus:
[docs]
def __init__(self, msg: str | AnyLabeledData | None = None, icon: str = '>',
icon_color: str | tuple[int, int, int] | None = None,
icon_attrs: list[str] | None = None, sep: str = ' ',
msg_color: str | tuple[int, int, int] | None = None,
msg_attrs: list[str] | None = None,
overwrite: bool = False):
"""
Status base class. Can be used to create custom status classes.
Predefined Status classes are available in the Status class.
:param msg: Message to display after the icon
:param icon: Character(s) to use as the icon. Icon can be any size. If no icon, seperator is not used.
:param icon_color: Color of the icon. Can be a termcolor color code or an RGB tuple
:param icon_attrs: Attributes of the icon. Can be a list of termcolor attributes
:param sep: Seperator between icon and message. Will only be used when message is provided.
:param msg_color: Color of the message. Can be a termcolor color code or an RGB tuple
:param msg_attrs: Attributes of the message. Can be a list of termcolor attributes
:param overwrite: If true, will overwrite the previous line with a new status.
"""
# Misc variables
self._overwrite: bool = overwrite
# Icon
self.icon: str = icon
self.icon_color: str | tuple[int, int, int] | None = None
self.icon_attrs: list[str] | None = None
if icon_color:
_validate_color(color=icon_color)
self.icon_color = icon_color
if icon_attrs:
_validate_attrs(attrs=icon_attrs)
self.icon_attrs = icon_attrs
# Message
self.seperator: str = sep
self.message: str | AnyLabeledData | None = msg
self.text_color: str | tuple[int, int, int] | None = None
self.text_attrs: list[str] | None = None
self.text_shorten: str = ''
if msg_color:
_validate_color(color=msg_color)
self.text_color = msg_color
if msg_attrs:
_validate_attrs(attrs=msg_attrs)
self.text_attrs = msg_attrs
self._term_width: int = _get_terminal_width()
# --- Setup text ---
# Icon
if self.icon:
icon: str = _format_col_text(text=self.icon, color=self.icon_color, attrs=self.icon_attrs)
else:
icon: str = ''
# Seperator
if self.seperator:
seperator: str = self.seperator
else:
seperator: str = ''
# Message
if self.message and isinstance(self.message, AnyLabeledData):
# Handle LabeledData separately
self.message.cutoff_text(limit=self._term_width - len(icon) - len(seperator))
message: str = self.message.text_shorten
elif self.message:
message: str = _format_col_text(text=self.message.__str__(), color=self.text_color, attrs=self.text_attrs)
else:
message: str = ''
if self.icon:
self.text: str = f"{icon}{seperator}{message}"
else:
self.text: str = str(message)
def __str__(self):
if self._overwrite:
# Move up and delete previous line
stdout.write(ANSIEscape.move_up())
stdout.write(ANSIEscape.ERASE_LINE)
return self.text
[docs]
def cutoff_text(self, limit: int, mode: Literal["char", "word", "middle"] = "char") -> str:
"""
Cutoffs text if it exceeds limit. Icon and seperator will always be shortened by character.
**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 limit: Maximum length of text
:param mode: Mode to use for shortening. See above for info.
:return: Cutoff text
"""
if len(self.text) <= limit:
return self.text
# Calculate text limit
text_limit: int = limit - len(self.icon)
text_limit -= len(self.seperator)
text_limit -= 1 # Ellipsis
icon_no_ansi: str = _remove_ansi(text=self.icon)
msg_no_ansi: str = _remove_ansi(text=self.message.__str__())
if len(icon_no_ansi) > limit // 2:
# Shorten icon
icon: str = icon_no_ansi[:(limit // 2) - 1] + '…'
# Add back difference
text_limit += len(icon_no_ansi) - len(icon)
else:
icon: str = icon_no_ansi
if len(self.seperator) > 10:
# Shorten seperator
seperator: str = self.seperator[:10]
# Add back difference
text_limit += len(self.seperator) - len(seperator)
else:
seperator: str = self.seperator
if self.message and len(msg_no_ansi) > text_limit:
match mode.lower():
case "char":
# Shorten by char
msg_short: str = f"{msg_no_ansi[:text_limit]}…"
case "word":
# Shorten by word
msg_short: str = textwrap.shorten(text=msg_no_ansi, width=text_limit, placeholder='…')
case "middle":
# Shorten by middle characters
msg_short: str = _truncate_text_middle(text=msg_no_ansi, limit=text_limit)
elif self.message:
msg_short: str = msg_no_ansi
else:
msg_short: str = ''
# Format Text
if self.icon:
icon = _format_col_text(text=icon, color=self.icon_color, attrs=self.icon_attrs)
if self.message:
msg_short = _format_col_text(text=msg_short, color=self.text_color, attrs=self.text_attrs)
if self.icon:
self.text_shorten: str = f"{icon}{seperator}{msg_short}"
else:
self.text_shorten: str = str(msg_short)
return self.text_shorten
def _shorten_icon(self, limit: int) -> str:
"""
Shorten icon to fit limit
:param limit: Maximum length of icon
:return: Shortened icon
"""
if len(self.icon) > limit:
self.icon_short = self.icon[:limit - 1] + '…'
return self.icon_short
def _shorten_message(self, limit: int, mode: Literal["char", "word"] = "char") -> str:
"""
Shorten message to fit limit
:param limit: Maximum length of message
:param mode: Mode to use for shortening. "char" for character, "word" for word
:return: Shortened message
"""
if len(self.message.__str__()) > limit:
match mode.lower():
case "char":
# Shorten by Character
self.message_short = self.message[:limit - 1] + '…'
case "word":
# Shorten by Word
msg_no_ansi: str = _remove_ansi(text=self.message.__str__())
self.message_short = textwrap.shorten(text=msg_no_ansi, width=limit, placeholder='…')
case _:
raise ValueError(f"Invalid mode: '{mode}'")
return self.message_short
[docs]
class Status:
"""
Predefined Status classes. Can be used with LabeledData and regular strings.
Each Status optionally takes a message and text color which creates a string in the format: `<icon> <message>`
Custom Status classes can be created from the `BaseStatus` class.
"""
# Status Subclasses
[docs]
class Info(BaseStatus):
"""
:param msg: Message to display after the icon
:param icon: Character(s) to use as the icon. Icon can be any size.
:param icon_color: Color of the icon. Can be a termcolor color code or an RGB tuple
:param icon_attrs: Attributes to apply to the icon. Can be a list of termcolor attributes
:param sep: Seperator between icon and message. Will only be used when message is provided.
:param msg_color: Color of the message. Can be a termcolor color code or an RGB tuple
:param msg_attrs: Attributes to apply to the message. Can be a list of termcolor attributes
:param overwrite: If true, will overwrite the previous line with a new status.
"""
[docs]
def __init__(self, msg: str | AnyLabeledData | None = None, icon: str = '?',
icon_color: str | tuple[int, int, int] = StatusColors.INFO,
icon_attrs: list[str] | None = None, sep: str = ' ',
msg_color: str | tuple[int, int, int] | None = None,
msg_attrs: list[str] | None = None,
overwrite: bool = False):
super().__init__(msg=msg, icon=icon, icon_color=icon_color, icon_attrs=icon_attrs, sep=sep,
msg_color=msg_color, msg_attrs=msg_attrs, overwrite=overwrite)
[docs]
class Action(BaseStatus):
"""
:param msg: Message to display after the icon
:param icon: Character(s) to use as the icon. Icon can be any size.
:param icon_color: Color of the icon. Can be a termcolor color code or an RGB tuple
:param icon_attrs: Attributes to apply to the icon. Can be a list of termcolor attributes
:param sep: Seperator between icon and message. Will only be used when message is provided.
:param msg_color: Color of the message. Can be a termcolor color code or an RGB tuple
:param msg_attrs: Attributes to apply to the message. Can be a list of termcolor attributes
:param overwrite: If true, will overwrite the previous line with a new status.
"""
[docs]
def __init__(self, msg: str | AnyLabeledData | None = None, icon: str = '➤',
icon_color: str | tuple[int, int, int] = StatusColors.ACTION,
icon_attrs: list[str] | None = None, sep: str = ' ',
msg_color: str | tuple[int, int, int] | None = None,
msg_attrs: list[str] | None = None,
overwrite: bool = False):
super().__init__(msg=msg, icon=icon, icon_color=icon_color, icon_attrs=icon_attrs, sep=sep,
msg_color=msg_color, msg_attrs=msg_attrs, overwrite=overwrite)
[docs]
class Success(BaseStatus):
"""
:param msg: Message to display after the icon
:param icon: Character(s) to use as the icon. Icon can be any size.
:param icon_color: Color of the icon. Can be a termcolor color code or an RGB tuple
:param icon_attrs: Attributes to apply to the icon. Can be a list of termcolor attributes
:param sep: Seperator between icon and message. Will only be used when message is provided.
:param msg_color: Color of the message. Can be a termcolor color code or an RGB tuple
:param msg_attrs: Attributes to apply to the message. Can be a list of termcolor attributes
:param overwrite: If true, will overwrite the previous line with a new status.
"""
[docs]
def __init__(self, msg: str | AnyLabeledData | None = None, icon: str = '✔',
icon_color: str | tuple[int, int, int] = StatusColors.SUCCESS,
icon_attrs: list[str] | None = None, sep: str = ' ',
msg_color: str | tuple[int, int, int] | None = StatusColors.SUCCESS,
msg_attrs: list[str] | None = None,
overwrite: bool = False):
super().__init__(msg=msg, icon=icon, icon_color=icon_color, icon_attrs=icon_attrs, sep=sep,
msg_color=msg_color, msg_attrs=msg_attrs, overwrite=overwrite)
[docs]
class Fail(BaseStatus):
"""
:param msg: Message to display after the icon
:param icon: Character(s) to use as the icon. Icon can be any size.
:param icon_color: Color of the icon. Can be a termcolor color code or an RGB tuple
:param icon_attrs: Attributes to apply to the icon. Can be a list of termcolor attributes
:param sep: Seperator between icon and message. Will only be used when message is provided.
:param msg_color: Color of the message. Can be a termcolor color code or an RGB tuple
:param msg_attrs: Attributes to apply to the message. Can be a list of termcolor attributes
:param overwrite: If true, will overwrite the previous line with a new status.
"""
[docs]
def __init__(self, msg: str | AnyLabeledData | None = None, icon: str = '✗',
icon_color: str | tuple[int, int, int] = StatusColors.FAIL,
icon_attrs: list[str] | None = None, sep: str = ' ',
msg_color: str | tuple[int, int, int] | None = StatusColors.FAIL,
msg_attrs: list[str] | None = None,
overwrite: bool = False):
super().__init__(msg=msg, icon=icon, icon_color=icon_color, icon_attrs=icon_attrs, sep=sep,
msg_color=msg_color, msg_attrs=msg_attrs, overwrite=overwrite)
[docs]
class Warn(BaseStatus):
"""
:param msg: Message to display after the icon
:param icon: Character(s) to use as the icon. Icon can be any size.
:param icon_color: Color of the icon. Can be a termcolor color code or an RGB tuple
:param icon_attrs: Attributes to apply to the icon. Can be a list of termcolor attributes
:param sep: Seperator between icon and message. Will only be used when message is provided.
:param msg_color: Color of the message. Can be a termcolor color code or an RGB tuple
:param msg_attrs: Attributes to apply to the message. Can be a list of termcolor attributes
:param overwrite: If true, will overwrite the previous line with a new status.
"""
[docs]
def __init__(self, msg: str | AnyLabeledData | None = None, icon: str = '⚠️',
icon_color: str | tuple[int, int, int] = StatusColors.WARN,
icon_attrs: list[str] | None = None, sep: str = '',
msg_color: str | tuple[int, int, int] | None = "yellow",
msg_attrs: list[str] | None = None,
overwrite: bool = False):
super().__init__(msg=msg, icon=icon, icon_color=icon_color, icon_attrs=icon_attrs, sep=sep,
msg_color=msg_color, msg_attrs=msg_attrs, overwrite=overwrite)
[docs]
class Hidden(BaseStatus):
"""
A special status which shows the text in a dark color. An alternative to ``Status.Action``
:param msg: Message to display after the icon
:param icon: Character(s) to use as the icon. Icon can be any size.
:param icon_color: Color of the icon. Can be a termcolor color code or an RGB tuple
:param icon_attrs: Attributes to apply to the icon. Can be a list of termcolor attributes
:param sep: Seperator between icon and message. Will only be used when message is provided.
:param msg_color: Color of the message. Can be a termcolor color code or an RGB tuple
:param msg_attrs: Attributes to apply to the message. Can be a list of termcolor attributes
:param overwrite: If true, will overwrite the previous line with a new status.
"""
[docs]
def __init__(self, msg: str | AnyLabeledData | None = None, icon: str = '➤',
icon_color: str | tuple[int, int, int] = StatusColors.HIDDEN,
icon_attrs: list[str] | None = None, sep: str = ' ',
msg_color: str | tuple[int, int, int] | None = StatusColors.HIDDEN,
msg_attrs: list[str] | None = None,
overwrite: bool = False):
super().__init__(msg=msg, icon=icon, icon_color=icon_color, icon_attrs=icon_attrs, sep=sep,
msg_color=msg_color, msg_attrs=msg_attrs, overwrite=overwrite)
# Typing Alias
AnyStatus = Status.Info | Status.Action | Status.Success | Status.Fail | Status.Warn | Status.Hidden