import textwrap
from sys import stdout
from typing import Literal
from termcolor import colored as col
from framed_text.color import Color
from framed_text.labeled_data import AnyLabeledData
from framed_text.utils import (
_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 Colors
----------------
- **INFO**: ``"yellow"``
- **ACTION**: ``"blue"``
- **SUCCESS**: ``"green"``
- **FAIL**: ``"red"``
- **WARN**: ``(239, 202, 19)``
- **HIDDEN**: ``(125, 125, 125)``
"""
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 = '>',
sep: str = ' ',
overwrite: bool = False,
icon_color: Color | None = None,
msg_color: Color | None = None,
icon_attrs: list[str] | None = None,
msg_attrs: list[str] | None = None):
"""
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 sep: Separator between icon and message. Will only be used when message is provided.
:param overwrite: If true, will overwrite the previous line with a new status.
:param icon_color: Color of the icon.
:param msg_color: Color of the message.
:param icon_attrs: Attributes of the icon. Can be a list of termcolor attributes
:param msg_attrs: Attributes of the message. Can be a list of termcolor attributes
"""
self._msg: str | AnyLabeledData | None = msg
self._icon: str = icon
self._sep: str = sep
self._overwrite: bool = overwrite
self._icon_color: Color = icon_color if icon_color else Color()
self._msg_color: Color = msg_color if msg_color else Color()
self._icon_attrs: list[str] | None = _validate_attrs(attrs=icon_attrs)
self._msg_attrs: list[str] | None = _validate_attrs(attrs=msg_attrs)
self._term_width: int = _get_terminal_width()
self.text: str = self._format_text()
self.text_shorten: str = ''
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
def _format_text(self) -> str:
"""
Format the status message
:return: Status message
"""
_icon: str = ''
_sep: str = ''
_msg: str = ''
if self._icon:
_icon = _format_col_text(text=self._icon, color=self._icon_color.tc_color, attrs=self._icon_attrs)
if self._sep:
_sep = self._sep
if self._msg is not None and isinstance(self._msg, AnyLabeledData):
# Handle LabeledData separately
self._msg.cutoff_text(limit=self._term_width - len(_icon) - len(_sep))
_msg = self._msg.text_shorten
elif self._msg is not None:
_msg = _format_col_text(text=self._msg.__str__(), color=self._msg_color.tc_color,
attrs=self._msg_attrs)
if self._icon:
return f"{_icon}{_sep}{_msg}"
else:
return _msg
[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:
# Text below limit. Nothing to do
return self.text
# Remove ANSI
icon_no_ansi: str = _remove_ansi(text=self._icon)
sep_no_ansi: str = _remove_ansi(text=self._sep)
msg_no_ansi: str = _remove_ansi(text=self._msg.__str__())
ina_len: int = len(icon_no_ansi)
sna_len: int = len(sep_no_ansi)
mna_len: int = len(msg_no_ansi)
# Calculate text limit
text_limit: int = limit - (ina_len + sna_len) # Icon + sep
text_limit -= 1 # Ellipsis
if len(icon_no_ansi) <= limit // 2:
# Icon below limit.
_icon: str = icon_no_ansi
else:
# Shorten icon
_icon: str = f"{icon_no_ansi[:(limit // 2) - 1]}…"
# Add back difference
text_limit += ina_len - len(_icon)
if sna_len <= 10:
# Separator below limit.
_sep: str = sep_no_ansi
else:
# Shorten separator
_sep: str = sep_no_ansi[:10]
# Add back difference
text_limit += sna_len - len(_sep)
_msg: str = ''
if msg_no_ansi and mna_len > text_limit:
match mode.lower():
case "char":
# Shorten by char
_msg: str = f"{msg_no_ansi[:text_limit]}…"
case "word":
# Shorten by word
_msg: str = textwrap.shorten(text=msg_no_ansi, width=text_limit, placeholder='…')
case "middle":
# Shorten by middle characters
_msg: str = _truncate_text_middle(text=msg_no_ansi, limit=text_limit)
elif msg_no_ansi:
_msg: str = msg_no_ansi
# Format Text
if _icon:
_icon = _format_col_text(text=_icon, color=self._icon_color.tc_color, attrs=self._icon_attrs)
if _msg:
_msg = _format_col_text(text=_msg, color=self._msg_color.tc_color, attrs=self._msg_attrs)
if _icon:
self.text_shorten: str = f"{_icon}{_sep}{_msg}"
else:
self.text_shorten: str = str(_msg)
return self.text_shorten
[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 sep: Separator between icon and message. Will only be used when message is provided.
:param overwrite: If true, will overwrite the previous line with a new status.
:param icon_color: Color of the icon.
:param msg_color: Color of the message.
:param icon_attrs: Attributes to apply to the icon. Can be a list of termcolor attributes
:param msg_attrs: Attributes to apply to the message. Can be a list of termcolor attributes
"""
[docs]
def __init__(self, msg: str | AnyLabeledData | None = None,
icon: str = StatusIcons.INFO,
sep: str = ' ',
overwrite: bool = False,
icon_color: str | tuple[int, int, int] | Color | None = StatusColors.INFO,
msg_color: str | tuple[int, int, int] | Color | None = None,
icon_attrs: list[str] | None = None,
msg_attrs: list[str] | None = None):
super().__init__(
msg=msg,
icon=icon,
sep=sep,
overwrite=overwrite,
icon_color=Color(icon_color) if icon_color else None,
msg_color=Color(msg_color) if msg_color else None,
icon_attrs=icon_attrs,
msg_attrs=msg_attrs
)
def __str__(self):
return super().__str__()
[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 sep: Separator between icon and message. Will only be used when message is provided.
:param overwrite: If true, will overwrite the previous line with a new status.
:param icon_color: Color of the icon.
:param msg_color: Color of the message.
:param icon_attrs: Attributes to apply to the icon. Can be a list of termcolor attributes
:param msg_attrs: Attributes to apply to the message. Can be a list of termcolor attributes
"""
[docs]
def __init__(self, msg: str | AnyLabeledData | None = None,
icon: str = StatusIcons.ACTION,
sep: str = ' ',
overwrite: bool = False,
icon_color: str | tuple[int, int, int] | Color | None = StatusColors.ACTION,
msg_color: str | tuple[int, int, int] | Color | None = None,
icon_attrs: list[str] | None = None,
msg_attrs: list[str] | None = None):
super().__init__(
msg=msg,
icon=icon,
sep=sep,
overwrite=overwrite,
icon_color=Color(icon_color) if icon_color else None,
msg_color=Color(msg_color) if msg_color else None,
icon_attrs=icon_attrs,
msg_attrs=msg_attrs
)
def __str__(self):
return super().__str__()
[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 sep: Separator between icon and message. Will only be used when message is provided.
:param overwrite: If true, will overwrite the previous line with a new status.
:param icon_color: Color of the icon.
:param msg_color: Color of the message.
:param icon_attrs: Attributes to apply to the icon. Can be a list of termcolor attributes
:param msg_attrs: Attributes to apply to the message. Can be a list of termcolor attributes
"""
[docs]
def __init__(self, msg: str | AnyLabeledData | None = None,
icon: str = StatusIcons.SUCCESS,
sep: str = ' ',
overwrite: bool = False,
icon_color: str | tuple[int, int, int] | Color | None = StatusColors.SUCCESS,
msg_color: str | tuple[int, int, int] | Color | None = StatusColors.SUCCESS,
icon_attrs: list[str] | None = None,
msg_attrs: list[str] | None = None):
super().__init__(
msg=msg,
icon=icon,
sep=sep,
overwrite=overwrite,
icon_color=Color(icon_color) if icon_color else None,
msg_color=Color(msg_color) if msg_color else None,
icon_attrs=icon_attrs,
msg_attrs=msg_attrs
)
def __str__(self):
return super().__str__()
[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 sep: Separator between icon and message. Will only be used when message is provided.
:param overwrite: If true, will overwrite the previous line with a new status.
:param icon_color: Color of the icon.
:param msg_color: Color of the message.
:param icon_attrs: Attributes to apply to the icon. Can be a list of termcolor attributes
:param msg_attrs: Attributes to apply to the message. Can be a list of termcolor attributes
"""
[docs]
def __init__(self, msg: str | AnyLabeledData | None = None,
icon: str = StatusIcons.FAIL,
sep: str = ' ',
overwrite: bool = False,
icon_color: str | tuple[int, int, int] | Color | None = StatusColors.FAIL,
msg_color: str | tuple[int, int, int] | Color | None = StatusColors.FAIL,
icon_attrs: list[str] | None = None,
msg_attrs: list[str] | None = None):
super().__init__(
msg=msg,
icon=icon,
sep=sep,
overwrite=overwrite,
icon_color=Color(icon_color) if icon_color else None,
msg_color=Color(msg_color) if msg_color else None,
icon_attrs=icon_attrs,
msg_attrs=msg_attrs
)
def __str__(self):
return super().__str__()
[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 sep: Separator between icon and message. Will only be used when message is provided.
:param overwrite: If true, will overwrite the previous line with a new status.
:param icon_color: Color of the icon.
:param msg_color: Color of the message.
:param icon_attrs: Attributes to apply to the icon. Can be a list of termcolor attributes
:param msg_attrs: Attributes to apply to the message. Can be a list of termcolor attributes
"""
[docs]
def __init__(self, msg: str | AnyLabeledData | None = None,
icon: str = StatusIcons.WARN,
sep: str = '',
overwrite: bool = False,
icon_color: str | tuple[int, int, int] | Color | None = StatusColors.WARN,
msg_color: str | tuple[int, int, int] | Color | None = "yellow",
icon_attrs: list[str] | None = None,
msg_attrs: list[str] | None = None):
super().__init__(
msg=msg,
icon=icon,
sep=sep,
overwrite=overwrite,
icon_color=Color(icon_color) if icon_color else None,
msg_color=Color(msg_color) if msg_color else None,
icon_attrs=icon_attrs,
msg_attrs=msg_attrs
)
def __str__(self):
return super().__str__()
[docs]
class Hidden(BaseStatus):
"""
:param msg: Message to display after the icon
:param icon: Character(s) to use as the icon. Icon can be any size.
:param sep: Separator between icon and message. Will only be used when message is provided.
:param overwrite: If true, will overwrite the previous line with a new status.
:param icon_color: Color of the icon.
:param msg_color: Color of the message.
:param icon_attrs: Attributes to apply to the icon. Can be a list of termcolor attributes
:param msg_attrs: Attributes to apply to the message. Can be a list of termcolor attributes
"""
[docs]
def __init__(self, msg: str | AnyLabeledData | None = None,
icon: str = StatusIcons.HIDDEN,
sep: str = ' ',
overwrite: bool = False,
icon_color: str | tuple[int, int, int] | Color | None = StatusColors.HIDDEN,
msg_color: str | tuple[int, int, int] | Color | None = StatusColors.HIDDEN,
icon_attrs: list[str] | None = None,
msg_attrs: list[str] | None = None):
super().__init__(
msg=msg,
icon=icon,
sep=sep,
overwrite=overwrite,
icon_color=Color(icon_color) if icon_color else None,
msg_color=Color(msg_color) if msg_color else None,
icon_attrs=icon_attrs,
msg_attrs=msg_attrs
)
def __str__(self):
return super().__str__()
# Typing Alias
AnyStatus = Status.Info | Status.Action | Status.Success | Status.Fail | Status.Warn | Status.Hidden