From 7d84d5deb4ed85b650918c28352b824b69a200aa Mon Sep 17 00:00:00 2001 From: Murilo Polese Date: Tue, 16 Apr 2024 16:26:49 +0200 Subject: [PATCH 1/3] Fix about page and split `index.js` into backend helper files --- README.md | 5 +- backend/helpers.js | 62 +++++++ backend/ipc.js | 110 +++++++++++ backend/menu.js | 143 ++++++++++++++ index.js | 307 +------------------------------ ui/arduino/media/about_image.png | Bin 0 -> 26072 bytes ui/arduino/views/about.css | 84 +++++++++ 7 files changed, 412 insertions(+), 299 deletions(-) create mode 100644 backend/helpers.js create mode 100644 backend/ipc.js create mode 100644 backend/menu.js create mode 100644 ui/arduino/media/about_image.png create mode 100644 ui/arduino/views/about.css diff --git a/README.md b/README.md index 852a702..93cef08 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,9 @@ This project is sponsored by Arduino, based on original work by [Murilo Polese]( ## Technical overview -Arduino Lab for MicroPython is an [Electron](https://www.electronjs.org/) app that has its main purpose to communicate over serial with a microprocessor running [MicroPython](https://micropython.org/). All Electron code is at `/index.js`. +Arduino Lab for MicroPython is an [Electron](https://www.electronjs.org/) app that has its main purpose to communicate over serial with a microprocessor running [MicroPython](https://micropython.org/). The Electron code is at `/index.js` and inside the folder `/backend`. -All operations over serial are abstracted and packaged on `/micropython.js` which is an attempt of porting `pyboard.py`. The port has its [own repository](https://github.com/arduino/micropython.js) but for the sake of simplicity and transparency, `micropython.js` is committed as source code. +All operations over serial are abstracted and packaged on `micropython.js` which is an attempt of porting `pyboard.py`. The module has its [own repository](https://github.com/arduino/micropython.js) with documentation and examples of usage. The User Interface (UI) source code stays inside `/ui` folder and is completely independent of the Electron code. @@ -49,6 +49,7 @@ At the root of the repository you will find: - `/build_resources`: Icons and other assets used during the build process. - `/ui`: Available user interfaces. - `/index.js`: Main Electron code. +- `/backend`: Electron helpers. - `/preload.js`: Creates Disk, Serial and Window APIs on Electron's main process and exposes it to Electron's renderer process (context bridge). ## User interface diff --git a/backend/helpers.js b/backend/helpers.js new file mode 100644 index 0000000..427d360 --- /dev/null +++ b/backend/helpers.js @@ -0,0 +1,62 @@ +const { dialog } = require('electron') +const fs = require('fs') +const path = require('path') + +async function openFolderDialog(win) { + // https://stackoverflow.com/questions/46027287/electron-open-folder-dialog + let dir = await dialog.showOpenDialog(win, { properties: [ 'openDirectory' ] }) + return dir.filePaths[0] || null +} + +function listFolder(folder) { + files = fs.readdirSync(path.resolve(folder)) + // Filter out directories + files = files.filter(f => { + let filePath = path.resolve(folder, f) + return !fs.lstatSync(filePath).isDirectory() + }) + return files +} + +function ilistFolder(folder) { + let files = fs.readdirSync(path.resolve(folder)) + files = files.filter(f => { + let filePath = path.resolve(folder, f) + return !fs.lstatSync(filePath).isSymbolicLink() + }) + files = files.map(f => { + let filePath = path.resolve(folder, f) + return { + path: f, + type: fs.lstatSync(filePath).isDirectory() ? 'folder' : 'file' + } + }) + // Filter out dot files + files = files.filter(f => f.path.indexOf('.') !== 0) + return files +} + +function getAllFiles(dirPath, arrayOfFiles) { + // https://coderrocketfuel.com/article/recursively-list-all-the-files-in-a-directory-using-node-js + files = ilistFolder(dirPath) + arrayOfFiles = arrayOfFiles || [] + files.forEach(function(file) { + const p = path.join(dirPath, file.path) + const stat = fs.statSync(p) + arrayOfFiles.push({ + path: p, + type: stat.isDirectory() ? 'folder' : 'file' + }) + if (stat.isDirectory()) { + arrayOfFiles = getAllFiles(p, arrayOfFiles) + } + }) + return arrayOfFiles +} + +module.exports = { + openFolderDialog, + listFolder, + ilistFolder, + getAllFiles +} diff --git a/backend/ipc.js b/backend/ipc.js new file mode 100644 index 0000000..c97daba --- /dev/null +++ b/backend/ipc.js @@ -0,0 +1,110 @@ +const fs = require('fs') +const { + openFolderDialog, + listFolder, + ilistFolder, + getAllFiles +} = require('./helpers.js') + +module.exports = function registerIPCHandlers(win, ipcMain) { + ipcMain.handle('open-folder', async (event) => { + console.log('ipcMain', 'open-folder') + const folder = await openFolderDialog(win) + let files = [] + if (folder) { + files = listFolder(folder) + } + return { folder, files } + }) + + ipcMain.handle('list-files', async (event, folder) => { + console.log('ipcMain', 'list-files', folder) + if (!folder) return [] + return listFolder(folder) + }) + + ipcMain.handle('ilist-files', async (event, folder) => { + console.log('ipcMain', 'ilist-files', folder) + if (!folder) return [] + return ilistFolder(folder) + }) + + ipcMain.handle('ilist-all-files', (event, folder) => { + console.log('ipcMain', 'ilist-all-files', folder) + if (!folder) return [] + return getAllFiles(folder) + }) + + ipcMain.handle('load-file', (event, filePath) => { + console.log('ipcMain', 'load-file', filePath) + let content = fs.readFileSync(filePath) + return content + }) + + ipcMain.handle('save-file', (event, filePath, content) => { + console.log('ipcMain', 'save-file', filePath, content) + fs.writeFileSync(filePath, content, 'utf8') + return true + }) + + ipcMain.handle('update-folder', (event, folder) => { + console.log('ipcMain', 'update-folder', folder) + let files = fs.readdirSync(path.resolve(folder)) + // Filter out directories + files = files.filter(f => { + let filePath = path.resolve(folder, f) + return !fs.lstatSync(filePath).isDirectory() + }) + return { folder, files } + }) + + ipcMain.handle('remove-file', (event, filePath) => { + console.log('ipcMain', 'remove-file', filePath) + fs.unlinkSync(filePath) + return true + }) + + ipcMain.handle('rename-file', (event, filePath, newFilePath) => { + console.log('ipcMain', 'rename-file', filePath, newFilePath) + fs.renameSync(filePath, newFilePath) + return true + }) + + ipcMain.handle('create-folder', (event, folderPath) => { + console.log('ipcMain', 'create-folder', folderPath) + try { + fs.mkdirSync(folderPath, { recursive: true }) + } catch(e) { + console.log('error', e) + return false + } + return true + }) + + ipcMain.handle('remove-folder', (event, folderPath) => { + console.log('ipcMain', 'remove-folder', folderPath) + fs.rmdirSync(folderPath, { recursive: true, force: true }) + return true + }) + + ipcMain.handle('file-exists', (event, filePath) => { + console.log('ipcMain', 'file-exists', filePath) + try { + fs.accessSync(filePath, fs.constants.F_OK) + return true + } catch(err) { + return false + } + }) + // WINDOW MANAGEMENT + + ipcMain.handle('set-window-size', (event, minWidth, minHeight) => { + console.log('ipcMain', 'set-window-size', minWidth, minHeight) + if (!win) { + console.log('No window defined') + return false + } + + win.setMinimumSize(minWidth, minHeight) + }) +} diff --git a/backend/menu.js b/backend/menu.js new file mode 100644 index 0000000..3ee40a6 --- /dev/null +++ b/backend/menu.js @@ -0,0 +1,143 @@ +const { app, Menu } = require('electron') +const path = require('path') +const openAboutWindow = require('about-window').default + +module.exports = function registerMenu(win) { + const isMac = process.platform === 'darwin' + const isDev = !app.isPackaged + const template = [ + ...(isMac ? [{ + label: app.name, + submenu: [ + { role: 'about'}, + { type: 'separator' }, + { role: 'services' }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideOthers' }, + { role: 'unhide' }, + { type: 'separator' }, + { role: 'quit' } + ] + }] : []), + { + label: 'File', + submenu: [ + isMac ? { role: 'close' } : { role: 'quit' } + ] + }, + { + label: 'Edit', + submenu: [ + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + ...(isMac ? [ + { role: 'pasteAndMatchStyle' }, + { role: 'selectAll' }, + { type: 'separator' }, + { + label: 'Speech', + submenu: [ + { role: 'startSpeaking' }, + { role: 'stopSpeaking' } + ] + } + ] : [ + { type: 'separator' }, + { role: 'selectAll' } + ]) + ] + }, + { + label: 'View', + submenu: [ + { role: 'reload' }, + { type: 'separator' }, + { role: 'resetZoom' }, + { role: 'zoomIn' }, + { role: 'zoomOut' }, + { type: 'separator' }, + { role: 'togglefullscreen' }, + ...(isDev ? [ + { type: 'separator' }, + { role: 'toggleDevTools' }, + ]:[ + ]) + ] + }, + { + label: 'Window', + submenu: [ + { role: 'minimize' }, + { role: 'zoom' }, + ...(isMac ? [ + { type: 'separator' }, + { role: 'front' }, + { type: 'separator' }, + { role: 'window' } + ] : [ + { role: 'close' } + ]) + ] + }, + { + role: 'help', + submenu: [ + { + label: 'Learn More', + click: async () => { + const { shell } = require('electron') + await shell.openExternal('https://github.com/arduino/lab-micropython-editor') + } + }, + { + label: 'Report an issue', + click: async () => { + const { shell } = require('electron') + await shell.openExternal('https://github.com/arduino/lab-micropython-editor/issues') + } + }, + { + label:'Info about this app', + click: () => { + openAboutWindow({ + icon_path: path.resolve(__dirname, '../ui/arduino/media/about_image.png'), + css_path: path.resolve(__dirname, '../ui/arduino/views/about.css'), + // about_page_dir: path.resolve(__dirname, '../ui/arduino/views/'), + copyright: '© Arduino SA 2022', + package_json_dir: path.resolve(__dirname, '..'), + bug_report_url: "https://github.com/arduino/lab-micropython-editor/issues", + bug_link_text: "report an issue", + homepage: "https://labs.arduino.cc", + use_version_info: false, + win_options: { + parent: win, + modal: true, + }, + show_close_button: 'Close', + }) + } + }, + ] + } + ] + + const menu = Menu.buildFromTemplate(template) + + app.setAboutPanelOptions({ + applicationName: app.name, + applicationVersion: app.getVersion(), + copyright: app.copyright, + credits: '(See "Info about this app" in the Help menu)', + authors: ['Arduino'], + website: 'https://arduino.cc', + iconPath: path.join(__dirname, '../assets/image.png'), + }) + + Menu.setApplicationMenu(menu) + +} diff --git a/index.js b/index.js index 76f7542..2992967 100644 --- a/index.js +++ b/index.js @@ -1,164 +1,11 @@ -const { app, BrowserWindow, Menu, ipcMain, dialog } = require('electron') +const { app, BrowserWindow, ipcMain } = require('electron') const path = require('path') const fs = require('fs') -const openAboutWindow = require('about-window').default -let win = null // main window - -// HELPERS -async function openFolderDialog() { - // https://stackoverflow.com/questions/46027287/electron-open-folder-dialog - let dir = await dialog.showOpenDialog(win, { properties: [ 'openDirectory' ] }) - return dir.filePaths[0] || null -} - -function listFolder(folder) { - files = fs.readdirSync(path.resolve(folder)) - // Filter out directories - files = files.filter(f => { - let filePath = path.resolve(folder, f) - return !fs.lstatSync(filePath).isDirectory() - }) - return files -} - -function ilistFolder(folder) { - let files = fs.readdirSync(path.resolve(folder)) - files = files.filter(f => { - let filePath = path.resolve(folder, f) - return !fs.lstatSync(filePath).isSymbolicLink() - }) - files = files.map(f => { - let filePath = path.resolve(folder, f) - return { - path: f, - type: fs.lstatSync(filePath).isDirectory() ? 'folder' : 'file' - } - }) - // Filter out dot files - files = files.filter(f => f.path.indexOf('.') !== 0) - return files -} - -function getAllFiles(dirPath, arrayOfFiles) { - // https://coderrocketfuel.com/article/recursively-list-all-the-files-in-a-directory-using-node-js - files = ilistFolder(dirPath) - arrayOfFiles = arrayOfFiles || [] - files.forEach(function(file) { - const p = path.join(dirPath, file.path) - const stat = fs.statSync(p) - arrayOfFiles.push({ - path: p, - type: stat.isDirectory() ? 'folder' : 'file' - }) - if (stat.isDirectory()) { - arrayOfFiles = getAllFiles(p, arrayOfFiles) - } - }) - return arrayOfFiles -} - -// LOCAL FILE SYSTEM ACCESS -ipcMain.handle('open-folder', async (event) => { - console.log('ipcMain', 'open-folder') - const folder = await openFolderDialog() - let files = [] - if (folder) { - files = listFolder(folder) - } - return { folder, files } -}) - -ipcMain.handle('list-files', async (event, folder) => { - console.log('ipcMain', 'list-files', folder) - if (!folder) return [] - return listFolder(folder) -}) - -ipcMain.handle('ilist-files', async (event, folder) => { - console.log('ipcMain', 'ilist-files', folder) - if (!folder) return [] - return ilistFolder(folder) -}) - -ipcMain.handle('ilist-all-files', (event, folder) => { - console.log('ipcMain', 'ilist-all-files', folder) - if (!folder) return [] - return getAllFiles(folder) -}) +const registerIPCHandlers = require('./backend/ipc.js') +const registerMenu = require('./backend/menu.js') -ipcMain.handle('load-file', (event, filePath) => { - console.log('ipcMain', 'load-file', filePath) - let content = fs.readFileSync(filePath) - return content -}) - -ipcMain.handle('save-file', (event, filePath, content) => { - console.log('ipcMain', 'save-file', filePath, content) - fs.writeFileSync(filePath, content, 'utf8') - return true -}) - -ipcMain.handle('update-folder', (event, folder) => { - console.log('ipcMain', 'update-folder', folder) - let files = fs.readdirSync(path.resolve(folder)) - // Filter out directories - files = files.filter(f => { - let filePath = path.resolve(folder, f) - return !fs.lstatSync(filePath).isDirectory() - }) - return { folder, files } -}) - -ipcMain.handle('remove-file', (event, filePath) => { - console.log('ipcMain', 'remove-file', filePath) - fs.unlinkSync(filePath) - return true -}) - -ipcMain.handle('rename-file', (event, filePath, newFilePath) => { - console.log('ipcMain', 'rename-file', filePath, newFilePath) - fs.renameSync(filePath, newFilePath) - return true -}) - -ipcMain.handle('create-folder', (event, folderPath) => { - console.log('ipcMain', 'create-folder', folderPath) - try { - fs.mkdirSync(folderPath, { recursive: true }) - } catch(e) { - console.log('error', e) - return false - } - return true -}) - -ipcMain.handle('remove-folder', (event, folderPath) => { - console.log('ipcMain', 'remove-folder', folderPath) - fs.rmdirSync(folderPath, { recursive: true, force: true }) - return true -}) - -ipcMain.handle('file-exists', (event, filePath) => { - console.log('ipcMain', 'file-exists', filePath) - try { - fs.accessSync(filePath, fs.constants.F_OK) - return true - } catch(err) { - return false - } -}) -// WINDOW MANAGEMENT - -ipcMain.handle('set-window-size', (event, minWidth, minHeight) => { - console.log('ipcMain', 'set-window-size', minWidth, minHeight) - if (!win) { - console.log('No window defined') - return false - } - - win.setMinimumSize(minWidth, minHeight) -}) +let win = null // main window // START APP function createWindow () { @@ -167,154 +14,20 @@ function createWindow () { width: 720, height: 640, webPreferences: { - nodeIntegration: true, - webSecurity: false, + nodeIntegration: false, + webSecurity: true, enableRemoteModule: false, preload: path.join(__dirname, "preload.js") } }) // and load the index.html of the app. win.loadFile('ui/arduino/index.html') - // win.loadFile('ui/sandbox/index.html') -} - -// TODO: Loading splash screen -const isMac = process.platform === 'darwin' -const isDev = !app.isPackaged -const template = [ - ...(isMac ? [{ - label: app.name, - submenu: [ - { role: 'about'}, - { type: 'separator' }, - { role: 'services' }, - { type: 'separator' }, - { role: 'hide' }, - { role: 'hideOthers' }, - { role: 'unhide' }, - { type: 'separator' }, - { role: 'quit' } - ] - }] : []), - { - label: 'File', - submenu: [ - isMac ? { role: 'close' } : { role: 'quit' } - ] - }, - { - label: 'Edit', - submenu: [ - { role: 'undo' }, - { role: 'redo' }, - { type: 'separator' }, - { role: 'cut' }, - { role: 'copy' }, - { role: 'paste' }, - ...(isMac ? [ - { role: 'pasteAndMatchStyle' }, - { role: 'selectAll' }, - { type: 'separator' }, - { - label: 'Speech', - submenu: [ - { role: 'startSpeaking' }, - { role: 'stopSpeaking' } - ] - } - ] : [ - { type: 'separator' }, - { role: 'selectAll' } - ]) - ] - }, - { - label: 'View', - submenu: [ - { role: 'reload' }, - { type: 'separator' }, - { role: 'resetZoom' }, - { role: 'zoomIn' }, - { role: 'zoomOut' }, - { type: 'separator' }, - { role: 'togglefullscreen' }, - ...(isDev ? [ - { type: 'separator' }, - { role: 'toggleDevTools' }, - ]:[ - ]) - ] - }, - { - label: 'Window', - submenu: [ - { role: 'minimize' }, - { role: 'zoom' }, - ...(isMac ? [ - { type: 'separator' }, - { role: 'front' }, - { type: 'separator' }, - { role: 'window' } - ] : [ - { role: 'close' } - ]) - ] - }, - { - role: 'help', - submenu: [ - { - label: 'Learn More', - click: async () => { - const { shell } = require('electron') - await shell.openExternal('https://github.com/arduino/lab-micropython-editor') - } - }, - { - label: 'Report an issue', - click: async () => { - const { shell } = require('electron') - await shell.openExternal('https://github.com/arduino/lab-micropython-editor/issues') - } - }, - { - label:'Info about this app', - click: () => { - openAboutWindow({ - icon_path: path.join(__dirname, 'ui/arduino/assets/about_image.png'), - css_path: path.join(__dirname, 'ui/arduino/about.css'), - copyright: '© Arduino SA 2022', - package_json_dir: __dirname, - bug_report_url: "https://github.com/arduino/lab-micropython-editor/issues", - bug_link_text: "report an issue", - homepage: "https://labs.arduino.cc", - use_version_info: false, - win_options: { - parent: win, - modal: true, - }, - show_close_button: 'Close', - }) - } - }, - ] - } -] - -const menu = Menu.buildFromTemplate(template) - -app.setAboutPanelOptions({ - applicationName: app.name, - applicationVersion: app.getVersion(), - copyright: app.copyright, - credits: '(See "Info about this app" in the Help menu)', - authors: ['Arduino'], - website: 'https://arduino.cc', - iconPath: path.join(__dirname, '../assets/image.png'), -}) + registerIPCHandlers(win, ipcMain) + registerMenu(win) +} -Menu.setApplicationMenu(menu) +// TODO: Loading splash screen app.whenReady().then(createWindow) diff --git a/ui/arduino/media/about_image.png b/ui/arduino/media/about_image.png new file mode 100644 index 0000000000000000000000000000000000000000..ed31dcd0c5196cf28ca481c3ac120e7b340d35a2 GIT binary patch literal 26072 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 Date: Tue, 16 Apr 2024 16:38:48 +0200 Subject: [PATCH 2/3] Bump version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index efa3446..8f33dba 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "arduino-lab-micropython-ide", "productName": "Arduino Lab for Micropython", - "version": "0.9.0", + "version": "0.9.1", "description": "Arduino Lab for MicroPython is a project sponsored by Arduino, based on original work by Murilo Polese.\nThis is an experimental pre-release software, please direct any questions exclusively to Github issues.", "main": "index.js", "scripts": { @@ -43,4 +43,4 @@ "engines": { "node": "18" } -} \ No newline at end of file +} From 8792acc33e3b89d7900ef0d276a8f0f02d473426 Mon Sep 17 00:00:00 2001 From: Murilo Polese Date: Wed, 17 Apr 2024 08:26:32 +0200 Subject: [PATCH 3/3] Capital P on MicroPython --- package.json | 2 +- preload.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 8f33dba..ad5c2cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "arduino-lab-micropython-ide", - "productName": "Arduino Lab for Micropython", + "productName": "Arduino Lab for MicroPython", "version": "0.9.1", "description": "Arduino Lab for MicroPython is a project sponsored by Arduino, based on original work by Murilo Polese.\nThis is an experimental pre-release software, please direct any questions exclusively to Github issues.", "main": "index.js", diff --git a/preload.js b/preload.js index 96a7339..d7b24e8 100644 --- a/preload.js +++ b/preload.js @@ -2,8 +2,8 @@ console.log('preload') const { contextBridge, ipcRenderer } = require('electron') const path = require('path') -const Micropython = require('micropython.js') -const board = new Micropython() +const MicroPython = require('micropython.js') +const board = new MicroPython() board.chunk_size = 192 board.chunk_sleep = 200 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy