hexicon 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hexicon/__init__.py +43 -0
- hexicon/__main__.py +5 -0
- hexicon/_core.py +304 -0
- hexicon/cli.py +145 -0
- hexicon-0.1.0.dist-info/METADATA +147 -0
- hexicon-0.1.0.dist-info/RECORD +9 -0
- hexicon-0.1.0.dist-info/WHEEL +4 -0
- hexicon-0.1.0.dist-info/entry_points.txt +2 -0
- hexicon-0.1.0.dist-info/licenses/LICENSE +21 -0
hexicon/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Hexicon – visual fingerprints for network addresses."""
|
|
2
|
+
|
|
3
|
+
from hexicon._core import (
|
|
4
|
+
CHARSET_DEFAULT,
|
|
5
|
+
DEFAULT_WIDTH,
|
|
6
|
+
LAYOUT_CHOICES,
|
|
7
|
+
OUTPUT_CHOICES,
|
|
8
|
+
TYPE_CHOICES,
|
|
9
|
+
AddressSchema,
|
|
10
|
+
Part,
|
|
11
|
+
addr_to_nibbles,
|
|
12
|
+
detect_type,
|
|
13
|
+
format_json,
|
|
14
|
+
format_text,
|
|
15
|
+
int_to_nibbles,
|
|
16
|
+
nibbles_to_bits,
|
|
17
|
+
parse_addr,
|
|
18
|
+
random_addr,
|
|
19
|
+
render_address,
|
|
20
|
+
render_grid,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"CHARSET_DEFAULT",
|
|
25
|
+
"DEFAULT_WIDTH",
|
|
26
|
+
"LAYOUT_CHOICES",
|
|
27
|
+
"OUTPUT_CHOICES",
|
|
28
|
+
"TYPE_CHOICES",
|
|
29
|
+
"AddressSchema",
|
|
30
|
+
"Part",
|
|
31
|
+
"addr_to_nibbles",
|
|
32
|
+
"detect_type",
|
|
33
|
+
"format_json",
|
|
34
|
+
"format_text",
|
|
35
|
+
"int_to_nibbles",
|
|
36
|
+
"nibbles_to_bits",
|
|
37
|
+
"parse_addr",
|
|
38
|
+
"random_addr",
|
|
39
|
+
"render_address",
|
|
40
|
+
"render_grid",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
__version__ = "0.1.0"
|
hexicon/__main__.py
ADDED
hexicon/_core.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Core rendering logic for hexicon"""
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import ipaddress
|
|
5
|
+
import json
|
|
6
|
+
import random
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
# ── Charset ───────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
CHARSET_DEFAULT = " ▄▀█" # index: top*2 + bottom → 0=' ' 1='▄' 2='▀' 3='█'
|
|
13
|
+
|
|
14
|
+
# ── Renderer ──────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
def nibbles_to_bits(nibbles):
|
|
17
|
+
"""Convert a list of nibbles to a flat list of bits, MSB first."""
|
|
18
|
+
bits = []
|
|
19
|
+
for n in nibbles:
|
|
20
|
+
for i in (3, 2, 1, 0):
|
|
21
|
+
bits.append((n >> i) & 1)
|
|
22
|
+
return bits
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def int_to_nibbles(n, count):
|
|
26
|
+
"""Extract count nibbles from integer n, MSB first"""
|
|
27
|
+
return [
|
|
28
|
+
(n >> (4 * shift)) & 0xF
|
|
29
|
+
for shift in reversed(range(count))
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ── Schema types ──────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
@dataclasses.dataclass
|
|
36
|
+
class Part:
|
|
37
|
+
name: str
|
|
38
|
+
nibbles: list
|
|
39
|
+
bit_range: list
|
|
40
|
+
label: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclasses.dataclass
|
|
44
|
+
class AddressSchema:
|
|
45
|
+
type: str
|
|
46
|
+
input: str
|
|
47
|
+
nibbles: list
|
|
48
|
+
parts: list
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ── Address → nibbles (unified schema) ────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
_MAC_RE = re.compile(
|
|
54
|
+
r'^([0-9a-fA-F]{2})[:\-]'
|
|
55
|
+
r'([0-9a-fA-F]{2})[:\-]'
|
|
56
|
+
r'([0-9a-fA-F]{2})[:\-]'
|
|
57
|
+
r'([0-9a-fA-F]{2})[:\-]'
|
|
58
|
+
r'([0-9a-fA-F]{2})[:\-]'
|
|
59
|
+
r'([0-9a-fA-F]{2})$'
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def detect_type(raw):
|
|
64
|
+
"""Guess address type from string format"""
|
|
65
|
+
raw = raw.strip()
|
|
66
|
+
if _MAC_RE.match(raw):
|
|
67
|
+
return "mac"
|
|
68
|
+
try:
|
|
69
|
+
ipaddress.IPv4Address(raw)
|
|
70
|
+
return "ipv4"
|
|
71
|
+
except ValueError:
|
|
72
|
+
pass
|
|
73
|
+
try:
|
|
74
|
+
ipaddress.IPv6Address(raw)
|
|
75
|
+
return "ipv6"
|
|
76
|
+
except ValueError:
|
|
77
|
+
pass
|
|
78
|
+
raise ValueError(f"Cannot detect address type for: {raw!r}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _parse_ip_nibbles(raw, cls, nibble_count):
|
|
82
|
+
"""Helper for IPv4/IPv6: parses, normalises, extracts nibbles"""
|
|
83
|
+
obj = cls(raw)
|
|
84
|
+
return str(obj), int_to_nibbles(int(obj), nibble_count)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _parse_ipv6(raw):
|
|
88
|
+
normalised, all_nibbles = _parse_ip_nibbles(raw, ipaddress.IPv6Address, 32)
|
|
89
|
+
return AddressSchema(
|
|
90
|
+
type="ipv6",
|
|
91
|
+
input=normalised,
|
|
92
|
+
nibbles=all_nibbles,
|
|
93
|
+
parts=[
|
|
94
|
+
Part(name="net", nibbles=all_nibbles[:16], bit_range=[0, 64], label="network"),
|
|
95
|
+
Part(name="host", nibbles=all_nibbles[16:], bit_range=[64, 128], label="host"),
|
|
96
|
+
],
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _parse_ipv4(raw):
|
|
101
|
+
normalised, all_nibbles = _parse_ip_nibbles(raw, ipaddress.IPv4Address, 8)
|
|
102
|
+
parts = [
|
|
103
|
+
Part(
|
|
104
|
+
name=f"octet{i}",
|
|
105
|
+
nibbles=all_nibbles[i*2 : i*2+2],
|
|
106
|
+
bit_range=[i * 8, (i + 1) * 8],
|
|
107
|
+
label=f"octet {i}",
|
|
108
|
+
)
|
|
109
|
+
for i in range(4)
|
|
110
|
+
]
|
|
111
|
+
return AddressSchema(
|
|
112
|
+
type="ipv4",
|
|
113
|
+
input=normalised,
|
|
114
|
+
nibbles=all_nibbles,
|
|
115
|
+
parts=parts,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _parse_mac(raw):
|
|
120
|
+
m = _MAC_RE.match(raw)
|
|
121
|
+
if not m:
|
|
122
|
+
raise ValueError(f"Invalid MAC address: {raw!r}")
|
|
123
|
+
octets = [int(g, 16) for g in m.groups()]
|
|
124
|
+
n = 0
|
|
125
|
+
for b in octets:
|
|
126
|
+
n = (n << 8) | b
|
|
127
|
+
all_nibbles = int_to_nibbles(n, 12)
|
|
128
|
+
normalised = ":".join(f"{b:02x}" for b in octets)
|
|
129
|
+
return AddressSchema(
|
|
130
|
+
type="mac",
|
|
131
|
+
input=normalised,
|
|
132
|
+
nibbles=all_nibbles,
|
|
133
|
+
parts=[
|
|
134
|
+
Part(name="oui", nibbles=all_nibbles[:6], bit_range=[0, 24], label="OUI"),
|
|
135
|
+
Part(name="nic", nibbles=all_nibbles[6:], bit_range=[24, 48], label="NIC"),
|
|
136
|
+
],
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
_PARSERS = {"ipv6": _parse_ipv6, "ipv4": _parse_ipv4, "mac": _parse_mac}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def addr_to_nibbles(addr, addr_type="auto"):
|
|
144
|
+
"""Parses an addr string and return an AddressSchema."""
|
|
145
|
+
raw = addr.strip()
|
|
146
|
+
|
|
147
|
+
if addr_type == "auto":
|
|
148
|
+
addr_type = detect_type(raw)
|
|
149
|
+
|
|
150
|
+
if addr_type not in _PARSERS:
|
|
151
|
+
raise ValueError(f"Unknown address type: {addr_type!r}")
|
|
152
|
+
return _PARSERS[addr_type](raw)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ── Constants ─────────────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
DEFAULT_WIDTH = {"ipv6": 8, "ipv4": 4, "mac": 6}
|
|
158
|
+
TYPE_CHOICES = ["auto", "ipv6", "ipv4", "mac"]
|
|
159
|
+
LAYOUT_CHOICES = ["auto", "grid", "split", "inline", "barcode"]
|
|
160
|
+
OUTPUT_CHOICES = ["text", "json"]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def render_grid(nibbles, width, charset, invert=False):
|
|
164
|
+
"""Render nibbles as scanline pairs using halfblock characters.
|
|
165
|
+
|
|
166
|
+
Converts nibbles to a flat bitstream, groups into scanlines of
|
|
167
|
+
`width` bits, pairs adjacent scanlines (top-bottom), and maps
|
|
168
|
+
each (top_bit, bottom_bit) pair to a halfblock character.
|
|
169
|
+
"""
|
|
170
|
+
bits = nibbles_to_bits(nibbles)
|
|
171
|
+
|
|
172
|
+
# Pad to fill complete scanline pairs
|
|
173
|
+
pair_size = width * 2
|
|
174
|
+
remainder = len(bits) % pair_size
|
|
175
|
+
if remainder:
|
|
176
|
+
bits = bits + [0] * (pair_size - remainder)
|
|
177
|
+
|
|
178
|
+
# Group into scanlines
|
|
179
|
+
scanlines = [bits[i:i+width] for i in range(0, len(bits), width)]
|
|
180
|
+
|
|
181
|
+
# Pad to even number of scanlines
|
|
182
|
+
if len(scanlines) % 2:
|
|
183
|
+
scanlines.append([0] * width)
|
|
184
|
+
|
|
185
|
+
rows = []
|
|
186
|
+
for i in range(0, len(scanlines), 2):
|
|
187
|
+
top = scanlines[i]
|
|
188
|
+
bot = scanlines[i + 1]
|
|
189
|
+
row = ''
|
|
190
|
+
for t, b in zip(top, bot):
|
|
191
|
+
idx = t * 2 + b
|
|
192
|
+
row += charset[3 - idx if invert else idx]
|
|
193
|
+
rows.append(row)
|
|
194
|
+
return rows
|
|
195
|
+
|
|
196
|
+
# ── Formatter ─────────────────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
def format_text(part_rows, layout, scale):
|
|
199
|
+
"""
|
|
200
|
+
part_rows: list of (part_name, [rendered_row_strings])
|
|
201
|
+
layout: "split" → side-by-side for 2-part addresses, else stacked
|
|
202
|
+
"""
|
|
203
|
+
lines = []
|
|
204
|
+
if layout == "split" and len(part_rows) == 2:
|
|
205
|
+
# side-by-side
|
|
206
|
+
left = part_rows[0][1]
|
|
207
|
+
right = part_rows[1][1]
|
|
208
|
+
left_w = len(left[0]) if left else 0
|
|
209
|
+
right_w = len(right[0]) if right else 0
|
|
210
|
+
max_len = max(len(left), len(right))
|
|
211
|
+
left = left + [" " * left_w] * (max_len - len(left))
|
|
212
|
+
right = right + [" " * right_w] * (max_len - len(right))
|
|
213
|
+
for l, r in zip(left, right):
|
|
214
|
+
row = l + " " + r
|
|
215
|
+
for _ in range(scale):
|
|
216
|
+
lines.append(row)
|
|
217
|
+
else:
|
|
218
|
+
for _name, rows in part_rows:
|
|
219
|
+
for row in rows:
|
|
220
|
+
for _ in range(scale):
|
|
221
|
+
lines.append(row)
|
|
222
|
+
return '\n'.join(lines)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def format_json(schema, part_rows):
|
|
226
|
+
parts_out = []
|
|
227
|
+
for name, rows in part_rows:
|
|
228
|
+
entry = {"name": name, "rows": rows}
|
|
229
|
+
part = next((p for p in schema.parts if p.name == name), None)
|
|
230
|
+
if part:
|
|
231
|
+
entry["bit_range"] = part.bit_range
|
|
232
|
+
entry["label"] = part.label
|
|
233
|
+
parts_out.append(entry)
|
|
234
|
+
return json.dumps({
|
|
235
|
+
"type": schema.type,
|
|
236
|
+
"address": schema.input,
|
|
237
|
+
"parts": parts_out,
|
|
238
|
+
}, ensure_ascii=False)
|
|
239
|
+
|
|
240
|
+
# ── Input handling ────────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
def parse_addr(raw, addr_type="auto"):
|
|
243
|
+
"""Validate a raw address string. Returns (normalised_str, resolved_type)"""
|
|
244
|
+
raw = raw.strip()
|
|
245
|
+
if addr_type == "auto":
|
|
246
|
+
addr_type = detect_type(raw)
|
|
247
|
+
# full parse to validate
|
|
248
|
+
addr_to_nibbles(raw, addr_type)
|
|
249
|
+
return raw, addr_type
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def random_addr(addr_type="ipv6"):
|
|
253
|
+
"""Generate a random address string of the given type"""
|
|
254
|
+
if addr_type == "ipv6":
|
|
255
|
+
return str(ipaddress.IPv6Address(random.getrandbits(128)))
|
|
256
|
+
if addr_type == "ipv4":
|
|
257
|
+
return str(ipaddress.IPv4Address(random.getrandbits(32)))
|
|
258
|
+
if addr_type == "mac":
|
|
259
|
+
octets = [random.randint(0, 255) for _ in range(6)]
|
|
260
|
+
return ":".join(f"{b:02x}" for b in octets)
|
|
261
|
+
raise ValueError(f"Cannot generate random address for type: {addr_type!r}")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ── Core render pipeline ──────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
def render_address(addr, addr_type="auto", charset=CHARSET_DEFAULT, invert=False,
|
|
267
|
+
scale=1, layout="auto", width="auto", output="text", show_bits=False):
|
|
268
|
+
schema = addr_to_nibbles(addr, addr_type)
|
|
269
|
+
atype = schema.type
|
|
270
|
+
|
|
271
|
+
# Resolve layout
|
|
272
|
+
if layout == "auto":
|
|
273
|
+
layout = "split" if len(schema.parts) == 2 else "grid"
|
|
274
|
+
|
|
275
|
+
# Resolve width
|
|
276
|
+
if width == "auto":
|
|
277
|
+
if layout == "inline":
|
|
278
|
+
total_bits = len(schema.nibbles) * 4
|
|
279
|
+
width = total_bits // 2
|
|
280
|
+
elif layout == "barcode":
|
|
281
|
+
total_bits = len(schema.nibbles) * 4
|
|
282
|
+
width = max(total_bits // 4, 1)
|
|
283
|
+
else:
|
|
284
|
+
width = DEFAULT_WIDTH[atype]
|
|
285
|
+
|
|
286
|
+
if show_bits:
|
|
287
|
+
for part in schema.parts:
|
|
288
|
+
bits = nibbles_to_bits(part.nibbles)
|
|
289
|
+
print(f"{part.name} bits: {''.join(str(b) for b in bits)}", file=sys.stderr)
|
|
290
|
+
|
|
291
|
+
# inline/barcode, render all nibbles as one block
|
|
292
|
+
if layout in ("inline", "barcode"):
|
|
293
|
+
rows = render_grid(schema.nibbles, width, charset, invert=invert)
|
|
294
|
+
part_rows = [("all", rows)]
|
|
295
|
+
else:
|
|
296
|
+
part_rows = []
|
|
297
|
+
for part in schema.parts:
|
|
298
|
+
rows = render_grid(part.nibbles, width, charset, invert=invert)
|
|
299
|
+
part_rows.append((part.name, rows))
|
|
300
|
+
|
|
301
|
+
if output == "json":
|
|
302
|
+
return format_json(schema, part_rows)
|
|
303
|
+
else:
|
|
304
|
+
return format_text(part_rows, layout, scale)
|
hexicon/cli.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Command-line interface for hexicon"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import io
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from hexicon._core import (
|
|
8
|
+
LAYOUT_CHOICES,
|
|
9
|
+
OUTPUT_CHOICES,
|
|
10
|
+
TYPE_CHOICES,
|
|
11
|
+
parse_addr,
|
|
12
|
+
random_addr,
|
|
13
|
+
render_address,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_parser():
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
prog="hexicon",
|
|
20
|
+
description="Render a human-readable identicon for an IPv6 address.",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
src = parser.add_mutually_exclusive_group(required=True)
|
|
24
|
+
src.add_argument(
|
|
25
|
+
"address", nargs="?", metavar="ADDRESS",
|
|
26
|
+
help="Address to render (IPv6, IPv4, or MAC). Use '-' to read from stdin.",
|
|
27
|
+
)
|
|
28
|
+
src.add_argument(
|
|
29
|
+
"--random", action="store_true",
|
|
30
|
+
help="Generate and render a random address.",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--type", choices=TYPE_CHOICES, default="auto",
|
|
35
|
+
dest="addr_type",
|
|
36
|
+
help="Address type (default: auto-detect).",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--invert", action="store_true",
|
|
41
|
+
help="Invert filled and empty pixels.",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--scale", type=int, default=1, metavar="N",
|
|
45
|
+
help="Repeat each row N times (default: 1).",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--layout", choices=LAYOUT_CHOICES,
|
|
49
|
+
default="auto",
|
|
50
|
+
help="Layout mode (default: auto). grid=stacked, split=side-by-side, "
|
|
51
|
+
"inline=single row, barcode=wide strip.",
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--width", default="auto", metavar="N|auto",
|
|
55
|
+
help="Bits per scanline row (default: auto, uses type-appropriate width).",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--output", choices=OUTPUT_CHOICES, default="text",
|
|
59
|
+
help="Output format (default: text).",
|
|
60
|
+
)
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"--no-newline", action="store_true",
|
|
63
|
+
help="Suppress trailing newline (useful for piping).",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--show-bits", action="store_true",
|
|
67
|
+
help="Print raw nibble values to stderr before rendering.",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return parser
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def emit(text, no_newline=False):
|
|
74
|
+
if no_newline:
|
|
75
|
+
print(text, end="")
|
|
76
|
+
else:
|
|
77
|
+
print(text)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main():
|
|
81
|
+
# ensure UTF8 output for cp1252 terminals
|
|
82
|
+
if isinstance(sys.stdout, io.TextIOWrapper):
|
|
83
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
84
|
+
if isinstance(sys.stderr, io.TextIOWrapper):
|
|
85
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
86
|
+
|
|
87
|
+
parser = build_parser()
|
|
88
|
+
args = parser.parse_args()
|
|
89
|
+
|
|
90
|
+
if args.width != "auto":
|
|
91
|
+
try:
|
|
92
|
+
width = int(args.width)
|
|
93
|
+
except ValueError:
|
|
94
|
+
parser.error("--width must be a positive integer or 'auto'")
|
|
95
|
+
if width < 1:
|
|
96
|
+
parser.error("--width must be a positive integer or 'auto'")
|
|
97
|
+
else:
|
|
98
|
+
width = "auto"
|
|
99
|
+
|
|
100
|
+
opts = dict(
|
|
101
|
+
addr_type = args.addr_type,
|
|
102
|
+
invert = args.invert,
|
|
103
|
+
scale = args.scale,
|
|
104
|
+
layout = args.layout,
|
|
105
|
+
width = width,
|
|
106
|
+
output = args.output,
|
|
107
|
+
show_bits = args.show_bits,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if args.scale < 1:
|
|
111
|
+
parser.error("--scale must be a positive integer")
|
|
112
|
+
|
|
113
|
+
if args.random:
|
|
114
|
+
addr = random_addr(args.addr_type if args.addr_type != "auto" else "ipv6")
|
|
115
|
+
if args.output == "text":
|
|
116
|
+
print(f"# {addr}", file=sys.stderr)
|
|
117
|
+
result = render_address(addr, **opts)
|
|
118
|
+
emit(result, args.no_newline)
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
# Stdin batch mode
|
|
122
|
+
if args.address == "-":
|
|
123
|
+
for line in sys.stdin:
|
|
124
|
+
line = line.strip()
|
|
125
|
+
if not line:
|
|
126
|
+
continue
|
|
127
|
+
try:
|
|
128
|
+
addr, _ = parse_addr(line, args.addr_type)
|
|
129
|
+
except ValueError as e:
|
|
130
|
+
print(f"hexicon: {e}", file=sys.stderr)
|
|
131
|
+
continue
|
|
132
|
+
result = render_address(addr, **opts)
|
|
133
|
+
emit(result, args.no_newline)
|
|
134
|
+
if not args.no_newline:
|
|
135
|
+
print() # blank separator between addresses
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
# normal single address mode
|
|
139
|
+
try:
|
|
140
|
+
addr, _ = parse_addr(args.address, args.addr_type)
|
|
141
|
+
except ValueError as e:
|
|
142
|
+
parser.error(str(e))
|
|
143
|
+
|
|
144
|
+
result = render_address(addr, **opts)
|
|
145
|
+
emit(result, args.no_newline)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hexicon
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Visual fingerprints for network addresses — lossless, bit-accurate identicons for IPv6, IPv4, and MAC addresses.
|
|
5
|
+
Project-URL: Homepage, https://github.com/phatbuddha/hexicon
|
|
6
|
+
Project-URL: Repository, https://github.com/phatbuddha/hexicon
|
|
7
|
+
Project-URL: Issues, https://github.com/phatbuddha/hexicon/issues
|
|
8
|
+
Author: phatbuddha
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: identicon,ipv4,ipv6,mac,network,terminal,visualization
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: System :: Networking
|
|
25
|
+
Classifier: Topic :: Utilities
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# Hexicon
|
|
30
|
+
|
|
31
|
+
**Visual fingerprints for network addresses.**
|
|
32
|
+
|
|
33
|
+
A terminal-first tool to turn IPv6, IPv4, and MAC addresses into bit-accurate, lossless, human-readable visual patterns. Visualize structured binary data in the terminal without losing bit order or structure.
|
|
34
|
+
Or identicons, but for network addresses.
|
|
35
|
+
|
|
36
|
+
## Why?
|
|
37
|
+
|
|
38
|
+
Network addresses are hard to compare at a glance.
|
|
39
|
+
Hexicon lets you compare addresses in logs visually, get an intuitive feel for address distribution, and simplify debugging.
|
|
40
|
+
Or just create pixel art to represent your addresses.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Features
|
|
45
|
+
|
|
46
|
+
- Supports **IPv6, IPv4, and MAC addresses**
|
|
47
|
+
- Bit-accurate rendering. No hashing, no data loss
|
|
48
|
+
- Semantic structure (network/host, OUI/NIC, octets)
|
|
49
|
+
- Terminal-friendly block rendering with multiple layouts: grid, split, inline, barcode
|
|
50
|
+
- JSON output for programmatic use
|
|
51
|
+
- Zero dependencies. Python stdlib only
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install hexicon
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## How it works
|
|
60
|
+
|
|
61
|
+
Hexicon converts addresses into bit-accurate patterns through a deterministic, lossless pipeline. No hashing.
|
|
62
|
+
|
|
63
|
+
Addresses are normalised to their canonical form, converted to a bitstream, grouped into scanlines of `width` bits, paired as adjacent scanlines (top/bottom), and mapped to half-block characters.
|
|
64
|
+
Each cell (half a block) represents a single bit.
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
### CLI
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
hexicon 2001:db8::1
|
|
72
|
+
|
|
73
|
+
hexicon --random
|
|
74
|
+
|
|
75
|
+
hexicon --random --type ipv6 --layout barcode --show-bits --output text
|
|
76
|
+
|
|
77
|
+
# Read from stdin
|
|
78
|
+
echo "::1" | hexicon -
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Python API
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from hexicon import render_address, addr_to_nibbles
|
|
85
|
+
|
|
86
|
+
# Render an address to text
|
|
87
|
+
print(render_address("2001:db8::1"))
|
|
88
|
+
|
|
89
|
+
# Get structured data
|
|
90
|
+
schema = addr_to_nibbles("2001:db8::1")
|
|
91
|
+
print(schema.type) # "ipv6"
|
|
92
|
+
print(schema.parts) # [Part(name="net", ...), Part(name="host", ...)]
|
|
93
|
+
|
|
94
|
+
# JSON output
|
|
95
|
+
print(render_address("::1", output="json"))
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Options
|
|
99
|
+
|
|
100
|
+
| Flag | Values | Default | Description |
|
|
101
|
+
|------|--------|---------|-------------|
|
|
102
|
+
| `--type` | `auto`, `ipv6`, `ipv4`, `mac` | `auto` | Address type |
|
|
103
|
+
| `--layout` | `auto`, `grid`, `split`, `inline`, `barcode` | `auto` | Layout mode |
|
|
104
|
+
| `--width` | `N` or `auto` | `auto` | Bits per scanline row |
|
|
105
|
+
| `--scale` | `N` | `1` | Vertical scaling factor |
|
|
106
|
+
| `--invert` | flag | — | Invert filled/empty pixels |
|
|
107
|
+
| `--output` | `text`, `json` | `text` | Output format |
|
|
108
|
+
| `--random` | flag | — | Generate a random address |
|
|
109
|
+
| `--show-bits` | flag | — | Debug: print bit values |
|
|
110
|
+
| `--no-newline` | flag | — | Suppress trailing newline |
|
|
111
|
+
|
|
112
|
+
## Layouts
|
|
113
|
+
|
|
114
|
+
### Split
|
|
115
|
+
Default view. Semantic separation of address parts. For IPv6: network │ host. For MAC: OUI │ NIC.
|
|
116
|
+
|
|
117
|
+
### Grid
|
|
118
|
+
Vertical stacked version of split view.
|
|
119
|
+
|
|
120
|
+
### Inline
|
|
121
|
+
Single 1-height continuous strip. Good for logs or embedding.
|
|
122
|
+
|
|
123
|
+
### Barcode
|
|
124
|
+
Compact 2-height view.
|
|
125
|
+
|
|
126
|
+
## JSON Output
|
|
127
|
+
|
|
128
|
+
Returns a JSON object with the following fields:
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"type": "ipv6",
|
|
133
|
+
"address": "2001:db8::1",
|
|
134
|
+
"parts": [
|
|
135
|
+
{
|
|
136
|
+
"name": "net",
|
|
137
|
+
"rows": ["▀ ▀▄", "..."],
|
|
138
|
+
"bit_range": [0, 64],
|
|
139
|
+
"label": "network"
|
|
140
|
+
}
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
hexicon/__init__.py,sha256=8tf51ciIPo3aRoGHZJqsHiFqAqQXebD99PmW67K3UxI,784
|
|
2
|
+
hexicon/__main__.py,sha256=FSvg-4kY7YZpcDHpO2O1qfhrxd71X8eHYHY4figN02s,91
|
|
3
|
+
hexicon/_core.py,sha256=YzOu8JoYBnKiA9s47WExFttgHd0XUAKuP5cV6IBUoRo,10076
|
|
4
|
+
hexicon/cli.py,sha256=OgJJ6xTRXVWXA3S8gbE-4HWU7zc2e8wZM5TRhzi0kec,4165
|
|
5
|
+
hexicon-0.1.0.dist-info/METADATA,sha256=BI4CZ9ru2ADlUocpQh_5GM4jQpX2zWdqfyM2IfJTNEo,4296
|
|
6
|
+
hexicon-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
hexicon-0.1.0.dist-info/entry_points.txt,sha256=rcrUa5lFjMpBoNEpw5FLRKjuxM4fDoMQTgoVJs9Qa_s,45
|
|
8
|
+
hexicon-0.1.0.dist-info/licenses/LICENSE,sha256=6GMdiHGZ5zQ6lGYe_XIV1blfmJkndxotU7HgbGxETVM,1063
|
|
9
|
+
hexicon-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 buddha
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|