11from __future__ import annotations
22
3+ import os
34import pprint
45import string
56import sys
1314 from collections .abc import Iterator
1415 from typing import Literal
1516
16- COLOR_RED = "\033 [1;31m"
17- COLOR_GREEN = "\033 [1;32m"
18- COLOR_YELLOW = "\033 [1;33m"
19- COLOR_BLUE = "\033 [1;34m"
20- COLOR_PURPLE = "\033 [1;35m"
21- COLOR_CYAN = "\033 [1;36m"
22- COLOR_WHITE = "\033 [1;37m"
23- COLOR_NORMAL = "\033 [1;0m"
24-
17+ # Regular ANSI colors
18+ COLOR_RED = "\033 [0;31m"
19+ COLOR_GREEN = "\033 [0;32m"
20+ COLOR_YELLOW = "\033 [0;93m"
21+ COLOR_BLUE = "\033 [0;34m"
22+ COLOR_PURPLE = "\033 [0;35m"
23+ COLOR_CYAN = "\033 [0;36m"
24+ COLOR_WHITE = "\033 [0;37m"
25+ COLOR_BLACK = "\033 [0;30m"
26+ COLOR_GREY = "\033 [0;90m"
27+
28+ # Bold ANSI colors
29+ COLOR_RED_BOLD = "\033 [1;31m"
30+ COLOR_GREEN_BOLD = "\033 [1;32m"
31+ COLOR_YELLOW_BOLD = "\033 [1;33m"
32+ COLOR_BLUE_BOLD = "\033 [1;34m"
33+ COLOR_PURPLE_BOLD = "\033 [1;35m"
34+ COLOR_CYAN_BOLD = "\033 [1;36m"
35+ COLOR_WHITE_BOLD = "\033 [1;37m"
36+ COLOR_BLACK_BOLD = "\033 [1;30m"
37+ COLOR_GREY_BOLD = "\033 [1;90m"
38+
39+ # Background ANSI colors
2540COLOR_BG_RED = "\033 [1;41m\033 [1;37m"
2641COLOR_BG_GREEN = "\033 [1;42m\033 [1;37m"
2742COLOR_BG_YELLOW = "\033 [1;43m\033 [1;37m"
3045COLOR_BG_CYAN = "\033 [1;46m\033 [1;37m"
3146COLOR_BG_WHITE = "\033 [1;47m\033 [1;30m"
3247
48+ # Reset ANSI codes
49+ COLOR_CLEAR = "\033 [0m"
50+ COLOR_CLEAR_BOLD = "\033 [1;0m"
51+
3352PRINTABLE = string .digits + string .ascii_letters + string .punctuation + " "
3453
3554ENDIANNESS_MAP : dict [str , Literal ["big" , "little" ]] = {
4463Palette = list [tuple [int , str ]]
4564
4665
47- def _hexdump (data : bytes , palette : Palette | None = None , offset : int = 0 , prefix : str = "" ) -> Iterator [str ]:
66+ def _human_colors () -> dict [str , str ]:
67+ """Generates a dictionary of characters with a human-readable ANSI color they should be in a hexdump.
68+
69+ Coloring logic implementation derived from HexFriend and ImHex.
70+ """
71+ # Make all characters not in any rules below light green
72+ colors = {chr (char ): COLOR_GREEN for char in range (256 )}
73+
74+ # Make all ASCII extended characters yellow
75+ for char in colors :
76+ if ord (char ) & 0x80 == 0 :
77+ colors [char ] = COLOR_YELLOW
78+
79+ # Make null bytes grey
80+ colors ["\00 " ] = COLOR_GREY
81+
82+ # Make printable ASCII characters bold white (0x32-0x7E)
83+ for char in PRINTABLE :
84+ colors [char ] = COLOR_WHITE_BOLD
85+
86+ # Make ASCII whitespace characters green bold (0x9, 0xA, 0xB, 0xC, 0xD, 0x20)
87+ for char in ("\t " , "\n " , "\11 " , "\12 " , "\r " , "\20 " ):
88+ colors [char ] = COLOR_GREEN_BOLD
89+
90+ return colors
91+
92+
93+ HUMAN_COLORS = _human_colors ()
94+
95+
96+ def _hexdump (
97+ data : bytes , palette : Palette | None = None , offset : int = 0 , prefix : str = "" , pretty : bool | None = False
98+ ) -> Iterator [str ]:
4899 """Hexdump some data.
49100
50101 Args:
51102 data: Bytes to hexdump.
103+ palette: Colorize the hexdump using this color pattern.
52104 offset: Byte offset of the hexdump.
53105 prefix: Optional prefix.
54- palette: Colorize the hexdump using this color pattern .
106+ pretty: Use pretty colors, mutual exclusive with palette .
55107 """
56108 if palette :
57109 palette = palette [::- 1 ]
58110
111+ # only happy little accidents
112+ if pretty and palette :
113+ raise ValueError ("Cannot use argument 'pretty' in combination with 'palette', please pick one" )
114+
59115 remaining = 0
60116 active = None
61117
@@ -87,20 +143,24 @@ def _hexdump(data: bytes, palette: Palette | None = None, offset: int = 0, prefi
87143
88144 if active :
89145 values += f"{ ord (char ):02x} "
90- chars .append (active + print_char + COLOR_NORMAL )
146+ chars .append (active + print_char + COLOR_CLEAR_BOLD )
91147 else :
92- values += f"{ ord (char ):02x} "
93- chars .append (print_char )
148+ if pretty and (color := HUMAN_COLORS .get (char , "" )):
149+ values += f"{ color } { ord (char ):02x} { COLOR_CLEAR } "
150+ chars .append (color + print_char + COLOR_CLEAR )
151+ else :
152+ values += f"{ ord (char ):02x} "
153+ chars .append (print_char )
94154
95155 remaining -= 1
96156 if remaining == 0 :
97157 active = None
98158
99159 if palette is not None :
100- values += COLOR_NORMAL
160+ values += COLOR_CLEAR_BOLD
101161
102162 if j == 15 and palette is not None :
103- values += COLOR_NORMAL
163+ values += COLOR_CLEAR_BOLD
104164
105165 values += " "
106166 if j == 7 :
@@ -111,18 +171,36 @@ def _hexdump(data: bytes, palette: Palette | None = None, offset: int = 0, prefi
111171
112172
113173def hexdump (
114- data : bytes , palette : Palette | None = None , offset : int = 0 , prefix : str = "" , output : str = "print"
174+ data : bytes ,
175+ palette : Palette | None = None ,
176+ offset : int = 0 ,
177+ prefix : str = "" ,
178+ output : str = "print" ,
179+ pretty : bool | None = None ,
115180) -> Iterator [str ] | str | None :
116181 """Hexdump some data.
117182
183+ Uses colored ANSI output with output type "print" by default. Disable with ``pretty=False``
184+ or set the environment variable ``NO_COLOR``.
185+
118186 Args:
119187 data: Bytes to hexdump.
120188 palette: Colorize the hexdump using this color pattern.
121189 offset: Byte offset of the hexdump.
122190 prefix: Optional prefix.
123191 output: Output format, can be 'print', 'generator' or 'string'.
192+ pretty: Use pretty colors for improved human readability.
124193 """
125- generator = _hexdump (data , palette , offset , prefix )
194+ # Enable pretty colors by default if ...
195+ if (
196+ output == "print" # the output type is set to 'print'
197+ and not palette # no palette is given (structdump only)
198+ and pretty is not False # pretty was not explicitly set to False
199+ and not os .environ .get ("NO_COLOR" ) # and the environment allows colors
200+ ):
201+ pretty = True
202+
203+ generator = _hexdump (data , palette , offset , prefix , pretty )
126204 if output == "print" :
127205 print ("\n " .join (generator ))
128206 return None
@@ -142,13 +220,13 @@ def _dumpstruct(
142220) -> str | None :
143221 palette = []
144222 colors = [
145- (COLOR_RED , COLOR_BG_RED ),
146- (COLOR_GREEN , COLOR_BG_GREEN ),
147- (COLOR_YELLOW , COLOR_BG_YELLOW ),
148- (COLOR_BLUE , COLOR_BG_BLUE ),
149- (COLOR_PURPLE , COLOR_BG_PURPLE ),
150- (COLOR_CYAN , COLOR_BG_CYAN ),
151- (COLOR_WHITE , COLOR_BG_WHITE ),
223+ (COLOR_RED_BOLD , COLOR_BG_RED ),
224+ (COLOR_GREEN_BOLD , COLOR_BG_GREEN ),
225+ (COLOR_YELLOW_BOLD , COLOR_BG_YELLOW ),
226+ (COLOR_BLUE_BOLD , COLOR_BG_BLUE ),
227+ (COLOR_PURPLE_BOLD , COLOR_BG_PURPLE ),
228+ (COLOR_CYAN_BOLD , COLOR_BG_CYAN ),
229+ (COLOR_WHITE_BOLD , COLOR_BG_WHITE ),
152230 ]
153231 ci = 0
154232 out = [f"struct { structure .__class__ .__name__ } :" ]
@@ -172,7 +250,7 @@ def _dumpstruct(
172250 size = structure .__sizes__ [field ._name ]
173251 palette .append ((size , background ))
174252 ci += 1
175- out .append (f"- { foreground } { field ._name } { COLOR_NORMAL } : { value } " )
253+ out .append (f"- { foreground } { field ._name } { COLOR_CLEAR_BOLD } : { value } " )
176254 else :
177255 out .append (f"- { field ._name } : { value } " )
178256
0 commit comments