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/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.3.0",
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": "node test/run.js"
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
+ }