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/__init__.py +134 -0
- dbxdebug/capture_io.py +210 -0
- dbxdebug/cli.py +618 -0
- dbxdebug/dbx_kbd.py +392 -0
- dbxdebug/gdb.py +297 -0
- dbxdebug/html.py +559 -0
- dbxdebug/keyboard.py +205 -0
- dbxdebug/qmp.py +239 -0
- dbxdebug/utils.py +92 -0
- dbxdebug/video.py +239 -0
- dbxdebug-0.2.1.dist-info/METADATA +164 -0
- dbxdebug-0.2.1.dist-info/RECORD +14 -0
- dbxdebug-0.2.1.dist-info/WHEEL +4 -0
- dbxdebug-0.2.1.dist-info/entry_points.txt +2 -0
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
|
+
"<": "<",
|
|
227
|
+
">": ">",
|
|
228
|
+
"&": "&",
|
|
229
|
+
'"': """,
|
|
230
|
+
"'": "'",
|
|
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)
|