235 lines
6.4 KiB
Python
235 lines
6.4 KiB
Python
|
|
'''
|
||
|
|
=== ANSI Escape Sequences ===
|
||
|
|
This file exists to organise and name
|
||
|
|
all important ANSI escape sequences
|
||
|
|
for management of the CLI.
|
||
|
|
'''
|
||
|
|
|
||
|
|
from sys import stdout
|
||
|
|
from enum import StrEnum
|
||
|
|
|
||
|
|
# Removes ALL set colors/styling
|
||
|
|
RESET = '\x1b[0m'
|
||
|
|
|
||
|
|
'''
|
||
|
|
Implements text foreground coloring.
|
||
|
|
'''
|
||
|
|
class Color(StrEnum):
|
||
|
|
BLACK = '\x1b[30m'
|
||
|
|
RED = '\x1b[31m'
|
||
|
|
GREEN = '\x1b[32m'
|
||
|
|
YELLOW = '\x1b[33m'
|
||
|
|
BLUE = '\x1b[34m'
|
||
|
|
MAGENTA = '\x1b[35m'
|
||
|
|
CYAN = '\x1b[36m'
|
||
|
|
WHITE = '\x1b[37m'
|
||
|
|
|
||
|
|
'''
|
||
|
|
Handles the application of a Color to a given text.
|
||
|
|
Unset the `temp` flag to leave this style on (after the given text)
|
||
|
|
'''
|
||
|
|
@staticmethod
|
||
|
|
def apply(color: 'Color', text: str, temp: bool = True) -> str:
|
||
|
|
return color + text + Color.WHITE if temp else ''
|
||
|
|
|
||
|
|
'''
|
||
|
|
Implements text effects.
|
||
|
|
'''
|
||
|
|
class Effect(StrEnum):
|
||
|
|
BOLD = '\x1b[1m'
|
||
|
|
DIM = '\x1b[2m'
|
||
|
|
ITALICS = '\x1b[3m'
|
||
|
|
UNDERLINE = '\x1b[4m'
|
||
|
|
BLINK = '\x1b[5m'
|
||
|
|
REVERSE = '\x1b[7m'
|
||
|
|
HIDE = '\x1b[8m'
|
||
|
|
|
||
|
|
'''
|
||
|
|
Returns the opposite ESC code to a given Effect.
|
||
|
|
ie the opposite of BOLD '\x1b[1m]' is '\x1b[21m'
|
||
|
|
'''
|
||
|
|
@staticmethod
|
||
|
|
def _inverse(effect: 'Effect') -> str:
|
||
|
|
return f'{effect[:2]}2{effect[2:]}'
|
||
|
|
|
||
|
|
'''
|
||
|
|
Handles the application of a Effect to a given text.
|
||
|
|
Unset the `temp` flag to leave this style on (after the given text)
|
||
|
|
'''
|
||
|
|
@staticmethod
|
||
|
|
def apply(effect: 'Effect', text: str, temp: bool = True) -> str:
|
||
|
|
return effect + text + Effect._inverse(effect) if temp else ''
|
||
|
|
|
||
|
|
'''
|
||
|
|
Applies styling (color/effects/etc) to a given string
|
||
|
|
Unset the `temp` flag to leave this style on (after the given text)
|
||
|
|
'''
|
||
|
|
def style(text: str, *args: Color | Effect, temp: bool = True) -> str:
|
||
|
|
# unsures we don't add redundant "set white" commands
|
||
|
|
unset_color = False
|
||
|
|
for arg in args:
|
||
|
|
unset = temp
|
||
|
|
if isinstance(arg, Color):
|
||
|
|
unset_color |= temp
|
||
|
|
unset = False
|
||
|
|
text = type(arg).apply(arg, text, temp=True)
|
||
|
|
if unset_color:
|
||
|
|
text += Color.WHITE
|
||
|
|
return text
|
||
|
|
|
||
|
|
|
||
|
|
'''
|
||
|
|
Implements cursor movement functionality.
|
||
|
|
NOTE:
|
||
|
|
The Cursor class currently has no ability
|
||
|
|
to handle EXACT line (row) numbers. Currently
|
||
|
|
I'm assuming all functionality of noether can
|
||
|
|
be implemented solely via relative movements.
|
||
|
|
'''
|
||
|
|
class Cursor(StrEnum):
|
||
|
|
# SAVE current / RESTORE last saved cursor position
|
||
|
|
_SAVE = f'\x1b[7'
|
||
|
|
_RESTORE = f'\x1b[8'
|
||
|
|
|
||
|
|
_MV_UP = 'A'
|
||
|
|
_MV_DOWN = 'B'
|
||
|
|
_MV_LEFT = 'C'
|
||
|
|
_MV_RIGHT = 'D'
|
||
|
|
# NEXT/PREV are the same as DOWN/UP (respectively)
|
||
|
|
# except that the cursor will reset to column 0
|
||
|
|
_MV_NEXT = 'E'
|
||
|
|
_MV_PREV = 'F'
|
||
|
|
# move cursor to the start of the current line
|
||
|
|
_MV_START = '\r' # there is no ESC CSI for carriage return
|
||
|
|
|
||
|
|
_MV_COLUMN = 'G'
|
||
|
|
|
||
|
|
'''
|
||
|
|
Generates an ESC code sequence corresponding
|
||
|
|
to a horizontal movement relative to the
|
||
|
|
cursor's current vertical position.
|
||
|
|
+ive = right
|
||
|
|
-ive = left
|
||
|
|
'''
|
||
|
|
@staticmethod
|
||
|
|
def _XMOVE_REL(n: int) -> str:
|
||
|
|
if n > 0:
|
||
|
|
return f'\x1b[{n}{Cursor._MV_RIGHT}'
|
||
|
|
if n < 0:
|
||
|
|
return f'\x1b[{n}{Cursor._MV_LEFT}'
|
||
|
|
return ''
|
||
|
|
|
||
|
|
'''
|
||
|
|
Generates an ESC code sequence corresponding
|
||
|
|
to a horizontal movement to an EXACT column.
|
||
|
|
'''
|
||
|
|
@staticmethod
|
||
|
|
def _XMOVE(n: int) -> str:
|
||
|
|
return f'\x1b[{n}{Cursor._MV_COLUMN}'
|
||
|
|
|
||
|
|
'''
|
||
|
|
Generates an ESC code sequence corresponding
|
||
|
|
to a vertical movement relative to the
|
||
|
|
cursor's current vertical position.
|
||
|
|
+ive = down
|
||
|
|
-ive = up
|
||
|
|
'''
|
||
|
|
@staticmethod
|
||
|
|
def _YMOVE_REL(n: int, reset: bool = False) -> str:
|
||
|
|
if n > 0:
|
||
|
|
if reset:
|
||
|
|
return f'\x1b[{n}{Cursor._MV_NEXT}'
|
||
|
|
return f'\x1b[{n}{Cursor._MV_DOWN}'
|
||
|
|
if n < 0:
|
||
|
|
if reset:
|
||
|
|
return f'\x1b[{n}{Cursor._MV_PREV}'
|
||
|
|
return f'\x1b[{n}{Cursor._MV_UP}'
|
||
|
|
return ''
|
||
|
|
|
||
|
|
'''
|
||
|
|
Sets the cursor column (horizontal) position to an exact value.
|
||
|
|
NOTE: does NOT flush stdout buffer
|
||
|
|
'''
|
||
|
|
@staticmethod
|
||
|
|
def set_x(n: int) -> None:
|
||
|
|
stdout.write(Cursor.XMOVE(n))
|
||
|
|
|
||
|
|
'''
|
||
|
|
Moves the cursor left/right n columns (relative).
|
||
|
|
NOTE: does NOT flush stdout buffer
|
||
|
|
'''
|
||
|
|
@staticmethod
|
||
|
|
def move_x(n: int) -> None:
|
||
|
|
stdout.write(Cursor._XMOVE_REL(n))
|
||
|
|
|
||
|
|
'''
|
||
|
|
Moves the cursor up/down n rows and resets the
|
||
|
|
cursor to be at the start of the line
|
||
|
|
NOTE: does NOT flush stdout buffer
|
||
|
|
'''
|
||
|
|
@staticmethod
|
||
|
|
def move_y(n: int, reset: bool = True) -> None:
|
||
|
|
stdout.write(Cursor._YMOVE_REL(n, reset=reset))
|
||
|
|
|
||
|
|
'''
|
||
|
|
Saves the current cursor position.
|
||
|
|
NOTE: does NOT flush stdout buffer
|
||
|
|
'''
|
||
|
|
@staticmethod
|
||
|
|
def save() -> None:
|
||
|
|
stdout.write(Cursor._SAVE)
|
||
|
|
|
||
|
|
'''
|
||
|
|
Restores the cursor position to a saved position.
|
||
|
|
NOTE: does NOT flush stdout buffer
|
||
|
|
'''
|
||
|
|
@staticmethod
|
||
|
|
def restore() -> None:
|
||
|
|
stdout.write(Cursor._RESTORE)
|
||
|
|
|
||
|
|
'''
|
||
|
|
Handles erasing content displayed on the screen.
|
||
|
|
NOTE that the cursor position is NOT updated
|
||
|
|
via these sequences. The \r code should be given after.
|
||
|
|
'''
|
||
|
|
class Erase(StrEnum):
|
||
|
|
# erase everything from the current cursor
|
||
|
|
# position to the START/END of the screen
|
||
|
|
_SCR_AFTER = '\x1b[0J'
|
||
|
|
_SCR_BEFORE = '\x1b[1J'
|
||
|
|
_SCR_ALL = '\x1b[3J' # erase screen and delete all saved cursors
|
||
|
|
|
||
|
|
# erase everything from the current cursor
|
||
|
|
# position to the START/END of the current line
|
||
|
|
_LINE_AFTER = '\x1b[0K'
|
||
|
|
_LINE_BEFORE = '\x1b[1K'
|
||
|
|
_LINE_ALL = '\x1b[2K'
|
||
|
|
|
||
|
|
'''
|
||
|
|
Erase characters on the entire screen.
|
||
|
|
Set `before` flag to only erase before the cursor,
|
||
|
|
set `after` flag to only erase after the cursor.
|
||
|
|
'''
|
||
|
|
@staticmethod
|
||
|
|
def screen(before: bool = False, after: bool = False) -> None:
|
||
|
|
if before:
|
||
|
|
stdout.write(Erase._SCR_BEFORE)
|
||
|
|
elif after:
|
||
|
|
stdout.write(Erase._SCR_AFTER)
|
||
|
|
else:
|
||
|
|
stdout.write(Erase._SCR_ALL)
|
||
|
|
|
||
|
|
'''
|
||
|
|
Erase characters on the current line.
|
||
|
|
Set `before` flag to only erase before the cursor,
|
||
|
|
set `after` flag to only erase after the cursor.
|
||
|
|
'''
|
||
|
|
@staticmethod
|
||
|
|
def line(before: bool = False, after: bool = False) -> None:
|
||
|
|
if before:
|
||
|
|
stdout.write(Erase._LINE_BEFORE)
|
||
|
|
elif after:
|
||
|
|
stdout.write(Erase._LINE_AFTER)
|
||
|
|
else:
|
||
|
|
stdout.write(Erase._LINE_ALL)
|