Skip to content

Commit

Permalink
Allow incoming string to contain ANSI graphic directives and have it …
Browse files Browse the repository at this point in the history
…parsed into the internal formats dictionary
  • Loading branch information
James Smith authored and James Smith committed Nov 17, 2024
1 parent 30d0ee4 commit 397c829
Show file tree
Hide file tree
Showing 4 changed files with 429 additions and 56 deletions.
54 changes: 51 additions & 3 deletions src/ansi_string/ansi_format.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
from enum import Enum, auto as enum_auto
from typing import Any, Union, List, Dict, Tuple
from .ansi_param import AnsiParam
from .ansi_param import AnsiParam, AnsiParamEffect

# The separator string used in an ANSI control sequence
ansi_sep = ';'
# The ansi escape character
ansi_escape = '\x1b'
# The start of an ANSI control sequence
ansi_control_sequence_introducer = ansi_escape + '['
# The last character of the ANSI control sequence
ansi_graphic_rendition_code_end = 'm'
# The escape sequence that needs to be formatted with command str
ansi_escape_format = '\x1b[{}m'
ansi_graphic_rendition_format = ansi_control_sequence_introducer + '{}' + ansi_graphic_rendition_code_end
# The escape sequence which will clear all previous formatting (empty command is same as 0)
ansi_escape_clear = ansi_escape_format.format('')
ansi_escape_clear = ansi_graphic_rendition_format.format('')

class AnsiSetting:
'''
Expand All @@ -34,6 +40,48 @@ def __eq__(self, value) -> bool:
def __str__(self) -> str:
return self._str

def to_list(self) -> List[Union[int, str]]:
'''
Returns a list of integers and strings. If a code is not a valid integer, it will be set in the list as string.
'''
val_list = []
for val in self._str.split(ansi_sep):
val = val.strip()
try:
val_int = int(val)
except ValueError:
val_list.append(val)
else:
val_list.append(val_int)
return val_list

def get_initial_param(self) -> AnsiParam:
'''
Returns the first parameter of this set which should define its function. This will return None if first value
in the set is not valid or the set is empty.
'''
val = self._str.split(ansi_sep, 1)
if not val:
return None

try:
return AnsiParam(int(val[0]))
except ValueError:
return None

def to_effect(self) -> AnsiParamEffect:
'''
Returns the effect of this setting based on the first code value of the set. This depends on the AnsiSetting
to have ben setup correctly, so the result is not guaranteed to be accurate.
'''
param = self.get_initial_param()

if param is None:
return None

return param.effect_type


class ColorComponentType(Enum):
FOREGROUND=enum_auto(),
BACKGROUND=enum_auto(),
Expand Down
167 changes: 155 additions & 12 deletions src/ansi_string/ansi_param.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,120 @@
from enum import Enum
from enum import Enum, IntEnum, auto as enum_auto
from typing import Dict, Tuple

class AnsiParam(Enum):
class AnsiParamEffect(Enum):
'''
Contains the different effect groups.
Only 1 of each group may be set at a time except for RESET which is stateless (resets state of all others).
'''
RESET=enum_auto()
BOLDNESS=enum_auto()
ITALICS=enum_auto()
UNDERLINE=enum_auto()
OVERLINE=enum_auto()
BLINKING=enum_auto()
SWAP_BG_FG=enum_auto()
VISIBILITY=enum_auto()
CROSSED_OUT=enum_auto()
FONT_TYPE=enum_auto()
SPACING=enum_auto()
BOXING=enum_auto()
FG_COLOR=enum_auto()
FG_COLOUR=FG_COLOR
BG_COLOR=enum_auto()
BG_COLOUR=BG_COLOR
UL_COLOR=enum_auto()
UL_COLOUR=UL_COLOR

class AnsiParamEffectFn(Enum):
RESET_ALL=enum_auto()
APPLY_SETTING=enum_auto()
CLEAR_SETTING=enum_auto()

# The dictionary lookup from code to effect type
# (Separate dict is used so that the key of AnsiParam is not affected)
_ANSI_CODE_TO_EFFECT:Dict[int, Tuple[AnsiParamEffect, bool]] = {
0: (AnsiParamEffect.RESET, AnsiParamEffectFn.RESET_ALL),
1: (AnsiParamEffect.BOLDNESS, AnsiParamEffectFn.APPLY_SETTING),
2: (AnsiParamEffect.BOLDNESS, AnsiParamEffectFn.APPLY_SETTING),
3: (AnsiParamEffect.ITALICS, AnsiParamEffectFn.APPLY_SETTING),
4: (AnsiParamEffect.UNDERLINE, AnsiParamEffectFn.APPLY_SETTING),
5: (AnsiParamEffect.BLINKING, AnsiParamEffectFn.APPLY_SETTING),
6: (AnsiParamEffect.BLINKING, AnsiParamEffectFn.APPLY_SETTING),
7: (AnsiParamEffect.SWAP_BG_FG, AnsiParamEffectFn.APPLY_SETTING),
8: (AnsiParamEffect.VISIBILITY, AnsiParamEffectFn.APPLY_SETTING),
9: (AnsiParamEffect.CROSSED_OUT, AnsiParamEffectFn.APPLY_SETTING),
10: (AnsiParamEffect.FONT_TYPE, AnsiParamEffectFn.APPLY_SETTING),
11: (AnsiParamEffect.FONT_TYPE, AnsiParamEffectFn.APPLY_SETTING),
12: (AnsiParamEffect.FONT_TYPE, AnsiParamEffectFn.APPLY_SETTING),
13: (AnsiParamEffect.FONT_TYPE, AnsiParamEffectFn.APPLY_SETTING),
14: (AnsiParamEffect.FONT_TYPE, AnsiParamEffectFn.APPLY_SETTING),
15: (AnsiParamEffect.FONT_TYPE, AnsiParamEffectFn.APPLY_SETTING),
16: (AnsiParamEffect.FONT_TYPE, AnsiParamEffectFn.APPLY_SETTING),
17: (AnsiParamEffect.FONT_TYPE, AnsiParamEffectFn.APPLY_SETTING),
18: (AnsiParamEffect.FONT_TYPE, AnsiParamEffectFn.APPLY_SETTING),
19: (AnsiParamEffect.FONT_TYPE, AnsiParamEffectFn.APPLY_SETTING),
20: (AnsiParamEffect.FONT_TYPE, AnsiParamEffectFn.APPLY_SETTING),
21: (AnsiParamEffect.UNDERLINE, AnsiParamEffectFn.APPLY_SETTING),
22: (AnsiParamEffect.BOLDNESS, AnsiParamEffectFn.CLEAR_SETTING),
23: (AnsiParamEffect.ITALICS, AnsiParamEffectFn.CLEAR_SETTING),
24: (AnsiParamEffect.UNDERLINE, AnsiParamEffectFn.CLEAR_SETTING),
25: (AnsiParamEffect.BLINKING, AnsiParamEffectFn.CLEAR_SETTING),
26: (AnsiParamEffect.SPACING, AnsiParamEffectFn.APPLY_SETTING),
27: (AnsiParamEffect.SWAP_BG_FG, AnsiParamEffectFn.CLEAR_SETTING),
28: (AnsiParamEffect.VISIBILITY, AnsiParamEffectFn.CLEAR_SETTING),
29: (AnsiParamEffect.CROSSED_OUT, AnsiParamEffectFn.CLEAR_SETTING),

30: (AnsiParamEffect.FG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
31: (AnsiParamEffect.FG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
32: (AnsiParamEffect.FG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
33: (AnsiParamEffect.FG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
34: (AnsiParamEffect.FG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
35: (AnsiParamEffect.FG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
36: (AnsiParamEffect.FG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
37: (AnsiParamEffect.FG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
38: (AnsiParamEffect.FG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
39: (AnsiParamEffect.FG_COLOR, AnsiParamEffectFn.CLEAR_SETTING),

40: (AnsiParamEffect.BG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
41: (AnsiParamEffect.BG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
42: (AnsiParamEffect.BG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
43: (AnsiParamEffect.BG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
44: (AnsiParamEffect.BG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
45: (AnsiParamEffect.BG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
46: (AnsiParamEffect.BG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
47: (AnsiParamEffect.BG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
48: (AnsiParamEffect.BG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
49: (AnsiParamEffect.BG_COLOR, AnsiParamEffectFn.CLEAR_SETTING),

50: (AnsiParamEffect.SPACING, AnsiParamEffectFn.CLEAR_SETTING),
51: (AnsiParamEffect.BOXING, AnsiParamEffectFn.APPLY_SETTING),
52: (AnsiParamEffect.BOXING, AnsiParamEffectFn.APPLY_SETTING),
53: (AnsiParamEffect.OVERLINE, AnsiParamEffectFn.APPLY_SETTING),
54: (AnsiParamEffect.BOXING, AnsiParamEffectFn.CLEAR_SETTING),
55: (AnsiParamEffect.OVERLINE, AnsiParamEffectFn.CLEAR_SETTING),
58: (AnsiParamEffect.UL_COLOR, AnsiParamEffectFn.APPLY_SETTING),
59: (AnsiParamEffect.UL_COLOR, AnsiParamEffectFn.CLEAR_SETTING),

90: (AnsiParamEffect.FG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
91: (AnsiParamEffect.FG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
92: (AnsiParamEffect.FG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
93: (AnsiParamEffect.FG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
94: (AnsiParamEffect.FG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
95: (AnsiParamEffect.FG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
96: (AnsiParamEffect.FG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
97: (AnsiParamEffect.FG_COLOR, AnsiParamEffectFn.APPLY_SETTING),

100: (AnsiParamEffect.BG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
101: (AnsiParamEffect.BG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
102: (AnsiParamEffect.BG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
103: (AnsiParamEffect.BG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
104: (AnsiParamEffect.BG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
105: (AnsiParamEffect.BG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
106: (AnsiParamEffect.BG_COLOR, AnsiParamEffectFn.APPLY_SETTING),
107: (AnsiParamEffect.BG_COLOR, AnsiParamEffectFn.APPLY_SETTING)
}

class AnsiParam(IntEnum):
'''
Contains raw ANSI control parameters
'''
Expand Down Expand Up @@ -35,16 +149,6 @@ class AnsiParam(Enum):
NO_SWAP_BG_FG=27
NO_HIDE=28
NO_CROSSED_OUT=29
NO_PROPORTIONAL_SPACING=50
FRAMED=51
ENCIRCLED=52
OVERLINED=53
NO_FRAMED_ENCIRCLED=54
NO_OVERLINED=55
SET_UNDERLINE_COLOR=58
SET_UNDERLINE_COLOUR=SET_UNDERLINE_COLOR # Alias for my British English friends
DEFAULT_UNDERLINE_COLOR=59
DEFAULT_UNDERLINE_COLOUR=DEFAULT_UNDERLINE_COLOR # Alias for my British English friends

FG_BLACK=30
FG_RED=31
Expand All @@ -67,3 +171,42 @@ class AnsiParam(Enum):
BG_WHITE=47
BG_SET=48
BG_DEFAULT=49

NO_PROPORTIONAL_SPACING=50
FRAMED=51
ENCIRCLED=52
OVERLINED=53
NO_FRAMED_ENCIRCLED=54
NO_OVERLINED=55
SET_UNDERLINE_COLOR=58
SET_UNDERLINE_COLOUR=SET_UNDERLINE_COLOR # Alias for my British English friends
DEFAULT_UNDERLINE_COLOR=59
DEFAULT_UNDERLINE_COLOUR=DEFAULT_UNDERLINE_COLOR # Alias for my British English friends

FG_BRIGHT_BLACK=90
FG_BRIGHT_RED=91
FG_BRIGHT_GREEN=92
FG_BRIGHT_YELLOW=93
FG_BRIGHT_BLUE=94
FG_BRIGHT_MAGENTA=95
FG_BRIGHT_CYAN=96
FG_BRIGHT_WHITE=97

BG_BRIGHT_BLACK=100
BG_BRIGHT_RED=101
BG_BRIGHT_GREEN=102
BG_BRIGHT_YELLOW=103
BG_BRIGHT_BLUE=104
BG_BRIGHT_MAGENTA=105
BG_BRIGHT_CYAN=106
BG_BRIGHT_WHITE=107

def __init__(self, code):
self.code:int = code
effect_settings = _ANSI_CODE_TO_EFFECT[code]
self.effect_type:AnsiParamEffect = effect_settings[0]
self.effect_fn:AnsiParamEffectFn = effect_settings[1]

# This dictionary has no use after AnsiParam is fully defined
# Lookup can be achieved through AnsiParam(<int>).effect_type
del _ANSI_CODE_TO_EFFECT
Loading

0 comments on commit 397c829

Please sign in to comment.