-
Notifications
You must be signed in to change notification settings - Fork 0
/
gui.py
269 lines (228 loc) · 8.57 KB
/
gui.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
import curses
from math import ceil
from emulate import Emulator
def enter_command(screen, cpu, source_pad, source_map):
"""Handle command mode."""
# Create command bar
scr_height, scr_width = screen.getmaxyx()
screen.attron(curses.color_pair(1))
screen.addstr(scr_height - 1, 0, ":" + " " * (scr_width - 2))
screen.move(scr_height - 1, 1) # Put cursor in status bar
curses.curs_set(1) # Switch cursor on
curses.echo()
screen.refresh()
# Capture command
cmd = screen.getstr(scr_height - 1, 1, 20)
message = handle_command(cmd, cpu, source_pad, source_map)
# Clear command bar
# todo: print error messages here
curses.noecho()
curses.curs_set(0)
screen.addstr(scr_height - 1, 0, " " * (scr_width - 1))
screen.attroff(curses.color_pair(1))
def handle_command(cmd, cpu, source_pad, source_map):
"""Handle the command that is typed in.
cmd is initially a byte array, so turn in to a string"""
cmd = cmd.decode("utf-8").split()
if not cmd:
return
operands = cmd[1:]
cmd = cmd[0]
if cmd == "q":
cpu.shutdown()
exit()
elif cmd == "reset":
unhighlight_source(source_pad, cpu, source_map)
cpu.reset()
highlight_source(source_pad, cpu, source_map)
elif cmd == "bd":
# Delete a breakpoint
line_address = {line: address for address, line in cpu.line_map.items()}
line = int(operands[0])
if line in line_address:
address = line_address[line]
cpu.delete_breakpoint(address)
source_pad.attron(curses.color_pair(3))
source_pad.addch(line - 1, 0, " ", curses.A_BOLD)
elif cmd == "ba":
# Add a breakpoint
line_address = {line: address for address, line in cpu.line_map.items()}
line = int(operands[0])
if line in line_address:
address = line_address[line]
cpu.add_breakpoint(address)
source_pad.attron(curses.color_pair(3))
source_pad.addch(line - 1, 0, "●", curses.A_BOLD)
def handle_step(pad, cpu, source_map):
"""Handle a single step of the CPU."""
unhighlight_source(pad, cpu, source_map)
cpu.step()
highlight_source(pad, cpu, source_map)
def highlight_source(pad, cpu, source_map):
"""Highlight the source row, based on current PC."""
if cpu.pc in source_map:
pad.attron(curses.color_pair(1))
y, text = source_map[cpu.pc]
pad.addstr(y, 1, text)
def unhighlight_source(pad, cpu, source_map):
"""Unhighlight the source row, based on current PC."""
if cpu.pc in source_map:
pad.attron(curses.color_pair(2))
y, text = source_map[cpu.pc]
pad.addstr(y, 1, text)
def draw_registers(win, cpu):
def ir2string(ir):
"""
Turn the machine code IR in to a printable string.
Chunk so easy to read
"""
s = format(ir, "016b")
return s[0:5] + " " + s[5:8] + " " + s[8:12] + " " + s[12:16]
def reg2string(reg):
"""Format the registers in to something readable."""
return f"{reg:3} 0x{reg:02X} b{reg:08b}"
win.attron(curses.color_pair(2))
win.addstr(0, 0, "Registers")
win.addstr(
1,
0,
f'S:{int(cpu.status["stop"])} C:{int(cpu.status["carry"])} Z:{int(cpu.status["zero"])}',
)
win.addstr(2, 0, f"A {reg2string(cpu.registers[0])}")
win.addstr(3, 0, f"B {reg2string(cpu.registers[1])}")
win.addstr(4, 0, f"C {reg2string(cpu.registers[2])}")
win.addstr(5, 0, f"D {reg2string(cpu.registers[3])}")
win.addstr(6, 0, f"E {reg2string(cpu.registers[4])}")
win.addstr(7, 0, f"PAGE {reg2string(cpu.registers[5])}")
win.addstr(8, 0, f"X {reg2string(cpu.registers[6])}")
win.addstr(9, 0, f"SP {reg2string(cpu.registers[7])}")
win.addstr(10, 0, f"PC 0x{cpu.pc:04X}")
win.addstr(11, 0, f"IR {ir2string(cpu.ir)}")
win.noutrefresh()
def draw_variables(win, cpu):
win.attron(curses.color_pair(2))
win.addstr(0, 0, "Variables")
y = 1
for name, v in cpu.variables.items():
values = [
cpu.memory[adr] for adr in range(v["address"], v["address"] + v["length"])
]
win.addstr(y, 0, f'{name}[{v["address"]:04X}]: {values}')
y += 3 # TODO: can I do this in the iterator?
if not cpu.variables:
win.addstr(y, 0, "--")
win.noutrefresh()
def draw_memory(pad, cpu):
pad.attron(curses.color_pair(2))
pad.addstr(0, 0, "Memory")
y = 1
for base_adr in range(0, max(cpu.memory), 8):
values = ""
for adr in range(base_adr, base_adr + 8):
try:
values += f"{cpu.memory[adr]:02X} "
except KeyError:
values += "-- "
pad.addstr(y, 0, f"{base_adr:04X} {values}")
y += 1
def run_emulator(stdscr, filename):
# Instantiate the emulator, and load the source file
cpu = Emulator(filename)
source = cpu.load_source(filename)
# Clear and refresh the screen for a blank canvas
stdscr.clear()
stdscr.refresh()
scr_height, scr_width = stdscr.getmaxyx()
# Set up colours
curses.start_color()
curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE) # Highlight
curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLACK) # Normal text
curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
# Source code pad
source_pad = curses.newpad(len(source), 100)
source_pad.attron(curses.color_pair(2))
line_address = {line: address for address, line in cpu.line_map.items()}
source_map = {}
for y, text in enumerate(source):
line_number = y + 1
try:
address = line_address[line_number]
string = f"{line_number:2d} {address:04X} {text}"
source_map[address] = (y, string)
except KeyError:
# Else the source line doenst have an address so print it but dont record
string = f"{line_number:2d} ---- {text}"
source_pad.addstr(y, 1, string)
top_row = 0
# Highlight the first row of executable code
source_pad.attron(curses.color_pair(1))
y, text = source_map[cpu.pc]
source_pad.addstr(y, 1, text)
# Width of the right hand bar
right_win_width = 30
# Registers window
reg_win_height = 12
reg_win = curses.newwin(
reg_win_height, right_win_width, 1, scr_width - right_win_width
)
# Variables window
var_win_height = 2 + len(cpu.variables) * 3 # TODO remove the *2
var_win = curses.newwin(
var_win_height, right_win_width, reg_win_height + 2, scr_width - right_win_width
)
# Memory pad
# todo: Memory on a line on its own, with a pad below so can scroll around memory
mem_pad_height = 2 + ceil(max(cpu.memory) / 8)
mem_pad = curses.newpad(mem_pad_height, right_win_width)
# Render title bar
stdscr.attron(curses.color_pair(1))
stdscr.addstr(0, 0, filename)
stdscr.addstr(0, len(filename), " " * (scr_width - len(filename) - 1))
stdscr.attroff(curses.color_pair(1))
# Render the command bar
curses.curs_set(0)
stdscr.attron(curses.color_pair(1))
stdscr.addstr(scr_height - 1, 0, " " * (scr_width - 1))
stdscr.attroff(curses.color_pair(1))
k = 0
while True:
# Code window
if k == ord(":"):
enter_command(stdscr, cpu, source_pad, source_map)
elif k == curses.KEY_DOWN:
top_row += 1
top_row = min(len(source) - scr_height + 2, top_row)
elif k == curses.KEY_UP:
top_row -= 1
top_row = max(0, top_row)
elif k == ord("s"):
handle_step(source_pad, cpu, source_map)
elif k == ord("r"):
unhighlight_source(source_pad, cpu, source_map)
cpu.run()
highlight_source(source_pad, cpu, source_map)
# Update the screen
source_pad.noutrefresh(
top_row, 0, 1, 0, scr_height - 2, scr_width - right_win_width
)
draw_registers(reg_win, cpu)
draw_variables(var_win, cpu)
draw_memory(mem_pad, cpu)
mem_pad.noutrefresh(
0,
0,
1 + reg_win_height + var_win_height + 2,
scr_width - right_win_width,
scr_height - 2,
scr_width,
)
# Refresh the screen
curses.doupdate()
# Wait for next input
k = stdscr.getch()
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("filename", help=".d8 file to load in to emulator")
args = parser.parse_args()
curses.wrapper(run_emulator, args.filename)