runlab 0.1.1 → 1.1.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.
@@ -2,15 +2,46 @@ import { Folder } from "./Folder.js";
2
2
  import { File } from "./File.js";
3
3
  import { updateEditorContentById, setActiveFile } from "../core/editor.js";
4
4
  import Swal from "sweetalert2";
5
+ import { setEditorActive, setViewActive } from "../app.js";
5
6
 
6
7
  export class FileExplorer {
7
8
  constructor(parentId) {
8
9
  this.container = document.getElementById(parentId);
9
10
  this.root = new Folder("root", null);
10
-
11
+ this.execFiles = [];
12
+ this.filesPath = [];
13
+ this.path = [];
11
14
  this.render();
12
15
  }
13
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
+
14
45
  /* =========================
15
46
  RENDER
16
47
  ========================== */
@@ -50,12 +81,17 @@ export class FileExplorer {
50
81
  btn.style.paddingBottom = "8px";
51
82
  btn.style.cursor = "pointer";
52
83
 
84
+
53
85
 
54
86
  btn.onmouseover = () => btn.style.backgroundColor = "#555";
55
87
  btn.onmouseout = () => btn.style.backgroundColor = "transparent";
56
88
 
57
89
  const ul = document.createElement("ul");
58
90
  ul.style.display = "none";
91
+ ul.style.listStyle = "none";
92
+ ul.style.margin = "0";
93
+ ul.style.padding = "0 0 0 24px";
94
+
59
95
 
60
96
  btn.onclick = () => this.toggle(ul);
61
97
  btn.oncontextmenu = e => {
@@ -79,9 +115,12 @@ export class FileExplorer {
79
115
  li.style.paddingTop = "8px";
80
116
  li.style.paddingBottom = "8px";
81
117
  li.style.cursor = "pointer";
118
+
82
119
 
83
120
  li.onclick = () => {
84
121
  setActiveFile(node);
122
+ setViewActive(false);
123
+ setEditorActive(true);
85
124
  updateEditorContentById("runlab-editor", node.content);
86
125
  };
87
126
 
@@ -92,8 +131,12 @@ export class FileExplorer {
92
131
  };
93
132
 
94
133
  parentUl.appendChild(li);
134
+ this.filesPath.push({ name: `${node.name}.${node.ext}`, path: node.parent.path });
135
+
95
136
  }
96
137
  });
138
+
139
+ this.setExecutables(this.filesPath);
97
140
  }
98
141
 
99
142
  toggle(el) {
@@ -138,6 +181,7 @@ export class FileExplorer {
138
181
  input: 'text',
139
182
  showCancelButton: true,
140
183
  confirmButtonText: 'Create',
184
+ draggable: true,
141
185
  preConfirm: (name) => {
142
186
  if (name) {
143
187
  this.addFolder(this.root, name);
@@ -151,20 +195,21 @@ export class FileExplorer {
151
195
  title: 'Choose extension and filename',
152
196
  html:
153
197
  '<select id="swal-select" class="swal2-input">' +
154
- '<option value="txt">.txt - Text</option>' +
155
- '<option value="md">.md - Text</option>' +
156
- '<option value="json">.json - Data</option>' +
157
- '<option value="yaml">.yaml - Data</option>' +
158
- '<option value="toml">.toml - Data</option>' +
159
- '<option value="html">.html - Markup</option>' +
160
- '<option value="xml">.xml - Markup</option>' +
161
- '<option value="css">.css - Style</option>' +
162
- '<option value="js">.js - JavaScript</option>' +
163
- '<option value="ts">.ts - TypeScript</option>' +
164
- '<option value="go">.go - GO</option>' +
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>' +
165
209
  '</select>' +
166
210
  '<input id="swal-input" class="swal2-input" placeholder="filename">',
167
211
  focusConfirm: false,
212
+ draggable: true,
168
213
  preConfirm: () => {
169
214
  return [
170
215
  document.getElementById('swal-select').value,
@@ -177,10 +222,21 @@ export class FileExplorer {
177
222
  this.addFile(this.root, text, selection);
178
223
  }
179
224
  })
180
- }
225
+ }
181
226
  ]);
182
227
 
183
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
+ });
184
240
  }
