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/package.json b/package.json index efa3446..ad5c2cd 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", + "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", "scripts": { @@ -43,4 +43,4 @@ "engines": { "node": "18" } -} \ No newline at end of file +} 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 diff --git a/ui/arduino/media/about_image.png b/ui/arduino/media/about_image.png new file mode 100644 index 0000000..ed31dcd Binary files /dev/null and b/ui/arduino/media/about_image.png differ diff --git a/ui/arduino/views/about.css b/ui/arduino/views/about.css new file mode 100644 index 0000000..d7028aa --- /dev/null +++ b/ui/arduino/views/about.css @@ -0,0 +1,84 @@ +body, +html { + width: 100%; + height: 100%; + -webkit-user-select: none; + user-select: none; + -webkit-app-region: drag; +} + +body { + margin: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: #333; + background-color: #eee; + font-size: 12px; + font-family: 'Helvetica', 'Arial', 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', 'メイリオ', Meiryo, 'ï¼­ï¼³ Pゴシック', 'MS PGothic', sans-serif; +} + +.logo { + width: 80%; + -webkit-user-select: none; + user-select: none; +} +.logo img{ + width: 100%; + height: auto; +} + +.title, +.copyright, +.description { + margin: 0.2em; +} + +.clickable { + cursor: pointer; +} + +.description { + text-align: left; + margin: 2em; + +} + +.versions { + border-collapse: collapse; + margin-top: 1em; +} + +.copyright, +.versions { + color: #999; +} + +.buttons { + margin-bottom: 1em; + text-align: center; +} + +.buttons button { + margin-top: 1em; + width: 100px; + height: 24px; +} + +.link { + cursor: pointer; + color: #80a0c2; +} + +.bug-report-link { + position: absolute; + right: 1em; + bottom: 1em; +} + +.clickable, +.bug-report-link, +.buttons button { + -webkit-app-region: no-drag; +} 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