mos-tui 1.0.0
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/bin/mos.js +14 -0
- package/core/OS_CP.c +297 -0
- package/core/mos.sh +44 -0
- package/core/mos_tui.py +744 -0
- package/package.json +11 -0
package/bin/mos.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn } = require("child_process");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const scriptPath = path.join(__dirname, "../core/mos.sh");
|
|
7
|
+
|
|
8
|
+
const child = spawn("bash", [scriptPath], {
|
|
9
|
+
stdio: "inherit",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
child.on("exit", (code) => {
|
|
13
|
+
process.exit(code);
|
|
14
|
+
});
|
package/core/OS_CP.c
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#include<stdio.h>
|
|
2
|
+
#include <stdbool.h>
|
|
3
|
+
#include<time.h>
|
|
4
|
+
|
|
5
|
+
// Declaring the required Memory and Registers
|
|
6
|
+
char M[100][4]; // main storage of size 100 x 4
|
|
7
|
+
char IR[4]; // IR (Instruction register) of size 4
|
|
8
|
+
char R[4]; // R (General Purpose register) of size 4
|
|
9
|
+
int IC; // Instruction Counter
|
|
10
|
+
int C; // Toggle Register
|
|
11
|
+
int SI; // Supervisor Interept
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
FILE *fin, *fout;
|
|
15
|
+
|
|
16
|
+
void START_EXECUTION();
|
|
17
|
+
void EXECUTE_USER_PROGRAM();
|
|
18
|
+
void MOS();
|
|
19
|
+
void READ();
|
|
20
|
+
void WRITE();
|
|
21
|
+
void TERMINATE();
|
|
22
|
+
|
|
23
|
+
// Function to Compare String
|
|
24
|
+
bool cmpString(char s1[], char s2[], int size) {
|
|
25
|
+
for (int i = 0; i < size; i++) {
|
|
26
|
+
if (s1[i] != s2[i]) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
int lenString(char s[]) {
|
|
34
|
+
int ans = 0;
|
|
35
|
+
while (s[ans] != '\0') {
|
|
36
|
+
ans++;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return ans;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// INIT Function (initialize the system by empty variable)
|
|
43
|
+
void init() {
|
|
44
|
+
for (int i = 0; i < 100; i++)
|
|
45
|
+
for (int j = 0; j < 4; j++)
|
|
46
|
+
M[i][j] = ' ';
|
|
47
|
+
|
|
48
|
+
for (int i = 0; i < 4; i++) {
|
|
49
|
+
IR[i] = ' ';
|
|
50
|
+
R[i] = ' ';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
IC = 0;
|
|
54
|
+
C = 0;
|
|
55
|
+
SI = 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
void LOAD() {
|
|
59
|
+
int m = 0; // It Points location at which instructions are stored
|
|
60
|
+
|
|
61
|
+
char word[100]; // Stores each instruction which is to be executed
|
|
62
|
+
|
|
63
|
+
while (fgets(word, sizeof(word), fin)) {
|
|
64
|
+
// printf("%s", word);
|
|
65
|
+
|
|
66
|
+
if (cmpString(word, "$AMJ", 4)) {
|
|
67
|
+
printf("%s", "AMJ (initializing the String)\n");
|
|
68
|
+
init();
|
|
69
|
+
m = 0;
|
|
70
|
+
}
|
|
71
|
+
else if (cmpString(word, "$DTA", 4)) {
|
|
72
|
+
printf("%s", "DTA (Starting Execution)\n");
|
|
73
|
+
START_EXECUTION();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
else if (cmpString(word, "$END", 4)) {
|
|
77
|
+
printf("%s", "END (Instruction Completed)\n");
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
int k = 0;
|
|
82
|
+
while (k < lenString(word) && word[k] != '\n')
|
|
83
|
+
{
|
|
84
|
+
|
|
85
|
+
// Validation for memory overflow
|
|
86
|
+
if (m >= 100) {
|
|
87
|
+
printf("Memory overflow\n");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
for (int j = 0; j < 4 && word[k] != '\n' && word[k] != '\0'; j++) {
|
|
91
|
+
M[m][j] = word[k++];
|
|
92
|
+
}
|
|
93
|
+
m++;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// START EXECUTION
|
|
100
|
+
void START_EXECUTION() {
|
|
101
|
+
IC = 0;
|
|
102
|
+
EXECUTE_USER_PROGRAM();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
void EXECUTE_USER_PROGRAM() {
|
|
106
|
+
int step = 0;
|
|
107
|
+
while (true)
|
|
108
|
+
{
|
|
109
|
+
// To avoid Infinite loop
|
|
110
|
+
if (step++ > 1000) {
|
|
111
|
+
printf("Infinite loop detected\n");
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
// Fetch the Instruction
|
|
115
|
+
for (int i = 0; i < 4; i++) {
|
|
116
|
+
IR[i] = M[IC][i];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
IC++;
|
|
120
|
+
|
|
121
|
+
// Decode and Execute the Instructions
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
// LR Instruction (Load Register)
|
|
126
|
+
if (IR[0] == 'L' && IR[1] == 'R') {
|
|
127
|
+
int addr = (IR[2]-'0')*10 + (IR[3]-'0');
|
|
128
|
+
if (!(IR[2] >= '0' && IR[2] <= '9' && IR[3] >= '0' && IR[3] <= '9')) {
|
|
129
|
+
printf("Invalid operand\n");
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
if (addr < 0 || addr >= 100) {
|
|
133
|
+
printf("Memory address out of bounds\n");
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
for (int i = 0; i < 4; i++) R[i] = M[addr][i];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// SR (Store Register)
|
|
140
|
+
else if (IR[0] == 'S' && IR[1] == 'R') {
|
|
141
|
+
int addr = (IR[2]-'0')*10 + (IR[3]-'0');
|
|
142
|
+
if (!(IR[2] >= '0' && IR[2] <= '9' && IR[3] >= '0' && IR[3] <= '9')) {
|
|
143
|
+
printf("Invalid operand\n");
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
if (addr < 0 || addr >= 100) {
|
|
147
|
+
printf("Memory address out of bounds\n");
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
for (int i = 0; i < 4; i++) M[addr][i] = R[i];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// CR (Compare Register)
|
|
154
|
+
else if (IR[0] == 'C' && IR[1] == 'R') {
|
|
155
|
+
int addr = (IR[2]-'0')*10 + (IR[3]-'0');
|
|
156
|
+
if (!(IR[2] >= '0' && IR[2] <= '9' && IR[3] >= '0' && IR[3] <= '9')) {
|
|
157
|
+
printf("Invalid operand\n");
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
if (addr < 0 || addr >= 100) {
|
|
161
|
+
printf("Memory address out of bounds\n");
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
C = 1;
|
|
165
|
+
for (int i = 0; i < 4; i++) {
|
|
166
|
+
if (R[i] != M[addr][i]) {
|
|
167
|
+
C = 0;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// BT (Branch if True)
|
|
174
|
+
else if (IR[0] == 'B' && IR[1] == 'T') {
|
|
175
|
+
int addr = (IR[2]-'0')*10 + (IR[3]-'0');
|
|
176
|
+
if (!(IR[2] >= '0' && IR[2] <= '9' && IR[3] >= '0' && IR[3] <= '9')) {
|
|
177
|
+
printf("Invalid operand\n");
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
if (addr < 0 || addr >= 100) {
|
|
181
|
+
printf("Memory address out of bounds\n");
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
if (C == 1)
|
|
185
|
+
IC = addr;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// GD (Get Data)
|
|
189
|
+
else if (IR[0] == 'G' && IR[1] == 'D') {
|
|
190
|
+
SI = 1;
|
|
191
|
+
MOS();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// PD (Put Data)
|
|
195
|
+
else if (IR[0] == 'P' && IR[1] == 'D') {
|
|
196
|
+
SI = 2;
|
|
197
|
+
MOS();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// H (Halt)
|
|
201
|
+
else if (IR[0] == 'H') {
|
|
202
|
+
SI = 3;
|
|
203
|
+
MOS();
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
else {
|
|
208
|
+
printf("Invalid instruction: %c%c%c%c\n", IR[0], IR[1], IR[2], IR[3]);
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
void READ() {
|
|
215
|
+
char word[40];
|
|
216
|
+
int addr = (IR[2]-'0')*10 + (IR[3]-'0');
|
|
217
|
+
|
|
218
|
+
// Reading if Input file is not Empty
|
|
219
|
+
if (fgets(word, sizeof(word), fin) == NULL) {
|
|
220
|
+
printf("No data available for GD\n");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
int k = 0;
|
|
225
|
+
for (int i = addr; i < addr + 10 && i < 100; i++) {
|
|
226
|
+
for (int j = 0; j < 4; j++) {
|
|
227
|
+
if (word[k] == '\n' || word[k] == '\0')
|
|
228
|
+
M[i][j] = ' ';
|
|
229
|
+
else
|
|
230
|
+
M[i][j] = word[k++];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
void WRITE() {
|
|
236
|
+
int addr = (IR[2]-'0')*10 + (IR[3]-'0');
|
|
237
|
+
|
|
238
|
+
for (int i = addr; i < addr + 10 && i < 100; i++) {
|
|
239
|
+
for (int j = 0; j < 4; j++) {
|
|
240
|
+
fputc(M[i][j], fout);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
fputc('\n', fout);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
void TERMINATE() {
|
|
247
|
+
fprintf(fout, "\n\n");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
void MOS() {
|
|
251
|
+
if (SI == 1)
|
|
252
|
+
READ();
|
|
253
|
+
else if (SI == 2)
|
|
254
|
+
WRITE();
|
|
255
|
+
else if (SI == 3)
|
|
256
|
+
TERMINATE();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
int main() {
|
|
260
|
+
clock_t start, end;
|
|
261
|
+
|
|
262
|
+
start = clock();
|
|
263
|
+
|
|
264
|
+
fin = fopen("input.txt", "r");
|
|
265
|
+
fout = fopen("output.txt", "w");
|
|
266
|
+
|
|
267
|
+
if (fin == NULL) {
|
|
268
|
+
printf("Input file not found\n");
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
if (fout == NULL) {
|
|
272
|
+
printf("Output file error\n");
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
LOAD();
|
|
277
|
+
|
|
278
|
+
fclose(fin);
|
|
279
|
+
fclose(fout);
|
|
280
|
+
|
|
281
|
+
end = clock();
|
|
282
|
+
double time_taken = (double)(end - start) / CLOCKS_PER_SEC;
|
|
283
|
+
printf("Execution time: %f seconds\n", time_taken);
|
|
284
|
+
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
/*
|
|
290
|
+
// Validations Added
|
|
291
|
+
1. Checking if Input and Output File exists
|
|
292
|
+
2. checking Memery Overflow
|
|
293
|
+
3. Invalid Instruction
|
|
294
|
+
4. Avoid Infinite Loop if Halt Instruction is not available
|
|
295
|
+
5. Added Validation for Correct Operand
|
|
296
|
+
6. Vadidation if input file is Empty
|
|
297
|
+
*/
|
package/core/mos.sh
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ─────────────────────────────────────────────────────────────
|
|
3
|
+
# MOS Terminal UI Launcher
|
|
4
|
+
# Usage: ./mos.sh
|
|
5
|
+
# Run from the same directory as OS_CP.c
|
|
6
|
+
# ─────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
set -e
|
|
9
|
+
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
TUI="$SCRIPT_DIR/mos_tui.py"
|
|
12
|
+
|
|
13
|
+
# Check Python 3
|
|
14
|
+
if ! command -v python3 &>/dev/null; then
|
|
15
|
+
echo "python3 not found. Install it with: sudo apt install python3"
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# Check gcc
|
|
20
|
+
if ! command -v gcc &>/dev/null; then
|
|
21
|
+
echo "gcc not found. Install it with: sudo apt install gcc"
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Check OS_CP.c exists in current dir
|
|
26
|
+
if [ ! -f "OS_CP.c" ]; then
|
|
27
|
+
echo " OS_CP.c not found in current directory."
|
|
28
|
+
echo " Place OS_CP.c here or cd into the right folder, then re-run."
|
|
29
|
+
echo " Launching TUI anyway — you can still use it without the source."
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Ensure the TUI script is alongside
|
|
33
|
+
if [ ! -f "$TUI" ]; then
|
|
34
|
+
echo " mos_tui.py not found at: $TUI"
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Set up terminal properly
|
|
39
|
+
export TERM="${TERM:-xterm-256color}"
|
|
40
|
+
|
|
41
|
+
echo " Starting MOS Terminal UI..."
|
|
42
|
+
sleep 0.3
|
|
43
|
+
|
|
44
|
+
python3 "$TUI"
|
package/core/mos_tui.py
ADDED
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
MOS Terminal UI — Multiprogramming OS Simulator
|
|
4
|
+
Run: python3 mos_tui.py
|
|
5
|
+
Compiles OS_CP.c and provides a full curses TUI.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import curses
|
|
9
|
+
import curses.textpad
|
|
10
|
+
import subprocess
|
|
11
|
+
import tempfile
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
import textwrap
|
|
16
|
+
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
|
+
BINARY = "./mos_bin"
|
|
32
|
+
SOURCE = "OS_CP.c"
|
|
33
|
+
|
|
34
|
+
# ── STATE ─────────────────────────────────────────────────────────────────────
|
|
35
|
+
class State:
|
|
36
|
+
def __init__(self):
|
|
37
|
+
self.input_lines = [] # lines in input editor
|
|
38
|
+
self.input_cursor = [0, 0] # [row, col]
|
|
39
|
+
self.debug_lines = [] # (tag, msg) tuples
|
|
40
|
+
self.output_lines = [] # strings
|
|
41
|
+
self.run_history = [] # (label, ms) for chart
|
|
42
|
+
self.run_count = 0
|
|
43
|
+
self.status = "READY"
|
|
44
|
+
self.compiled = False
|
|
45
|
+
self.compile_error = ""
|
|
46
|
+
self.exec_time_ms = None
|
|
47
|
+
self.focus = 0 # 0=input, 1=debug, 2=output, 3=chart
|
|
48
|
+
self.input_scroll = 0
|
|
49
|
+
self.debug_scroll = 0
|
|
50
|
+
self.output_scroll = 0
|
|
51
|
+
self.lock = threading.Lock()
|
|
52
|
+
|
|
53
|
+
def add_debug(self, tag, msg):
|
|
54
|
+
ts = time.strftime("%H:%M:%S")
|
|
55
|
+
with self.lock:
|
|
56
|
+
self.debug_lines.append((ts, tag, msg))
|
|
57
|
+
|
|
58
|
+
def add_output(self, line):
|
|
59
|
+
with self.lock:
|
|
60
|
+
self.output_lines.append(line)
|
|
61
|
+
|
|
62
|
+
state = State()
|
|
63
|
+
|
|
64
|
+
# ── COMPILE ───────────────────────────────────────────────────────────────────
|
|
65
|
+
def compile_source():
|
|
66
|
+
if not os.path.exists(SOURCE):
|
|
67
|
+
state.compile_error = f"'{SOURCE}' not found in current directory."
|
|
68
|
+
state.add_debug("ERR", f"Source file {SOURCE} not found")
|
|
69
|
+
return False
|
|
70
|
+
state.add_debug("INFO", f"Compiling {SOURCE}...")
|
|
71
|
+
result = subprocess.run(
|
|
72
|
+
["gcc", "-o", BINARY, SOURCE, "-lm"],
|
|
73
|
+
capture_output=True, text=True
|
|
74
|
+
)
|
|
75
|
+
if result.returncode != 0:
|
|
76
|
+
state.compile_error = result.stderr.strip()
|
|
77
|
+
state.add_debug("ERR", "Compilation failed")
|
|
78
|
+
for line in result.stderr.strip().splitlines()[:6]:
|
|
79
|
+
state.add_debug("ERR", line)
|
|
80
|
+
return False
|
|
81
|
+
state.compiled = True
|
|
82
|
+
state.add_debug("OK", f"Compiled → {BINARY}")
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
# ── RUN ───────────────────────────────────────────────────────────────────────
|
|
86
|
+
def run_program(input_text, callback):
|
|
87
|
+
state.status = "RUNNING"
|
|
88
|
+
state.add_debug("INFO", f"─── Run #{state.run_count} started ───")
|
|
89
|
+
|
|
90
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False, prefix='mos_in_') as fin:
|
|
91
|
+
fin.write(input_text)
|
|
92
|
+
fin_path = fin.name
|
|
93
|
+
|
|
94
|
+
fout_path = fin_path.replace('mos_in_', 'mos_out_').replace('.txt', '_out.txt')
|
|
95
|
+
|
|
96
|
+
t0 = time.perf_counter()
|
|
97
|
+
try:
|
|
98
|
+
result = subprocess.run(
|
|
99
|
+
[BINARY],
|
|
100
|
+
stdin=open(fin_path),
|
|
101
|
+
capture_output=True,
|
|
102
|
+
text=True,
|
|
103
|
+
timeout=10,
|
|
104
|
+
env={**os.environ, 'MOS_INPUT': fin_path, 'MOS_OUTPUT': fout_path}
|
|
105
|
+
)
|
|
106
|
+
elapsed_ms = (time.perf_counter() - t0) * 1000
|
|
107
|
+
|
|
108
|
+
# Parse stdout debug lines from the C program
|
|
109
|
+
for line in result.stdout.strip().splitlines():
|
|
110
|
+
line = line.strip()
|
|
111
|
+
if not line:
|
|
112
|
+
continue
|
|
113
|
+
if "Compilation" in line or "compile" in line.lower():
|
|
114
|
+
state.add_debug("ERR", line)
|
|
115
|
+
elif line.startswith("AMJ") or "initializ" in line.lower():
|
|
116
|
+
state.add_debug("OK", line)
|
|
117
|
+
elif line.startswith("DTA") or "execut" in line.lower():
|
|
118
|
+
state.add_debug("EXEC", line)
|
|
119
|
+
elif line.startswith("END") or "complet" in line.lower():
|
|
120
|
+
state.add_debug("OK", line)
|
|
121
|
+
elif "error" in line.lower() or "invalid" in line.lower() or "overflow" in line.lower():
|
|
122
|
+
state.add_debug("ERR", line)
|
|
123
|
+
elif "infinite" in line.lower() or "loop" in line.lower():
|
|
124
|
+
state.add_debug("WARN", line)
|
|
125
|
+
elif "time" in line.lower():
|
|
126
|
+
state.add_debug("INFO", line)
|
|
127
|
+
else:
|
|
128
|
+
state.add_debug("INFO", line)
|
|
129
|
+
|
|
130
|
+
if result.stderr.strip():
|
|
131
|
+
for line in result.stderr.strip().splitlines():
|
|
132
|
+
state.add_debug("ERR", line)
|
|
133
|
+
|
|
134
|
+
# Read output.txt if it was produced
|
|
135
|
+
output_content = []
|
|
136
|
+
if os.path.exists("output.txt"):
|
|
137
|
+
with open("output.txt") as f:
|
|
138
|
+
output_content = f.read().splitlines()
|
|
139
|
+
elif os.path.exists(fout_path):
|
|
140
|
+
with open(fout_path) as f:
|
|
141
|
+
output_content = f.read().splitlines()
|
|
142
|
+
|
|
143
|
+
for line in output_content:
|
|
144
|
+
state.add_output(line)
|
|
145
|
+
|
|
146
|
+
if not output_content and result.returncode == 0:
|
|
147
|
+
state.add_output("[No output written — check PD instruction addresses]")
|
|
148
|
+
|
|
149
|
+
state.exec_time_ms = elapsed_ms
|
|
150
|
+
state.run_history.append((f"Run #{state.run_count}", elapsed_ms))
|
|
151
|
+
state.add_debug("OK", f"Done in {elapsed_ms:.3f}ms | exit={result.returncode}")
|
|
152
|
+
state.status = "HALTED" if result.returncode == 0 else "ERROR"
|
|
153
|
+
|
|
154
|
+
except subprocess.TimeoutExpired:
|
|
155
|
+
state.add_debug("ERR", "Execution timed out (10s limit)")
|
|
156
|
+
state.status = "TIMEOUT"
|
|
157
|
+
except Exception as e:
|
|
158
|
+
state.add_debug("ERR", f"Runner error: {e}")
|
|
159
|
+
state.status = "ERROR"
|
|
160
|
+
finally:
|
|
161
|
+
try: os.unlink(fin_path)
|
|
162
|
+
except: pass
|
|
163
|
+
|
|
164
|
+
callback()
|
|
165
|
+
|
|
166
|
+
# ── DRAWING HELPERS ───────────────────────────────────────────────────────────
|
|
167
|
+
def safe_addstr(win, y, x, s, attr=0):
|
|
168
|
+
h, w = win.getmaxyx()
|
|
169
|
+
if y < 0 or y >= h or x < 0 or x >= w:
|
|
170
|
+
return
|
|
171
|
+
max_len = w - x - 1
|
|
172
|
+
if max_len <= 0:
|
|
173
|
+
return
|
|
174
|
+
try:
|
|
175
|
+
win.addstr(y, x, s[:max_len], attr)
|
|
176
|
+
except curses.error:
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
def draw_box(win, title="", active=False):
|
|
180
|
+
h, w = win.getmaxyx()
|
|
181
|
+
col = curses.color_pair(C_BORDER) | (curses.A_BOLD if active else 0)
|
|
182
|
+
try:
|
|
183
|
+
win.border(
|
|
184
|
+
curses.ACS_VLINE, curses.ACS_VLINE,
|
|
185
|
+
curses.ACS_HLINE, curses.ACS_HLINE,
|
|
186
|
+
curses.ACS_ULCORNER, curses.ACS_URCORNER,
|
|
187
|
+
curses.ACS_LLCORNER, curses.ACS_LRCORNER
|
|
188
|
+
)
|
|
189
|
+
except curses.error:
|
|
190
|
+
pass
|
|
191
|
+
if title:
|
|
192
|
+
label = f" {title} "
|
|
193
|
+
attr = curses.color_pair(C_TITLE) | curses.A_BOLD if active else curses.color_pair(C_DIM)
|
|
194
|
+
safe_addstr(win, 0, 2, label, attr)
|
|
195
|
+
|
|
196
|
+
def fill_line(win, y, attr=0):
|
|
197
|
+
h, w = win.getmaxyx()
|
|
198
|
+
try:
|
|
199
|
+
win.addstr(y, 1, " " * (w - 2), attr)
|
|
200
|
+
except curses.error:
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
# ── PANEL: INPUT ──────────────────────────────────────────────────────────────
|
|
204
|
+
def draw_input_panel(win, active):
|
|
205
|
+
draw_box(win, "INPUT [F1]", active)
|
|
206
|
+
h, w = win.getmaxyx()
|
|
207
|
+
inner_h = h - 3
|
|
208
|
+
inner_w = w - 2
|
|
209
|
+
lines = state.input_lines
|
|
210
|
+
|
|
211
|
+
# hint bar
|
|
212
|
+
hint = " ↑↓ scroll F5 run F2 sample F3 clear "
|
|
213
|
+
safe_addstr(win, 1, 1, hint[:inner_w], curses.color_pair(C_DIM))
|
|
214
|
+
|
|
215
|
+
for row in range(inner_h):
|
|
216
|
+
li = row + state.input_scroll
|
|
217
|
+
y = row + 2
|
|
218
|
+
fill_line(win, y)
|
|
219
|
+
if li < len(lines):
|
|
220
|
+
line = lines[li]
|
|
221
|
+
col = curses.color_pair(C_INPUT)
|
|
222
|
+
# color directives
|
|
223
|
+
if line.startswith('$'):
|
|
224
|
+
col = curses.color_pair(C_YELLOW) | curses.A_BOLD
|
|
225
|
+
safe_addstr(win, y, 1, line[:inner_w], col)
|
|
226
|
+
# draw cursor
|
|
227
|
+
cr, cc = state.input_cursor
|
|
228
|
+
if li == cr and active:
|
|
229
|
+
cx = min(cc, inner_w - 1)
|
|
230
|
+
try:
|
|
231
|
+
ch = lines[cr][cc] if cr < len(lines) and cc < len(lines[cr]) else ' '
|
|
232
|
+
win.addch(y, 1 + cx, ch, curses.color_pair(C_HEADER) | curses.A_BOLD)
|
|
233
|
+
except curses.error:
|
|
234
|
+
pass
|
|
235
|
+
|
|
236
|
+
# ── PANEL: DEBUG ──────────────────────────────────────────────────────────────
|
|
237
|
+
TAG_COLORS = {
|
|
238
|
+
"INFO": C_BORDER,
|
|
239
|
+
"OK": C_GREEN,
|
|
240
|
+
"WARN": C_YELLOW,
|
|
241
|
+
"ERR": C_RED,
|
|
242
|
+
"EXEC": C_ORANGE,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
def draw_debug_panel(win, active):
|
|
246
|
+
draw_box(win, "DEBUG LOG [F2 nav]", active)
|
|
247
|
+
h, w = win.getmaxyx()
|
|
248
|
+
inner_h = h - 2
|
|
249
|
+
inner_w = w - 2
|
|
250
|
+
|
|
251
|
+
lines = state.debug_lines
|
|
252
|
+
# auto-scroll to bottom unless user scrolled
|
|
253
|
+
max_scroll = max(0, len(lines) - inner_h)
|
|
254
|
+
if state.debug_scroll > max_scroll:
|
|
255
|
+
state.debug_scroll = max_scroll
|
|
256
|
+
|
|
257
|
+
for row in range(inner_h):
|
|
258
|
+
li = row + state.debug_scroll
|
|
259
|
+
y = row + 1
|
|
260
|
+
fill_line(win, y)
|
|
261
|
+
if li >= len(lines):
|
|
262
|
+
continue
|
|
263
|
+
ts, tag, msg = lines[li]
|
|
264
|
+
|
|
265
|
+
x = 1
|
|
266
|
+
# timestamp
|
|
267
|
+
safe_addstr(win, y, x, ts, curses.color_pair(C_DIM))
|
|
268
|
+
x += len(ts) + 1
|
|
269
|
+
|
|
270
|
+
# tag badge
|
|
271
|
+
tag_col = curses.color_pair(TAG_COLORS.get(tag, C_DIM)) | curses.A_BOLD
|
|
272
|
+
badge = f"[{tag:<4}]"
|
|
273
|
+
safe_addstr(win, y, x, badge, tag_col)
|
|
274
|
+
x += len(badge) + 1
|
|
275
|
+
|
|
276
|
+
# message
|
|
277
|
+
remaining = inner_w - x
|
|
278
|
+
safe_addstr(win, y, x, msg[:remaining], curses.color_pair(C_NORMAL))
|
|
279
|
+
|
|
280
|
+
# ── PANEL: OUTPUT ─────────────────────────────────────────────────────────────
|
|
281
|
+
def draw_output_panel(win, active):
|
|
282
|
+
draw_box(win, "OUTPUT [F3 nav]", active)
|
|
283
|
+
h, w = win.getmaxyx()
|
|
284
|
+
inner_h = h - 2
|
|
285
|
+
inner_w = w - 2
|
|
286
|
+
|
|
287
|
+
lines = state.output_lines
|
|
288
|
+
max_scroll = max(0, len(lines) - inner_h)
|
|
289
|
+
if state.output_scroll > max_scroll:
|
|
290
|
+
state.output_scroll = max_scroll
|
|
291
|
+
|
|
292
|
+
if not lines:
|
|
293
|
+
msg = "No output yet — press F5 to run"
|
|
294
|
+
safe_addstr(win, inner_h // 2, max(1, (inner_w - len(msg)) // 2), msg, curses.color_pair(C_DIM))
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
for row in range(inner_h):
|
|
298
|
+
li = row + state.output_scroll
|
|
299
|
+
y = row + 1
|
|
300
|
+
fill_line(win, y)
|
|
301
|
+
if li < len(lines):
|
|
302
|
+
line = lines[li]
|
|
303
|
+
col = curses.color_pair(C_GREEN)
|
|
304
|
+
if not line.strip():
|
|
305
|
+
col = curses.color_pair(C_DIM)
|
|
306
|
+
safe_addstr(win, y, 1, line[:inner_w], col)
|
|
307
|
+
|
|
308
|
+
# ── PANEL: CHART ──────────────────────────────────────────────────────────────
|
|
309
|
+
SPARK_CHARS = " ▁▂▃▄▅▆▇█"
|
|
310
|
+
|
|
311
|
+
def draw_chart_panel(win, active):
|
|
312
|
+
draw_box(win, "EXECUTION METRICS [F4 nav]", active)
|
|
313
|
+
h, w = win.getmaxyx()
|
|
314
|
+
inner_h = h - 2
|
|
315
|
+
inner_w = w - 2
|
|
316
|
+
y = 1
|
|
317
|
+
|
|
318
|
+
# Registers / stats row
|
|
319
|
+
et = f"{state.exec_time_ms:.3f}ms" if state.exec_time_ms else "--"
|
|
320
|
+
runs = str(state.run_count)
|
|
321
|
+
hist = state.run_history
|
|
322
|
+
|
|
323
|
+
stats = [
|
|
324
|
+
("EXEC TIME", et, C_GREEN),
|
|
325
|
+
("RUNS", runs, C_BORDER),
|
|
326
|
+
("LAST RUN", hist[-1][0] if hist else "--", C_YELLOW),
|
|
327
|
+
]
|
|
328
|
+
|
|
329
|
+
col_w = (inner_w) // 3
|
|
330
|
+
for i, (label, val, col) in enumerate(stats):
|
|
331
|
+
bx = 1 + i * col_w
|
|
332
|
+
safe_addstr(win, y, bx, label[:col_w-1], curses.color_pair(C_DIM))
|
|
333
|
+
safe_addstr(win, y+1, bx, val[:col_w-1], curses.color_pair(col) | curses.A_BOLD)
|
|
334
|
+
y += 3
|
|
335
|
+
|
|
336
|
+
if y >= inner_h + 1:
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
# separator
|
|
340
|
+
safe_addstr(win, y, 1, "─" * inner_w, curses.color_pair(C_DIM))
|
|
341
|
+
y += 1
|
|
342
|
+
|
|
343
|
+
# chart title
|
|
344
|
+
safe_addstr(win, y, 1, " EXEC TIME HISTORY (ms):", curses.color_pair(C_DIM))
|
|
345
|
+
y += 1
|
|
346
|
+
|
|
347
|
+
if not hist:
|
|
348
|
+
safe_addstr(win, y, 1, " No runs yet", curses.color_pair(C_DIM))
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
# bar chart area
|
|
352
|
+
chart_h = inner_h - (y - 1)
|
|
353
|
+
if chart_h < 3:
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
max_ms = max(r[1] for r in hist)
|
|
357
|
+
bar_area_h = chart_h - 2 # leave rows for labels and scale
|
|
358
|
+
|
|
359
|
+
# how many bars fit
|
|
360
|
+
bar_w = 5
|
|
361
|
+
spacing = 1
|
|
362
|
+
max_bars = max(1, inner_w // (bar_w + spacing))
|
|
363
|
+
visible = hist[-max_bars:]
|
|
364
|
+
|
|
365
|
+
bar_colors = [C_BORDER, C_GREEN, C_YELLOW, C_ORANGE, C_RED]
|
|
366
|
+
|
|
367
|
+
for bi, (label, ms) in enumerate(visible):
|
|
368
|
+
bar_height = max(1, int((ms / max_ms) * bar_area_h)) if max_ms > 0 else 1
|
|
369
|
+
bx = 1 + bi * (bar_w + spacing)
|
|
370
|
+
col = curses.color_pair(bar_colors[bi % len(bar_colors)]) | curses.A_BOLD
|
|
371
|
+
|
|
372
|
+
# draw bar from bottom up
|
|
373
|
+
for brow in range(bar_area_h):
|
|
374
|
+
ry = y + bar_area_h - 1 - brow
|
|
375
|
+
if ry >= h - 1:
|
|
376
|
+
continue
|
|
377
|
+
if brow < bar_height:
|
|
378
|
+
safe_addstr(win, ry, bx, "█" * bar_w, col)
|
|
379
|
+
else:
|
|
380
|
+
safe_addstr(win, ry, bx, "░" * bar_w, curses.color_pair(C_DIM))
|
|
381
|
+
|
|
382
|
+
# value above bar
|
|
383
|
+
val_str = f"{ms:.1f}"[:bar_w]
|
|
384
|
+
safe_addstr(win, y - 1, bx, val_str.center(bar_w), curses.color_pair(C_YELLOW))
|
|
385
|
+
|
|
386
|
+
# label below
|
|
387
|
+
lbl = (label.split()[1] if ' ' in label else label)[:bar_w]
|
|
388
|
+
ly = y + bar_area_h
|
|
389
|
+
if ly < h - 1:
|
|
390
|
+
safe_addstr(win, ly, bx, lbl.center(bar_w), curses.color_pair(C_DIM))
|
|
391
|
+
|
|
392
|
+
# sparkline summary
|
|
393
|
+
spark_y = y + bar_area_h + 1
|
|
394
|
+
if spark_y < h - 1 and len(hist) > 1:
|
|
395
|
+
max_v = max(r[1] for r in hist)
|
|
396
|
+
spark = "".join(SPARK_CHARS[min(8, int((r[1]/max_v)*8))] for r in hist[-inner_w+4:])
|
|
397
|
+
safe_addstr(win, spark_y, 1, "▸ " + spark, curses.color_pair(C_BORDER))
|
|
398
|
+
|
|
399
|
+
# ── STATUS BAR ────────────────────────────────────────────────────────────────
|
|
400
|
+
STATUS_COLORS = {
|
|
401
|
+
"READY": C_GREEN,
|
|
402
|
+
"RUNNING": C_YELLOW,
|
|
403
|
+
"HALTED": C_BORDER,
|
|
404
|
+
"ERROR": C_RED,
|
|
405
|
+
"TIMEOUT": C_RED,
|
|
406
|
+
"COMPILING": C_YELLOW,
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
def draw_statusbar(stdscr, H, W):
|
|
410
|
+
y = H - 1
|
|
411
|
+
try:
|
|
412
|
+
stdscr.addstr(y, 0, " " * (W - 1), curses.color_pair(C_HEADER))
|
|
413
|
+
except curses.error:
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
sc = STATUS_COLORS.get(state.status, C_NORMAL)
|
|
417
|
+
left = f" MOS v1.0 │ {state.status} │ Runs: {state.run_count} │ {SOURCE}"
|
|
418
|
+
right = f"F1:Input F2:Debug F3:Output F4:Chart F5:Run F6:Compile Q:Quit "
|
|
419
|
+
safe_addstr(stdscr, y, 0, left, curses.color_pair(C_HEADER) | curses.A_BOLD)
|
|
420
|
+
rx = W - len(right) - 1
|
|
421
|
+
if rx > len(left):
|
|
422
|
+
safe_addstr(stdscr, y, rx, right, curses.color_pair(C_HEADER))
|
|
423
|
+
|
|
424
|
+
def draw_titlebar(stdscr, W):
|
|
425
|
+
title = " ┌─ MOS ─┐ Multiprogramming OS Simulator ── OS_CP.c "
|
|
426
|
+
compiled_str = " [COMPILED ✓]" if state.compiled else " [NOT COMPILED]"
|
|
427
|
+
full = title + compiled_str
|
|
428
|
+
col = curses.color_pair(C_HEADER) | curses.A_BOLD
|
|
429
|
+
try:
|
|
430
|
+
stdscr.addstr(0, 0, " " * (W - 1), col)
|
|
431
|
+
stdscr.addstr(0, 0, full[:W-1], col)
|
|
432
|
+
except curses.error:
|
|
433
|
+
pass
|
|
434
|
+
|
|
435
|
+
# ── SAMPLE PROGRAMS ───────────────────────────────────────────────────────────
|
|
436
|
+
SAMPLES = [
|
|
437
|
+
"""$AMJ
|
|
438
|
+
LR10
|
|
439
|
+
GD20
|
|
440
|
+
SR30
|
|
441
|
+
LR30
|
|
442
|
+
PD30
|
|
443
|
+
H
|
|
444
|
+
$DTA
|
|
445
|
+
Hello MOS!
|
|
446
|
+
$END""",
|
|
447
|
+
"""$AMJ
|
|
448
|
+
GD10
|
|
449
|
+
PD10
|
|
450
|
+
GD20
|
|
451
|
+
PD20
|
|
452
|
+
H
|
|
453
|
+
$DTA
|
|
454
|
+
First Data
|
|
455
|
+
Second Data
|
|
456
|
+
$END""",
|
|
457
|
+
"""$AMJ
|
|
458
|
+
LR05
|
|
459
|
+
CR05
|
|
460
|
+
BT07
|
|
461
|
+
PD05
|
|
462
|
+
H
|
|
463
|
+
$DTA
|
|
464
|
+
$END""",
|
|
465
|
+
]
|
|
466
|
+
_sample_idx = [0]
|
|
467
|
+
|
|
468
|
+
def load_sample():
|
|
469
|
+
prog = SAMPLES[_sample_idx[0] % len(SAMPLES)].strip()
|
|
470
|
+
_sample_idx[0] += 1
|
|
471
|
+
state.input_lines = prog.splitlines()
|
|
472
|
+
state.input_cursor = [0, 0]
|
|
473
|
+
state.input_scroll = 0
|
|
474
|
+
state.add_debug("INFO", f"Sample #{_sample_idx[0]} loaded — press F5 to run")
|
|
475
|
+
|
|
476
|
+
# ── CURSOR / INPUT NAVIGATION ─────────────────────────────────────────────────
|
|
477
|
+
def ensure_line(r):
|
|
478
|
+
while len(state.input_lines) <= r:
|
|
479
|
+
state.input_lines.append("")
|
|
480
|
+
|
|
481
|
+
def cur_line():
|
|
482
|
+
r, c = state.input_cursor
|
|
483
|
+
ensure_line(r)
|
|
484
|
+
return state.input_lines[r]
|
|
485
|
+
|
|
486
|
+
def input_key(key, panel_h):
|
|
487
|
+
r, c = state.input_cursor
|
|
488
|
+
inner_h = panel_h - 3
|
|
489
|
+
|
|
490
|
+
if key == curses.KEY_UP:
|
|
491
|
+
if r > 0:
|
|
492
|
+
r -= 1
|
|
493
|
+
c = min(c, len(state.input_lines[r]) if r < len(state.input_lines) else 0)
|
|
494
|
+
if r < state.input_scroll:
|
|
495
|
+
state.input_scroll = r
|
|
496
|
+
elif key == curses.KEY_DOWN:
|
|
497
|
+
if r < len(state.input_lines) - 1:
|
|
498
|
+
r += 1
|
|
499
|
+
c = min(c, len(state.input_lines[r]))
|
|
500
|
+
if r >= state.input_scroll + inner_h:
|
|
501
|
+
state.input_scroll = r - inner_h + 1
|
|
502
|
+
elif key == curses.KEY_LEFT:
|
|
503
|
+
if c > 0:
|
|
504
|
+
c -= 1
|
|
505
|
+
elif r > 0:
|
|
506
|
+
r -= 1
|
|
507
|
+
c = len(state.input_lines[r])
|
|
508
|
+
elif key == curses.KEY_RIGHT:
|
|
509
|
+
line = cur_line()
|
|
510
|
+
if c < len(line):
|
|
511
|
+
c += 1
|
|
512
|
+
elif r < len(state.input_lines) - 1:
|
|
513
|
+
r += 1; c = 0
|
|
514
|
+
elif key == curses.KEY_HOME:
|
|
515
|
+
c = 0
|
|
516
|
+
elif key == curses.KEY_END:
|
|
517
|
+
c = len(cur_line())
|
|
518
|
+
elif key in (curses.KEY_BACKSPACE, 127, 8):
|
|
519
|
+
ensure_line(r)
|
|
520
|
+
line = state.input_lines[r]
|
|
521
|
+
if c > 0:
|
|
522
|
+
state.input_lines[r] = line[:c-1] + line[c:]
|
|
523
|
+
c -= 1
|
|
524
|
+
elif r > 0:
|
|
525
|
+
prev = state.input_lines[r-1]
|
|
526
|
+
c = len(prev)
|
|
527
|
+
state.input_lines[r-1] = prev + line
|
|
528
|
+
del state.input_lines[r]
|
|
529
|
+
r -= 1
|
|
530
|
+
elif key == curses.KEY_DC: # Delete
|
|
531
|
+
ensure_line(r)
|
|
532
|
+
line = state.input_lines[r]
|
|
533
|
+
if c < len(line):
|
|
534
|
+
state.input_lines[r] = line[:c] + line[c+1:]
|
|
535
|
+
elif r < len(state.input_lines) - 1:
|
|
536
|
+
state.input_lines[r] = line + state.input_lines[r+1]
|
|
537
|
+
del state.input_lines[r+1]
|
|
538
|
+
elif key in (10, 13): # Enter
|
|
539
|
+
ensure_line(r)
|
|
540
|
+
line = state.input_lines[r]
|
|
541
|
+
state.input_lines[r] = line[:c]
|
|
542
|
+
state.input_lines.insert(r+1, line[c:])
|
|
543
|
+
r += 1; c = 0
|
|
544
|
+
if r >= state.input_scroll + inner_h:
|
|
545
|
+
state.input_scroll += 1
|
|
546
|
+
elif 32 <= key <= 126: # printable
|
|
547
|
+
ensure_line(r)
|
|
548
|
+
line = state.input_lines[r]
|
|
549
|
+
state.input_lines[r] = line[:c] + chr(key) + line[c:]
|
|
550
|
+
c += 1
|
|
551
|
+
|
|
552
|
+
state.input_cursor = [r, c]
|
|
553
|
+
|
|
554
|
+
# ── MAIN TUI LOOP ─────────────────────────────────────────────────────────────
|
|
555
|
+
def main(stdscr):
|
|
556
|
+
curses.curs_set(0)
|
|
557
|
+
curses.start_color()
|
|
558
|
+
curses.use_default_colors()
|
|
559
|
+
|
|
560
|
+
curses.init_pair(C_NORMAL, curses.COLOR_WHITE, -1)
|
|
561
|
+
curses.init_pair(C_BORDER, curses.COLOR_CYAN, -1)
|
|
562
|
+
curses.init_pair(C_TITLE, curses.COLOR_CYAN, -1)
|
|
563
|
+
curses.init_pair(C_GREEN, curses.COLOR_GREEN, -1)
|
|
564
|
+
curses.init_pair(C_YELLOW, curses.COLOR_YELLOW, -1)
|
|
565
|
+
curses.init_pair(C_RED, curses.COLOR_RED, -1)
|
|
566
|
+
curses.init_pair(C_DIM, curses.COLOR_WHITE, -1)
|
|
567
|
+
curses.init_pair(C_HEADER, curses.COLOR_BLACK, curses.COLOR_CYAN)
|
|
568
|
+
curses.init_pair(C_INPUT, curses.COLOR_GREEN, -1)
|
|
569
|
+
curses.init_pair(C_ORANGE, curses.COLOR_YELLOW, -1)
|
|
570
|
+
curses.init_pair(C_STAT_VAL, curses.COLOR_CYAN, -1)
|
|
571
|
+
|
|
572
|
+
stdscr.nodelay(True)
|
|
573
|
+
stdscr.keypad(True)
|
|
574
|
+
|
|
575
|
+
state.add_debug("INFO", "MOS Terminal UI started")
|
|
576
|
+
state.add_debug("INFO", f"Looking for {SOURCE} in: {os.getcwd()}")
|
|
577
|
+
if os.path.exists(SOURCE):
|
|
578
|
+
state.add_debug("OK", f"Found {SOURCE} — press F6 to compile")
|
|
579
|
+
else:
|
|
580
|
+
state.add_debug("WARN", f"{SOURCE} not found in current directory")
|
|
581
|
+
state.add_debug("WARN", "Place OS_CP.c here and press F6")
|
|
582
|
+
|
|
583
|
+
load_sample()
|
|
584
|
+
|
|
585
|
+
_running_thread = [None]
|
|
586
|
+
|
|
587
|
+
def redraw():
|
|
588
|
+
try:
|
|
589
|
+
H, W = stdscr.getmaxyx()
|
|
590
|
+
if H < 20 or W < 60:
|
|
591
|
+
stdscr.clear()
|
|
592
|
+
stdscr.addstr(0, 0, "Terminal too small! Need 60x20 minimum.", curses.A_BOLD)
|
|
593
|
+
stdscr.refresh()
|
|
594
|
+
return
|
|
595
|
+
|
|
596
|
+
stdscr.erase()
|
|
597
|
+
|
|
598
|
+
# Layout: title(1) + panels(H-3) + statusbar(1) + gap(1)
|
|
599
|
+
content_h = H - 2 # minus title and status
|
|
600
|
+
half_h = content_h // 2
|
|
601
|
+
half_w = W // 2
|
|
602
|
+
|
|
603
|
+
draw_titlebar(stdscr, W)
|
|
604
|
+
draw_statusbar(stdscr, H, W)
|
|
605
|
+
|
|
606
|
+
# Create sub-windows (top-left, top-right, bot-left, bot-right)
|
|
607
|
+
panels = [
|
|
608
|
+
stdscr.derwin(half_h, half_w, 1, 0),
|
|
609
|
+
stdscr.derwin(half_h, W - half_w, 1, half_w),
|
|
610
|
+
stdscr.derwin(content_h - half_h, half_w, 1+half_h, 0),
|
|
611
|
+
stdscr.derwin(content_h - half_h, W-half_w, 1+half_h, half_w),
|
|
612
|
+
]
|
|
613
|
+
|
|
614
|
+
draw_input_panel (panels[0], state.focus == 0)
|
|
615
|
+
draw_debug_panel (panels[1], state.focus == 1)
|
|
616
|
+
draw_output_panel(panels[2], state.focus == 2)
|
|
617
|
+
draw_chart_panel (panels[3], state.focus == 3)
|
|
618
|
+
|
|
619
|
+
stdscr.refresh()
|
|
620
|
+
for p in panels:
|
|
621
|
+
p.refresh()
|
|
622
|
+
except curses.error:
|
|
623
|
+
pass
|
|
624
|
+
|
|
625
|
+
last_draw = [0.0]
|
|
626
|
+
|
|
627
|
+
while True:
|
|
628
|
+
now = time.monotonic()
|
|
629
|
+
if now - last_draw[0] > 0.08: # ~12fps
|
|
630
|
+
redraw()
|
|
631
|
+
last_draw[0] = now
|
|
632
|
+
|
|
633
|
+
key = stdscr.getch()
|
|
634
|
+
if key == -1:
|
|
635
|
+
time.sleep(0.02)
|
|
636
|
+
continue
|
|
637
|
+
|
|
638
|
+
H, W = stdscr.getmaxyx()
|
|
639
|
+
half_h = (H - 2) // 2
|
|
640
|
+
|
|
641
|
+
# ── GLOBAL KEYS ──
|
|
642
|
+
if key in (ord('q'), ord('Q')):
|
|
643
|
+
break
|
|
644
|
+
|
|
645
|
+
elif key == curses.KEY_F1:
|
|
646
|
+
state.focus = 0
|
|
647
|
+
elif key == curses.KEY_F2:
|
|
648
|
+
state.focus = 1
|
|
649
|
+
elif key == curses.KEY_F3:
|
|
650
|
+
state.focus = 2
|
|
651
|
+
elif key == curses.KEY_F4:
|
|
652
|
+
state.focus = 3
|
|
653
|
+
|
|
654
|
+
elif key == curses.KEY_F6:
|
|
655
|
+
# Compile
|
|
656
|
+
state.status = "COMPILING"
|
|
657
|
+
state.compiled = False
|
|
658
|
+
redraw()
|
|
659
|
+
if compile_source():
|
|
660
|
+
state.status = "READY"
|
|
661
|
+
else:
|
|
662
|
+
state.status = "ERROR"
|
|
663
|
+
|
|
664
|
+
elif key == curses.KEY_F5:
|
|
665
|
+
# Run
|
|
666
|
+
if not state.compiled:
|
|
667
|
+
state.add_debug("WARN", "Not compiled — press F6 first")
|
|
668
|
+
elif _running_thread[0] and _running_thread[0].is_alive():
|
|
669
|
+
state.add_debug("WARN", "Already running")
|
|
670
|
+
else:
|
|
671
|
+
input_text = "\n".join(state.input_lines) + "\n"
|
|
672
|
+
state.run_count += 1
|
|
673
|
+
state.output_lines = []
|
|
674
|
+
state.output_scroll = 0
|
|
675
|
+
state.debug_scroll = 0
|
|
676
|
+
|
|
677
|
+
def done():
|
|
678
|
+
redraw()
|
|
679
|
+
|
|
680
|
+
t = threading.Thread(target=run_program, args=(input_text, done), daemon=True)
|
|
681
|
+
_running_thread[0] = t
|
|
682
|
+
t.start()
|
|
683
|
+
|
|
684
|
+
# ── F2 sample key (also mapped to load sample when focus=input) ──
|
|
685
|
+
elif key == ord('s') or key == ord('S'):
|
|
686
|
+
if state.focus == 0:
|
|
687
|
+
load_sample()
|
|
688
|
+
|
|
689
|
+
elif key == ord('c') or key == ord('C'):
|
|
690
|
+
if state.focus == 0:
|
|
691
|
+
state.input_lines = []
|
|
692
|
+
state.input_cursor = [0, 0]
|
|
693
|
+
state.input_scroll = 0
|
|
694
|
+
state.add_debug("INFO", "Input cleared")
|
|
695
|
+
elif state.focus == 1:
|
|
696
|
+
state.debug_lines = []
|
|
697
|
+
elif state.focus == 2:
|
|
698
|
+
state.output_lines = []
|
|
699
|
+
|
|
700
|
+
# ── FOCUS-SPECIFIC NAVIGATION ──
|
|
701
|
+
elif state.focus == 0:
|
|
702
|
+
input_key(key, half_h)
|
|
703
|
+
|
|
704
|
+
elif state.focus == 1:
|
|
705
|
+
if key == curses.KEY_UP:
|
|
706
|
+
state.debug_scroll = max(0, state.debug_scroll - 1)
|
|
707
|
+
elif key == curses.KEY_DOWN:
|
|
708
|
+
state.debug_scroll += 1
|
|
709
|
+
elif key == curses.KEY_PPAGE:
|
|
710
|
+
state.debug_scroll = max(0, state.debug_scroll - 10)
|
|
711
|
+
elif key == curses.KEY_NPAGE:
|
|
712
|
+
state.debug_scroll += 10
|
|
713
|
+
elif key == ord('G'):
|
|
714
|
+
state.debug_scroll = max(0, len(state.debug_lines) - (half_h - 2))
|
|
715
|
+
|
|
716
|
+
elif state.focus == 2:
|
|
717
|
+
if key == curses.KEY_UP:
|
|
718
|
+
state.output_scroll = max(0, state.output_scroll - 1)
|
|
719
|
+
elif key == curses.KEY_DOWN:
|
|
720
|
+
state.output_scroll += 1
|
|
721
|
+
elif key == curses.KEY_PPAGE:
|
|
722
|
+
state.output_scroll = max(0, state.output_scroll - 10)
|
|
723
|
+
elif key == curses.KEY_NPAGE:
|
|
724
|
+
state.output_scroll += 10
|
|
725
|
+
|
|
726
|
+
elif state.focus == 3:
|
|
727
|
+
pass # chart panel is display-only
|
|
728
|
+
|
|
729
|
+
# ── ENTRY POINT ───────────────────────────────────────────────────────────────
|
|
730
|
+
def entry():
|
|
731
|
+
os.environ.setdefault('TERM', 'xterm-256color')
|
|
732
|
+
try:
|
|
733
|
+
curses.wrapper(main)
|
|
734
|
+
except KeyboardInterrupt:
|
|
735
|
+
pass
|
|
736
|
+
finally:
|
|
737
|
+
# cleanup binary
|
|
738
|
+
if os.path.exists(BINARY):
|
|
739
|
+
try: os.unlink(BINARY)
|
|
740
|
+
except: pass
|
|
741
|
+
print("\n\033[36mMOS TUI exited.\033[0m Thanks for using the simulator!")
|
|
742
|
+
|
|
743
|
+
if __name__ == "__main__":
|
|
744
|
+
entry()
|