Skip to content

Instantly share code, notes, and snippets.

@Pinacolada64
Last active February 11, 2025 06:51
Show Gist options
  • Save Pinacolada64/c9adae5bdb9404224daf2b5be259cb6d to your computer and use it in GitHub Desktop.
Save Pinacolada64/c9adae5bdb9404224daf2b5be259cb6d to your computer and use it in GitHub Desktop.
A color string parser meant to handle ANSI and Commodore terminal colors
import enum
import logging
from dataclasses import dataclass
from enum import Enum
import codecs
import cbmcodecs2
from colorama import init, Fore
def replace_with_color_code(exception):
"""
Handles UnicodeEncodeError by replacing the unmapped character
with the corresponding CBM color code.
"""
logging.debug("got exception: %s" % exception)
logging.debug("got exception.start: %s" % exception.start)
try:
# Get the color code character
color_code = chr(int(CBMColors[exception.object[exception.start]].value))
# Return the color code character and the position to continue encoding
return color_code, exception.start + 1 # Increment start to skip the color code character
except IndexError:
# If the character is not a recognized color, return a placeholder
return '?', exception.start + 1 # Increment start to skip the unmapped character
class Translation(str, Enum):
PETSCII = "PETSCII"
ANSI = "ANSI"
def process_color_code(color_name: str, translation: Translation) -> str:
"""
Process a color code (e.g., RED) and return the appropriate replacement
based on the player's translation (PETSCII or ANSI).
"""
try:
if translation == Translation.PETSCII:
return chr(getattr(CBMColors, color_name).value) # PETSCII single-byte color codes
elif translation == Translation.ANSI:
color_code = getattr(ColoramaColors, color_name).value
return color_code if color_code != 'None' else ''
except AttributeError:
logging.warning(f"Unknown color: {color_name}")
return ''
@dataclass
class Player:
translation: Translation = Translation.PETSCII
def output(self, string: str):
"""
Outputs the given string, replacing color codes within braces {}
with the corresponding CBM or ANSI color codes, depending on the translation.
"""
output_text = []
i = 0
while i < len(string):
if string[i] == '{':
end_brace = string.find('}', i)
if end_brace != -1:
color_name = string[i + 1:end_brace].strip().upper()
output_text.append(process_color_code(color_name, self.translation))
i = end_brace # Skip to after the closing brace
else:
output_text.append(string[i]) # Append unmatched '{'
else:
output_text.append(string[i])
i += 1
final_output = ''.join(output_text)
print(final_output)
return final_output
@dataclass
class Player:
translation: Translation = Translation.PETSCII
def output(self, string: str):
"""
Outputs the given string, replacing color codes within braces {}
with the corresponding CBM or ANSI color codes, depending on the translation.
"""
result = "" # Initialize result as a string for simplicity
for i in range(len(string)):
if string[i] == '{':
end_brace = string.find('}', i)
if end_brace != -1:
color_name = string[i + 1:end_brace].upper()
try:
if self.translation == Translation.PETSCII:
color_code = chr(getattr(CBMColors, color_name).value)
result += color_code # PETSCII uses single-byte color codes
elif self.translation == Translation.ANSI:
color_code = getattr(ColoramaColors, color_name).value
if color_code != 'None':
result += color_code # ANSI uses sequences from Colorama
except AttributeError:
logging.warning(f"Unknown color: {color_name}")
i = end_brace # Skip to the closing brace
else:
result += string[i]
else:
result += string[i]
print(result) # Output the colorized string directly to the terminal
return result
class CBMColors(int, Enum):
BLACK = 144
WHITE = 5
RED = 28
CYAN = 159
PURPLE = 156
DARK_GREEN = 30
DARK_BLUE = 31
YELLOW = 158
ORANGE = 129
BROWN = 149
LIGHT_RED = 150
DARK_GRAY = 151
MEDIUM_GRAY = 152
LIGHT_GREEN = 153
LIGHT_BLUE = 154
LIGHT_GRAY = 155
RESET = MEDIUM_GRAY
class ColoramaColors(str, Enum):
BLACK = Fore.BLACK
WHITE = Fore.WHITE
RED = Fore.RED
CYAN = Fore.CYAN
PURPLE = None
DARK_GREEN = Fore.GREEN
DARK_BLUE = Fore.BLUE
YELLOW = Fore.YELLOW
ORANGE = None
BROWN = None
LIGHT_RED = Fore.LIGHTRED_EX
DARK_GRAY = Fore.LIGHTBLACK_EX
MEDIUM_GRAY = Fore.LIGHTBLACK_EX
LIGHT_GREEN = Fore.LIGHTGREEN_EX
LIGHT_BLUE = Fore.LIGHTBLUE_EX
LIGHT_GRAY = Fore.LIGHTBLACK_EX
RESET = Fore.RESET
if __name__ == '__main__':
# Set up logging
logging.basicConfig(format='%(levelname)10s | %(funcName)15s() | %(message)s', level=logging.DEBUG)
# Register the codec error handler
# codecs.register_error('custom_replace', replace_with_color_code)
# Modify the decoding table to add PetSCII color codes directly
# This is a simplified example and may need further refinement
modified_decoding_table = list(cbmcodecs2.petscii_c64en_lc.decoding_table)
# Add CBMColors Enum values as individual elements into the decoding table
for color in CBMColors:
modified_decoding_table[color.value] = chr(color.value)
modified_decoding_table = tuple(modified_decoding_table)
# Create a new codec instance with the modified table
class MyPetsciiCodec(cbmcodecs2.petscii_c64en_lc.Codec):
is_text_encoding = None
incrementaldecoders = None
incrementalencoders = None
encoding = 'my_petscii_c64en_lc'
decode_table = modified_decoding_table
def __init__(self, encoding_name):
self.encoding_name = encoding_name
def encoders(self, input, errors='strict'):
return super().encode(input, errors)
def decoders(self, input, errors='strict'):
return super().decode(input, errors)
def streamreaders(self, stream, errors='strict'):
return super().streamreader(stream, errors)
def streamwriters(self, stream, errors='strict'):
return super().streamwriter(stream, errors)
# Register the new codec
# Constants for readability and reusability
MY_PETSCII_ENCODING = 'my_petscii_c64en_lc'
"""
class MyPetsciiCodec(cbmcodecs2.petscii_c64en_lc.Codec):
is_text_encoding = None
incremental_decoders = None # Renamed for clarity
incremental_encoders = None # Renamed for consistency
encoding = MY_PETSCII_ENCODING # Use shared constant for encoding
decode_table = modified_decoding_table
def __init__(self, encoding_name):
pass # Assuming the body remains unchanged
# Renamed methods for better understanding
def encode(self, input, errors='strict'):
pass
def decode(self, input, errors='strict'):
pass
def create_stream_reader(self, stream, errors='strict'):
pass
def create_stream_writer(self, stream, errors='strict'):
pass
"""
# Using a dictionary to improve clarity during registration
codecs.register(
name=MY_PETSCII_ENCODING,
encoder=MyPetsciiCodec.encode,
decoder=MyPetsciiCodec.decode,
incrementalencoder=MyPetsciiCodec.incremental_encoders,
incrementaldecoder=MyPetsciiCodec.incremental_decoders,
streamreader=MyPetsciiCodec.create_stream_reader,
streamwriter=MyPetsciiCodec.create_stream_writer,
_is_text_encoding=MyPetsciiCodec.is_text_encoding
)
# Test the modified codec
try:
print("useless pause")
black_test = chr(144).encode(encoding="my_petscii_c64en_lc")
print(black_test.__repr__())
except LookupError as e:
print(f"Character encoding {e} was not found in the modified table.")
except UnicodeEncodeError as e:
logging.warning(f"Error encoding black character: {e}")
# Initialize colorama
init()
player = Player(translation=Translation.PETSCII)
# Example usage
color_string = ("{red}Red {orange}Orange {yellow}Yellow {dark_green}Green {dark_blue}Blue {purple}Purple "
"{cyan}Cyan {light_green}-{dark_green}-{reset}! {plum}")
player.output(color_string)
@Pinacolada64
Copy link
Author

A bunch of stuff that the codecs library needs, apparently. When I try to run the PetSCII string through .encode():

Traceback (most recent call last):
  File "/home/ryan/.config/JetBrains/PyCharmCE2024.3/scratches/color_map.py", line 230, in <module>
    incrementalencoder=MyPetsciiCodec.incremental_encoders,
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: type object 'MyPetsciiCodec' has no attribute 'incremental_encoders'. Did you mean: 'incrementalencoders'?

This version takes a different tactic, subclasses the codec decoding table. I couldn't get the error_handler to work in the previous version.

@Pinacolada64
Copy link
Author

Most of this was suggested by JetBrains AI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment