mos-tui 1.0.2 → 1.0.3
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.
- package/core/OS_CP.c +68 -97
- package/core/mos_tui.py +858 -492
- package/package.json +1 -1
package/core/mos_tui.py
CHANGED
|
@@ -1,91 +1,116 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
3
|
MOS Terminal UI — Multiprogramming OS Simulator
|
|
4
|
-
Run: python3 mos_tui.py
|
|
5
|
-
Compiles OS_CP.c and provides a full curses TUI.
|
|
4
|
+
Run: python3 mos_tui.py (from the folder containing OS_CP.c)
|
|
6
5
|
"""
|
|
7
6
|
|
|
8
7
|
import curses
|
|
9
|
-
import curses.textpad
|
|
10
8
|
import subprocess
|
|
11
9
|
import tempfile
|
|
12
10
|
import os
|
|
13
|
-
import sys
|
|
14
11
|
import time
|
|
15
|
-
import textwrap
|
|
16
12
|
import threading
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
13
|
+
import shutil
|
|
14
|
+
|
|
15
|
+
# ── COLORS ────────────────────────────────────────────────────────────────────
|
|
16
|
+
C_NORMAL = 1
|
|
17
|
+
C_BORDER = 2
|
|
18
|
+
C_TITLE = 3
|
|
19
|
+
C_GREEN = 4
|
|
20
|
+
C_YELLOW = 5
|
|
21
|
+
C_RED = 6
|
|
22
|
+
C_DIM = 7
|
|
23
|
+
C_HEADER = 8
|
|
24
|
+
C_INPUT = 9
|
|
25
|
+
C_ORANGE = 10
|
|
26
|
+
C_MAGENTA = 11
|
|
27
|
+
C_BLUE = 12
|
|
28
|
+
|
|
29
|
+
C_MEM_EMPTY = C_DIM
|
|
30
|
+
C_MEM_INSTR = C_BORDER
|
|
31
|
+
C_MEM_DATA = C_GREEN
|
|
32
|
+
C_MEM_IC = C_YELLOW
|
|
33
|
+
|
|
34
|
+
# ── SOURCE LOCATION ───────────────────────────────────────────────────────────
|
|
36
35
|
def _find_source():
|
|
37
|
-
# Env override — highest priority
|
|
38
36
|
env_src = os.environ.get("MOS_SOURCE")
|
|
39
37
|
if env_src and os.path.exists(env_src):
|
|
40
38
|
return os.path.abspath(env_src)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
search = parent
|
|
52
|
-
|
|
53
|
-
# Search upward from script location
|
|
54
|
-
search = os.path.dirname(os.path.abspath(__file__))
|
|
55
|
-
for _ in range(6):
|
|
56
|
-
candidate = os.path.join(search, "OS_CP.c")
|
|
57
|
-
if os.path.exists(candidate):
|
|
58
|
-
return candidate
|
|
59
|
-
parent = os.path.dirname(search)
|
|
60
|
-
if parent == search:
|
|
61
|
-
break
|
|
62
|
-
search = parent
|
|
63
|
-
|
|
64
|
-
# Fallback — cwd (will show a clear error)
|
|
39
|
+
for base in [os.getcwd(), os.path.dirname(os.path.abspath(__file__))]:
|
|
40
|
+
search = base
|
|
41
|
+
for _ in range(6):
|
|
42
|
+
c = os.path.join(search, "OS_CP.c")
|
|
43
|
+
if os.path.exists(c):
|
|
44
|
+
return c
|
|
45
|
+
parent = os.path.dirname(search)
|
|
46
|
+
if parent == search:
|
|
47
|
+
break
|
|
48
|
+
search = parent
|
|
65
49
|
return os.path.join(os.getcwd(), "OS_CP.c")
|
|
66
50
|
|
|
67
51
|
SOURCE = _find_source()
|
|
68
|
-
_CWD = os.path.dirname(SOURCE)
|
|
52
|
+
_CWD = os.path.dirname(SOURCE)
|
|
69
53
|
BINARY = os.path.join(_CWD, "mos_bin")
|
|
70
54
|
|
|
55
|
+
# ── MEMORY SNAPSHOT ───────────────────────────────────────────────────────────
|
|
56
|
+
class MemSnapshot:
|
|
57
|
+
def __init__(self):
|
|
58
|
+
self.cells = [(' ', 'E')] * 100
|
|
59
|
+
self.ic = 0
|
|
60
|
+
self.c_reg = 0
|
|
61
|
+
self.si = 0
|
|
62
|
+
self.ir = ' '
|
|
63
|
+
self.r_reg = ' '
|
|
64
|
+
self.loaded = False
|
|
65
|
+
|
|
66
|
+
def parse(self, dump_text):
|
|
67
|
+
self.cells = [(' ', 'E')] * 100
|
|
68
|
+
self.loaded = False
|
|
69
|
+
for line in dump_text.splitlines():
|
|
70
|
+
line = line.strip()
|
|
71
|
+
if not line:
|
|
72
|
+
continue
|
|
73
|
+
parts = line.split('|')
|
|
74
|
+
if parts[0] == 'REG':
|
|
75
|
+
for kv in parts[1:]:
|
|
76
|
+
if '=' in kv:
|
|
77
|
+
k, v = kv.split('=', 1)
|
|
78
|
+
if k == 'IC': self.ic = int(v)
|
|
79
|
+
elif k == 'C': self.c_reg = int(v)
|
|
80
|
+
elif k == 'SI': self.si = int(v)
|
|
81
|
+
elif k == 'IR': self.ir = v.strip()
|
|
82
|
+
elif k == 'R': self.r_reg = v.strip()
|
|
83
|
+
elif len(parts) == 3:
|
|
84
|
+
try:
|
|
85
|
+
addr = int(parts[0])
|
|
86
|
+
content = parts[1]
|
|
87
|
+
typ = parts[2]
|
|
88
|
+
if 0 <= addr < 100:
|
|
89
|
+
self.cells[addr] = (content, typ)
|
|
90
|
+
except ValueError:
|
|
91
|
+
pass
|
|
92
|
+
self.loaded = True
|
|
93
|
+
|
|
71
94
|
# ── STATE ─────────────────────────────────────────────────────────────────────
|
|
72
95
|
class State:
|
|
73
96
|
def __init__(self):
|
|
74
|
-
self.input_lines = []
|
|
75
|
-
self.input_cursor = [0, 0]
|
|
76
|
-
self.debug_lines = []
|
|
77
|
-
self.output_lines = []
|
|
78
|
-
self.run_history = []
|
|
97
|
+
self.input_lines = []
|
|
98
|
+
self.input_cursor = [0, 0]
|
|
99
|
+
self.debug_lines = []
|
|
100
|
+
self.output_lines = []
|
|
101
|
+
self.run_history = []
|
|
79
102
|
self.run_count = 0
|
|
80
103
|
self.status = "READY"
|
|
81
104
|
self.compiled = False
|
|
82
|
-
self.compile_error = ""
|
|
83
105
|
self.exec_time_ms = None
|
|
84
|
-
self.focus = 0
|
|
106
|
+
self.focus = 0
|
|
85
107
|
self.input_scroll = 0
|
|
86
108
|
self.debug_scroll = 0
|
|
87
109
|
self.output_scroll = 0
|
|
110
|
+
self.mem_scroll = 0
|
|
111
|
+
self.mem_view = 'grid'
|
|
88
112
|
self.lock = threading.Lock()
|
|
113
|
+
self.memory = MemSnapshot()
|
|
89
114
|
|
|
90
115
|
def add_debug(self, tag, msg):
|
|
91
116
|
ts = time.strftime("%H:%M:%S")
|
|
@@ -101,144 +126,193 @@ state = State()
|
|
|
101
126
|
# ── COMPILE ───────────────────────────────────────────────────────────────────
|
|
102
127
|
def compile_source():
|
|
103
128
|
if not os.path.exists(SOURCE):
|
|
104
|
-
state.add_debug("ERR",
|
|
105
|
-
state.add_debug("WARN", f"
|
|
106
|
-
state.add_debug("WARN", f"Current dir: {_CWD}")
|
|
129
|
+
state.add_debug("ERR", f"OS_CP.c not found at: {SOURCE}")
|
|
130
|
+
state.add_debug("WARN", f"Base dir searched: {_CWD}")
|
|
107
131
|
return False
|
|
108
|
-
state.add_debug("INFO", f"Compiling {SOURCE}...")
|
|
132
|
+
state.add_debug("INFO", f"Compiling {os.path.basename(SOURCE)} ...")
|
|
109
133
|
result = subprocess.run(
|
|
110
134
|
["gcc", "-o", BINARY, SOURCE, "-lm"],
|
|
111
|
-
cwd=_CWD,
|
|
112
|
-
capture_output=True, text=True
|
|
135
|
+
cwd=_CWD, capture_output=True, text=True
|
|
113
136
|
)
|
|
114
137
|
if result.returncode != 0:
|
|
115
|
-
state.compile_error = result.stderr.strip()
|
|
116
138
|
state.add_debug("ERR", "Compilation failed")
|
|
117
|
-
for
|
|
118
|
-
state.add_debug("ERR",
|
|
139
|
+
for ln in result.stderr.strip().splitlines()[:8]:
|
|
140
|
+
state.add_debug("ERR", ln)
|
|
119
141
|
return False
|
|
120
142
|
state.compiled = True
|
|
121
|
-
state.add_debug("OK", f"Compiled → {BINARY}")
|
|
143
|
+
state.add_debug("OK", f"Compiled → {os.path.basename(BINARY)}")
|
|
122
144
|
return True
|
|
123
145
|
|
|
146
|
+
# ── MEMORY RECONSTRUCTION (fallback when C has no dump_memory) ──────────────
|
|
147
|
+
def _reconstruct_memory_from_input(input_text):
|
|
148
|
+
"""Simulate LOAD phase to show memory even without dump_memory() in C."""
|
|
149
|
+
snap = state.memory
|
|
150
|
+
snap.cells = [(' ', 'E')] * 100
|
|
151
|
+
snap.ic = snap.c_reg = snap.si = 0
|
|
152
|
+
snap.ir = snap.r_reg = ' '
|
|
153
|
+
snap.loaded = False
|
|
154
|
+
|
|
155
|
+
lines = input_text.splitlines()
|
|
156
|
+
m = 0
|
|
157
|
+
in_prog = False
|
|
158
|
+
data_lines = []
|
|
159
|
+
|
|
160
|
+
for line in lines:
|
|
161
|
+
ls = line.rstrip()
|
|
162
|
+
if ls.startswith('$AMJ'):
|
|
163
|
+
snap.cells = [(' ', 'E')] * 100
|
|
164
|
+
m = 0; in_prog = True; data_lines = []
|
|
165
|
+
elif ls.startswith('$DTA'):
|
|
166
|
+
in_prog = False
|
|
167
|
+
elif ls.startswith('$END'):
|
|
168
|
+
break
|
|
169
|
+
elif in_prog:
|
|
170
|
+
k = 0
|
|
171
|
+
while k < len(ls):
|
|
172
|
+
if m >= 100: break
|
|
173
|
+
cell = (ls[k:k+4] + ' ')[:4]
|
|
174
|
+
snap.cells[m] = (cell, 'I')
|
|
175
|
+
m += 1; k += 4
|
|
176
|
+
else:
|
|
177
|
+
data_lines.append(ls)
|
|
178
|
+
|
|
179
|
+
# Place data by simulating GD instructions
|
|
180
|
+
data_idx = 0
|
|
181
|
+
for addr in range(100):
|
|
182
|
+
content, typ = snap.cells[addr]
|
|
183
|
+
if content[:2] == 'GD':
|
|
184
|
+
try:
|
|
185
|
+
d_addr = int(content[2]) * 10 + int(content[3])
|
|
186
|
+
if data_idx < len(data_lines):
|
|
187
|
+
dline = data_lines[data_idx]; data_idx += 1
|
|
188
|
+
for i in range(d_addr, min(d_addr + 10, 100)):
|
|
189
|
+
k = (i - d_addr) * 4
|
|
190
|
+
snap.cells[i] = ((dline[k:k+4] + ' ')[:4], 'D')
|
|
191
|
+
except (ValueError, IndexError):
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
snap.loaded = True
|
|
195
|
+
used = sum(1 for _, t in snap.cells if t != 'E')
|
|
196
|
+
instr = sum(1 for _, t in snap.cells if t == 'I')
|
|
197
|
+
data = sum(1 for _, t in snap.cells if t == 'D')
|
|
198
|
+
state.add_debug("OK", f"Memory reconstructed: {used}/100 ({instr} instr, {data} data)")
|
|
199
|
+
state.add_debug("INFO", "For post-execution state, use the updated OS_CP.c")
|
|
200
|
+
|
|
201
|
+
|
|
124
202
|
# ── RUN ───────────────────────────────────────────────────────────────────────
|
|
125
203
|
def run_program(input_text, callback):
|
|
126
204
|
state.status = "RUNNING"
|
|
127
205
|
state.add_debug("INFO", f"─── Run #{state.run_count} started ───")
|
|
128
206
|
|
|
129
|
-
|
|
130
|
-
run_dir = tempfile.mkdtemp(prefix='mos_run_')
|
|
207
|
+
run_dir = tempfile.mkdtemp(prefix='mos_run_')
|
|
131
208
|
fin_path = os.path.join(run_dir, "input.txt")
|
|
132
209
|
fout_path = os.path.join(run_dir, "output.txt")
|
|
210
|
+
dump_path = os.path.join(run_dir, "memory_dump.txt")
|
|
133
211
|
|
|
134
212
|
with open(fin_path, 'w') as f:
|
|
135
213
|
f.write(input_text)
|
|
136
214
|
|
|
137
|
-
state.add_debug("INFO", f"Input written → {fin_path}")
|
|
138
|
-
|
|
139
215
|
t0 = time.perf_counter()
|
|
140
216
|
try:
|
|
141
217
|
result = subprocess.run(
|
|
142
218
|
[BINARY],
|
|
143
|
-
capture_output=True,
|
|
144
|
-
|
|
145
|
-
timeout=10,
|
|
146
|
-
cwd=run_dir, # binary runs inside temp dir
|
|
219
|
+
capture_output=True, text=True, timeout=10,
|
|
220
|
+
cwd=run_dir,
|
|
147
221
|
env={**os.environ,
|
|
148
|
-
'MOS_INPUT':
|
|
149
|
-
'MOS_OUTPUT':
|
|
222
|
+
'MOS_INPUT': fin_path,
|
|
223
|
+
'MOS_OUTPUT': fout_path,
|
|
224
|
+
'MOS_MEMDUMP': dump_path}
|
|
150
225
|
)
|
|
151
226
|
elapsed_ms = (time.perf_counter() - t0) * 1000
|
|
152
|
-
|
|
153
227
|
state.add_debug("INFO", f"Process exited (code={result.returncode})")
|
|
154
228
|
|
|
155
|
-
# Parse stdout debug lines from the C program
|
|
156
229
|
for line in result.stdout.strip().splitlines():
|
|
157
230
|
line = line.strip()
|
|
158
|
-
if not line:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
elif line.startswith("AMJ") or "initializ" in line.lower():
|
|
163
|
-
state.add_debug("OK", line)
|
|
164
|
-
elif line.startswith("DTA") or "execut" in line.lower():
|
|
231
|
+
if not line: continue
|
|
232
|
+
if "initializ" in line.lower() or line.startswith("AMJ"):
|
|
233
|
+
state.add_debug("OK", line)
|
|
234
|
+
elif "execut" in line.lower() or line.startswith("DTA"):
|
|
165
235
|
state.add_debug("EXEC", line)
|
|
166
|
-
elif
|
|
167
|
-
state.add_debug("OK",
|
|
168
|
-
elif
|
|
169
|
-
state.add_debug("ERR",
|
|
236
|
+
elif "complet" in line.lower() or line.startswith("END"):
|
|
237
|
+
state.add_debug("OK", line)
|
|
238
|
+
elif any(w in line.lower() for w in ("error","invalid","overflow","not found")):
|
|
239
|
+
state.add_debug("ERR", line)
|
|
170
240
|
elif "infinite" in line.lower() or "loop" in line.lower():
|
|
171
241
|
state.add_debug("WARN", line)
|
|
172
|
-
elif "time" in line.lower():
|
|
173
|
-
state.add_debug("INFO", line)
|
|
174
242
|
else:
|
|
175
243
|
state.add_debug("INFO", line)
|
|
176
244
|
|
|
177
245
|
if result.stderr.strip():
|
|
178
|
-
for
|
|
179
|
-
state.add_debug("ERR",
|
|
246
|
+
for ln in result.stderr.strip().splitlines():
|
|
247
|
+
state.add_debug("ERR", ln)
|
|
180
248
|
|
|
181
|
-
# Read output.txt from the run directory (guaranteed location)
|
|
182
249
|
output_content = []
|
|
183
250
|
if os.path.exists(fout_path):
|
|
184
251
|
with open(fout_path) as f:
|
|
185
252
|
output_content = f.read().splitlines()
|
|
186
|
-
state.add_debug("OK", f"Output
|
|
187
|
-
else:
|
|
188
|
-
state.add_debug("WARN", f"output.txt not created at {fout_path}")
|
|
253
|
+
state.add_debug("OK", f"Output: {len(output_content)} lines")
|
|
189
254
|
|
|
190
|
-
# Debug: show raw repr of each line so we can see spaces vs real content
|
|
191
255
|
for idx, line in enumerate(output_content):
|
|
192
256
|
stripped = line.rstrip()
|
|
193
257
|
if stripped:
|
|
194
258
|
state.add_output(stripped)
|
|
195
259
|
else:
|
|
196
|
-
|
|
197
|
-
# but log them so user can see in debug
|
|
198
|
-
state.add_debug("INFO", f"output line {idx+1}: (blank/whitespace only)")
|
|
260
|
+
state.add_debug("INFO", f"output line {idx+1}: (blank)")
|
|
199
261
|
|
|
200
262
|
if result.returncode == 0:
|
|
201
263
|
if not output_content:
|
|
202
|
-
state.add_output("[output.txt
|
|
264
|
+
state.add_output("[output.txt empty — check PD address]")
|
|
203
265
|
elif not any(l.strip() for l in output_content):
|
|
204
|
-
state.add_output("[output
|
|
205
|
-
state.add_debug("WARN", "
|
|
206
|
-
|
|
266
|
+
state.add_output("[All output lines blank — GD/PD address mismatch?]")
|
|
267
|
+
state.add_debug("WARN", "GD10 reads into addr 10-19 → use PD10 to print")
|
|
268
|
+
|
|
269
|
+
state.add_debug("INFO", f"Checking dump at: {dump_path}")
|
|
270
|
+
if os.path.exists(dump_path):
|
|
271
|
+
with open(dump_path) as f:
|
|
272
|
+
dump_text = f.read()
|
|
273
|
+
state.add_debug("INFO", f"Dump size: {len(dump_text)} bytes, {len(dump_text.splitlines())} lines")
|
|
274
|
+
state.memory.parse(dump_text)
|
|
275
|
+
state.add_debug("INFO", f"Parse done, loaded={state.memory.loaded}")
|
|
276
|
+
used = sum(1 for _, t in state.memory.cells if t != 'E')
|
|
277
|
+
instr = sum(1 for _, t in state.memory.cells if t == 'I')
|
|
278
|
+
data = sum(1 for _, t in state.memory.cells if t == 'D')
|
|
279
|
+
state.add_debug("OK", f"Memory: {used}/100 cells used ({instr} instr, {data} data)")
|
|
280
|
+
else:
|
|
281
|
+
state.add_debug("ERR", f"No dump file at: {dump_path}")
|
|
282
|
+
state.add_debug("WARN", "Using OS_CP.c without dump_memory() — reconstructing from input")
|
|
283
|
+
# ── FALLBACK: reconstruct memory by simulating the LOAD phase ──
|
|
284
|
+
_reconstruct_memory_from_input(input_text)
|
|
207
285
|
|
|
208
286
|
state.exec_time_ms = elapsed_ms
|
|
209
287
|
state.run_history.append((f"Run #{state.run_count}", elapsed_ms))
|
|
210
|
-
state.add_debug("OK", f"Done in {elapsed_ms:.3f}ms
|
|
288
|
+
state.add_debug("OK", f"Done in {elapsed_ms:.3f}ms")
|
|
211
289
|
state.status = "HALTED" if result.returncode == 0 else "ERROR"
|
|
212
290
|
|
|
213
291
|
except subprocess.TimeoutExpired:
|
|
214
|
-
state.add_debug("ERR", "
|
|
292
|
+
state.add_debug("ERR", "Timed out (10s)")
|
|
215
293
|
state.status = "TIMEOUT"
|
|
216
294
|
except Exception as e:
|
|
217
295
|
state.add_debug("ERR", f"Runner error: {e}")
|
|
218
296
|
state.status = "ERROR"
|
|
219
297
|
finally:
|
|
220
|
-
import shutil
|
|
221
298
|
try: shutil.rmtree(run_dir, ignore_errors=True)
|
|
222
299
|
except: pass
|
|
223
300
|
|
|
224
301
|
callback()
|
|
225
302
|
|
|
226
|
-
# ──
|
|
303
|
+
# ── DRAW HELPERS ──────────────────────────────────────────────────────────────
|
|
227
304
|
def safe_addstr(win, y, x, s, attr=0):
|
|
228
305
|
h, w = win.getmaxyx()
|
|
229
306
|
if y < 0 or y >= h or x < 0 or x >= w:
|
|
230
307
|
return
|
|
231
|
-
|
|
232
|
-
if
|
|
233
|
-
return
|
|
308
|
+
s = s[:max(0, w - x - 1)]
|
|
309
|
+
if not s: return
|
|
234
310
|
try:
|
|
235
|
-
win.addstr(y, x, s
|
|
311
|
+
win.addstr(y, x, s, attr)
|
|
236
312
|
except curses.error:
|
|
237
313
|
pass
|
|
238
314
|
|
|
239
315
|
def draw_box(win, title="", active=False):
|
|
240
|
-
h, w = win.getmaxyx()
|
|
241
|
-
col = curses.color_pair(C_BORDER) | (curses.A_BOLD if active else 0)
|
|
242
316
|
try:
|
|
243
317
|
win.border(
|
|
244
318
|
curses.ACS_VLINE, curses.ACS_VLINE,
|
|
@@ -249,16 +323,13 @@ def draw_box(win, title="", active=False):
|
|
|
249
323
|
except curses.error:
|
|
250
324
|
pass
|
|
251
325
|
if title:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
safe_addstr(win, 0, 2, label, attr)
|
|
326
|
+
attr = (curses.color_pair(C_TITLE) | curses.A_BOLD) if active else curses.color_pair(C_DIM)
|
|
327
|
+
safe_addstr(win, 0, 2, f" {title} ", attr)
|
|
255
328
|
|
|
256
|
-
def fill_line(win, y
|
|
329
|
+
def fill_line(win, y):
|
|
257
330
|
h, w = win.getmaxyx()
|
|
258
|
-
try:
|
|
259
|
-
|
|
260
|
-
except curses.error:
|
|
261
|
-
pass
|
|
331
|
+
try: win.addstr(y, 1, " " * (w - 2))
|
|
332
|
+
except curses.error: pass
|
|
262
333
|
|
|
263
334
|
# ── PANEL: INPUT ──────────────────────────────────────────────────────────────
|
|
264
335
|
def draw_input_panel(win, active):
|
|
@@ -266,373 +337,659 @@ def draw_input_panel(win, active):
|
|
|
266
337
|
h, w = win.getmaxyx()
|
|
267
338
|
inner_h = h - 3
|
|
268
339
|
inner_w = w - 2
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
hint = " ↑↓ scroll F5 run F2 sample F3 clear "
|
|
273
|
-
safe_addstr(win, 1, 1, hint[:inner_w], curses.color_pair(C_DIM))
|
|
274
|
-
|
|
340
|
+
safe_addstr(win, 1, 1,
|
|
341
|
+
" arrows:move S:sample C:clear R:run F6:compile "[:inner_w],
|
|
342
|
+
curses.color_pair(C_DIM))
|
|
275
343
|
for row in range(inner_h):
|
|
276
344
|
li = row + state.input_scroll
|
|
277
|
-
y
|
|
345
|
+
y = row + 2
|
|
278
346
|
fill_line(win, y)
|
|
279
|
-
if li < len(
|
|
280
|
-
line =
|
|
281
|
-
col
|
|
282
|
-
# color directives
|
|
347
|
+
if li < len(state.input_lines):
|
|
348
|
+
line = state.input_lines[li]
|
|
349
|
+
col = curses.color_pair(C_INPUT)
|
|
283
350
|
if line.startswith('$'):
|
|
284
351
|
col = curses.color_pair(C_YELLOW) | curses.A_BOLD
|
|
352
|
+
elif line[:2] in ('LR','SR','CR','BT','GD','PD') or line.rstrip() == 'H':
|
|
353
|
+
col = curses.color_pair(C_BORDER)
|
|
285
354
|
safe_addstr(win, y, 1, line[:inner_w], col)
|
|
286
|
-
# draw cursor
|
|
287
355
|
cr, cc = state.input_cursor
|
|
288
356
|
if li == cr and active:
|
|
289
357
|
cx = min(cc, inner_w - 1)
|
|
358
|
+
ch = (state.input_lines[cr][cc]
|
|
359
|
+
if cr < len(state.input_lines) and cc < len(state.input_lines[cr])
|
|
360
|
+
else ' ')
|
|
290
361
|
try:
|
|
291
|
-
ch = lines[cr][cc] if cr < len(lines) and cc < len(lines[cr]) else ' '
|
|
292
362
|
win.addch(y, 1 + cx, ch, curses.color_pair(C_HEADER) | curses.A_BOLD)
|
|
293
363
|
except curses.error:
|
|
294
364
|
pass
|
|
295
365
|
|
|
296
366
|
# ── PANEL: DEBUG ──────────────────────────────────────────────────────────────
|
|
297
|
-
|
|
298
|
-
"INFO": C_BORDER,
|
|
299
|
-
"OK": C_GREEN,
|
|
300
|
-
"WARN": C_YELLOW,
|
|
301
|
-
"ERR": C_RED,
|
|
302
|
-
"EXEC": C_ORANGE,
|
|
303
|
-
}
|
|
367
|
+
TAG_ATTR = {"INFO": C_BORDER, "OK": C_GREEN, "WARN": C_YELLOW, "ERR": C_RED, "EXEC": C_ORANGE}
|
|
304
368
|
|
|
305
369
|
def draw_debug_panel(win, active):
|
|
306
|
-
draw_box(win, "DEBUG LOG [F2
|
|
307
|
-
h, w
|
|
370
|
+
draw_box(win, "DEBUG LOG [F2]", active)
|
|
371
|
+
h, w = win.getmaxyx()
|
|
308
372
|
inner_h = h - 2
|
|
309
373
|
inner_w = w - 2
|
|
310
|
-
|
|
311
|
-
lines
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if state.debug_scroll > max_scroll:
|
|
315
|
-
state.debug_scroll = max_scroll
|
|
316
|
-
|
|
374
|
+
lines = state.debug_lines
|
|
375
|
+
max_s = max(0, len(lines) - inner_h)
|
|
376
|
+
if state.debug_scroll > max_s:
|
|
377
|
+
state.debug_scroll = max_s
|
|
317
378
|
for row in range(inner_h):
|
|
318
379
|
li = row + state.debug_scroll
|
|
319
|
-
y
|
|
380
|
+
y = row + 1
|
|
320
381
|
fill_line(win, y)
|
|
321
|
-
if li >= len(lines):
|
|
322
|
-
continue
|
|
382
|
+
if li >= len(lines): continue
|
|
323
383
|
ts, tag, msg = lines[li]
|
|
324
|
-
|
|
325
384
|
x = 1
|
|
326
|
-
# timestamp
|
|
327
385
|
safe_addstr(win, y, x, ts, curses.color_pair(C_DIM))
|
|
328
386
|
x += len(ts) + 1
|
|
329
|
-
|
|
330
|
-
# tag badge
|
|
331
|
-
tag_col = curses.color_pair(TAG_COLORS.get(tag, C_DIM)) | curses.A_BOLD
|
|
332
387
|
badge = f"[{tag:<4}]"
|
|
333
|
-
safe_addstr(win, y, x, badge,
|
|
388
|
+
safe_addstr(win, y, x, badge, curses.color_pair(TAG_ATTR.get(tag, C_DIM)) | curses.A_BOLD)
|
|
334
389
|
x += len(badge) + 1
|
|
335
|
-
|
|
336
|
-
# message
|
|
337
|
-
remaining = inner_w - x
|
|
338
|
-
safe_addstr(win, y, x, msg[:remaining], curses.color_pair(C_NORMAL))
|
|
390
|
+
safe_addstr(win, y, x, msg[:inner_w - x], curses.color_pair(C_NORMAL))
|
|
339
391
|
|
|
340
392
|
# ── PANEL: OUTPUT ─────────────────────────────────────────────────────────────
|
|
341
393
|
def draw_output_panel(win, active):
|
|
342
|
-
draw_box(win, "OUTPUT [F3
|
|
343
|
-
h, w
|
|
394
|
+
draw_box(win, "OUTPUT [F3]", active)
|
|
395
|
+
h, w = win.getmaxyx()
|
|
344
396
|
inner_h = h - 2
|
|
345
397
|
inner_w = w - 2
|
|
346
|
-
|
|
347
|
-
lines
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
state.output_scroll = max_scroll
|
|
351
|
-
|
|
398
|
+
lines = state.output_lines
|
|
399
|
+
max_s = max(0, len(lines) - inner_h)
|
|
400
|
+
if state.output_scroll > max_s:
|
|
401
|
+
state.output_scroll = max_s
|
|
352
402
|
if not lines:
|
|
353
|
-
msg = "No output yet — press
|
|
354
|
-
safe_addstr(win, inner_h // 2, max(1, (inner_w - len(msg)) // 2),
|
|
403
|
+
msg = "No output yet — press R to run"
|
|
404
|
+
safe_addstr(win, inner_h // 2, max(1, (inner_w - len(msg)) // 2),
|
|
405
|
+
msg, curses.color_pair(C_DIM))
|
|
355
406
|
return
|
|
356
|
-
|
|
357
407
|
for row in range(inner_h):
|
|
358
408
|
li = row + state.output_scroll
|
|
359
|
-
y
|
|
409
|
+
y = row + 1
|
|
360
410
|
fill_line(win, y)
|
|
361
411
|
if li < len(lines):
|
|
362
412
|
line = lines[li]
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
col = curses.color_pair(C_GREEN)
|
|
367
|
-
if not line.strip():
|
|
368
|
-
safe_addstr(win, y, 5, "~", curses.color_pair(C_DIM))
|
|
413
|
+
safe_addstr(win, y, 1, f"{li+1:3} ", curses.color_pair(C_DIM))
|
|
414
|
+
if line.strip():
|
|
415
|
+
safe_addstr(win, y, 5, line[:inner_w - 5], curses.color_pair(C_GREEN))
|
|
369
416
|
else:
|
|
370
|
-
safe_addstr(win, y, 5,
|
|
371
|
-
|
|
372
|
-
# ── PANEL: CHART ──────────────────────────────────────────────────────────────
|
|
373
|
-
SPARK_CHARS = " ▁▂▃▄▅▆▇█"
|
|
417
|
+
safe_addstr(win, y, 5, "~", curses.color_pair(C_DIM))
|
|
374
418
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
419
|
+
# ── PANEL: MEMORY MAP ─────────────────────────────────────────────────────────
|
|
420
|
+
def _cell_attr(typ, is_ic):
|
|
421
|
+
if is_ic:
|
|
422
|
+
return curses.color_pair(C_MEM_IC) | curses.A_BOLD | curses.A_REVERSE
|
|
423
|
+
if typ == 'I': return curses.color_pair(C_MEM_INSTR) | curses.A_BOLD
|
|
424
|
+
if typ == 'D': return curses.color_pair(C_MEM_DATA)
|
|
425
|
+
return curses.color_pair(C_MEM_EMPTY)
|
|
426
|
+
|
|
427
|
+
def draw_memory_panel(win, active):
|
|
428
|
+
# mem_view: 'grid' | 'list' | 'chart' — Tab cycles through all three
|
|
429
|
+
h, w = win.getmaxyx()
|
|
378
430
|
inner_h = h - 2
|
|
379
431
|
inner_w = w - 2
|
|
380
|
-
y = 1
|
|
381
432
|
|
|
382
|
-
#
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
433
|
+
# ── Tab bar (row 0 = border, row 1 = tabs) ──────────────────────────────
|
|
434
|
+
view = state.mem_view
|
|
435
|
+
tabs = [('MEMORY GRID', 'grid'), ('MEMORY LIST', 'list'), ('EXEC CHART', 'chart')]
|
|
436
|
+
title_parts = []
|
|
437
|
+
for lbl, key in tabs:
|
|
438
|
+
marker = f"[{lbl}]" if view == key else f" {lbl} "
|
|
439
|
+
title_parts.append((marker, key))
|
|
440
|
+
|
|
441
|
+
# Draw box with dynamic title
|
|
442
|
+
active_lbl = next(lbl for lbl, key in tabs if key == view)
|
|
443
|
+
draw_box(win, f"{active_lbl} Tab:switch T:grid/list", active)
|
|
444
|
+
|
|
445
|
+
# ── Tab row ──────────────────────────────────────────────────────────────
|
|
446
|
+
x = 1
|
|
447
|
+
for lbl, key in tabs:
|
|
448
|
+
attr = (curses.color_pair(C_HEADER) | curses.A_BOLD) if view == key else curses.color_pair(C_DIM)
|
|
449
|
+
tag = f"[{lbl}]" if view == key else f" {lbl} "
|
|
450
|
+
safe_addstr(win, 1, x, tag, attr)
|
|
451
|
+
x += len(tag) + 1
|
|
452
|
+
|
|
453
|
+
# ── Route to sub-view ────────────────────────────────────────────────────
|
|
454
|
+
if view == 'chart':
|
|
455
|
+
_draw_chart_content(win, 2, inner_h - 2, inner_w)
|
|
456
|
+
return
|
|
386
457
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
(
|
|
391
|
-
|
|
458
|
+
mem = state.memory
|
|
459
|
+
if not mem.loaded:
|
|
460
|
+
msg = "Run a program first (R) to see memory"
|
|
461
|
+
safe_addstr(win, inner_h // 2,
|
|
462
|
+
max(1, (inner_w - len(msg)) // 2),
|
|
463
|
+
msg, curses.color_pair(C_DIM))
|
|
464
|
+
return
|
|
392
465
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
466
|
+
# Row 2: registers
|
|
467
|
+
reg_line = (f" IC={mem.ic:02d} IR={mem.ir.rstrip():<6}"
|
|
468
|
+
f"R={mem.r_reg.rstrip():<6}C={mem.c_reg} SI={mem.si}")
|
|
469
|
+
safe_addstr(win, 2, 1, reg_line[:inner_w], curses.color_pair(C_YELLOW) | curses.A_BOLD)
|
|
470
|
+
|
|
471
|
+
# Row 3: legend + stats
|
|
472
|
+
used = sum(1 for _, t in mem.cells if t != 'E')
|
|
473
|
+
instr = sum(1 for _, t in mem.cells if t == 'I')
|
|
474
|
+
data = sum(1 for _, t in mem.cells if t == 'D')
|
|
475
|
+
safe_addstr(win, 3, 1, " INSTR DATA EMPTY", curses.color_pair(C_DIM))
|
|
476
|
+
try:
|
|
477
|
+
win.addstr(3, 1, "█", curses.color_pair(C_MEM_INSTR) | curses.A_BOLD)
|
|
478
|
+
win.addstr(3, 8, "█", curses.color_pair(C_MEM_DATA) | curses.A_BOLD)
|
|
479
|
+
win.addstr(3,14, "█", curses.color_pair(C_DIM))
|
|
480
|
+
except curses.error:
|
|
481
|
+
pass
|
|
482
|
+
safe_addstr(win, 3, 21,
|
|
483
|
+
f" {used}/100 used {instr} instr {data} data"[:inner_w-21],
|
|
484
|
+
curses.color_pair(C_DIM))
|
|
485
|
+
|
|
486
|
+
content_y = 4
|
|
487
|
+
content_h = inner_h - content_y
|
|
399
488
|
|
|
400
|
-
if
|
|
489
|
+
if view == 'grid':
|
|
490
|
+
_draw_mem_grid(win, content_y, content_h, inner_w, mem)
|
|
491
|
+
else:
|
|
492
|
+
_draw_mem_list(win, content_y, content_h, inner_w, mem)
|
|
493
|
+
|
|
494
|
+
def _draw_mem_grid(win, start_y, avail_h, inner_w, mem):
|
|
495
|
+
"""
|
|
496
|
+
Adaptive grid: fills available width.
|
|
497
|
+
Each cell is "XX[CCCC]" = 8 chars. Prefix column = 0 chars (no row label).
|
|
498
|
+
Columns = max that fit: inner_w // 8, capped at 10.
|
|
499
|
+
Rows = ceil(100 / cols), displayed with scrolling.
|
|
500
|
+
"""
|
|
501
|
+
cell_w = 8 # "XX[CCCC]" — addr(2)+bracket+content(4)+bracket = 8
|
|
502
|
+
cols = max(1, min(10, inner_w // cell_w))
|
|
503
|
+
total_rows = (100 + cols - 1) // cols
|
|
504
|
+
|
|
505
|
+
max_row_s = max(0, total_rows - avail_h)
|
|
506
|
+
if state.mem_scroll > max_row_s:
|
|
507
|
+
state.mem_scroll = max_row_s
|
|
508
|
+
|
|
509
|
+
for r in range(avail_h):
|
|
510
|
+
row_idx = r + state.mem_scroll
|
|
511
|
+
if row_idx >= total_rows:
|
|
512
|
+
break
|
|
513
|
+
y = start_y + r
|
|
514
|
+
for col in range(cols):
|
|
515
|
+
addr = row_idx * cols + col
|
|
516
|
+
if addr >= 100:
|
|
517
|
+
break
|
|
518
|
+
content, typ = mem.cells[addr]
|
|
519
|
+
is_ic = (addr == mem.ic)
|
|
520
|
+
x = col * cell_w
|
|
521
|
+
if x + cell_w > inner_w:
|
|
522
|
+
break
|
|
523
|
+
# addr in dim, content colored
|
|
524
|
+
safe_addstr(win, y, x, f"{addr:02d}", curses.color_pair(C_DIM))
|
|
525
|
+
safe_addstr(win, y, x + 2, f"[{content}]", _cell_attr(typ, is_ic))
|
|
526
|
+
|
|
527
|
+
def _decode_instr(content):
|
|
528
|
+
op = content[:2].strip()
|
|
529
|
+
arg = content[2:].strip()
|
|
530
|
+
return {
|
|
531
|
+
'LR': f"Load R ← M[{arg}]",
|
|
532
|
+
'SR': f"Store M[{arg}] ← R",
|
|
533
|
+
'CR': f"Compare R, M[{arg}]",
|
|
534
|
+
'BT': f"Branch→{arg} if C=1",
|
|
535
|
+
'GD': f"Read input→M[{arg}]",
|
|
536
|
+
'PD': f"Print M[{arg}]",
|
|
537
|
+
'H': "Halt",
|
|
538
|
+
}.get(op, "")
|
|
539
|
+
|
|
540
|
+
def _draw_mem_list(win, start_y, avail_h, inner_w, mem):
|
|
541
|
+
"""Detailed list — only non-empty cells + current IC row."""
|
|
542
|
+
rows = [(i, c, t, i == mem.ic)
|
|
543
|
+
for i, (c, t) in enumerate(mem.cells)
|
|
544
|
+
if t != 'E' or i == mem.ic]
|
|
545
|
+
|
|
546
|
+
if not rows:
|
|
547
|
+
safe_addstr(win, start_y, 1, " All cells empty.", curses.color_pair(C_DIM))
|
|
401
548
|
return
|
|
402
549
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
550
|
+
max_s = max(0, len(rows) - (avail_h - 1))
|
|
551
|
+
if state.mem_scroll > max_s:
|
|
552
|
+
state.mem_scroll = max_s
|
|
406
553
|
|
|
407
|
-
#
|
|
408
|
-
safe_addstr(win,
|
|
409
|
-
|
|
554
|
+
# header
|
|
555
|
+
safe_addstr(win, start_y, 1,
|
|
556
|
+
f" {'ADDR':>4} {'CONTENT':<8} {'TYPE':<6} DECODED",
|
|
557
|
+
curses.color_pair(C_DIM) | curses.A_BOLD)
|
|
558
|
+
|
|
559
|
+
for row in range(avail_h - 1):
|
|
560
|
+
ri = row + state.mem_scroll
|
|
561
|
+
y = start_y + 1 + row
|
|
562
|
+
fill_line(win, y)
|
|
563
|
+
if ri >= len(rows): continue
|
|
564
|
+
addr, content, typ, is_ic = rows[ri]
|
|
565
|
+
type_lbl = {'I':'INSTR','D':'DATA','E':'EMPTY'}.get(typ, typ)
|
|
566
|
+
note = "← IC" if is_ic else (_decode_instr(content) if typ == 'I' else "")
|
|
567
|
+
line = f" {addr:>4} {repr(content):<10} {type_lbl:<6} {note}"
|
|
568
|
+
safe_addstr(win, y, 1, line[:inner_w], _cell_attr(typ, is_ic))
|
|
569
|
+
|
|
570
|
+
# ── PANEL: CHART ──────────────────────────────────────────────────────────────
|
|
571
|
+
SPARK = " ▁▂▃▄▅▆▇█"
|
|
572
|
+
|
|
573
|
+
def _draw_chart_content(win, start_y, avail_h, inner_w):
|
|
574
|
+
"""Draw the execution time chart starting at start_y inside any window."""
|
|
575
|
+
hist = state.run_history
|
|
576
|
+
y = start_y
|
|
577
|
+
|
|
578
|
+
# Stats row
|
|
579
|
+
et = f"{state.exec_time_ms:.3f}ms" if state.exec_time_ms else "--"
|
|
580
|
+
cw = inner_w // 3
|
|
581
|
+
for i, (lbl, val, col) in enumerate([
|
|
582
|
+
("EXEC TIME", et, C_GREEN),
|
|
583
|
+
("RUNS", str(state.run_count), C_BORDER),
|
|
584
|
+
("LAST RUN", hist[-1][0] if hist else "--", C_YELLOW),
|
|
585
|
+
]):
|
|
586
|
+
bx = 1 + i * cw
|
|
587
|
+
safe_addstr(win, y, bx, lbl[:cw-1], curses.color_pair(C_DIM))
|
|
588
|
+
safe_addstr(win, y+1, bx, val[:cw-1], curses.color_pair(col) | curses.A_BOLD)
|
|
589
|
+
y += 3
|
|
590
|
+
|
|
591
|
+
if y - start_y >= avail_h:
|
|
592
|
+
return
|
|
593
|
+
safe_addstr(win, y, 1, "─" * inner_w, curses.color_pair(C_DIM)); y += 1
|
|
594
|
+
safe_addstr(win, y, 1, " EXEC TIME HISTORY (ms):", curses.color_pair(C_DIM)); y += 1
|
|
410
595
|
|
|
411
596
|
if not hist:
|
|
412
|
-
safe_addstr(win, y, 1, " No runs yet", curses.color_pair(C_DIM))
|
|
597
|
+
safe_addstr(win, y, 1, " No runs yet — press R to run", curses.color_pair(C_DIM))
|
|
413
598
|
return
|
|
414
599
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
600
|
+
h, _ = win.getmaxyx()
|
|
601
|
+
chart_bottom = start_y + avail_h # don't draw below this row
|
|
602
|
+
bar_area = chart_bottom - y - 2 # rows available for bars
|
|
603
|
+
if bar_area < 2:
|
|
418
604
|
return
|
|
419
605
|
|
|
420
|
-
max_ms
|
|
421
|
-
|
|
606
|
+
max_ms = max(r[1] for r in hist)
|
|
607
|
+
bar_w, sp = 5, 1
|
|
608
|
+
max_bars = max(1, inner_w // (bar_w + sp))
|
|
609
|
+
visible = hist[-max_bars:]
|
|
610
|
+
BAR_COLS = [C_BORDER, C_GREEN, C_YELLOW, C_ORANGE, C_RED]
|
|
422
611
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
612
|
+
for bi, (label, ms) in enumerate(visible):
|
|
613
|
+
bh = max(1, int((ms / max_ms) * bar_area)) if max_ms > 0 else 1
|
|
614
|
+
bx = 1 + bi * (bar_w + sp)
|
|
615
|
+
col = curses.color_pair(BAR_COLS[bi % len(BAR_COLS)]) | curses.A_BOLD
|
|
616
|
+
for brow in range(bar_area):
|
|
617
|
+
ry = y + bar_area - 1 - brow
|
|
618
|
+
if ry >= h - 1 or ry >= chart_bottom:
|
|
619
|
+
continue
|
|
620
|
+
safe_addstr(win, ry, bx,
|
|
621
|
+
("█" * bar_w) if brow < bh else ("░" * bar_w),
|
|
622
|
+
col if brow < bh else curses.color_pair(C_DIM))
|
|
623
|
+
# value label above bar
|
|
624
|
+
val_row = y - 1
|
|
625
|
+
if val_row >= start_y:
|
|
626
|
+
safe_addstr(win, val_row, bx,
|
|
627
|
+
f"{ms:.1f}"[:bar_w].center(bar_w),
|
|
628
|
+
curses.color_pair(C_YELLOW))
|
|
629
|
+
# run label below bar
|
|
630
|
+
lbl_row = y + bar_area
|
|
631
|
+
if lbl_row < chart_bottom:
|
|
632
|
+
safe_addstr(win, lbl_row, bx,
|
|
633
|
+
label.split()[-1][:bar_w].center(bar_w),
|
|
634
|
+
curses.color_pair(C_DIM))
|
|
635
|
+
|
|
636
|
+
# Sparkline
|
|
637
|
+
spark_y = y + bar_area + 1
|
|
638
|
+
if spark_y < chart_bottom and len(hist) > 1:
|
|
639
|
+
max_v = max(r[1] for r in hist)
|
|
640
|
+
SPARK = " ▁▂▃▄▅▆▇█"
|
|
641
|
+
sp_str = "".join(SPARK[min(8, int((r[1]/max_v)*8))] for r in hist[-inner_w+4:])
|
|
642
|
+
safe_addstr(win, spark_y, 1, "▸ " + sp_str, curses.color_pair(C_BORDER))
|
|
428
643
|
|
|
429
|
-
bar_colors = [C_BORDER, C_GREEN, C_YELLOW, C_ORANGE, C_RED]
|
|
430
644
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
645
|
+
def draw_chart_panel(win, active):
|
|
646
|
+
draw_box(win, "EXEC TIME CHART", active)
|
|
647
|
+
h, w = win.getmaxyx()
|
|
648
|
+
inner_h = h - 2
|
|
649
|
+
inner_w = w - 2
|
|
650
|
+
hist = state.run_history
|
|
651
|
+
y = 1
|
|
652
|
+
|
|
653
|
+
cw = inner_w // 3
|
|
654
|
+
for i, (lbl, val, col) in enumerate([
|
|
655
|
+
("EXEC TIME", f"{state.exec_time_ms:.3f}ms" if state.exec_time_ms else "--", C_GREEN),
|
|
656
|
+
("RUNS", str(state.run_count), C_BORDER),
|
|
657
|
+
("LAST", hist[-1][0] if hist else "--", C_YELLOW),
|
|
658
|
+
]):
|
|
659
|
+
bx = 1 + i * cw
|
|
660
|
+
safe_addstr(win, y, bx, lbl[:cw-1], curses.color_pair(C_DIM))
|
|
661
|
+
safe_addstr(win, y+1, bx, val[:cw-1], curses.color_pair(col) | curses.A_BOLD)
|
|
662
|
+
y += 3
|
|
435
663
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
ry = y + bar_area_h - 1 - brow
|
|
439
|
-
if ry >= h - 1:
|
|
440
|
-
continue
|
|
441
|
-
if brow < bar_height:
|
|
442
|
-
safe_addstr(win, ry, bx, "█" * bar_w, col)
|
|
443
|
-
else:
|
|
444
|
-
safe_addstr(win, ry, bx, "░" * bar_w, curses.color_pair(C_DIM))
|
|
664
|
+
safe_addstr(win, y, 1, "─" * inner_w, curses.color_pair(C_DIM)); y += 1
|
|
665
|
+
safe_addstr(win, y, 1, " EXEC TIME HISTORY (ms):", curses.color_pair(C_DIM)); y += 1
|
|
445
666
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
667
|
+
if not hist:
|
|
668
|
+
safe_addstr(win, y, 1, " No runs yet", curses.color_pair(C_DIM))
|
|
669
|
+
return
|
|
449
670
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
671
|
+
chart_h = inner_h - (y - 1)
|
|
672
|
+
if chart_h < 3: return
|
|
673
|
+
bar_area = chart_h - 2
|
|
674
|
+
max_ms = max(r[1] for r in hist)
|
|
675
|
+
bar_w, sp = 5, 1
|
|
676
|
+
max_bars = max(1, inner_w // (bar_w + sp))
|
|
677
|
+
visible = hist[-max_bars:]
|
|
678
|
+
BAR_COLS = [C_BORDER, C_GREEN, C_YELLOW, C_ORANGE, C_RED]
|
|
455
679
|
|
|
456
|
-
|
|
457
|
-
|
|
680
|
+
for bi, (label, ms) in enumerate(visible):
|
|
681
|
+
bh = max(1, int((ms / max_ms) * bar_area)) if max_ms > 0 else 1
|
|
682
|
+
bx = 1 + bi * (bar_w + sp)
|
|
683
|
+
col = curses.color_pair(BAR_COLS[bi % len(BAR_COLS)]) | curses.A_BOLD
|
|
684
|
+
for brow in range(bar_area):
|
|
685
|
+
ry = y + bar_area - 1 - brow
|
|
686
|
+
if ry >= h - 1: continue
|
|
687
|
+
safe_addstr(win, ry, bx,
|
|
688
|
+
("█" * bar_w) if brow < bh else ("░" * bar_w),
|
|
689
|
+
col if brow < bh else curses.color_pair(C_DIM))
|
|
690
|
+
safe_addstr(win, y-1, bx, f"{ms:.1f}"[:bar_w].center(bar_w), curses.color_pair(C_YELLOW))
|
|
691
|
+
if y + bar_area < h - 1:
|
|
692
|
+
safe_addstr(win, y + bar_area, bx, label.split()[-1][:bar_w].center(bar_w),
|
|
693
|
+
curses.color_pair(C_DIM))
|
|
694
|
+
|
|
695
|
+
spark_y = y + bar_area + 1
|
|
458
696
|
if spark_y < h - 1 and len(hist) > 1:
|
|
459
|
-
max_v
|
|
460
|
-
|
|
461
|
-
safe_addstr(win, spark_y, 1, "▸ " +
|
|
462
|
-
|
|
463
|
-
# ── STATUS BAR ────────────────────────────────────────────────────────────────
|
|
464
|
-
STATUS_COLORS = {
|
|
465
|
-
"READY": C_GREEN,
|
|
466
|
-
"RUNNING": C_YELLOW,
|
|
467
|
-
"HALTED": C_BORDER,
|
|
468
|
-
"ERROR": C_RED,
|
|
469
|
-
"TIMEOUT": C_RED,
|
|
470
|
-
"COMPILING": C_YELLOW,
|
|
471
|
-
}
|
|
697
|
+
max_v = max(r[1] for r in hist)
|
|
698
|
+
sp_str = "".join(SPARK[min(8, int((r[1]/max_v)*8))] for r in hist[-inner_w+4:])
|
|
699
|
+
safe_addstr(win, spark_y, 1, "▸ " + sp_str, curses.color_pair(C_BORDER))
|
|
472
700
|
|
|
473
|
-
|
|
474
|
-
|
|
701
|
+
# ── STATUS / TITLE ────────────────────────────────────────────────────────────
|
|
702
|
+
def draw_titlebar(stdscr, W):
|
|
703
|
+
comp = " [COMPILED ✓]" if state.compiled else " [NOT COMPILED — F6]"
|
|
704
|
+
full = f" MOS Multiprogramming OS Simulator ── {os.path.basename(SOURCE)}{comp}"
|
|
475
705
|
try:
|
|
476
|
-
stdscr.addstr(
|
|
706
|
+
stdscr.addstr(0, 0, " " * (W - 1), curses.color_pair(C_HEADER))
|
|
707
|
+
stdscr.addstr(0, 0, full[:W-1], curses.color_pair(C_HEADER) | curses.A_BOLD)
|
|
477
708
|
except curses.error:
|
|
478
709
|
pass
|
|
479
710
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
rx = W - len(right) - 1
|
|
485
|
-
if rx > len(left):
|
|
486
|
-
safe_addstr(stdscr, y, rx, right, curses.color_pair(C_HEADER))
|
|
487
|
-
|
|
488
|
-
def draw_titlebar(stdscr, W):
|
|
489
|
-
title = " ┌─ MOS ─┐ Multiprogramming OS Simulator ── OS_CP.c "
|
|
490
|
-
compiled_str = " [COMPILED ✓]" if state.compiled else " [NOT COMPILED]"
|
|
491
|
-
full = title + compiled_str
|
|
492
|
-
col = curses.color_pair(C_HEADER) | curses.A_BOLD
|
|
711
|
+
def draw_statusbar(stdscr, H, W):
|
|
712
|
+
y = H - 1
|
|
713
|
+
left = f" {state.status} │ Runs:{state.run_count} │ {_CWD}"
|
|
714
|
+
right = " F1:Input F2:Debug F3:Output F4:Memory F5:Metrics R:Run F6:Compile Q:Quit "
|
|
493
715
|
try:
|
|
494
|
-
stdscr.addstr(
|
|
495
|
-
stdscr.addstr(0, 0, full[:W-1], col)
|
|
716
|
+
stdscr.addstr(y, 0, " " * (W - 1), curses.color_pair(C_HEADER))
|
|
496
717
|
except curses.error:
|
|
497
718
|
pass
|
|
719
|
+
safe_addstr(stdscr, y, 0, left[:W-1], curses.color_pair(C_HEADER) | curses.A_BOLD)
|
|
720
|
+
rx = W - len(right) - 1
|
|
721
|
+
if rx > len(left):
|
|
722
|
+
safe_addstr(stdscr, y, rx, right, curses.color_pair(C_HEADER))
|
|
498
723
|
|
|
499
|
-
# ──
|
|
724
|
+
# ── SAMPLES ───────────────────────────────────────────────────────────────────
|
|
500
725
|
SAMPLES = [
|
|
501
|
-
|
|
502
|
-
"
|
|
503
|
-
|
|
504
|
-
PD10
|
|
505
|
-
H
|
|
506
|
-
$DTA
|
|
507
|
-
Hello MOS!
|
|
508
|
-
$END""",
|
|
509
|
-
# Sample 2: Read two lines, print both
|
|
510
|
-
"""$AMJ
|
|
511
|
-
GD10
|
|
512
|
-
PD10
|
|
513
|
-
GD30
|
|
514
|
-
PD30
|
|
515
|
-
H
|
|
516
|
-
$DTA
|
|
517
|
-
First Line
|
|
518
|
-
Second Line
|
|
519
|
-
$END""",
|
|
520
|
-
# Sample 3: Load, store, compare, branch
|
|
521
|
-
"""$AMJ
|
|
522
|
-
GD10
|
|
523
|
-
LR10
|
|
524
|
-
SR20
|
|
525
|
-
PD20
|
|
526
|
-
H
|
|
527
|
-
$DTA
|
|
528
|
-
Stored Data
|
|
529
|
-
$END""",
|
|
726
|
+
"$AMJ\nGD10\nPD10\nH\n$DTA\nHello MOS!\n$END",
|
|
727
|
+
"$AMJ\nGD10\nPD10\nGD30\nPD30\nH\n$DTA\nFirst Line\nSecond Line\n$END",
|
|
728
|
+
"$AMJ\nGD10\nLR10\nSR20\nPD20\nH\n$DTA\nStored Data\n$END",
|
|
530
729
|
]
|
|
531
|
-
|
|
730
|
+
_sidx = [0]
|
|
532
731
|
|
|
533
732
|
def load_sample():
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
state.input_lines = prog.splitlines()
|
|
733
|
+
state.input_lines = SAMPLES[_sidx[0] % len(SAMPLES)].strip().splitlines()
|
|
734
|
+
_sidx[0] += 1
|
|
537
735
|
state.input_cursor = [0, 0]
|
|
538
736
|
state.input_scroll = 0
|
|
539
|
-
state.add_debug("INFO", f"Sample #{
|
|
737
|
+
state.add_debug("INFO", f"Sample #{_sidx[0]} loaded — press R to run")
|
|
540
738
|
|
|
541
|
-
# ──
|
|
739
|
+
# ── INPUT EDITING ─────────────────────────────────────────────────────────────
|
|
542
740
|
def ensure_line(r):
|
|
543
741
|
while len(state.input_lines) <= r:
|
|
544
742
|
state.input_lines.append("")
|
|
545
743
|
|
|
546
|
-
def cur_line():
|
|
547
|
-
r, c = state.input_cursor
|
|
548
|
-
ensure_line(r)
|
|
549
|
-
return state.input_lines[r]
|
|
550
|
-
|
|
551
744
|
def input_key(key, panel_h):
|
|
552
|
-
r, c
|
|
745
|
+
r, c = state.input_cursor
|
|
553
746
|
inner_h = panel_h - 3
|
|
554
|
-
|
|
555
747
|
if key == curses.KEY_UP:
|
|
556
748
|
if r > 0:
|
|
557
|
-
r -= 1
|
|
558
|
-
|
|
559
|
-
if r < state.input_scroll:
|
|
560
|
-
state.input_scroll = r
|
|
749
|
+
r -= 1; c = min(c, len(state.input_lines[r]) if r < len(state.input_lines) else 0)
|
|
750
|
+
if r < state.input_scroll: state.input_scroll = r
|
|
561
751
|
elif key == curses.KEY_DOWN:
|
|
562
752
|
if r < len(state.input_lines) - 1:
|
|
563
|
-
r += 1
|
|
564
|
-
|
|
565
|
-
if r >= state.input_scroll + inner_h:
|
|
566
|
-
state.input_scroll = r - inner_h + 1
|
|
753
|
+
r += 1; c = min(c, len(state.input_lines[r]))
|
|
754
|
+
if r >= state.input_scroll + inner_h: state.input_scroll = r - inner_h + 1
|
|
567
755
|
elif key == curses.KEY_LEFT:
|
|
568
|
-
if c > 0:
|
|
569
|
-
|
|
570
|
-
elif r > 0:
|
|
571
|
-
r -= 1
|
|
572
|
-
c = len(state.input_lines[r])
|
|
756
|
+
if c > 0: c -= 1
|
|
757
|
+
elif r > 0: r -= 1; c = len(state.input_lines[r])
|
|
573
758
|
elif key == curses.KEY_RIGHT:
|
|
574
|
-
line = cur_line()
|
|
575
|
-
if c < len(line):
|
|
576
|
-
c += 1
|
|
577
|
-
elif r < len(state.input_lines) - 1:
|
|
578
|
-
r += 1; c = 0
|
|
579
|
-
elif key == curses.KEY_HOME:
|
|
580
|
-
c = 0
|
|
581
|
-
elif key == curses.KEY_END:
|
|
582
|
-
c = len(cur_line())
|
|
583
|
-
elif key in (curses.KEY_BACKSPACE, 127, 8):
|
|
584
759
|
ensure_line(r)
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
760
|
+
if c < len(state.input_lines[r]): c += 1
|
|
761
|
+
elif r < len(state.input_lines) - 1: r += 1; c = 0
|
|
762
|
+
elif key == curses.KEY_HOME: c = 0
|
|
763
|
+
elif key == curses.KEY_END: ensure_line(r); c = len(state.input_lines[r])
|
|
764
|
+
elif key in (curses.KEY_BACKSPACE, 127, 8):
|
|
765
|
+
ensure_line(r); ln = state.input_lines[r]
|
|
766
|
+
if c > 0: state.input_lines[r] = ln[:c-1] + ln[c:]; c -= 1
|
|
589
767
|
elif r > 0:
|
|
590
|
-
prev = state.input_lines[r-1]
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
elif key == curses.KEY_DC: # Delete
|
|
596
|
-
ensure_line(r)
|
|
597
|
-
line = state.input_lines[r]
|
|
598
|
-
if c < len(line):
|
|
599
|
-
state.input_lines[r] = line[:c] + line[c+1:]
|
|
768
|
+
prev = state.input_lines[r-1]; c = len(prev)
|
|
769
|
+
state.input_lines[r-1] = prev + ln; del state.input_lines[r]; r -= 1
|
|
770
|
+
elif key == curses.KEY_DC:
|
|
771
|
+
ensure_line(r); ln = state.input_lines[r]
|
|
772
|
+
if c < len(ln): state.input_lines[r] = ln[:c] + ln[c+1:]
|
|
600
773
|
elif r < len(state.input_lines) - 1:
|
|
601
|
-
state.input_lines[r] =
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
line = state.input_lines[r]
|
|
606
|
-
state.input_lines[r] = line[:c]
|
|
607
|
-
state.input_lines.insert(r+1, line[c:])
|
|
774
|
+
state.input_lines[r] = ln + state.input_lines[r+1]; del state.input_lines[r+1]
|
|
775
|
+
elif key in (10, 13):
|
|
776
|
+
ensure_line(r); ln = state.input_lines[r]
|
|
777
|
+
state.input_lines[r] = ln[:c]; state.input_lines.insert(r+1, ln[c:])
|
|
608
778
|
r += 1; c = 0
|
|
609
|
-
if r >= state.input_scroll + inner_h:
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
line = state.input_lines[r]
|
|
614
|
-
state.input_lines[r] = line[:c] + chr(key) + line[c:]
|
|
615
|
-
c += 1
|
|
616
|
-
|
|
779
|
+
if r >= state.input_scroll + inner_h: state.input_scroll += 1
|
|
780
|
+
elif 32 <= key <= 126:
|
|
781
|
+
ensure_line(r); ln = state.input_lines[r]
|
|
782
|
+
state.input_lines[r] = ln[:c] + chr(key) + ln[c:]; c += 1
|
|
617
783
|
state.input_cursor = [r, c]
|
|
618
784
|
|
|
619
|
-
# ── MAIN
|
|
785
|
+
# ── MAIN ──────────────────────────────────────────────────────────────────────
|
|
786
|
+
|
|
787
|
+
# ── IMPROVED MEMORY GRID PANEL ────────────────────────────────────────────────
|
|
788
|
+
def draw_mem_grid_panel(win, active):
|
|
789
|
+
"""
|
|
790
|
+
Standalone memory grid panel with improved spacing and visual design.
|
|
791
|
+
Layout:
|
|
792
|
+
Row 0 : border + title
|
|
793
|
+
Row 1 : register bar
|
|
794
|
+
Row 2 : separator + legend
|
|
795
|
+
Row 3+ : grid cells, each rendered as a styled block
|
|
796
|
+
Each cell: addr(2) + space + [content(4)] = 9 chars wide with 1-char gap = 10
|
|
797
|
+
Colour: cyan=instruction green=data dim=empty yellow+reverse=IC pointer
|
|
798
|
+
"""
|
|
799
|
+
draw_box(win, "MEMORY [F4] ↑↓scroll", active)
|
|
800
|
+
h, w = win.getmaxyx()
|
|
801
|
+
inner_h = h - 2
|
|
802
|
+
inner_w = w - 2
|
|
803
|
+
mem = state.memory
|
|
804
|
+
|
|
805
|
+
if not mem.loaded:
|
|
806
|
+
msg = "No memory yet — run a program (R)"
|
|
807
|
+
safe_addstr(win, inner_h // 2,
|
|
808
|
+
max(1, (inner_w - len(msg)) // 2),
|
|
809
|
+
msg, curses.color_pair(C_DIM))
|
|
810
|
+
return
|
|
811
|
+
|
|
812
|
+
# ── Row 1: register bar ───────────────────────────────────────────────────
|
|
813
|
+
ir_s = mem.ir.rstrip() or '----'
|
|
814
|
+
r_s = mem.r_reg.rstrip() or '----'
|
|
815
|
+
reg = f" IC={mem.ic:02d} IR={ir_s:<4} R={r_s:<4} C={mem.c_reg} SI={mem.si}"
|
|
816
|
+
safe_addstr(win, 1, 1, reg[:inner_w], curses.color_pair(C_YELLOW) | curses.A_BOLD)
|
|
817
|
+
|
|
818
|
+
# ── Row 2: legend ─────────────────────────────────────────────────────────
|
|
819
|
+
used = sum(1 for _, t in mem.cells if t != 'E')
|
|
820
|
+
instr = sum(1 for _, t in mem.cells if t == 'I')
|
|
821
|
+
data = sum(1 for _, t in mem.cells if t == 'D')
|
|
822
|
+
stats = f" {used}/100 cells {instr} instr {data} data"
|
|
823
|
+
safe_addstr(win, 2, 1, stats[:inner_w], curses.color_pair(C_DIM))
|
|
824
|
+
# colour swatches
|
|
825
|
+
try:
|
|
826
|
+
win.addstr(2, 2, "▐", curses.color_pair(C_MEM_INSTR) | curses.A_BOLD)
|
|
827
|
+
win.addstr(2, 12, "▐", curses.color_pair(C_MEM_DATA))
|
|
828
|
+
except curses.error:
|
|
829
|
+
pass
|
|
830
|
+
|
|
831
|
+
# ── Row 3: column headers ─────────────────────────────────────────────────
|
|
832
|
+
# cell_w = 9: " XX[CCCC]" — space + addr(2) + bracket + content(4) + bracket
|
|
833
|
+
cell_w = 9
|
|
834
|
+
cols = max(1, min(10, inner_w // cell_w))
|
|
835
|
+
# header row: column indices 0..cols-1
|
|
836
|
+
hdr = ""
|
|
837
|
+
for c in range(cols):
|
|
838
|
+
hdr += f" +{c} "[:cell_w]
|
|
839
|
+
safe_addstr(win, 3, 1, hdr[:inner_w], curses.color_pair(C_DIM))
|
|
840
|
+
|
|
841
|
+
# ── Rows 4+: grid ─────────────────────────────────────────────────────────
|
|
842
|
+
content_y = 4
|
|
843
|
+
avail_h = inner_h - content_y
|
|
844
|
+
total_rows = (100 + cols - 1) // cols
|
|
845
|
+
|
|
846
|
+
max_scroll = max(0, total_rows - avail_h)
|
|
847
|
+
if state.mem_scroll > max_scroll:
|
|
848
|
+
state.mem_scroll = max_scroll
|
|
849
|
+
|
|
850
|
+
for r in range(avail_h):
|
|
851
|
+
row_idx = r + state.mem_scroll
|
|
852
|
+
if row_idx >= total_rows:
|
|
853
|
+
break
|
|
854
|
+
y = content_y + r
|
|
855
|
+
|
|
856
|
+
# row base address label
|
|
857
|
+
base = row_idx * cols
|
|
858
|
+
lbl = f"{base:02d}"
|
|
859
|
+
safe_addstr(win, y, 1, lbl, curses.color_pair(C_DIM) | curses.A_BOLD)
|
|
860
|
+
|
|
861
|
+
for c in range(cols):
|
|
862
|
+
addr = base + c
|
|
863
|
+
if addr >= 100:
|
|
864
|
+
break
|
|
865
|
+
cell_content, typ = mem.cells[addr]
|
|
866
|
+
is_ic = (addr == mem.ic)
|
|
867
|
+
x = 1 + c * cell_w # start of this cell
|
|
868
|
+
|
|
869
|
+
# addr in dim (small, 2 chars)
|
|
870
|
+
safe_addstr(win, y, x, f"{addr:02d}", curses.color_pair(C_DIM))
|
|
871
|
+
|
|
872
|
+
# content block: [CCCC]
|
|
873
|
+
bracket_attr = _cell_attr(typ, is_ic)
|
|
874
|
+
if is_ic:
|
|
875
|
+
block = f">{cell_content}<"
|
|
876
|
+
elif typ == 'E':
|
|
877
|
+
block = " · " # empty placeholder — dots look cleaner
|
|
878
|
+
else:
|
|
879
|
+
block = cell_content # exactly 4 chars from dump
|
|
880
|
+
safe_addstr(win, y, x + 2, f"[{block}]", bracket_attr)
|
|
881
|
+
|
|
882
|
+
# ── Scrollbar indicator ───────────────────────────────────────────────────
|
|
883
|
+
if total_rows > avail_h:
|
|
884
|
+
pct = state.mem_scroll / max(1, total_rows - avail_h)
|
|
885
|
+
bar_y = content_y + int(pct * (avail_h - 1))
|
|
886
|
+
safe_addstr(win, bar_y, inner_w, "▐", curses.color_pair(C_DIM))
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
# ── EXEC METRICS PANEL ────────────────────────────────────────────────────────
|
|
890
|
+
def draw_metrics_panel(win, active):
|
|
891
|
+
draw_box(win, "EXECUTION METRICS [F5]", active)
|
|
892
|
+
h, w = win.getmaxyx()
|
|
893
|
+
inner_h = h - 2
|
|
894
|
+
inner_w = w - 2
|
|
895
|
+
hist = state.run_history
|
|
896
|
+
y = 1
|
|
897
|
+
|
|
898
|
+
# ── Stat cards ────────────────────────────────────────────────────────────
|
|
899
|
+
et = f"{state.exec_time_ms:.4f}ms" if state.exec_time_ms else "---"
|
|
900
|
+
cw = max(1, inner_w // 3)
|
|
901
|
+
for i, (lbl, val, col) in enumerate([
|
|
902
|
+
("EXEC TIME", et, C_GREEN),
|
|
903
|
+
("TOTAL RUNS", str(state.run_count), C_BORDER),
|
|
904
|
+
("LAST RUN", hist[-1][0] if hist else "---", C_YELLOW),
|
|
905
|
+
]):
|
|
906
|
+
bx = 1 + i * cw
|
|
907
|
+
safe_addstr(win, y, bx, lbl[:cw-1],
|
|
908
|
+
curses.color_pair(C_DIM))
|
|
909
|
+
safe_addstr(win, y+1, bx, val[:cw-1],
|
|
910
|
+
curses.color_pair(col) | curses.A_BOLD)
|
|
911
|
+
y += 3
|
|
912
|
+
|
|
913
|
+
safe_addstr(win, y, 1, "─" * inner_w, curses.color_pair(C_DIM)); y += 1
|
|
914
|
+
|
|
915
|
+
if not hist:
|
|
916
|
+
safe_addstr(win, y, 1, " Run a program to see chart", curses.color_pair(C_DIM))
|
|
917
|
+
return
|
|
918
|
+
|
|
919
|
+
# ── Bar chart ─────────────────────────────────────────────────────────────
|
|
920
|
+
safe_addstr(win, y, 1, " EXEC TIME (ms) per run:",
|
|
921
|
+
curses.color_pair(C_DIM)); y += 1
|
|
922
|
+
|
|
923
|
+
bottom = inner_h # last usable row inside border
|
|
924
|
+
bar_area = bottom - y - 2
|
|
925
|
+
if bar_area < 2:
|
|
926
|
+
return
|
|
927
|
+
|
|
928
|
+
max_ms = max(r[1] for r in hist)
|
|
929
|
+
BAR_W, SP = 6, 2
|
|
930
|
+
max_bars = max(1, inner_w // (BAR_W + SP))
|
|
931
|
+
visible = hist[-max_bars:]
|
|
932
|
+
BAR_COLS = [C_BORDER, C_GREEN, C_YELLOW, C_ORANGE, C_RED]
|
|
933
|
+
|
|
934
|
+
for bi, (label, ms) in enumerate(visible):
|
|
935
|
+
bh = max(1, int((ms / max_ms) * bar_area)) if max_ms > 0 else 1
|
|
936
|
+
bx = 1 + bi * (BAR_W + SP)
|
|
937
|
+
col = curses.color_pair(BAR_COLS[bi % len(BAR_COLS)]) | curses.A_BOLD
|
|
938
|
+
dim = curses.color_pair(C_DIM)
|
|
939
|
+
|
|
940
|
+
for brow in range(bar_area):
|
|
941
|
+
ry = y + bar_area - 1 - brow
|
|
942
|
+
if ry >= h - 1:
|
|
943
|
+
continue
|
|
944
|
+
if brow < bh:
|
|
945
|
+
safe_addstr(win, ry, bx, "█" * BAR_W, col)
|
|
946
|
+
else:
|
|
947
|
+
safe_addstr(win, ry, bx, "░" * BAR_W, dim)
|
|
948
|
+
|
|
949
|
+
# value above bar
|
|
950
|
+
val_s = f"{ms:.2f}"[:BAR_W]
|
|
951
|
+
safe_addstr(win, y - 1, bx, val_s.center(BAR_W),
|
|
952
|
+
curses.color_pair(C_YELLOW))
|
|
953
|
+
|
|
954
|
+
# label below
|
|
955
|
+
run_lbl = label.split()[-1][:BAR_W]
|
|
956
|
+
lbl_y = y + bar_area
|
|
957
|
+
if lbl_y < h - 1:
|
|
958
|
+
safe_addstr(win, lbl_y, bx, run_lbl.center(BAR_W),
|
|
959
|
+
curses.color_pair(C_DIM))
|
|
960
|
+
|
|
961
|
+
# ── Sparkline ─────────────────────────────────────────────────────────────
|
|
962
|
+
spark_y = y + bar_area + 1
|
|
963
|
+
if spark_y < h - 1 and len(hist) > 1:
|
|
964
|
+
SPARK = " ▁▂▃▄▅▆▇█"
|
|
965
|
+
max_v = max(r[1] for r in hist)
|
|
966
|
+
sp_str = "".join(
|
|
967
|
+
SPARK[min(8, int((r[1] / max_v) * 8))]
|
|
968
|
+
for r in hist[-(inner_w - 4):]
|
|
969
|
+
)
|
|
970
|
+
safe_addstr(win, spark_y, 1, "▸ " + sp_str, curses.color_pair(C_BORDER))
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
# ── MAIN ──────────────────────────────────────────────────────────────────────
|
|
620
974
|
def main(stdscr):
|
|
621
975
|
curses.curs_set(0)
|
|
622
976
|
curses.start_color()
|
|
623
977
|
curses.use_default_colors()
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
978
|
+
for pair, fg, bg in [
|
|
979
|
+
(C_NORMAL, curses.COLOR_WHITE, -1),
|
|
980
|
+
(C_BORDER, curses.COLOR_CYAN, -1),
|
|
981
|
+
(C_TITLE, curses.COLOR_CYAN, -1),
|
|
982
|
+
(C_GREEN, curses.COLOR_GREEN, -1),
|
|
983
|
+
(C_YELLOW, curses.COLOR_YELLOW, -1),
|
|
984
|
+
(C_RED, curses.COLOR_RED, -1),
|
|
985
|
+
(C_DIM, curses.COLOR_WHITE, -1),
|
|
986
|
+
(C_HEADER, curses.COLOR_BLACK, curses.COLOR_CYAN),
|
|
987
|
+
(C_INPUT, curses.COLOR_GREEN, -1),
|
|
988
|
+
(C_ORANGE, curses.COLOR_YELLOW, -1),
|
|
989
|
+
(C_MAGENTA, curses.COLOR_MAGENTA, -1),
|
|
990
|
+
(C_BLUE, curses.COLOR_BLUE, -1),
|
|
991
|
+
]:
|
|
992
|
+
curses.init_pair(pair, fg, bg)
|
|
636
993
|
|
|
637
994
|
stdscr.nodelay(True)
|
|
638
995
|
stdscr.keypad(True)
|
|
@@ -640,49 +997,71 @@ def main(stdscr):
|
|
|
640
997
|
state.add_debug("INFO", "MOS Terminal UI started")
|
|
641
998
|
state.add_debug("INFO", f"Working dir: {_CWD}")
|
|
642
999
|
if os.path.exists(SOURCE):
|
|
643
|
-
state.add_debug("OK",
|
|
1000
|
+
state.add_debug("OK", "Found OS_CP.c — F6 compile, R run")
|
|
1001
|
+
state.add_debug("INFO", "F1:Input F2:Debug F3:Output F4:Memory F5:Metrics")
|
|
644
1002
|
else:
|
|
645
1003
|
state.add_debug("ERR", f"OS_CP.c not found in: {_CWD}")
|
|
646
|
-
state.add_debug("WARN", "cd into
|
|
1004
|
+
state.add_debug("WARN", "cd into project folder and rerun ./mos.sh")
|
|
647
1005
|
|
|
648
1006
|
load_sample()
|
|
649
|
-
|
|
650
|
-
_running_thread = [None]
|
|
1007
|
+
_running = [None]
|
|
651
1008
|
|
|
652
1009
|
def redraw():
|
|
653
1010
|
try:
|
|
654
1011
|
H, W = stdscr.getmaxyx()
|
|
655
|
-
if H <
|
|
1012
|
+
if H < 24 or W < 80:
|
|
656
1013
|
stdscr.clear()
|
|
657
|
-
stdscr.addstr(0, 0,
|
|
1014
|
+
stdscr.addstr(0, 0,
|
|
1015
|
+
f"Terminal too small — need 80x24 min (now {W}x{H})",
|
|
1016
|
+
curses.A_BOLD)
|
|
658
1017
|
stdscr.refresh()
|
|
659
1018
|
return
|
|
660
1019
|
|
|
661
1020
|
stdscr.erase()
|
|
662
1021
|
|
|
663
|
-
# Layout
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
1022
|
+
# ── Layout ────────────────────────────────────────────────────────
|
|
1023
|
+
#
|
|
1024
|
+
# title (1 row)
|
|
1025
|
+
# ┌──────────────────┬──────────────────────────────┐
|
|
1026
|
+
# │ INPUT [F1] │ DEBUG LOG [F2] │ top_h rows
|
|
1027
|
+
# ├────────┬─────────┼──────────────────────────────┤
|
|
1028
|
+
# │ OUTPUT │ MEMORY │ EXEC METRICS [F5] │ bot_h rows
|
|
1029
|
+
# │ [F3] │ [F4] │ │
|
|
1030
|
+
# └────────┴─────────┴──────────────────────────────┘
|
|
1031
|
+
# status (1 row)
|
|
1032
|
+
#
|
|
1033
|
+
content_h = H - 2 # minus title + status
|
|
1034
|
+
top_h = content_h // 2
|
|
1035
|
+
bot_h = content_h - top_h
|
|
1036
|
+
|
|
1037
|
+
# Column split: left 60% | right 40%
|
|
1038
|
+
left_w = (W * 6) // 10
|
|
1039
|
+
right_w = W - left_w
|
|
1040
|
+
|
|
1041
|
+
# Bottom-left further split: output | memory (50/50 of left_w)
|
|
1042
|
+
out_w = left_w // 2
|
|
1043
|
+
mem_w = left_w - out_w
|
|
667
1044
|
|
|
668
1045
|
draw_titlebar(stdscr, W)
|
|
669
1046
|
draw_statusbar(stdscr, H, W)
|
|
670
1047
|
|
|
671
|
-
#
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
1048
|
+
# Top row
|
|
1049
|
+
p_input = stdscr.derwin(top_h, left_w, 1, 0)
|
|
1050
|
+
p_debug = stdscr.derwin(top_h, right_w, 1, left_w)
|
|
1051
|
+
|
|
1052
|
+
# Bottom row
|
|
1053
|
+
p_output = stdscr.derwin(bot_h, out_w, 1 + top_h, 0)
|
|
1054
|
+
p_memory = stdscr.derwin(bot_h, mem_w, 1 + top_h, out_w)
|
|
1055
|
+
p_metrics = stdscr.derwin(bot_h, right_w, 1 + top_h, left_w)
|
|
678
1056
|
|
|
679
|
-
draw_input_panel
|
|
680
|
-
draw_debug_panel
|
|
681
|
-
draw_output_panel(
|
|
682
|
-
|
|
1057
|
+
draw_input_panel (p_input, state.focus == 0)
|
|
1058
|
+
draw_debug_panel (p_debug, state.focus == 1)
|
|
1059
|
+
draw_output_panel (p_output, state.focus == 2)
|
|
1060
|
+
draw_mem_grid_panel(p_memory, state.focus == 3)
|
|
1061
|
+
draw_metrics_panel (p_metrics, state.focus == 4)
|
|
683
1062
|
|
|
684
1063
|
stdscr.refresh()
|
|
685
|
-
for p in
|
|
1064
|
+
for p in (p_input, p_debug, p_output, p_memory, p_metrics):
|
|
686
1065
|
p.refresh()
|
|
687
1066
|
except curses.error:
|
|
688
1067
|
pass
|
|
@@ -691,109 +1070,97 @@ def main(stdscr):
|
|
|
691
1070
|
|
|
692
1071
|
while True:
|
|
693
1072
|
now = time.monotonic()
|
|
694
|
-
if now - last_draw[0] > 0.
|
|
695
|
-
redraw()
|
|
696
|
-
last_draw[0] = now
|
|
1073
|
+
if now - last_draw[0] > 0.05:
|
|
1074
|
+
redraw(); last_draw[0] = now
|
|
697
1075
|
|
|
698
1076
|
key = stdscr.getch()
|
|
699
1077
|
if key == -1:
|
|
700
|
-
time.sleep(0.02)
|
|
701
|
-
continue
|
|
1078
|
+
time.sleep(0.02); continue
|
|
702
1079
|
|
|
703
|
-
H, W
|
|
704
|
-
|
|
1080
|
+
H, W = stdscr.getmaxyx()
|
|
1081
|
+
top_h = (H - 2) // 2
|
|
705
1082
|
|
|
706
|
-
# ──
|
|
1083
|
+
# ── Global keys ───────────────────────────────────────────────────────
|
|
707
1084
|
if key in (ord('q'), ord('Q')):
|
|
708
1085
|
break
|
|
709
1086
|
|
|
710
|
-
elif key == curses.KEY_F1:
|
|
711
|
-
|
|
712
|
-
elif key == curses.
|
|
713
|
-
|
|
714
|
-
elif key == curses.
|
|
715
|
-
state.focus = 2
|
|
716
|
-
elif key == curses.KEY_F4:
|
|
717
|
-
state.focus = 3
|
|
1087
|
+
elif key == curses.KEY_F1: state.focus = 0
|
|
1088
|
+
elif key == curses.KEY_F2: state.focus = 1
|
|
1089
|
+
elif key == curses.KEY_F3: state.focus = 2
|
|
1090
|
+
elif key == curses.KEY_F4: state.focus = 3
|
|
1091
|
+
elif key == curses.KEY_F5: state.focus = 4
|
|
718
1092
|
|
|
719
1093
|
elif key == curses.KEY_F6:
|
|
720
|
-
|
|
721
|
-
state.status = "
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
elif key
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
state.add_debug("WARN", "Not compiled — press F6 first")
|
|
733
|
-
elif _running_thread[0] and _running_thread[0].is_alive():
|
|
734
|
-
state.add_debug("WARN", "Already running")
|
|
735
|
-
else:
|
|
736
|
-
input_text = "\n".join(state.input_lines) + "\n"
|
|
737
|
-
state.run_count += 1
|
|
738
|
-
state.output_lines = []
|
|
739
|
-
state.output_scroll = 0
|
|
740
|
-
state.debug_scroll = 0
|
|
741
|
-
|
|
742
|
-
def done():
|
|
743
|
-
state.focus = 2 # auto-switch to Output panel
|
|
744
|
-
state.output_scroll = 0 # scroll to top of output
|
|
745
|
-
redraw()
|
|
746
|
-
|
|
747
|
-
t = threading.Thread(target=run_program, args=(input_text, done), daemon=True)
|
|
748
|
-
_running_thread[0] = t
|
|
749
|
-
t.start()
|
|
750
|
-
|
|
751
|
-
# ── F2 sample key (also mapped to load sample when focus=input) ──
|
|
752
|
-
elif key == ord('s') or key == ord('S'):
|
|
753
|
-
if state.focus == 0:
|
|
754
|
-
load_sample()
|
|
755
|
-
|
|
756
|
-
elif key == ord('c') or key == ord('C'):
|
|
757
|
-
if state.focus == 0:
|
|
758
|
-
state.input_lines = []
|
|
1094
|
+
state.status = "COMPILING"; state.compiled = False; redraw()
|
|
1095
|
+
state.status = "READY" if compile_source() else "ERROR"
|
|
1096
|
+
|
|
1097
|
+
elif key in (ord('r'), ord('R')):
|
|
1098
|
+
_do_run(_running, redraw)
|
|
1099
|
+
|
|
1100
|
+
elif key in (ord('s'), ord('S')):
|
|
1101
|
+
if state.focus == 0: load_sample()
|
|
1102
|
+
|
|
1103
|
+
elif key in (ord('c'), ord('C')):
|
|
1104
|
+
if state.focus == 0:
|
|
1105
|
+
state.input_lines = []
|
|
759
1106
|
state.input_cursor = [0, 0]
|
|
760
1107
|
state.input_scroll = 0
|
|
761
1108
|
state.add_debug("INFO", "Input cleared")
|
|
762
|
-
elif state.focus == 1:
|
|
763
|
-
|
|
764
|
-
elif state.focus == 2:
|
|
765
|
-
state.output_lines = []
|
|
1109
|
+
elif state.focus == 1: state.debug_lines = []
|
|
1110
|
+
elif state.focus == 2: state.output_lines = []
|
|
766
1111
|
|
|
767
|
-
# ──
|
|
1112
|
+
# ── Focus-specific nav ────────────────────────────────────────────────
|
|
768
1113
|
elif state.focus == 0:
|
|
769
|
-
input_key(key,
|
|
1114
|
+
input_key(key, top_h)
|
|
770
1115
|
|
|
771
1116
|
elif state.focus == 1:
|
|
772
|
-
if
|
|
773
|
-
|
|
774
|
-
elif key == curses.
|
|
775
|
-
|
|
776
|
-
elif key == curses.KEY_PPAGE:
|
|
777
|
-
state.debug_scroll = max(0, state.debug_scroll - 10)
|
|
778
|
-
elif key == curses.KEY_NPAGE:
|
|
779
|
-
state.debug_scroll += 10
|
|
1117
|
+
if key == curses.KEY_UP: state.debug_scroll = max(0, state.debug_scroll - 1)
|
|
1118
|
+
elif key == curses.KEY_DOWN: state.debug_scroll += 1
|
|
1119
|
+
elif key == curses.KEY_PPAGE: state.debug_scroll = max(0, state.debug_scroll - 10)
|
|
1120
|
+
elif key == curses.KEY_NPAGE: state.debug_scroll += 10
|
|
780
1121
|
elif key == ord('G'):
|
|
781
|
-
state.debug_scroll = max(0, len(state.debug_lines) - (
|
|
1122
|
+
state.debug_scroll = max(0, len(state.debug_lines) - (top_h - 2))
|
|
782
1123
|
|
|
783
1124
|
elif state.focus == 2:
|
|
784
|
-
if
|
|
785
|
-
|
|
786
|
-
elif key == curses.
|
|
787
|
-
|
|
788
|
-
elif key == curses.KEY_PPAGE:
|
|
789
|
-
state.output_scroll = max(0, state.output_scroll - 10)
|
|
790
|
-
elif key == curses.KEY_NPAGE:
|
|
791
|
-
state.output_scroll += 10
|
|
1125
|
+
if key == curses.KEY_UP: state.output_scroll = max(0, state.output_scroll - 1)
|
|
1126
|
+
elif key == curses.KEY_DOWN: state.output_scroll += 1
|
|
1127
|
+
elif key == curses.KEY_PPAGE: state.output_scroll = max(0, state.output_scroll - 10)
|
|
1128
|
+
elif key == curses.KEY_NPAGE: state.output_scroll += 10
|
|
792
1129
|
|
|
793
1130
|
elif state.focus == 3:
|
|
794
|
-
|
|
1131
|
+
if key == curses.KEY_UP: state.mem_scroll = max(0, state.mem_scroll - 1)
|
|
1132
|
+
elif key == curses.KEY_DOWN: state.mem_scroll += 1
|
|
1133
|
+
elif key == curses.KEY_PPAGE: state.mem_scroll = max(0, state.mem_scroll - 5)
|
|
1134
|
+
elif key == curses.KEY_NPAGE: state.mem_scroll += 5
|
|
1135
|
+
|
|
1136
|
+
elif state.focus == 4:
|
|
1137
|
+
pass # metrics panel is display-only
|
|
1138
|
+
|
|
1139
|
+
|
|
1140
|
+
def _do_run(running_ref, redraw_fn):
|
|
1141
|
+
if not state.compiled:
|
|
1142
|
+
state.add_debug("WARN", "Not compiled — press F6 first"); return
|
|
1143
|
+
if running_ref[0] and running_ref[0].is_alive():
|
|
1144
|
+
state.add_debug("WARN", "Already running"); return
|
|
1145
|
+
|
|
1146
|
+
input_text = "\n".join(state.input_lines) + "\n"
|
|
1147
|
+
state.run_count += 1
|
|
1148
|
+
state.output_lines = []
|
|
1149
|
+
state.output_scroll = 0
|
|
1150
|
+
state.debug_scroll = 0
|
|
1151
|
+
state.mem_scroll = 0
|
|
1152
|
+
|
|
1153
|
+
def done():
|
|
1154
|
+
state.focus = 2 # switch to output first, user can press F4 for memory
|
|
1155
|
+
state.mem_scroll = 0
|
|
1156
|
+
redraw_fn()
|
|
1157
|
+
|
|
1158
|
+
t = threading.Thread(target=run_program, args=(input_text, done), daemon=True)
|
|
1159
|
+
running_ref[0] = t
|
|
1160
|
+
t.start()
|
|
1161
|
+
|
|
795
1162
|
|
|
796
|
-
# ── ENTRY
|
|
1163
|
+
# ── ENTRY ─────────────────────────────────────────────────────────────────────
|
|
797
1164
|
def entry():
|
|
798
1165
|
os.environ.setdefault('TERM', 'xterm-256color')
|
|
799
1166
|
try:
|
|
@@ -801,11 +1168,10 @@ def entry():
|
|
|
801
1168
|
except KeyboardInterrupt:
|
|
802
1169
|
pass
|
|
803
1170
|
finally:
|
|
804
|
-
# cleanup binary
|
|
805
1171
|
if os.path.exists(BINARY):
|
|
806
1172
|
try: os.unlink(BINARY)
|
|
807
1173
|
except: pass
|
|
808
|
-
print("\n\033[36mMOS TUI exited.\033[0m
|
|
1174
|
+
print("\n\033[36mMOS TUI exited.\033[0m")
|
|
809
1175
|
|
|
810
1176
|
if __name__ == "__main__":
|
|
811
1177
|
entry()
|