runlab 0.1.0 → 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.
@@ -0,0 +1,405 @@
1
+ import { Folder } from "./Folder.js";
2
+ import { File } from "./File.js";
3
+ import { updateEditorContentById, setActiveFile } from "../core/editor.js";
4
+ import Swal from "sweetalert2";
5
+ import { setEditorActive, setViewActive } from "../app.js";
6
+
7
+ export class FileExplorer {
8
+ constructor(parentId) {
9
+ this.container = document.getElementById(parentId);
10
+ this.root = new Folder("root", null);
11
+ this.execFiles = [];
12
+ this.filesPath = [];
13
+ this.path = [];
14
+ this.render();
15
+ }
16
+
17
+ getRoot() {
18
+ return this.root;
19
+ }
20
+
21
+ getFileNode(file) {
22
+ const name = file.name.split(".")[0];
23
+ const ext = file.name.split(".")[1];
24
+ const path = file.path;
25
+ let currentNode = this.root;
26
+
27
+ for (let i = 1; i < path.length; i++) {
28
+ const part = path[i];
29
+ const nextNode = currentNode.children.find(c => c.name === part && !c.ext);
30
+ if (!nextNode) return null;
31
+ currentNode = nextNode;
32
+ }
33
+
34
+ return currentNode.children.find(c => c.name === name && c.ext === ext);
35
+ }
36
+
37
+ getExecutables() {
38
+ return this.execFiles;
39
+ }
40
+
41
+ setExecutables(files) {
42
+ this.execFiles = files;
43
+ }
44
+
45
+ /* =========================
46
+ RENDER
47
+ ========================== */
48
+
49
+ render() {
50
+ this.container.innerHTML = "";
51
+
52
+ const rootDiv = document.createElement("div");
53
+ rootDiv.style.width = "100%";
54
+ rootDiv.style.height = "100%";
55
+ rootDiv.style.display = "block";
56
+ rootDiv.style.pointerEvents = "auto";
57
+ rootDiv.style.position = "relative";
58
+
59
+ rootDiv.oncontextmenu = e => {
60
+ e.preventDefault();
61
+ this.showRootMenu(e);
62
+ };
63
+
64
+
65
+ this.renderFolder(this.root, rootDiv);
66
+
67
+ this.container.append(rootDiv);
68
+ }
69
+
70
+ renderFolder(folder, parentUl) {
71
+ folder.children.forEach(node => {
72
+ if (node instanceof Folder) {
73
+ const btn = document.createElement("button");
74
+ btn.textContent = `📁 ${node.name}`;
75
+ btn.style.width = "100%";
76
+ btn.style.textAlign = "left";
77
+ btn.style.border = "none";
78
+ btn.style.background = "none";
79
+ btn.style.color = "white";
80
+ btn.style.paddingTop = "8px";
81
+ btn.style.paddingBottom = "8px";
82
+ btn.style.cursor = "pointer";
83
+
84
+
85
+
86
+ btn.onmouseover = () => btn.style.backgroundColor = "#555";
87
+ btn.onmouseout = () => btn.style.backgroundColor = "transparent";
88
+
89
+ const ul = document.createElement("ul");
90
+ ul.style.display = "none";
91
+ ul.style.listStyle = "none";
92
+ ul.style.margin = "0";
93
+ ul.style.padding = "0 0 0 24px";
94
+
95
+
96
+ btn.onclick = () => this.toggle(ul);
97
+ btn.oncontextmenu = e => {
98
+ e.preventDefault();
99
+ e.stopPropagation();
100
+ this.showFolderMenu(e, node);
101
+ };
102
+
103
+ parentUl.append(btn, ul);
104
+ this.renderFolder(node, ul);
105
+ }
106
+
107
+ if (node instanceof File) {
108
+ const li = document.createElement("li");
109
+ li.textContent = `📄 ${node.name}.${node.ext}`;
110
+ li.style.width = "100%";
111
+ li.style.textAlign = "left";
112
+ li.style.border = "none";
113
+ li.style.background = "none";
114
+ li.style.color = "white";
115
+ li.style.paddingTop = "8px";
116
+ li.style.paddingBottom = "8px";
117
+ li.style.cursor = "pointer";
118
+
119
+
120
+ li.onclick = () => {
121
+ setActiveFile(node);
122
+ setViewActive(false);
123
+ setEditorActive(true);
124
+ updateEditorContentById("runlab-editor", node.content);
125
+ };
126
+
127
+ li.oncontextmenu = e => {
128
+ e.preventDefault();
129
+ e.stopPropagation();
130
+ this.showFileMenu(e, node);
131
+ };
132
+
133
+ parentUl.appendChild(li);
134
+ this.filesPath.push({ name: `${node.name}.${node.ext}`, path: node.parent.path });
135
+
136
+ }
137
+ });
138
+
139
+ this.setExecutables(this.filesPath);
140
+ }
141
+
142
+ toggle(el) {
143
+ el.style.display = el.style.display === "none" ? "block" : "none";
144
+ }
145
+
146
+ /* =========================
147
+ AÇÕES (MODEL)
148
+ ========================== */
149
+
150
+ addFolder(parent, name) {
151
+ parent.children.push(new Folder(name, parent));
152
+ this.render();
153
+ }
154
+
155
+ addFile(parent, name, ext=".txt") {
156
+ parent.children.push(new File(name, parent, ext));
157
+ this.render();
158
+ }
159
+
160
+ rename(node, newName) {
161
+ node.name = newName;
162
+ this.render();
163
+ }
164
+
165
+ delete(node) {
166
+ const parent = node.parent;
167
+ parent.children = parent.children.filter(c => c !== node);
168
+ this.render();
169
+ }
170
+
171
+ /* =========================
172
+ MENUS
173
+ ========================== */
174
+
175
+ showRootMenu(e) {
176
+ const menu = this.createMenu([
177
+ {
178
+ label: "New Folder",
179
+ action: () => Swal.fire({
180
+ title: 'Folder name',
181
+ input: 'text',
182
+ showCancelButton: true,
183
+ confirmButtonText: 'Create',
184
+ draggable: true,
185
+ preConfirm: (name) => {
186
+ if (name) {
187
+ this.addFolder(this.root, name);
188
+ }
189
+ }
190
+ })
191
+ },
192
+ {
193
+ label: "New File",
194
+ action: () => Swal.fire({
195
+ title: 'Choose extension and filename',
196
+ html:
197
+ '<select id="swal-select" class="swal2-input">' +
198
+ '<option value="txt">📄 .txt - Text</option>' +
199
+ '<option value="md">📝 .md - Text</option>' +
200
+ '<option value="json">🧩 .json - Data</option>' +
201
+ '<option value="yaml">🧩 .yaml - Data</option>' +
202
+ '<option value="toml">🧩 .toml - Data</option>' +
203
+ '<option value="html">🌐 .html - Markup</option>' +
204
+ '<option value="xml">🌐 .xml - Markup</option>' +
205
+ '<option value="css">🎨 .css - Style</option>' +
206
+ '<option value="js">🟨 .js - JavaScript</option>' +
207
+ '<option value="ts">🟦 .ts - TypeScript</option>' +
208
+ '<option value="py">🐍 .py - Python</option>' +
209
+ '</select>' +
210
+ '<input id="swal-input" class="swal2-input" placeholder="filename">',
211
+ focusConfirm: false,
212
+ draggable: true,
213
+ preConfirm: () => {
214
+ return [
215
+ document.getElementById('swal-select').value,
216
+ document.getElementById('swal-input').value
217
+ ];
218
+ }
219
+ }).then((result) => {
220
+ if (result.value) {
221
+ const [selection, text] = result.value;
222
+ this.addFile(this.root, text, selection);
223
+ }
224
+ })
225
+ }
226
+ ]);
227
+
228
+ this.openMenu(e, menu);
229
+
230
+ const closeMenu = () => {
231
+ menu.remove();
232
+ document.removeEventListener("click", closeMenu);
233
+ document.removeEventListener("contextmenu", closeMenu);
234
+ };
235
+
236
+ setTimeout(() => {
237
+ document.addEventListener("click", closeMenu);
238
+ document.addEventListener("contextmenu", closeMenu);
239
+ });
240
+ }
241
+
242
+ showFolderMenu(e, folder) {
243
+ const menu = this.createMenu([
244
+ { label: "New Folder", action: () => Swal.fire({
245
+ title: 'Folder name',
246
+ input: 'text',
247
+ showCancelButton: true,
248
+ confirmButtonText: 'Create',
249
+ draggable: true,
250
+ preConfirm: (name) => {
251
+ if (name) {
252
+ this.addFolder(folder, name);
253
+ }
254
+ }
255
+ })},
256
+ { label: "New File", action: () => Swal.fire({
257
+ title: 'Choose extension and filename',
258
+ html:
259
+ '<select id="swal-select" class="swal2-input">' +
260
+ '<option value="txt">📄 .txt - Text</option>' +
261
+ '<option value="md">📝 .md - Text</option>' +
262
+ '<option value="json">🧩 .json - Data</option>' +
263
+ '<option value="yaml">🧩 .yaml - Data</option>' +
264
+ '<option value="toml">🧩 .toml - Data</option>' +
265
+ '<option value="html">🌐 .html - Markup</option>' +
266
+ '<option value="xml">🌐 .xml - Markup</option>' +
267
+ '<option value="css">🎨 .css - Style</option>' +
268
+ '<option value="js">🟨 .js - JavaScript</option>' +
269
+ '<option value="ts">🟦 .ts - TypeScript</option>' +
270
+ '<option value="py">🐍 .py - Python</option>' +
271
+ '</select>' +
272
+ '<input id="swal-input" class="swal2-input" placeholder="filename">',
273
+ focusConfirm: false,
274
+ draggable: true,
275
+ preConfirm: () => {
276
+ return [
277
+ document.getElementById('swal-select').value,
278
+ document.getElementById('swal-input').value
279
+ ];
280
+ }
281
+ }).then((result) => {
282
+ if (result.value) {
283
+ const [selection, text] = result.value;
284
+ this.addFile(folder, text, selection);
285
+ }
286
+ })
287
+ },
288
+ { label: "Rename", action: () => Swal.fire({
289
+ title: 'New name',
290
+ input: 'text',
291
+ showCancelButton: true,
292
+ confirmButtonText: 'Rename',
293
+ draggable: true,
294
+ preConfirm: (name) => {
295
+ if (name) {
296
+ this.rename(folder, name);
297
+ }
298
+ }
299
+ }) },
300
+ { label: "Delete", action: () => this.delete(folder) }
301
+ ]);
302
+
303
+ this.openMenu(e, menu);
304
+
305
+ const closeMenu = () => {
306
+ menu.remove();
307
+ document.removeEventListener("click", closeMenu);
308
+ document.removeEventListener("contextmenu", closeMenu);
309
+ };
310
+
311
+ setTimeout(() => {
312
+ document.addEventListener("click", closeMenu);
313
+ document.addEventListener("contextmenu", closeMenu);
314
+ });
315
+ }
316
+
317
+ showFileMenu(e, file) {
318
+ const menu = this.createMenu([
319
+ { label: "Rename", action: () => Swal.fire({
320
+ title: 'New name',
321
+ input: 'text',
322
+ showCancelButton: true,
323
+ confirmButtonText: 'Rename',
324
+ draggable: true,
325
+ preConfirm: (name) => {
326
+ if (name) {
327
+ this.rename(file, name);
328
+ }
329
+ }
330
+ }) },
331
+ { label: "Delete", action: () => Swal.fire({
332
+ title: '',
333
+ showCancelButton: true,
334
+ confirmButtonText: 'Delete',
335
+ draggable: true,
336
+ preConfirm: (name) => {
337
+ if (name) {
338
+ this.delete(file);
339
+ }
340
+ }
341
+ }) }
342
+ ]);
343
+
344
+ this.openMenu(e, menu);
345
+
346
+ const closeMenu = () => {
347
+ menu.remove();
348
+ document.removeEventListener("click", closeMenu);
349
+ document.removeEventListener("contextmenu", closeMenu);
350
+ };
351
+
352
+ setTimeout(() => {
353
+ document.addEventListener("click", closeMenu);
354
+ document.addEventListener("contextmenu", closeMenu);
355
+ });
356
+ }
357
+
358
+ /* =========================
359
+ HELPERS
360
+ ========================== */
361
+
362
+ prompt(label, callback) {
363
+ const value = prompt(label);
364
+ if (value) callback(value);
365
+ }
366
+
367
+ createMenu(items) {
368
+ const div = document.createElement("div");
369
+ div.className = "context-menu";
370
+ div.style.position = "absolute";
371
+ div.style.backgroundColor = "#333";
372
+ div.style.border = "1px solid #222";
373
+ div.style.padding = "8px";
374
+ div.style.zIndex = "3000";
375
+ div.style.color = "white";
376
+ div.style.minWidth = "100px";
377
+
378
+ items.forEach(i => {
379
+ const item = document.createElement("div");
380
+ item.textContent = i.label;
381
+ item.style.padding = "4px 8px";
382
+ item.style.cursor = "pointer";
383
+ item.onmouseover = () => item.style.backgroundColor = "#555";
384
+ item.onmouseout = () => item.style.backgroundColor = "transparent";
385
+ item.style.border = "1px solid #444";
386
+ item.onclick = () => {
387
+ i.action();
388
+ div.remove();
389
+ };
390
+ div.appendChild(item);
391
+ });
392
+
393
+ return div;
394
+ }
395
+
396
+ openMenu(e, menu) {
397
+ document.querySelectorAll(".context-menu").forEach(m => m.remove());
398
+
399
+ menu.style.position = "absolute";
400
+ menu.style.top = `${e.clientY}px`;
401
+ menu.style.left = `${e.clientX}px`;
402
+
403
+ document.body.appendChild(menu);
404
+ }
405
+ }
@@ -0,0 +1,12 @@
1
+ import { FSNode } from "./FSNode.js";
2
+
3
+ class Folder extends FSNode {
4
+ constructor(name, parent) {
5
+ super(name);
6
+ this.parent = parent;
7
+ this.children = [];
8
+ this.path = parent ? [...parent.path, name] : [name];
9
+ }
10
+ }
11
+
12
+ export { Folder };
@@ -0,0 +1,117 @@
1
+ import { getCurrentDir, getCurrentPath, setCurrentDir, setCurrentPath } from "./terminal.js"
2
+ import { sendCode } from "../../index.js";
3
+ import { setViewActive, getFeExecutables, getFileNode } from "../app.js";
4
+
5
+ export function print(text = "") {
6
+ const div = document.createElement("div")
7
+ div.textContent = text
8
+ const output = document.getElementById("runlab-terminal-output")
9
+ output.appendChild(div)
10
+
11
+ }
12
+
13
+ export function handleLS() {
14
+
15
+ const currentDirNode = getCurrentDir()
16
+
17
+ if (!currentDirNode.children || currentDirNode.children.length === 0) {
18
+ print("")
19
+ return
20
+ }
21
+
22
+ const names = currentDirNode.children.map(
23
+ child => `${child.ext ? `${child.name}.${child.ext}` : `/${child.name}`}`
24
+ )
25
+ print(names.join(" "))
26
+ }
27
+
28
+ export function handleClear() {
29
+ const output = document.getElementById("runlab-terminal-output")
30
+ output.innerHTML = ""
31
+ }
32
+
33
+ export function handleChangeDirectory(cmd) {
34
+ const dirName = cmd.split(" ")[1]
35
+ const currentDirNode = getCurrentDir()
36
+ const childrenNames = currentDirNode.children.map(child => child.name)
37
+
38
+ if (dirName === "..") {
39
+ const path = getCurrentPath()
40
+
41
+ if (path.length <= 1) {
42
+ print("Already at root directory.")
43
+ return
44
+ } // já está na raiz
45
+
46
+ path.pop()
47
+ setCurrentPath(path)
48
+ setCurrentDir(getCurrentDir().parent)
49
+
50
+ document.getElementById("runlab-terminal-prompt").textContent = `${path.join("/")}> `
51
+
52
+ return
53
+ }
54
+ if (dirName && childrenNames.includes(dirName)) {
55
+ const child = currentDirNode.children.find(c => c.name === dirName)
56
+ if (child && !child.ext) {
57
+ setCurrentDir(child)
58
+ setCurrentPath([...getCurrentPath(), dirName])
59
+
60
+ document.getElementById("runlab-terminal-prompt").textContent = `${getCurrentPath().join("/")}> `
61
+
62
+ return
63
+ }
64
+ }
65
+
66
+ print("Directory not found or inaccessible.")
67
+ }`
68
+ `
69
+ function parseRunCommand(cmd) {
70
+ const arg = cmd.split(" ")[1]
71
+ if (!arg) return null
72
+
73
+ const clean = arg.replace(/^\/+/, "")
74
+ const parts = clean.split("/")
75
+
76
+ const fileName = parts.pop()
77
+ const path = parts
78
+
79
+ return { fileName, path }
80
+ }
81
+
82
+ function resolvePathRelative(parsed, currentPath) {
83
+ return [...currentPath, ...parsed.path]
84
+ }
85
+
86
+ function samePath(a, b) {
87
+ if (a.length !== b.length) return false
88
+ return a.every((dir, i) => dir === b[i])
89
+ }
90
+
91
+ export function handleRun(cmd) {
92
+ const parsed = parseRunCommand(cmd)
93
+ if (!parsed) {
94
+ print("Executable file command not valid!")
95
+ return
96
+ }
97
+
98
+ const executables = getFeExecutables()
99
+ const currentPath = getCurrentPath()
100
+
101
+ const resolvedPath = resolvePathRelative(parsed, currentPath)
102
+
103
+
104
+ const file = executables.find(e =>
105
+ e.name === parsed.fileName &&
106
+ samePath(e.path, resolvedPath)
107
+ )
108
+
109
+ if (!file) {
110
+ print("Executable file not found!")
111
+ return
112
+ }
113
+
114
+ const fileNode = getFileNode(file)
115
+ sendCode(fileNode.content, fileNode.ext)
116
+ setViewActive(true)
117
+ }
@@ -0,0 +1,126 @@
1
+ import {print, handleLS, handleClear, handleChangeDirectory, handleRun} from "./commands.js"
2
+
3
+ export let currentDirNode = null
4
+ export let currentPath = []
5
+
6
+ export function setCurrentDir(node) {
7
+ currentDirNode = node
8
+ }
9
+
10
+ export function getCurrentDir() {
11
+ return currentDirNode
12
+ }
13
+
14
+ export function setCurrentPath(path) {
15
+ currentPath = path
16
+ }
17
+
18
+ export function getCurrentPath() {
19
+ return currentPath
20
+ }
21
+
22
+ export function createTerminal(parentId,root) {
23
+ const parent = document.getElementById(parentId)
24
+
25
+ const terminal = document.createElement("div")
26
+ terminal.id = "runlab-inside-terminal"
27
+
28
+ Object.assign(terminal.style, {
29
+ backgroundColor: "#1E1E1E",
30
+ color: "#c7cbd9",
31
+ fontFamily: "monospace",
32
+ padding: "12px",
33
+ height: "100%",
34
+ overflowY: "auto",
35
+ boxSizing: "border-box"
36
+ })
37
+
38
+ terminal.tabIndex = 0
39
+
40
+ // ===== estrutura =====
41
+ const output = document.createElement("div")
42
+ output.id = "runlab-terminal-output"
43
+
44
+ const line = document.createElement("div")
45
+
46
+ const prompt = document.createElement("span")
47
+ prompt.id = "runlab-terminal-prompt"
48
+ prompt.textContent = "root> "
49
+
50
+ const input = document.createElement("span")
51
+
52
+ const cursor = document.createElement("span")
53
+ cursor.textContent = "█"
54
+ cursor.style.marginLeft = "2px"
55
+
56
+ line.append(prompt, input, cursor)
57
+ terminal.append(output, line)
58
+ parent.appendChild(terminal)
59
+
60
+ // ===== estado =====
61
+ const state = {
62
+ buffer: ""
63
+ }
64
+
65
+ currentDirNode = root
66
+ currentPath.push("root")
67
+
68
+ // ===== teclado =====
69
+ terminal.addEventListener("keydown", (e) => {
70
+ e.preventDefault()
71
+
72
+ if (e.key === "Backspace") {
73
+ state.buffer = state.buffer.slice(0, -1)
74
+ }
75
+ else if (e.key === "Enter") {
76
+ print(`${currentPath.join("/")}> ${state.buffer}`)
77
+ handleCommand(state.buffer,currentDirNode)
78
+ state.buffer = ""
79
+ }
80
+ else if (e.key.length === 1) {
81
+ state.buffer += e.key
82
+ }
83
+
84
+ input.textContent = state.buffer
85
+ terminal.scrollTop = terminal.scrollHeight
86
+ })
87
+
88
+ terminal.addEventListener("mousedown", () => {
89
+ terminal.focus()
90
+ })
91
+
92
+ terminal.focus()
93
+ }
94
+
95
+ function handleCommand(cmd) {
96
+ const trimmed = cmd.trim()
97
+ const split = trimmed.split(" ")
98
+
99
+
100
+ if (trimmed === "ls") {
101
+ handleLS()
102
+
103
+ } else if (trimmed === "clear") {
104
+ handleClear()
105
+
106
+ } else if (split[0] === "cd") {
107
+ handleChangeDirectory(trimmed)
108
+
109
+ }else if (split[0] === "run") {
110
+ handleRun(trimmed)
111
+
112
+ } else if (trimmed === "pwd") {
113
+ print(currentPath.join("/"))
114
+
115
+ } else if (trimmed === "help" || trimmed === "h") {
116
+ print("Available commands:")
117
+ print("- ls: List directory contents")
118
+ print("- cd: Change directory")
119
+ print("- clear: Clear the terminal")
120
+ print("- pwd: Print current directory")
121
+ print("- run: Run a file (e.g., run script.js)")
122
+ print("- help | h: Show this help message")
123
+ } else if (trimmed !== "") {
124
+ print(`command not found: ${trimmed}`)
125
+ }
126
+ }