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.
Files changed (3) hide show
  1. package/core/OS_CP.c +68 -97
  2. package/core/mos_tui.py +858 -492
  3. 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
- # ── COLORS (pair indices) ─────────────────────────────────────────────────────
19
- C_NORMAL = 1 # white on black
20
- C_BORDER = 2 # cyan on black
21
- C_TITLE = 3 # cyan bold
22
- C_GREEN = 4 # green on black
23
- C_YELLOW = 5 # yellow on black
24
- C_RED = 6 # red on black
25
- C_DIM = 7 # dim white
26
- C_HEADER = 8 # black on cyan
27
- C_INPUT = 9 # green on black (input text)
28
- C_ORANGE = 10 # yellow bright (used for EXEC tags)
29
- C_STAT_VAL = 11 # cyan bold for stat values
30
-
31
- # Locate OS_CP.c by searching:
32
- # 1. Directory explicitly set via MOS_SOURCE env var
33
- # 2. Current working directory
34
- # 3. Walk UP from cwd (handles npm package subdirectory installs)
35
- # 4. Walk UP from script location
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
- # Search upward from cwd
43
- search = os.getcwd()
44
- for _ in range(6):
45
- candidate = os.path.join(search, "OS_CP.c")
46
- if os.path.exists(candidate):
47
- return candidate
48
- parent = os.path.dirname(search)
49
- if parent == search:
50
- break
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) # always the folder that contains OS_CP.c
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 = [] # lines in input editor
75
- self.input_cursor = [0, 0] # [row, col]
76
- self.debug_lines = [] # (tag, msg) tuples
77
- self.output_lines = [] # strings
78
- self.run_history = [] # (label, ms) for chart
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 # 0=input, 1=debug, 2=output, 3=chart
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", f"OS_CP.c not found at: {SOURCE}")
105
- state.add_debug("WARN", f"Run this tool from the folder containing OS_CP.c")
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 line in result.stderr.strip().splitlines()[:6]:
118
- state.add_debug("ERR", line)
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
- # Use a dedicated temp directory so input.txt and output.txt are isolated
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
- text=True,
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': fin_path, # env var override (your updated C uses this)
149
- 'MOS_OUTPUT': fout_path}
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
- continue
160
- if "Compilation" in line or "compile" in line.lower():
161
- state.add_debug("ERR", line)
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 line.startswith("END") or "complet" in line.lower():
167
- state.add_debug("OK", line)
168
- elif "error" in line.lower() or "invalid" in line.lower() or "overflow" in line.lower():
169
- state.add_debug("ERR", line)
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 line in result.stderr.strip().splitlines():
179
- state.add_debug("ERR", line)
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 read from {fout_path} ({len(output_content)} lines)")
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
- # Don't add pure-blank lines from TERMINATE()'s \n\n — skip them
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 was empty — check your PD instruction address]")
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.txt had only blank lines — PD address may point to empty memory]")
205
- state.add_debug("WARN", "All output lines were blank GD and PD addresses must match")
206
- state.add_debug("WARN", "e.g. GD10 reads into addr 10-19, so use PD10 to print it")
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 | exit={result.returncode}")
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", "Execution timed out (10s limit)")
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
- # ── DRAWING HELPERS ───────────────────────────────────────────────────────────
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
- max_len = w - x - 1
232
- if max_len <= 0:
233
- return
308
+ s = s[:max(0, w - x - 1)]
309
+ if not s: return
234
310
  try:
235
- win.addstr(y, x, s[:max_len], attr)
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
- label = f" {title} "
253
- attr = curses.color_pair(C_TITLE) | curses.A_BOLD if active else curses.color_pair(C_DIM)
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, attr=0):
329
+ def fill_line(win, y):
257
330
  h, w = win.getmaxyx()
258
- try:
259
- win.addstr(y, 1, " " * (w - 2), attr)
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
- lines = state.input_lines
270
-
271
- # hint bar
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 = row + 2
345
+ y = row + 2
278
346
  fill_line(win, y)
279
- if li < len(lines):
280
- line = lines[li]
281
- col = curses.color_pair(C_INPUT)
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
- TAG_COLORS = {
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 nav]", active)
307
- h, w = win.getmaxyx()
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 = state.debug_lines
312
- # auto-scroll to bottom unless user scrolled
313
- max_scroll = max(0, len(lines) - inner_h)
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 = row + 1
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, tag_col)
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 nav]", active)
343
- h, w = win.getmaxyx()
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 = state.output_lines
348
- max_scroll = max(0, len(lines) - inner_h)
349
- if state.output_scroll > max_scroll:
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 F5 to run"
354
- safe_addstr(win, inner_h // 2, max(1, (inner_w - len(msg)) // 2), msg, curses.color_pair(C_DIM))
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 = row + 1
409
+ y = row + 1
360
410
  fill_line(win, y)
361
411
  if li < len(lines):
362
412
  line = lines[li]
363
- # line number gutter
364
- gutter = f"{li+1:3} "
365
- safe_addstr(win, y, 1, gutter, curses.color_pair(C_DIM))
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, line[:inner_w-5], col)
371
-
372
- # ── PANEL: CHART ──────────────────────────────────────────────────────────────
373
- SPARK_CHARS = " ▁▂▃▄▅▆▇█"
417
+ safe_addstr(win, y, 5, "~", curses.color_pair(C_DIM))
374
418
 
375
- def draw_chart_panel(win, active):
376
- draw_box(win, "EXECUTION METRICS [F4 nav]", active)
377
- h, w = win.getmaxyx()
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
- # Registers / stats row
383
- et = f"{state.exec_time_ms:.3f}ms" if state.exec_time_ms else "--"
384
- runs = str(state.run_count)
385
- hist = state.run_history
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
- stats = [
388
- ("EXEC TIME", et, C_GREEN),
389
- ("RUNS", runs, C_BORDER),
390
- ("LAST RUN", hist[-1][0] if hist else "--", C_YELLOW),
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
- col_w = (inner_w) // 3
394
- for i, (label, val, col) in enumerate(stats):
395
- bx = 1 + i * col_w
396
- safe_addstr(win, y, bx, label[:col_w-1], curses.color_pair(C_DIM))
397
- safe_addstr(win, y+1, bx, val[:col_w-1], curses.color_pair(col) | curses.A_BOLD)
398
- y += 3
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 y >= inner_h + 1:
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
- # separator
404
- safe_addstr(win, y, 1, "─" * inner_w, curses.color_pair(C_DIM))
405
- y += 1
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
- # chart title
408
- safe_addstr(win, y, 1, " EXEC TIME HISTORY (ms):", curses.color_pair(C_DIM))
409
- y += 1
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
- # bar chart area
416
- chart_h = inner_h - (y - 1)
417
- if chart_h < 3:
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 = max(r[1] for r in hist)
421
- bar_area_h = chart_h - 2 # leave rows for labels and scale
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
- # how many bars fit
424
- bar_w = 5
425
- spacing = 1
426
- max_bars = max(1, inner_w // (bar_w + spacing))
427
- visible = hist[-max_bars:]
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
- for bi, (label, ms) in enumerate(visible):
432
- bar_height = max(1, int((ms / max_ms) * bar_area_h)) if max_ms > 0 else 1
433
- bx = 1 + bi * (bar_w + spacing)
434
- col = curses.color_pair(bar_colors[bi % len(bar_colors)]) | curses.A_BOLD
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
- # draw bar from bottom up
437
- for brow in range(bar_area_h):
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
- # value above bar
447
- val_str = f"{ms:.1f}"[:bar_w]
448
- safe_addstr(win, y - 1, bx, val_str.center(bar_w), curses.color_pair(C_YELLOW))
667
+ if not hist:
668
+ safe_addstr(win, y, 1, " No runs yet", curses.color_pair(C_DIM))
669
+ return
449
670
 
450
- # label below
451
- lbl = (label.split()[1] if ' ' in label else label)[:bar_w]
452
- ly = y + bar_area_h
453
- if ly < h - 1:
454
- safe_addstr(win, ly, bx, lbl.center(bar_w), curses.color_pair(C_DIM))
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
- # sparkline summary
457
- spark_y = y + bar_area_h + 1
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 = max(r[1] for r in hist)
460
- spark = "".join(SPARK_CHARS[min(8, int((r[1]/max_v)*8))] for r in hist[-inner_w+4:])
461
- safe_addstr(win, spark_y, 1, "▸ " + spark, curses.color_pair(C_BORDER))
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
- def draw_statusbar(stdscr, H, W):
474
- y = H - 1
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(y, 0, " " * (W - 1), curses.color_pair(C_HEADER))
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
- sc = STATUS_COLORS.get(state.status, C_NORMAL)
481
- left = f" MOS v1.0 │ {state.status} │ Runs: {state.run_count} │ {SOURCE}"
482
- right = f"F1:Input F2:Debug F3:Output F4:Chart F5:Run F6:Compile Q:Quit "
483
- safe_addstr(stdscr, y, 0, left, curses.color_pair(C_HEADER) | curses.A_BOLD)
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(0, 0, " " * (W - 1), col)
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
- # ── SAMPLE PROGRAMS ───────────────────────────────────────────────────────────
724
+ # ── SAMPLES ───────────────────────────────────────────────────────────────────
500
725
  SAMPLES = [
501
- # Sample 1: Read data into addr 10, print from addr 10
502
- """$AMJ
503
- GD10
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
- _sample_idx = [0]
730
+ _sidx = [0]
532
731
 
533
732
  def load_sample():
534
- prog = SAMPLES[_sample_idx[0] % len(SAMPLES)].strip()
535
- _sample_idx[0] += 1
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 #{_sample_idx[0]} loaded — press F5 to run")
737
+ state.add_debug("INFO", f"Sample #{_sidx[0]} loaded — press R to run")
540
738
 
541
- # ── CURSOR / INPUT NAVIGATION ─────────────────────────────────────────────────
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 = state.input_cursor
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
- c = min(c, len(state.input_lines[r]) if r < len(state.input_lines) else 0)
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
- c = min(c, len(state.input_lines[r]))
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
- c -= 1
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
- line = state.input_lines[r]
586
- if c > 0:
587
- state.input_lines[r] = line[:c-1] + line[c:]
588
- c -= 1
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
- c = len(prev)
592
- state.input_lines[r-1] = prev + line
593
- del state.input_lines[r]
594
- r -= 1
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] = line + state.input_lines[r+1]
602
- del state.input_lines[r+1]
603
- elif key in (10, 13): # Enter
604
- ensure_line(r)
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
- state.input_scroll += 1
611
- elif 32 <= key <= 126: # printable
612
- ensure_line(r)
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 TUI LOOP ─────────────────────────────────────────────────────────────
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
- curses.init_pair(C_NORMAL, curses.COLOR_WHITE, -1)
626
- curses.init_pair(C_BORDER, curses.COLOR_CYAN, -1)
627
- curses.init_pair(C_TITLE, curses.COLOR_CYAN, -1)
628
- curses.init_pair(C_GREEN, curses.COLOR_GREEN, -1)
629
- curses.init_pair(C_YELLOW, curses.COLOR_YELLOW, -1)
630
- curses.init_pair(C_RED, curses.COLOR_RED, -1)
631
- curses.init_pair(C_DIM, curses.COLOR_WHITE, -1)
632
- curses.init_pair(C_HEADER, curses.COLOR_BLACK, curses.COLOR_CYAN)
633
- curses.init_pair(C_INPUT, curses.COLOR_GREEN, -1)
634
- curses.init_pair(C_ORANGE, curses.COLOR_YELLOW, -1)
635
- curses.init_pair(C_STAT_VAL, curses.COLOR_CYAN, -1)
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", f"Found OS_CP.c — press F6 to compile")
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 your project folder, then rerun ./mos.sh")
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 < 20 or W < 60:
1012
+ if H < 24 or W < 80:
656
1013
  stdscr.clear()
657
- stdscr.addstr(0, 0, "Terminal too small! Need 60x20 minimum.", curses.A_BOLD)
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: title(1) + panels(H-3) + statusbar(1) + gap(1)
664
- content_h = H - 2 # minus title and status
665
- half_h = content_h // 2
666
- half_w = W // 2
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
- # Create sub-windows (top-left, top-right, bot-left, bot-right)
672
- panels = [
673
- stdscr.derwin(half_h, half_w, 1, 0),
674
- stdscr.derwin(half_h, W - half_w, 1, half_w),
675
- stdscr.derwin(content_h - half_h, half_w, 1+half_h, 0),
676
- stdscr.derwin(content_h - half_h, W-half_w, 1+half_h, half_w),
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 (panels[0], state.focus == 0)
680
- draw_debug_panel (panels[1], state.focus == 1)
681
- draw_output_panel(panels[2], state.focus == 2)
682
- draw_chart_panel (panels[3], state.focus == 3)
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 panels:
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.08: # ~12fps
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 = stdscr.getmaxyx()
704
- half_h = (H - 2) // 2
1080
+ H, W = stdscr.getmaxyx()
1081
+ top_h = (H - 2) // 2
705
1082
 
706
- # ── GLOBAL KEYS ──
1083
+ # ── Global keys ───────────────────────────────────────────────────────
707
1084
  if key in (ord('q'), ord('Q')):
708
1085
  break
709
1086
 
710
- elif key == curses.KEY_F1:
711
- state.focus = 0
712
- elif key == curses.KEY_F2:
713
- state.focus = 1
714
- elif key == curses.KEY_F3:
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
- # Compile
721
- state.status = "COMPILING"
722
- state.compiled = False
723
- redraw()
724
- if compile_source():
725
- state.status = "READY"
726
- else:
727
- state.status = "ERROR"
728
-
729
- elif key == curses.KEY_F5:
730
- # Run
731
- if not state.compiled:
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
- state.debug_lines = []
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
- # ── FOCUS-SPECIFIC NAVIGATION ──
1112
+ # ── Focus-specific nav ────────────────────────────────────────────────
768
1113
  elif state.focus == 0:
769
- input_key(key, half_h)
1114
+ input_key(key, top_h)
770
1115
 
771
1116
  elif state.focus == 1:
772
- if key == curses.KEY_UP:
773
- state.debug_scroll = max(0, state.debug_scroll - 1)
774
- elif key == curses.KEY_DOWN:
775
- state.debug_scroll += 1
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) - (half_h - 2))
1122
+ state.debug_scroll = max(0, len(state.debug_lines) - (top_h - 2))
782
1123
 
783
1124
  elif state.focus == 2:
784
- if key == curses.KEY_UP:
785
- state.output_scroll = max(0, state.output_scroll - 1)
786
- elif key == curses.KEY_DOWN:
787
- state.output_scroll += 1
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
- pass # chart panel is display-only
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 POINT ───────────────────────────────────────────────────────────────
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 Thanks for using the simulator!")
1174
+ print("\n\033[36mMOS TUI exited.\033[0m")
809
1175
 
810
1176
  if __name__ == "__main__":
811
1177
  entry()