structscript 1.3.0 → 1.4.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/structscript.js +67 -2
- package/lib/editor.html +3247 -0
- package/lib/main.js +261 -0
- package/lib/preload.js +21 -0
- package/package.json +7 -5
package/lib/main.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// ============================================================
|
|
3
|
+
// StructScript Editor — Electron Main Process
|
|
4
|
+
// ============================================================
|
|
5
|
+
|
|
6
|
+
const { app, BrowserWindow, ipcMain, dialog, Menu, shell } = require('electron');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
// The interpreter lives next to this file in lib/
|
|
12
|
+
const { Interpreter, SSError, buildWebDoc } = require('./interpreter');
|
|
13
|
+
|
|
14
|
+
const RECENT_FILE = path.join(os.homedir(), '.ss_recent.json');
|
|
15
|
+
const EDITOR_HTML = path.join(__dirname, 'editor.html');
|
|
16
|
+
|
|
17
|
+
// ── Recent files ─────────────────────────────────────────────
|
|
18
|
+
function loadRecent() {
|
|
19
|
+
try { return JSON.parse(fs.readFileSync(RECENT_FILE, 'utf8')); } catch (_) { return []; }
|
|
20
|
+
}
|
|
21
|
+
function saveRecent(list) {
|
|
22
|
+
try { fs.writeFileSync(RECENT_FILE, JSON.stringify(list.slice(0, 20)), 'utf8'); } catch (_) {}
|
|
23
|
+
}
|
|
24
|
+
function addRecent(filePath) {
|
|
25
|
+
const abs = path.resolve(filePath);
|
|
26
|
+
const list = loadRecent().filter(f => f !== abs);
|
|
27
|
+
list.unshift(abs);
|
|
28
|
+
saveRecent(list);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Run interpreter, capture output ──────────────────────────
|
|
32
|
+
function runSource(source, inputValues) {
|
|
33
|
+
const lines = [];
|
|
34
|
+
const interp = new Interpreter({
|
|
35
|
+
output: msg => lines.push({ text: String(msg), cls: '' }),
|
|
36
|
+
warn: msg => lines.push({ text: String(msg), cls: 'warn' }),
|
|
37
|
+
});
|
|
38
|
+
if (Array.isArray(inputValues)) interp._inputQueue = [...inputValues];
|
|
39
|
+
try {
|
|
40
|
+
interp.run(source);
|
|
41
|
+
return { ok: true, lines };
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return {
|
|
44
|
+
ok: false, lines,
|
|
45
|
+
error: {
|
|
46
|
+
message: e.message || String(e),
|
|
47
|
+
line: e.ssLine !== undefined ? e.ssLine + 1 : null,
|
|
48
|
+
snippet: e.snippet || null,
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── IPC handlers ─────────────────────────────────────────────
|
|
55
|
+
function registerIPC() {
|
|
56
|
+
|
|
57
|
+
ipcMain.handle('run', (_, { source, inputs }) => runSource(source, inputs || []));
|
|
58
|
+
|
|
59
|
+
ipcMain.handle('save', (_, { filePath, content }) => {
|
|
60
|
+
if (!filePath) return { error: 'No path' };
|
|
61
|
+
const abs = path.resolve(filePath);
|
|
62
|
+
try {
|
|
63
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
64
|
+
fs.writeFileSync(abs, content, 'utf8');
|
|
65
|
+
addRecent(abs);
|
|
66
|
+
return { ok: true, path: abs };
|
|
67
|
+
} catch (e) { return { error: e.message }; }
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
ipcMain.handle('open', (_, { filePath }) => {
|
|
71
|
+
if (!filePath) return { error: 'No path' };
|
|
72
|
+
const abs = path.resolve(filePath);
|
|
73
|
+
try {
|
|
74
|
+
const content = fs.readFileSync(abs, 'utf8');
|
|
75
|
+
addRecent(abs);
|
|
76
|
+
return { ok: true, path: abs, content, name: path.basename(abs) };
|
|
77
|
+
} catch (e) { return { error: e.message }; }
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
ipcMain.handle('recent', () => {
|
|
81
|
+
const files = loadRecent().filter(f => { try { fs.accessSync(f); return true; } catch (_) { return false; } });
|
|
82
|
+
return { files };
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
ipcMain.handle('new', (_, { name } = {}) => {
|
|
86
|
+
const base = (name || 'untitled.ss').replace(/\.ss$/i, '') + '.ss';
|
|
87
|
+
const dir = os.homedir();
|
|
88
|
+
const full = path.join(dir, base);
|
|
89
|
+
const starter = '// New StructScript file\n\nlet name = "World"\nsay "Hello, {name}!"\n';
|
|
90
|
+
try {
|
|
91
|
+
if (!fs.existsSync(full)) fs.writeFileSync(full, starter, 'utf8');
|
|
92
|
+
addRecent(full);
|
|
93
|
+
return { ok: true, path: full, content: fs.readFileSync(full, 'utf8'), name: path.basename(full) };
|
|
94
|
+
} catch (e) { return { error: e.message }; }
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
ipcMain.handle('browse', (_, { dir }) => {
|
|
98
|
+
const target = dir || os.homedir();
|
|
99
|
+
try {
|
|
100
|
+
const entries = fs.readdirSync(target, { withFileTypes: true });
|
|
101
|
+
const items = entries
|
|
102
|
+
.filter(e => e.isDirectory() || e.name.endsWith('.ss'))
|
|
103
|
+
.map(e => ({ name: e.name, isDir: e.isDirectory(), path: path.join(target, e.name) }));
|
|
104
|
+
return { dir: target, parent: path.dirname(target), items };
|
|
105
|
+
} catch (e) { return { error: e.message }; }
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Native open-file dialog
|
|
109
|
+
ipcMain.handle('dialog-open', async (event) => {
|
|
110
|
+
const win = BrowserWindow.fromWebContents(event.sender);
|
|
111
|
+
const result = await dialog.showOpenDialog(win, {
|
|
112
|
+
title: 'Open StructScript File',
|
|
113
|
+
filters: [{ name: 'StructScript', extensions: ['ss'] }, { name: 'All Files', extensions: ['*'] }],
|
|
114
|
+
properties: ['openFile'],
|
|
115
|
+
});
|
|
116
|
+
if (result.canceled || !result.filePaths.length) return { canceled: true };
|
|
117
|
+
const filePath = result.filePaths[0];
|
|
118
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
119
|
+
addRecent(filePath);
|
|
120
|
+
return { ok: true, path: filePath, content, name: path.basename(filePath) };
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Native save-as dialog
|
|
124
|
+
ipcMain.handle('dialog-save', async (event, { suggested, content }) => {
|
|
125
|
+
const win = BrowserWindow.fromWebContents(event.sender);
|
|
126
|
+
const result = await dialog.showSaveDialog(win, {
|
|
127
|
+
title: 'Save StructScript File',
|
|
128
|
+
defaultPath: suggested || path.join(os.homedir(), 'untitled.ss'),
|
|
129
|
+
filters: [{ name: 'StructScript', extensions: ['ss'] }, { name: 'All Files', extensions: ['*'] }],
|
|
130
|
+
});
|
|
131
|
+
if (result.canceled || !result.filePath) return { canceled: true };
|
|
132
|
+
fs.writeFileSync(result.filePath, content, 'utf8');
|
|
133
|
+
addRecent(result.filePath);
|
|
134
|
+
return { ok: true, path: result.filePath, name: path.basename(result.filePath) };
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Save HTML output
|
|
138
|
+
ipcMain.handle('dialog-save-html', async (event, { content, suggested }) => {
|
|
139
|
+
const win = BrowserWindow.fromWebContents(event.sender);
|
|
140
|
+
const result = await dialog.showSaveDialog(win, {
|
|
141
|
+
title: 'Save Web Page',
|
|
142
|
+
defaultPath: suggested || path.join(os.homedir(), 'page.html'),
|
|
143
|
+
filters: [{ name: 'HTML File', extensions: ['html'] }],
|
|
144
|
+
});
|
|
145
|
+
if (result.canceled || !result.filePath) return { canceled: true };
|
|
146
|
+
fs.writeFileSync(result.filePath, content, 'utf8');
|
|
147
|
+
return { ok: true, path: result.filePath };
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Reveal file in Finder / Explorer
|
|
151
|
+
ipcMain.handle('show-in-folder', (_, { filePath }) => {
|
|
152
|
+
if (filePath) shell.showItemInFolder(filePath);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Homedir
|
|
156
|
+
ipcMain.handle('homedir', () => os.homedir());
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Window ────────────────────────────────────────────────────
|
|
160
|
+
function createWindow(filePath) {
|
|
161
|
+
const win = new BrowserWindow({
|
|
162
|
+
width: 1200,
|
|
163
|
+
height: 780,
|
|
164
|
+
minWidth: 700,
|
|
165
|
+
minHeight: 480,
|
|
166
|
+
title: 'StructScript Editor',
|
|
167
|
+
backgroundColor: '#0a1614',
|
|
168
|
+
webPreferences: {
|
|
169
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
170
|
+
contextIsolation: true,
|
|
171
|
+
nodeIntegration: false,
|
|
172
|
+
},
|
|
173
|
+
// macOS traffic lights look better with this
|
|
174
|
+
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
|
|
175
|
+
icon: path.join(__dirname, 'icon.png'), // optional — won't crash if missing
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
win.loadFile(EDITOR_HTML, filePath ? { query: { file: filePath } } : {});
|
|
179
|
+
|
|
180
|
+
// Remove default menu on Windows/Linux; keep a minimal one
|
|
181
|
+
buildAppMenu(win);
|
|
182
|
+
|
|
183
|
+
return win;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function buildAppMenu(win) {
|
|
187
|
+
const isMac = process.platform === 'darwin';
|
|
188
|
+
const template = [
|
|
189
|
+
...(isMac ? [{ role: 'appMenu' }] : []),
|
|
190
|
+
{
|
|
191
|
+
label: 'File',
|
|
192
|
+
submenu: [
|
|
193
|
+
{ label: 'New File', accelerator: 'CmdOrCtrl+N', click: () => win.webContents.executeJavaScript('newFile()') },
|
|
194
|
+
{ label: 'Open…', accelerator: 'CmdOrCtrl+O', click: () => win.webContents.executeJavaScript('openFile()') },
|
|
195
|
+
{ type: 'separator' },
|
|
196
|
+
{ label: 'Save', accelerator: 'CmdOrCtrl+S', click: () => win.webContents.executeJavaScript('saveFile()') },
|
|
197
|
+
{ label: 'Save As…', accelerator: 'CmdOrCtrl+Shift+S', click: () => win.webContents.executeJavaScript('saveFileAs()') },
|
|
198
|
+
{ type: 'separator' },
|
|
199
|
+
isMac ? { role: 'close' } : { role: 'quit' },
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
label: 'Edit',
|
|
204
|
+
submenu: [
|
|
205
|
+
{ role: 'undo' }, { role: 'redo' }, { type: 'separator' },
|
|
206
|
+
{ role: 'cut' }, { role: 'copy' }, { role: 'paste' },
|
|
207
|
+
{ role: 'selectAll' },
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
label: 'Run',
|
|
212
|
+
submenu: [
|
|
213
|
+
{ label: 'Run Script', accelerator: 'CmdOrCtrl+Return', click: () => win.webContents.executeJavaScript('runCode()') },
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
label: 'View',
|
|
218
|
+
submenu: [
|
|
219
|
+
{ role: 'reload' },
|
|
220
|
+
{ type: 'separator' },
|
|
221
|
+
{ role: 'toggleDevTools' },
|
|
222
|
+
{ type: 'separator' },
|
|
223
|
+
{ role: 'resetZoom' }, { role: 'zoomIn' }, { role: 'zoomOut' },
|
|
224
|
+
{ type: 'separator' },
|
|
225
|
+
{ role: 'togglefullscreen' },
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
role: 'help',
|
|
230
|
+
submenu: [
|
|
231
|
+
{ label: 'StructScript Docs', click: () => shell.openExternal('https://structscript.dev') },
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
];
|
|
235
|
+
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── App lifecycle ─────────────────────────────────────────────
|
|
239
|
+
app.whenReady().then(() => {
|
|
240
|
+
registerIPC();
|
|
241
|
+
|
|
242
|
+
// File passed as CLI arg (e.g. double-click a .ss file on desktop)
|
|
243
|
+
const filePath = process.argv.find(a => a.endsWith('.ss') && !a.includes('node_modules'));
|
|
244
|
+
createWindow(filePath || null);
|
|
245
|
+
|
|
246
|
+
// macOS: re-open window when dock icon is clicked
|
|
247
|
+
app.on('activate', () => {
|
|
248
|
+
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
app.on('window-all-closed', () => {
|
|
253
|
+
if (process.platform !== 'darwin') app.quit();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// macOS: open files dragged onto the dock icon
|
|
257
|
+
app.on('open-file', (event, filePath) => {
|
|
258
|
+
event.preventDefault();
|
|
259
|
+
if (app.isReady()) createWindow(filePath);
|
|
260
|
+
else app.once('ready', () => createWindow(filePath));
|
|
261
|
+
});
|
package/lib/preload.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// ============================================================
|
|
3
|
+
// StructScript Editor — Preload (context bridge)
|
|
4
|
+
// Exposes safe IPC methods to the renderer (editor.html)
|
|
5
|
+
// ============================================================
|
|
6
|
+
|
|
7
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
8
|
+
|
|
9
|
+
contextBridge.exposeInMainWorld('electronAPI', {
|
|
10
|
+
run: (args) => ipcRenderer.invoke('run', args),
|
|
11
|
+
save: (args) => ipcRenderer.invoke('save', args),
|
|
12
|
+
open: (args) => ipcRenderer.invoke('open', args),
|
|
13
|
+
recent: () => ipcRenderer.invoke('recent'),
|
|
14
|
+
newFile: (args) => ipcRenderer.invoke('new', args || {}),
|
|
15
|
+
browse: (args) => ipcRenderer.invoke('browse', args || {}),
|
|
16
|
+
dialogOpen: () => ipcRenderer.invoke('dialog-open'),
|
|
17
|
+
dialogSave: (args) => ipcRenderer.invoke('dialog-save', args),
|
|
18
|
+
dialogSaveHtml:(args) => ipcRenderer.invoke('dialog-save-html', args),
|
|
19
|
+
showInFolder: (args) => ipcRenderer.invoke('show-in-folder', args),
|
|
20
|
+
homedir: () => ipcRenderer.invoke('homedir'),
|
|
21
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "structscript",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "The StructScript programming language — clean, readable scripting for everyone",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "The StructScript programming language — clean, readable scripting for everyone. Includes a built-in visual editor.",
|
|
5
5
|
"keywords": ["structscript", "language", "interpreter", "scripting", "programming-language"],
|
|
6
6
|
"author": "StructScript",
|
|
7
7
|
"license": "MIT",
|
|
@@ -19,12 +19,14 @@
|
|
|
19
19
|
},
|
|
20
20
|
"homepage": "https://structscript.dev",
|
|
21
21
|
"scripts": {
|
|
22
|
-
"test":
|
|
22
|
+
"test": "node test/run.js",
|
|
23
|
+
"editor": "electron lib/main.js"
|
|
23
24
|
},
|
|
24
25
|
"files": [
|
|
25
26
|
"bin/",
|
|
26
27
|
"lib/",
|
|
27
28
|
"examples/",
|
|
28
29
|
"README.md"
|
|
29
|
-
]
|
|
30
|
-
}
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {}
|
|
32
|
+
}
|