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 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"
@@ -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()
package/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "mos-tui",
3
+ "version": "1.0.0",
4
+ "description": "MOS Terminal UI Launcher",
5
+ "bin": {
6
+ "mos": "./bin/mos.js"
7
+ },
8
+ "type": "commonjs",
9
+ "author": "Ayush",
10
+ "license": "MIT"
11
+ }