dbxdebug 0.2.1__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.
dbxdebug/html.py ADDED
@@ -0,0 +1,559 @@
1
+ """
2
+ HTML generation utilities for DOS video analysis.
3
+ """
4
+
5
+ # VGA color palette (RGB values)
6
+ VGA_COLORS = [
7
+ "#000000", # 0: Black
8
+ "#0000AA", # 1: Blue
9
+ "#00AA00", # 2: Green
10
+ "#00AAAA", # 3: Cyan
11
+ "#AA0000", # 4: Red
12
+ "#AA00AA", # 5: Magenta
13
+ "#AA5500", # 6: Brown
14
+ "#AAAAAA", # 7: Light Gray
15
+ "#555555", # 8: Dark Gray
16
+ "#5555FF", # 9: Light Blue
17
+ "#55FF55", # 10: Light Green
18
+ "#55FFFF", # 11: Light Cyan
19
+ "#FF5555", # 12: Light Red
20
+ "#FF55FF", # 13: Light Magenta
21
+ "#FFFF55", # 14: Yellow
22
+ "#FFFFFF", # 15: White
23
+ ]
24
+
25
+ VGA_COLOR_NAMES = [
26
+ "Black",
27
+ "Blue",
28
+ "Green",
29
+ "Cyan",
30
+ "Red",
31
+ "Magenta",
32
+ "Brown",
33
+ "Light Gray",
34
+ "Dark Gray",
35
+ "Light Blue",
36
+ "Light Green",
37
+ "Light Cyan",
38
+ "Light Red",
39
+ "Light Magenta",
40
+ "Yellow",
41
+ "White",
42
+ ]
43
+
44
+ # CP437 (DOS) to Unicode mapping for extended characters
45
+ CP437_MAP = {
46
+ # Box drawing characters (single line)
47
+ 179: "│",
48
+ 180: "┤",
49
+ 191: "┐",
50
+ 192: "└",
51
+ 193: "┴",
52
+ 194: "┬",
53
+ 195: "├",
54
+ 196: "─",
55
+ 197: "┼",
56
+ 217: "┘",
57
+ 218: "┌",
58
+ # Box drawing characters (double line)
59
+ 186: "║",
60
+ 187: "╗",
61
+ 188: "╝",
62
+ 200: "╚",
63
+ 201: "╔",
64
+ 202: "╩",
65
+ 203: "╦",
66
+ 204: "╠",
67
+ 205: "═",
68
+ 206: "╬",
69
+ # Mixed single/double box drawing
70
+ 181: "╡",
71
+ 182: "╢",
72
+ 183: "╖",
73
+ 184: "╕",
74
+ 185: "╣",
75
+ 198: "╞",
76
+ 199: "╟",
77
+ 207: "╧",
78
+ 208: "╨",
79
+ 209: "╤",
80
+ 210: "╥",
81
+ 211: "╙",
82
+ 212: "╘",
83
+ 213: "╒",
84
+ 214: "╓",
85
+ 215: "╫",
86
+ 216: "╪",
87
+ # Block elements
88
+ 176: "░",
89
+ 177: "▒",
90
+ 178: "▓",
91
+ 219: "█",
92
+ 220: "▄",
93
+ 221: "▌",
94
+ 222: "▐",
95
+ 223: "▀",
96
+ # Special characters
97
+ 1: "☺",
98
+ 2: "☻",
99
+ 3: "♥",
100
+ 4: "♦",
101
+ 5: "♣",
102
+ 6: "♠",
103
+ 7: "•",
104
+ 8: "◘",
105
+ 9: "○",
106
+ 10: "◙",
107
+ 11: "♂",
108
+ 12: "♀",
109
+ 13: "♪",
110
+ 14: "♫",
111
+ 15: "☼",
112
+ 16: "►",
113
+ 17: "◄",
114
+ 18: "↕",
115
+ 19: "‼",
116
+ 20: "¶",
117
+ 21: "§",
118
+ 22: "▬",
119
+ 23: "↨",
120
+ 24: "↑",
121
+ 25: "↓",
122
+ 26: "→",
123
+ 27: "←",
124
+ 28: "∟",
125
+ 29: "↔",
126
+ 30: "▲",
127
+ 31: "▼",
128
+ # Extended ASCII characters
129
+ 128: "Ç",
130
+ 129: "ü",
131
+ 130: "é",
132
+ 131: "â",
133
+ 132: "ä",
134
+ 133: "à",
135
+ 134: "å",
136
+ 135: "ç",
137
+ 136: "ê",
138
+ 137: "ë",
139
+ 138: "è",
140
+ 139: "ï",
141
+ 140: "î",
142
+ 141: "ì",
143
+ 142: "Ä",
144
+ 143: "Å",
145
+ 144: "É",
146
+ 145: "æ",
147
+ 146: "Æ",
148
+ 147: "ô",
149
+ 148: "ö",
150
+ 149: "ò",
151
+ 150: "û",
152
+ 151: "ù",
153
+ 152: "ÿ",
154
+ 153: "Ö",
155
+ 154: "Ü",
156
+ 155: "¢",
157
+ 156: "£",
158
+ 157: "¥",
159
+ 158: "₧",
160
+ 159: "ƒ",
161
+ 160: "á",
162
+ 161: "í",
163
+ 162: "ó",
164
+ 163: "ú",
165
+ 164: "ñ",
166
+ 165: "Ñ",
167
+ 166: "ª",
168
+ 167: "º",
169
+ 168: "¿",
170
+ 169: "⌐",
171
+ 170: "¬",
172
+ 171: "½",
173
+ 172: "¼",
174
+ 173: "¡",
175
+ 174: "«",
176
+ 175: "»",
177
+ 224: "α",
178
+ 225: "ß",
179
+ 226: "Γ",
180
+ 227: "π",
181
+ 228: "Σ",
182
+ 229: "σ",
183
+ 230: "µ",
184
+ 231: "τ",
185
+ 232: "Φ",
186
+ 233: "Θ",
187
+ 234: "Ω",
188
+ 235: "δ",
189
+ 236: "∞",
190
+ 237: "φ",
191
+ 238: "ε",
192
+ 239: "∩",
193
+ 240: "≡",
194
+ 241: "±",
195
+ 242: "≥",
196
+ 243: "≤",
197
+ 244: "⌠",
198
+ 245: "⌡",
199
+ 246: "÷",
200
+ 247: "≈",
201
+ 248: "°",
202
+ 249: "∙",
203
+ 250: "·",
204
+ 251: "√",
205
+ 252: "ⁿ",
206
+ 253: "²",
207
+ 254: "■",
208
+ 255: " ",
209
+ }
210
+
211
+
212
+ def char_to_html(char_code: int) -> str:
213
+ """Convert DOS character code to HTML-safe string with CP437 mapping."""
214
+ if char_code == 0:
215
+ return " "
216
+ elif char_code in CP437_MAP:
217
+ return CP437_MAP[char_code]
218
+ elif char_code == 32:
219
+ return " "
220
+ elif char_code < 32:
221
+ return "□"
222
+ elif char_code < 127:
223
+ char = chr(char_code)
224
+ if char in ["<", ">", "&", '"', "'"]:
225
+ return {
226
+ "<": "&lt;",
227
+ ">": "&gt;",
228
+ "&": "&amp;",
229
+ '"': "&quot;",
230
+ "'": "&#39;",
231
+ }[char]
232
+ return char
233
+ else:
234
+ return f"&#{char_code};"
235
+
236
+
237
+ def analyze_dos_video_colors(video_pages: list[bytes], cols: int = 80, rows: int = 25) -> dict:
238
+ """
239
+ Analyze all foreground and background colors used across multiple DOS video pages.
240
+
241
+ Args:
242
+ video_pages: list of bytes objects, each containing character/attribute pairs
243
+ cols: number of columns per page (default 80)
244
+ rows: number of rows per page (default 25)
245
+
246
+ Returns:
247
+ dict containing color analysis results
248
+ """
249
+ fg_colors: set[int] = set()
250
+ bg_colors: set[int] = set()
251
+ color_combinations: set[tuple[int, int]] = set()
252
+ blink_used = False
253
+
254
+ min_col = cols
255
+ max_col = -1
256
+ min_row = rows
257
+ max_row = -1
258
+ first_char_pos: tuple[int, int, int] | None = None
259
+ last_char_pos: tuple[int, int, int] | None = None
260
+
261
+ total_cells = 0
262
+ content_cells = 0
263
+ fg_color_counts = {i: 0 for i in range(16)}
264
+ bg_color_counts = {i: 0 for i in range(16)}
265
+ combination_counts: dict[str, int] = {}
266
+ page_stats = []
267
+
268
+ for page_idx, video_data in enumerate(video_pages):
269
+ if not video_data:
270
+ page_stats.append(
271
+ {"page": page_idx, "cells": 0, "fg_colors": 0, "bg_colors": 0, "combinations": 0}
272
+ )
273
+ continue
274
+
275
+ page_fg_colors: set[int] = set()
276
+ page_bg_colors: set[int] = set()
277
+ page_combinations: set[tuple[int, int]] = set()
278
+ page_cells = 0
279
+ page_content_cells = 0
280
+ page_min_col = cols
281
+ page_max_col = -1
282
+ page_min_row = rows
283
+ page_max_row = -1
284
+ page_first_char: tuple[int, int] | None = None
285
+ page_last_char: tuple[int, int] | None = None
286
+
287
+ for row in range(rows):
288
+ for col in range(cols):
289
+ offset = (row * cols + col) * 2
290
+
291
+ if offset + 1 < len(video_data):
292
+ char_code = video_data[offset]
293
+ attr_byte = video_data[offset + 1]
294
+ else:
295
+ continue
296
+
297
+ foreground = attr_byte & 0x0F
298
+ background = (attr_byte & 0x70) >> 4
299
+ blink = (attr_byte & 0x80) != 0
300
+
301
+ is_content = char_code != 0 and char_code != 32
302
+
303
+ if is_content:
304
+ content_cells += 1
305
+ page_content_cells += 1
306
+
307
+ min_col = min(min_col, col)
308
+ max_col = max(max_col, col)
309
+ min_row = min(min_row, row)
310
+ max_row = max(max_row, row)
311
+
312
+ page_min_col = min(page_min_col, col)
313
+ page_max_col = max(page_max_col, col)
314
+ page_min_row = min(page_min_row, row)
315
+ page_max_row = max(page_max_row, row)
316
+
317
+ char_pos = (page_idx, row, col)
318
+ if first_char_pos is None:
319
+ first_char_pos = char_pos
320
+ if page_first_char is None:
321
+ page_first_char = (row, col)
322
+
323
+ last_char_pos = char_pos
324
+ page_last_char = (row, col)
325
+
326
+ fg_colors.add(foreground)
327
+ bg_colors.add(background)
328
+ color_combinations.add((foreground, background))
329
+
330
+ page_fg_colors.add(foreground)
331
+ page_bg_colors.add(background)
332
+ page_combinations.add((foreground, background))
333
+
334
+ fg_color_counts[foreground] += 1
335
+ bg_color_counts[background] += 1
336
+
337
+ combination_key = f"{foreground}:{background}"
338
+ combination_counts[combination_key] = combination_counts.get(combination_key, 0) + 1
339
+
340
+ if blink:
341
+ blink_used = True
342
+
343
+ total_cells += 1
344
+ page_cells += 1
345
+
346
+ page_stats.append(
347
+ {
348
+ "page": page_idx,
349
+ "cells": page_cells,
350
+ "content_cells": page_content_cells,
351
+ "fg_colors": len(page_fg_colors),
352
+ "bg_colors": len(page_bg_colors),
353
+ "combinations": len(page_combinations),
354
+ "fg_color_list": sorted(page_fg_colors),
355
+ "bg_color_list": sorted(page_bg_colors),
356
+ "bounds": {
357
+ "min_col": page_min_col if page_content_cells > 0 else None,
358
+ "max_col": page_max_col if page_content_cells > 0 else None,
359
+ "min_row": page_min_row if page_content_cells > 0 else None,
360
+ "max_row": page_max_row if page_content_cells > 0 else None,
361
+ "width": (page_max_col - page_min_col + 1) if page_content_cells > 0 else 0,
362
+ "height": (page_max_row - page_min_row + 1) if page_content_cells > 0 else 0,
363
+ "first_char": page_first_char,
364
+ "last_char": page_last_char,
365
+ },
366
+ }
367
+ )
368
+
369
+ sorted_combinations = sorted(combination_counts.items(), key=lambda x: x[1], reverse=True)
370
+
371
+ fg_color_info = []
372
+ for color_id in sorted(fg_colors):
373
+ fg_color_info.append(
374
+ {
375
+ "id": color_id,
376
+ "name": VGA_COLOR_NAMES[color_id],
377
+ "hex": VGA_COLORS[color_id],
378
+ "count": fg_color_counts[color_id],
379
+ "percentage": (fg_color_counts[color_id] / total_cells * 100)
380
+ if total_cells > 0
381
+ else 0,
382
+ }
383
+ )
384
+
385
+ bg_color_info = []
386
+ for color_id in sorted(bg_colors):
387
+ bg_color_info.append(
388
+ {
389
+ "id": color_id,
390
+ "name": VGA_COLOR_NAMES[color_id],
391
+ "hex": VGA_COLORS[color_id],
392
+ "count": bg_color_counts[color_id],
393
+ "percentage": (bg_color_counts[color_id] / total_cells * 100)
394
+ if total_cells > 0
395
+ else 0,
396
+ }
397
+ )
398
+
399
+ combination_info = []
400
+ for combo_key, count in sorted_combinations:
401
+ fg_id, bg_id = map(int, combo_key.split(":"))
402
+ combination_info.append(
403
+ {
404
+ "foreground": {
405
+ "id": fg_id,
406
+ "name": VGA_COLOR_NAMES[fg_id],
407
+ "hex": VGA_COLORS[fg_id],
408
+ },
409
+ "background": {
410
+ "id": bg_id,
411
+ "name": VGA_COLOR_NAMES[bg_id],
412
+ "hex": VGA_COLORS[bg_id],
413
+ },
414
+ "count": count,
415
+ "percentage": (count / total_cells * 100) if total_cells > 0 else 0,
416
+ }
417
+ )
418
+
419
+ return {
420
+ "summary": {
421
+ "total_pages": len(video_pages),
422
+ "total_cells": total_cells,
423
+ "content_cells": content_cells,
424
+ "unique_fg_colors": len(fg_colors),
425
+ "unique_bg_colors": len(bg_colors),
426
+ "unique_combinations": len(color_combinations),
427
+ "blink_used": blink_used,
428
+ },
429
+ "content_bounds": {
430
+ "global": {
431
+ "min_col": min_col if content_cells > 0 else None,
432
+ "max_col": max_col if content_cells > 0 else None,
433
+ "min_row": min_row if content_cells > 0 else None,
434
+ "max_row": max_row if content_cells > 0 else None,
435
+ "width": (max_col - min_col + 1) if content_cells > 0 else 0,
436
+ "height": (max_row - min_row + 1) if content_cells > 0 else 0,
437
+ "first_char": first_char_pos,
438
+ "last_char": last_char_pos,
439
+ }
440
+ },
441
+ "foreground_colors": fg_color_info,
442
+ "background_colors": bg_color_info,
443
+ "color_combinations": combination_info,
444
+ "page_statistics": page_stats,
445
+ "raw_data": {
446
+ "fg_color_ids": sorted(fg_colors),
447
+ "bg_color_ids": sorted(bg_colors),
448
+ "fg_counts": fg_color_counts,
449
+ "bg_counts": bg_color_counts,
450
+ "combination_counts": combination_counts,
451
+ },
452
+ }
453
+
454
+
455
+ def dos_video_to_html(video_data: bytes, cols: int = 80, rows: int = 25) -> str:
456
+ """
457
+ Convert DOS video page data to HTML with colors and interactive tooltips.
458
+
459
+ Args:
460
+ video_data: bytes object containing character/attribute pairs
461
+ cols: number of columns (default 80)
462
+ rows: number of rows (default 25)
463
+
464
+ Returns:
465
+ HTML string representing the video page
466
+ """
467
+
468
+ def decode_attribute(attr_byte: int) -> tuple[int, int, bool]:
469
+ foreground = attr_byte & 0x0F
470
+ background = (attr_byte & 0x70) >> 4
471
+ blink = (attr_byte & 0x80) != 0
472
+ return foreground, background, blink
473
+
474
+ html = """<!DOCTYPE html>
475
+ <html>
476
+ <head>
477
+ <meta charset="UTF-8">
478
+ <title>DOS Video Page</title>
479
+ <style>
480
+ body {
481
+ font-family: 'Consolas', 'Monaco', monospace;
482
+ margin: 20px;
483
+ background-color: #1a1a1a;
484
+ color: #ccc;
485
+ }
486
+ .dos-screen {
487
+ display: inline-block;
488
+ background-color: #000;
489
+ border: 3px solid #555;
490
+ padding: 12px;
491
+ }
492
+ .dos-row {
493
+ display: block;
494
+ height: 16px;
495
+ line-height: 16px;
496
+ white-space: nowrap;
497
+ font-size: 0;
498
+ }
499
+ .dos-char {
500
+ display: inline-block;
501
+ width: 9px;
502
+ height: 16px;
503
+ font-size: 14px;
504
+ text-align: center;
505
+ }
506
+ .dos-char.blink {
507
+ animation: blink 1s infinite;
508
+ }
509
+ @keyframes blink {
510
+ 0%, 50% { opacity: 1; }
511
+ 51%, 100% { opacity: 0; }
512
+ }
513
+ </style>
514
+ </head>
515
+ <body>
516
+ <div class="dos-screen">
517
+ """
518
+
519
+ for row in range(rows):
520
+ html += ' <div class="dos-row">'
521
+
522
+ for col in range(cols):
523
+ offset = (row * cols + col) * 2
524
+
525
+ if offset + 1 < len(video_data):
526
+ char_code = video_data[offset]
527
+ attr_byte = video_data[offset + 1]
528
+ else:
529
+ char_code = 0
530
+ attr_byte = 0x07
531
+
532
+ fg, bg, blink = decode_attribute(attr_byte)
533
+ fg_color = VGA_COLORS[fg]
534
+ bg_color = VGA_COLORS[bg]
535
+ char_html = char_to_html(char_code)
536
+
537
+ css_class = "dos-char"
538
+ if blink:
539
+ css_class += " blink"
540
+
541
+ style = f"color: {fg_color}; background-color: {bg_color};"
542
+ html += f'<span class="{css_class}" style="{style}">{char_html}</span>'
543
+
544
+ html += "</div>\n"
545
+
546
+ html += """ </div>
547
+ </body>
548
+ </html>"""
549
+
550
+ return html
551
+
552
+
553
+ def save_dos_video_html(
554
+ video_data: bytes, filename: str = "dos_video.html", cols: int = 80, rows: int = 25
555
+ ) -> None:
556
+ """Save DOS video data as HTML file."""
557
+ html = dos_video_to_html(video_data, cols, rows)
558
+ with open(filename, "w", encoding="utf-8") as f:
559
+ f.write(html)