185
241
 
186
242
  showFolderMenu(e, folder) {
@@ -190,6 +246,7 @@ export class FileExplorer {
190
246
  input: 'text',
191
247
  showCancelButton: true,
192
248
  confirmButtonText: 'Create',
249
+ draggable: true,
193
250
  preConfirm: (name) => {
194
251
  if (name) {
195
252
  this.addFolder(folder, name);
@@ -200,20 +257,21 @@ export class FileExplorer {
200
257
  title: 'Choose extension and filename',
201
258
  html:
202
259
  '<select id="swal-select" class="swal2-input">' +
203
- '<option value="txt">.txt - Text</option>' +
204
- '<option value="md">.md - Text</option>' +
205
- '<option value="json">.json - Data</option>' +
206
- '<option value="yaml">.yaml - Data</option>' +
207
- '<option value="toml">.toml - Data</option>' +
208
- '<option value="html">.html - Markup</option>' +
209
- '<option value="xml">.xml - Markup</option>' +
210
- '<option value="css">.css - Style</option>' +
211
- '<option value="js">.js - JavaScript</option>' +
212
- '<option value="ts">.ts - TypeScript</option>' +
213
- '<option value="go">.go - GO</option>' +
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>' +
214
271
  '</select>' +
215
272
  '<input id="swal-input" class="swal2-input" placeholder="filename">',
216
273
  focusConfirm: false,
274
+ draggable: true,
217
275
  preConfirm: () => {
218
276
  return [
219
277
  document.getElementById('swal-select').value,
@@ -223,7 +281,7 @@ export class FileExplorer {
223
281
  }).then((result) => {
224
282
  if (result.value) {
225
283
  const [selection, text] = result.value;
226
- this.addFile(this.root, text, selection);
284
+ this.addFile(folder, text, selection);
227
285
  }
228
286
  })
229
287
  },
@@ -232,6 +290,7 @@ export class FileExplorer {
232
290
  input: 'text',
233
291
  showCancelButton: true,
234
292
  confirmButtonText: 'Rename',
293
+ draggable: true,
235
294
  preConfirm: (name) => {
236
295
  if (name) {
237
296
  this.rename(folder, name);
@@ -242,6 +301,17 @@ export class FileExplorer {
242
301
  ]);
243
302
 
244
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
+ });
245
315
  }
246
316
 
247
317
  showFileMenu(e, file) {
@@ -251,6 +321,7 @@ export class FileExplorer {
251
321
  input: 'text',
252
322
  showCancelButton: true,
253
323
  confirmButtonText: 'Rename',
324
+ draggable: true,
254
325
  preConfirm: (name) => {
255
326
  if (name) {
256
327
  this.rename(file, name);
@@ -261,6 +332,7 @@ export class FileExplorer {
261
332
  title: '',
262
333
  showCancelButton: true,
263
334
  confirmButtonText: 'Delete',
335
+ draggable: true,
264
336
  preConfirm: (name) => {
265
337
  if (name) {
266
338
  this.delete(file);
@@ -270,6 +342,17 @@ export class FileExplorer {
270
342
  ]);
271
343
 
272
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
+ });
273
356
  }
274
357
 
275
358
  /* =========================
@@ -5,6 +5,7 @@ class Folder extends FSNode {
5
5
  super(name);
6
6
  this.parent = parent;
7
7
  this.children = [];
8
+ this.path = parent ? [...parent.path, name] : [name];
8
9
  }
9
10
  }
10
11
 
@@ -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
+ }
package/dist/index.js CHANGED
@@ -1,27 +1,52 @@
1
- import "./wasm/wasm_exec.js";
2
- import wasmUrl from "./wasm/runlab.wasm?url";
3
1
  import { generateContainer } from "./app/app.js";
2
+ import { appendViewContent } from "./app/app.js";
3
+ import { ReactRunlabButton } from "./reactButton.jsx";
4
4
 
5
- const go = new Go();
6
-
7
- export async function run(parentId, width = 1200, height = 800) {
8
- console.log("WASM URL resolvido:", wasmUrl);
9
- const response = await fetch(wasmUrl);
10
- const bytes = await response.arrayBuffer();
11
-
12
- const result = await WebAssembly.instantiate(bytes, go.importObject);
13
- generateContainer(parentId, width, height);
14
- go.run(result.instance);
5
+ let runtimeUrl = "";
6
+ function setRuntimeUrl(url) {
7
+ runtimeUrl = url;
15
8
  }
16
-
17
- async function updateFileExplorer() {
18
-
9
+ function getRuntimeUrl() {
10
+ return runtimeUrl;
19
11
  }
20
12
 
21
- async function loadTerminal() {
13
+ export async function run({
14
+ parentId,
15
+ runtimeUrl = ""
16
+ }) {
17
+ generateContainer(parentId);
18
+ setRuntimeUrl(runtimeUrl);
19
+ }
22
20
 
21
+ export async function sendCode(code, ext = "txt") {
22
+
23
+ const runnableExtensions = ["js", "ts", "py"];
24
+ if (!runnableExtensions.includes(ext)) {
25
+ appendViewContent(code);
26
+ return;
27
+ }
28
+
29
+ try {
30
+ const response = await fetch(getRuntimeUrl(), {
31
+ method: "POST",
32
+ headers: {
33
+ "Content-Type": "application/json"
34
+ },
35
+ body: JSON.stringify({ code, ext })
36
+ });
37
+
38
+ if (!response.ok) {
39
+ throw new Error("Failed to send code");
40
+ }
41
+
42
+ const data = await response.json();
43
+
44
+ appendViewContent(data.stdout || data.stderr);
45
+ } catch (err) {
46
+ console.error(err);
47
+ }
23
48
  }
24
49
 
25
- async function loadEditor() {
50
+ //=============================== Buttons ===============================
26
51
 
27
- }
52
+ export { ReactRunlabButton };
@@ -0,0 +1,46 @@
1
+ import { useEffect } from 'react';
2
+ import { run } from "./index.js";
3
+
4
+ export function ReactRunlabButton(props = { runConfig: {} }) {
5
+ const { runConfig } = props;
6
+
7
+ useEffect(() => {
8
+ if (typeof window !== 'undefined' && window.__runlab_started) return;
9
+
10
+ if (typeof window !== 'undefined') {
11
+ window.__runlab_started = true;
12
+ }
13
+
14
+ run(runConfig);
15
+ }, []);
16
+
17
+ const showApp = () => {
18
+ const app = document.getElementById("runlab-custom-div");
19
+ const btn = document.getElementById("runlab-custom-btn");
20
+
21
+ const displayStyle = window.getComputedStyle(app).display;
22
+
23
+ if (displayStyle === "none") {
24
+ app.style.display = "block";
25
+ btn.textContent = "Hide App";
26
+ }
27
+ if (displayStyle === "block") {
28
+ app.style.display = "none";
29
+ btn.textContent = "Show App";
30
+ }
31
+ };
32
+
33
+ return (
34
+ <>
35
+ <button
36
+ id="runlab-custom-btn"
37
+ onClick={showApp}
38
+ className="show-app-button"
39
+ >
40
+ Show App
41
+ </button>
42
+
43
+ <div id="runlab-custom-div" style={{ display: "none" }}></div>
44
+ </>
45
+ );
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runlab",
3
- "version": "0.1.1",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -26,7 +26,6 @@
26
26
  "@codemirror/lang-markdown": "^6",
27
27
  "@codemirror/lang-xml": "^6",
28
28
  "@codemirror/lang-yaml": "^6",
29
- "@codemirror/lang-go": "^6",
30
29
 
31
30
  "@uiw/codemirror-theme-vscode": "^4.21.0",
32
31
  "@replit/codemirror-vscode-keymap": "^6.0.0"
Binary file