From 5eb412f6a41db53671e4afe06811345f5aa67f33 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sat, 7 Dec 2024 08:56:49 +0100 Subject: [PATCH 01/41] Hide navigation dots (..) when navigation path is root. Signed-off-by: ubi de feo --- ui/arduino/views/components/file-list.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/ui/arduino/views/components/file-list.js b/ui/arduino/views/components/file-list.js index 2767478..83e7d59 100644 --- a/ui/arduino/views/components/file-list.js +++ b/ui/arduino/views/components/file-list.js @@ -104,15 +104,18 @@ function generateFileList(source) { } return 0 }) + const parentNavigationDots = html`
emit(`navigate-${source}-parent`)} + style="cursor: pointer" + > + .. +
` + const list = html`
-
emit(`navigate-${source}-parent`)} - style="cursor: pointer" - > - .. -
+ ${source === 'disk' && state.diskNavigationPath != '/' ? parentNavigationDots : ''} + ${source === 'board' && state.boardNavigationPath != '/' ? parentNavigationDots : ''} ${state.creatingFile == source ? newFileItem : null} ${state.creatingFolder == source ? newFolderItem : null} ${files.map(FileItem)} From e159d5fa9609bfa66809b7b097d52d8116b624de Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sun, 8 Dec 2024 23:22:58 +0100 Subject: [PATCH 02/41] Implemented keyboard shortcuts. Signed-off-by: ubi de feo --- backend/ipc.js | 6 ++++ index.js | 48 ++++++++++++++++++++++++-- preload.js | 25 ++++++++++++-- ui/arduino/main.js | 6 +++- ui/arduino/store.js | 44 +++++++++++++++++++++++ ui/arduino/views/components/toolbar.js | 13 +++---- 6 files changed, 131 insertions(+), 11 deletions(-) diff --git a/backend/ipc.js b/backend/ipc.js index 8bace22..467cbff 100644 --- a/backend/ipc.js +++ b/backend/ipc.js @@ -129,9 +129,15 @@ module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) { return response != opt.cancelId }) + ipcMain.handle('reload', (event, message = 'ciao, cane') => { + console.log('reload?') + win.webContents.send('reload', message) + }) + win.on('close', (event) => { console.log('BrowserWindow', 'close') event.preventDefault() win.webContents.send('check-before-close') }) + } diff --git a/index.js b/index.js index 57eba4c..dcf8def 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -const { app, BrowserWindow, ipcMain, dialog } = require('electron') +const { app, BrowserWindow, ipcMain, dialog, globalShortcut } = require('electron') const path = require('path') const fs = require('fs') @@ -57,4 +57,48 @@ function createWindow () { }) } -app.on('ready', createWindow) +function shortcutAction(key) { + win.webContents.send('shortcut-cmd', key); +} + +// Shortcuts +function registerShortcuts() { + globalShortcut.register('CommandOrControl+R', () => { + console.log('Running Program') + shortcutAction('r') + }) + globalShortcut.register('CommandOrControl+H', () => { + console.log('Stopping Program (Halt)') + shortcutAction('h') + }) + globalShortcut.register('CommandOrControl+S', () => { + console.log('Saving File') + shortcutAction('s') + }) + + globalShortcut.register('CommandOrControl+Shift+R', () => { + console.log('Resetting Board') + shortcutAction('R') + }) + globalShortcut.register('CommandOrControl+Shift+C', () => { + console.log('Connect to Board') + shortcutAction('C') + }) + globalShortcut.register('CommandOrControl+Shift+D', () => { + console.log('Disconnect from Board') + shortcutAction('D') + }) +} + +app.on('ready', () => { + createWindow() + registerShortcuts() + + win.on('focus', () => { + registerShortcuts() + }) + win.on('blur', () => { + globalShortcut.unregisterAll() + }) + +}) \ No newline at end of file diff --git a/preload.js b/preload.js index ddcb8aa..0ed2abb 100644 --- a/preload.js +++ b/preload.js @@ -3,6 +3,8 @@ const { contextBridge, ipcRenderer } = require('electron') const path = require('path') const MicroPython = require('micropython.js') +const { emit, platform } = require('process') +// const { platform } = requireprocess.platform const board = new MicroPython() board.chunk_size = 192 board.chunk_sleep = 200 @@ -155,12 +157,31 @@ const Window = { setWindowSize: (minWidth, minHeight) => { ipcRenderer.invoke('set-window-size', minWidth, minHeight) }, + anyShortcut: (callback, key) => { + ipcRenderer.on('shortcut-cmd', (event, k) => { + // Get the active element + const activeElement = document.activeElement; + // Check if the active element is the terminal + const isTerminalFocused = activeElement.classList.contains('xterm-helper-textarea'); + // Only trigger callback if terminal is not focused AND we're in editor view + if (!isTerminalFocused) { + console.log('shortcut-cmd-r executed') + callback(k); + } + }) + }, + + beforeClose: (callback) => ipcRenderer.on('check-before-close', callback), confirmClose: () => ipcRenderer.invoke('confirm-close'), isPackaged: () => ipcRenderer.invoke('is-packaged'), - openDialog: (opt) => ipcRenderer.invoke('open-dialog', opt) -} + openDialog: (opt) => ipcRenderer.invoke('open-dialog', opt), + getOS: () => platform, + isWindows: () => platform === 'win32', + isMac: () => platform === 'darwin', + isLinux: () => platform === 'linux' +} contextBridge.exposeInMainWorld('BridgeSerial', Serial) contextBridge.exposeInMainWorld('BridgeDisk', Disk) diff --git a/ui/arduino/main.js b/ui/arduino/main.js index bf693df..267e6d1 100644 --- a/ui/arduino/main.js +++ b/ui/arduino/main.js @@ -46,11 +46,15 @@ window.addEventListener('load', () => { app.use(store); app.route('*', App) app.mount('#app') - app.emitter.on('DOMContentLoaded', () => { if (app.state.diskNavigationRoot) { app.emitter.emit('refresh-files') } }) + app.emitter.on('', () => { + if (app.state.diskNavigationRoot) { + app.emitter.emit('refresh-files') + } + }) }) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index c748905..be77d8f 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -24,6 +24,7 @@ async function confirm(msg, cancelMsg, confirmMsg) { async function store(state, emitter) { win.setWindowSize(720, 640) + state.platform = window.BridgeWindow.getOS() state.view = 'editor' state.diskNavigationPath = '/' state.diskNavigationRoot = getDiskNavigationRootFromStorage() @@ -57,6 +58,8 @@ async function store(state, emitter) { state.isTerminalBound = false + state.shortcutsContext = 'editor' + const newFile = createEmptyFile({ parentFolder: null, // Null parent folder means not saved? source: 'disk' @@ -1360,6 +1363,47 @@ async function store(state, emitter) { await win.confirmClose() }) + // win.shortcutCmdR(() => { + // // Only run if we can execute + + // }) + + win.anyShortcut((key) => { + if (key === 'C') { + emitter.emit('open-connection-dialog') + } + if (key === 'D') { + emitter.emit('disconnect') + } + if (key === 'R') { + if (state.view != 'editor') return + emitter.emit('reset') + } + if (key === 'r') { + if (state.view != 'editor') return + runCode() + } + if (key === 'h') { + if (state.view != 'editor') return + stopCode() + } + if (key === 's') { + if (state.view != 'editor') return + emitter.emit('save') + } + + }) + + function runCode() { + if (canExecute({ view: state.view, isConnected: state.isConnected })) { + emitter.emit('run') + } + } + function stopCode() { + if (canExecute({ view: state.view, isConnected: state.isConnected })) { + emitter.emit('stop') + } + } function createFile(args) { const { source, diff --git a/ui/arduino/views/components/toolbar.js b/ui/arduino/views/components/toolbar.js index 3512ef9..fa8e87e 100644 --- a/ui/arduino/views/components/toolbar.js +++ b/ui/arduino/views/components/toolbar.js @@ -9,12 +9,13 @@ function Toolbar(state, emit) { view: state.view, isConnected: state.isConnected }) - + const metaKeyString = state.platform === 'darwin' ? 'Cmd' : 'Ctrl' + return html`
${Button({ icon: state.isConnected ? 'connect.svg' : 'disconnect.svg', - tooltip: state.isConnected ? 'Disconnect' : 'Connect', + tooltip: state.isConnected ? `Disconnect (${metaKeyString}+Shift+D)` : `Connect (${metaKeyString}+Shift+C)`, onClick: () => emit('open-connection-dialog'), active: state.isConnected })} @@ -23,19 +24,19 @@ function Toolbar(state, emit) { ${Button({ icon: 'run.svg', - tooltip: 'Run', + tooltip: `Run (${metaKeyString}+r)`, disabled: !_canExecute, onClick: () => emit('run') })} ${Button({ icon: 'stop.svg', - tooltip: 'Stop', + tooltip: `Stop (${metaKeyString}+h)`, disabled: !_canExecute, onClick: () => emit('stop') })} ${Button({ icon: 'reboot.svg', - tooltip: 'Reset', + tooltip: `Reset (${metaKeyString}+Shift+r)`, disabled: !_canExecute, onClick: () => emit('reset') })} @@ -44,7 +45,7 @@ function Toolbar(state, emit) { ${Button({ icon: 'save.svg', - tooltip: 'Save', + tooltip: `Save (${metaKeyString}+s)`, disabled: !_canSave, onClick: () => emit('save') })} From bfcc2c68d24d33eaeb60f4e0b27c4d5f447cde02 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sun, 8 Dec 2024 23:31:45 +0100 Subject: [PATCH 03/41] Renamed method to onKeyboardShortcut. Signed-off-by: ubi de feo --- backend/ipc.js | 5 ----- preload.js | 2 +- ui/arduino/store.js | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/backend/ipc.js b/backend/ipc.js index 467cbff..dfe5917 100644 --- a/backend/ipc.js +++ b/backend/ipc.js @@ -129,11 +129,6 @@ module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) { return response != opt.cancelId }) - ipcMain.handle('reload', (event, message = 'ciao, cane') => { - console.log('reload?') - win.webContents.send('reload', message) - }) - win.on('close', (event) => { console.log('BrowserWindow', 'close') event.preventDefault() diff --git a/preload.js b/preload.js index 0ed2abb..bf31e73 100644 --- a/preload.js +++ b/preload.js @@ -157,7 +157,7 @@ const Window = { setWindowSize: (minWidth, minHeight) => { ipcRenderer.invoke('set-window-size', minWidth, minHeight) }, - anyShortcut: (callback, key) => { + onKeyboardShortcut: (callback, key) => { ipcRenderer.on('shortcut-cmd', (event, k) => { // Get the active element const activeElement = document.activeElement; diff --git a/ui/arduino/store.js b/ui/arduino/store.js index be77d8f..b471329 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -1368,7 +1368,7 @@ async function store(state, emitter) { // }) - win.anyShortcut((key) => { + win.onKeyboardShortcut((key) => { if (key === 'C') { emitter.emit('open-connection-dialog') } From c6d8d91d5fe7d243428801d0cc3101d6eac3b743 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sun, 8 Dec 2024 23:33:20 +0100 Subject: [PATCH 04/41] Removed whitespace. Signed-off-by: ubi de feo --- backend/ipc.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/ipc.js b/backend/ipc.js index dfe5917..cabb54e 100644 --- a/backend/ipc.js +++ b/backend/ipc.js @@ -133,6 +133,5 @@ module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) { console.log('BrowserWindow', 'close') event.preventDefault() win.webContents.send('check-before-close') - }) - + }) } From 45a582a188b21e84e2e0f64a8688316822f51773 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sun, 8 Dec 2024 23:33:46 +0100 Subject: [PATCH 05/41] Removed whitespace. Signed-off-by: ubi de feo --- backend/ipc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/ipc.js b/backend/ipc.js index cabb54e..8bace22 100644 --- a/backend/ipc.js +++ b/backend/ipc.js @@ -133,5 +133,5 @@ module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) { console.log('BrowserWindow', 'close') event.preventDefault() win.webContents.send('check-before-close') - }) + }) } From 4c648a3add143d194f1f6934243db7cbd49a9dee Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sun, 8 Dec 2024 23:39:56 +0100 Subject: [PATCH 06/41] Removed leftover test code from main.js. Signed-off-by: ubi de feo --- ui/arduino/main.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ui/arduino/main.js b/ui/arduino/main.js index 267e6d1..ce52be1 100644 --- a/ui/arduino/main.js +++ b/ui/arduino/main.js @@ -51,10 +51,4 @@ window.addEventListener('load', () => { app.emitter.emit('refresh-files') } }) - app.emitter.on('', () => { - if (app.state.diskNavigationRoot) { - app.emitter.emit('refresh-files') - } - }) - }) From ed7f83955bb127f0cfab2afa0be80b0023708af1 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sun, 8 Dec 2024 23:43:58 +0100 Subject: [PATCH 07/41] Removed unnecessary shortcutsContext member. --- ui/arduino/store.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index b471329..0a1d337 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -58,8 +58,6 @@ async function store(state, emitter) { state.isTerminalBound = false - state.shortcutsContext = 'editor' - const newFile = createEmptyFile({ parentFolder: null, // Null parent folder means not saved? source: 'disk' From 9a490c23090387e8e814d9b0f0c58e2fe6681a43 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Sun, 8 Dec 2024 23:57:23 +0100 Subject: [PATCH 08/41] Removed accelerator shortcuts from Menu items for Reload and Dev Tools. Signed-off-by: ubi de feo --- backend/menu.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/menu.js b/backend/menu.js index 6b62cdf..6fa13f5 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -10,7 +10,6 @@ module.exports = function registerMenu(win) { submenu: [ { role: 'about'}, { type: 'separator' }, - { role: 'services' }, { type: 'separator' }, { role: 'hide' }, { role: 'hideOthers' }, @@ -54,8 +53,8 @@ module.exports = function registerMenu(win) { { label: 'View', submenu: [ - { role: 'reload' }, - { role: 'toggleDevTools' }, + { role: 'reload', accelerator: '' }, + { role: 'toggleDevTools', accelerator: ''}, { type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, From 177b7d1117b9f3e4f561b98ef6a302af385b39dc Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Mon, 9 Dec 2024 08:30:35 +0100 Subject: [PATCH 09/41] Implemented Board menu item. Signed-off-by: ubi de feo --- backend/ipc.js | 5 +++++ backend/menu.js | 38 ++++++++++++++++++++++++++++++++++++-- index.js | 7 ++++++- preload.js | 7 ++++++- ui/arduino/store.js | 13 +++++++++++++ 5 files changed, 66 insertions(+), 4 deletions(-) diff --git a/backend/ipc.js b/backend/ipc.js index 8bace22..f42e822 100644 --- a/backend/ipc.js +++ b/backend/ipc.js @@ -129,6 +129,11 @@ module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) { return response != opt.cancelId }) + ipcMain.handle('update-menu-state', (event, state) => { + const registerMenu = require('./menu.js') + registerMenu(win, state) + }) + win.on('close', (event) => { console.log('BrowserWindow', 'close') event.preventDefault() diff --git a/backend/menu.js b/backend/menu.js index 6fa13f5..ef56591 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -2,7 +2,7 @@ const { app, Menu } = require('electron') const path = require('path') const openAboutWindow = require('about-window').default -module.exports = function registerMenu(win) { +module.exports = function registerMenu(win, state = {}) { const isMac = process.platform === 'darwin' const template = [ ...(isMac ? [{ @@ -11,7 +11,7 @@ module.exports = function registerMenu(win) { { role: 'about'}, { type: 'separator' }, { type: 'separator' }, - { role: 'hide' }, + { role: 'hide', accelerator: 'CmdOrCtrl+Shift+H' }, { role: 'hideOthers' }, { role: 'unhide' }, { type: 'separator' }, @@ -50,6 +50,40 @@ module.exports = function registerMenu(win) { ]) ] }, + { + label: 'Board', + submenu: [ + { + label: 'Connect', + accelerator: 'CmdOrCtrl+Shift+C', + click: () => win.webContents.send('shortcut-cmd', 'C') + }, + { + label: 'Disconnect', + accelerator: 'CmdOrCtrl+Shift+D', + click: () => win.webContents.send('shortcut-cmd', 'D') + }, + { role: 'separator' }, + { + label: 'Run', + accelerator: 'CmdOrCtrl+R', + enabled: state.isConnected && state.view === 'editor', + click: () => win.webContents.send('shortcut-cmd', 'r') + }, + { + label: 'Stop', + accelerator: 'CmdOrCtrl+H', + enabled: state.isConnected && state.view === 'editor', + click: () => win.webContents.send('shortcut-cmd', 'h') + }, + { + label: 'Reset', + accelerator: 'CmdOrCtrl+Shift+R', + enabled: state.isConnected && state.view === 'editor', + click: () => win.webContents.send('shortcut-cmd', 'R') + } + ] + }, { label: 'View', submenu: [ diff --git a/index.js b/index.js index dcf8def..68bc3c8 100644 --- a/index.js +++ b/index.js @@ -49,8 +49,13 @@ function createWindow () { win.show() }) + const initialMenuState = { + isConnected: false, + view: 'editor' + } + registerIPCHandlers(win, ipcMain, app, dialog) - registerMenu(win) + registerMenu(win, initialMenuState) app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow() diff --git a/preload.js b/preload.js index bf31e73..a88a4dd 100644 --- a/preload.js +++ b/preload.js @@ -180,7 +180,12 @@ const Window = { getOS: () => platform, isWindows: () => platform === 'win32', isMac: () => platform === 'darwin', - isLinux: () => platform === 'linux' + isLinux: () => platform === 'linux', + + updateMenuState: (state) => { + return ipcRenderer.invoke('update-menu-state', state) + } + } contextBridge.exposeInMainWorld('BridgeSerial', Serial) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 0a1d337..10193fc 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -81,6 +81,14 @@ async function store(state, emitter) { emitter.emit('render') } + // Menu management + const updateMenu = () => { + window.BridgeWindow.updateMenuState({ + isConnected: state.isConnected, + view: state.view + }) + } + // START AND BASIC ROUTING emitter.on('select-disk-navigation-root', async () => { const folder = await selectDiskFolder() @@ -98,6 +106,7 @@ async function store(state, emitter) { emitter.emit('refresh-files') } emitter.emit('render') + updateMenu() }) // CONNECTION DIALOG @@ -143,11 +152,13 @@ async function store(state, emitter) { } // Stop whatever is going on // Recover from getting stuck in raw repl + await serial.getPrompt() clearTimeout(timeout_id) // Connected and ready state.isConnecting = false state.isConnected = true + updateMenu() if (state.view === 'editor' && state.panelHeight <= PANEL_CLOSED) { state.panelHeight = state.savedPanelHeight } @@ -181,6 +192,7 @@ async function store(state, emitter) { state.boardNavigationPath = '/' emitter.emit('refresh-files') emitter.emit('render') + updateMenu() }) emitter.on('connection-timeout', async () => { state.isConnected = false @@ -1646,4 +1658,5 @@ async function getHelperFullPath() { '' ) } + } From c6ec1804ae42b7786cd8a4ac2be3c5de7f736dfc Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Mon, 9 Dec 2024 08:55:42 +0100 Subject: [PATCH 10/41] Menu cleanup. Signed-off-by: ubi de feo --- backend/menu.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/menu.js b/backend/menu.js index ef56591..574a798 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -34,7 +34,6 @@ module.exports = function registerMenu(win, state = {}) { { role: 'copy' }, { role: 'paste' }, ...(isMac ? [ - { role: 'pasteAndMatchStyle' }, { role: 'selectAll' }, { type: 'separator' }, { @@ -108,7 +107,7 @@ module.exports = function registerMenu(win, state = {}) { { type: 'separator' }, { role: 'window' } ] : [ - { role: 'close' } + ]) ] }, From 1fb588e01529cb6c0c4d4ccc146bfdb99a082e89 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Mon, 9 Dec 2024 13:48:32 +0100 Subject: [PATCH 11/41] Added ESC key listener. Enabled it on connection dialog. Signed-off-by: ubi de feo --- index.js | 3 +++ ui/arduino/store.js | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/index.js b/index.js index 68bc3c8..97cffc5 100644 --- a/index.js +++ b/index.js @@ -92,6 +92,9 @@ function registerShortcuts() { globalShortcut.register('CommandOrControl+Shift+D', () => { console.log('Disconnect from Board') shortcutAction('D') + }), + globalShortcut.register('Escape', () => { + shortcutAction('ESC') }) } diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 10193fc..396050c 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -1401,6 +1401,11 @@ async function store(state, emitter) { if (state.view != 'editor') return emitter.emit('save') } + if (key === 'ESC') { + if (state.isConnectionDialogOpen) { + emitter.emit('close-connection-dialog') + } + } }) From b021b25d2ab9460742c36fa9aef67dfd1294243e Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Mon, 9 Dec 2024 13:58:47 +0100 Subject: [PATCH 12/41] Ability to run code selection. Selecting whitespace runs the whole script. Signed-off-by: ubi de feo --- ui/arduino/store.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index c748905..656b772 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -193,7 +193,23 @@ async function store(state, emitter) { emitter.on('run', async () => { log('run') const openFile = state.openFiles.find(f => f.id == state.editingFile) - const code = openFile.editor.editor.state.doc.toString() + let code = openFile.editor.editor.state.doc.toString() + + // If there is a selection, run only the selected code + const startIndex = openFile.editor.editor.state.selection.ranges[0].from + const endIndex = openFile.editor.editor.state.selection.ranges[0].to + if (endIndex - startIndex > 0) { + selectedCode = openFile.editor.editor.state.doc.toString().substring(startIndex, endIndex) + // Checking to see if the user accidentally double-clicked some whitespace + // While a random selection would yield an error when executed, + // selecting only whitespace would not and the user would have no feedback. + // This check only replaces the full content of the currently selected tab + // with a text selection if the selection is not empty and contains only whitespace. + if (selectedCode.trim().length > 0) { + code = selectedCode + } + } + emitter.emit('open-panel') emitter.emit('render') try { From 861b658c590bb243ba076157adf9aa1caee9054e Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Mon, 9 Dec 2024 18:34:46 +0100 Subject: [PATCH 13/41] Added Clear Terminal shortcut Meta+k. Signed-off-by: ubi de feo --- index.js | 9 +++++++++ preload.js | 19 +++++++++++-------- ui/arduino/store.js | 9 +++++++++ ui/arduino/views/components/repl-panel.js | 2 +- 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index 97cffc5..67375fe 100644 --- a/index.js +++ b/index.js @@ -93,6 +93,15 @@ function registerShortcuts() { console.log('Disconnect from Board') shortcutAction('D') }), + globalShortcut.register('CommandOrControl+K', () => { + console.log('Clear Terminal') + shortcutAction('K') + }), + // Future: Toggle REPL Panel + // globalShortcut.register('CommandOrControl+T', () => { + // console.log('Toggle Terminal') + // shortcutAction('T') + // }), globalShortcut.register('Escape', () => { shortcutAction('ESC') }) diff --git a/preload.js b/preload.js index a88a4dd..dfb48d1 100644 --- a/preload.js +++ b/preload.js @@ -159,15 +159,18 @@ const Window = { }, onKeyboardShortcut: (callback, key) => { ipcRenderer.on('shortcut-cmd', (event, k) => { - // Get the active element - const activeElement = document.activeElement; - // Check if the active element is the terminal - const isTerminalFocused = activeElement.classList.contains('xterm-helper-textarea'); + + // Only trigger callback if terminal is not focused AND we're in editor view - if (!isTerminalFocused) { - console.log('shortcut-cmd-r executed') - callback(k); - } + // This has been deemed unnecessary since there are no real conflicts with the terminal + // The REPL shortcuts Ctrl+a|b|c|d are not used as application shortcuts and will + // only be triggered when the user has focused the REPL + // The code is left here for reference + // const activeElement = document.activeElement; + // const isTerminalFocused = activeElement.classList.contains('xterm-helper-textarea'); + // if (!isTerminalFocused) { + callback(k); + // } }) }, diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 396050c..540c19f 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -1389,6 +1389,15 @@ async function store(state, emitter) { if (state.view != 'editor') return emitter.emit('reset') } + if (key === 'K') { + if (state.view != 'editor') return + emitter.emit('clear-terminal') + } + // Future: Toggle REPL panel + // if (key === 'T') { + // if (state.view != 'editor') return + // emitter.emit('clear-terminal') + // } if (key === 'r') { if (state.view != 'editor') return runCode() diff --git a/ui/arduino/views/components/repl-panel.js b/ui/arduino/views/components/repl-panel.js index ac1760c..b56c531 100644 --- a/ui/arduino/views/components/repl-panel.js +++ b/ui/arduino/views/components/repl-panel.js @@ -50,7 +50,7 @@ function ReplOperations(state, emit) { Button({ icon: 'delete.svg', size: 'small', - tooltip: 'Clean', + tooltip: `Clean (${state.platform === 'darwin' ? 'Cmd' : 'Ctrl'}+k)`, onClick: () => emit('clear-terminal') }) ] From 99930a15507006016f2d55958d6691cd9edc8907 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Mon, 9 Dec 2024 22:20:54 +0100 Subject: [PATCH 14/41] Polished icons. Signed-off-by: ubi de feo --- ui/arduino/media/board.svg | 68 ++++++++++++++++++++++++++ ui/arduino/media/computer.svg | 27 +++++++++- ui/arduino/media/editor.svg | 24 +++++++++ ui/arduino/media/files.svg | 5 +- ui/arduino/views/components/tabs.js | 2 +- ui/arduino/views/components/toolbar.js | 2 +- ui/arduino/views/file-manager.js | 2 +- 7 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 ui/arduino/media/board.svg create mode 100644 ui/arduino/media/editor.svg diff --git a/ui/arduino/media/board.svg b/ui/arduino/media/board.svg new file mode 100644 index 0000000..0977345 --- /dev/null +++ b/ui/arduino/media/board.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/arduino/media/computer.svg b/ui/arduino/media/computer.svg index 9d4cb1e..f0f8efb 100644 --- a/ui/arduino/media/computer.svg +++ b/ui/arduino/media/computer.svg @@ -1,3 +1,26 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/arduino/media/editor.svg b/ui/arduino/media/editor.svg new file mode 100644 index 0000000..327fc19 --- /dev/null +++ b/ui/arduino/media/editor.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/arduino/media/files.svg b/ui/arduino/media/files.svg index 36d96a0..59ffe3f 100644 --- a/ui/arduino/media/files.svg +++ b/ui/arduino/media/files.svg @@ -1,4 +1,3 @@ - - - + + diff --git a/ui/arduino/views/components/tabs.js b/ui/arduino/views/components/tabs.js index f8d72a5..750f8f8 100644 --- a/ui/arduino/views/components/tabs.js +++ b/ui/arduino/views/components/tabs.js @@ -4,7 +4,7 @@ function Tabs(state, emit) { ${state.openFiles.map((file) => { return Tab({ text: file.fileName, - icon: file.source === 'board'? 'connect.svg': 'computer.svg', + icon: file.source === 'board'? 'board.svg': 'computer.svg', active: file.id === state.editingFile, renaming: file.id === state.renamingTab, hasChanges: file.hasChanges, diff --git a/ui/arduino/views/components/toolbar.js b/ui/arduino/views/components/toolbar.js index 3512ef9..4ba1011 100644 --- a/ui/arduino/views/components/toolbar.js +++ b/ui/arduino/views/components/toolbar.js @@ -52,7 +52,7 @@ function Toolbar(state, emit) {
${Button({ - icon: 'console.svg', + icon: 'editor.svg', tooltip: 'Editor and REPL', active: state.view === 'editor', onClick: () => emit('change-view', 'editor') diff --git a/ui/arduino/views/file-manager.js b/ui/arduino/views/file-manager.js index 04626ec..eafdf65 100644 --- a/ui/arduino/views/file-manager.js +++ b/ui/arduino/views/file-manager.js @@ -12,7 +12,7 @@ function FileManagerView(state, emit) {
- +
emit('open-connection-dialog')} class="text"> ${boardFullPath}
From da271aeb9dc7208ab106cbcca9623b9a3400daf6 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Mon, 9 Dec 2024 22:26:37 +0100 Subject: [PATCH 15/41] Icons: updated folder. Signed-off-by: ubi de feo --- ui/arduino/media/folder.svg | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ui/arduino/media/folder.svg b/ui/arduino/media/folder.svg index 87e8555..22a8362 100644 --- a/ui/arduino/media/folder.svg +++ b/ui/arduino/media/folder.svg @@ -1,3 +1,15 @@ - - + + + + + + + + + + + + + + From 243c58ed71838d14a3b11cd573c744a6eada7e48 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Tue, 10 Dec 2024 11:24:34 +0100 Subject: [PATCH 16/41] Updated micropython.js to v1.5.1 Signed-off-by: ubi de feo --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index d6726c0..42738a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "arduino-lab-micropython-ide", - "version": "0.10.0", + "version": "0.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "arduino-lab-micropython-ide", - "version": "0.10.0", + "version": "0.11.0", "hasInstallScript": true, "license": "MIT", "dependencies": { "about-window": "^1.15.2", - "micropython.js": "github:arduino/micropython.js#v1.5.0" + "micropython.js": "github:arduino/micropython.js#v1.5.1" }, "devDependencies": { "electron": "^19.0.10", diff --git a/package.json b/package.json index 0c4d3a5..e86c5ba 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "license": "MIT", "dependencies": { "about-window": "^1.15.2", - "micropython.js": "github:arduino/micropython.js#v1.5.0" + "micropython.js": "github:arduino/micropython.js#v1.5.1" }, "engines": { "node": "18" From afadd3fa9ea7c4bfb87be481ec6d03650bf34d4a Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Tue, 10 Dec 2024 12:54:37 +0100 Subject: [PATCH 17/41] Testing disconnect before manual reload. Signed-off-by: ubi de feo --- backend/ipc.js | 5 +++++ backend/menu.js | 15 ++++++++++++++- index.js | 18 ++++++++++++++++++ preload.js | 9 +++++++++ ui/arduino/store.js | 12 ++++++++++++ 5 files changed, 58 insertions(+), 1 deletion(-) diff --git a/backend/ipc.js b/backend/ipc.js index f42e822..eefd348 100644 --- a/backend/ipc.js +++ b/backend/ipc.js @@ -139,4 +139,9 @@ module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) { event.preventDefault() win.webContents.send('check-before-close') }) + + // handle disconnection before reload + ipcMain.handle('prepare-reload', async (event) => { + return win.webContents.send('before-reload') + }) } diff --git a/backend/menu.js b/backend/menu.js index 574a798..6aa2051 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -86,7 +86,20 @@ module.exports = function registerMenu(win, state = {}) { { label: 'View', submenu: [ - { role: 'reload', accelerator: '' }, + { + label: 'Reload', + accelerator: '', + click: async () => { + try { + win.webContents.send('cleanup-before-reload') + setTimeout(() => { + win.reload() + }, 500) + } catch(e) { + console.error('Reload from menu failed:', e) + } + } + }, { role: 'toggleDevTools', accelerator: ''}, { type: 'separator' }, { role: 'resetZoom' }, diff --git a/index.js b/index.js index 67375fe..8c0409e 100644 --- a/index.js +++ b/index.js @@ -49,6 +49,24 @@ function createWindow () { win.show() }) + win.webContents.on('before-reload', async (event) => { + // Prevent the default reload behavior + event.preventDefault() + + try { + // Tell renderer to do cleanup + win.webContents.send('cleanup-before-reload') + + // Wait for cleanup then reload + setTimeout(() => { + // This will trigger a page reload, but won't trigger 'before-reload' again + win.reload() + }, 500) + } catch(e) { + console.error('Reload preparation failed:', e) + } + }) + const initialMenuState = { isConnected: false, view: 'editor' diff --git a/preload.js b/preload.js index dfb48d1..210d81f 100644 --- a/preload.js +++ b/preload.js @@ -174,6 +174,15 @@ const Window = { }) }, + onBeforeReload: (callback) => { + ipcRenderer.on('cleanup-before-reload', async () => { + try { + await callback() + } catch(e) { + console.error('Cleanup before reload failed:', e) + } + }) + }, beforeClose: (callback) => ipcRenderer.on('check-before-close', callback), confirmClose: () => ipcRenderer.invoke('confirm-close'), diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 540c19f..a43cfcc 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -1364,6 +1364,18 @@ async function store(state, emitter) { emitter.emit('render') }) + win.onBeforeReload(async () => { + // Perform any cleanup needed + if (state.isConnected) { + await serial.disconnect() + state.isConnected = false + state.panelHeight = PANEL_CLOSED + state.boardFiles = [] + state.boardNavigationPath = '/' + } + // Any other cleanup needed + }) + win.beforeClose(async () => { const hasChanges = !!state.openFiles.find(f => f.hasChanges) if (hasChanges) { From e60703bdfc718a54f1c183c2d7611adaa41c69b4 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Tue, 10 Dec 2024 14:57:21 +0100 Subject: [PATCH 18/41] Fixed folder background. Signed-off-by: ubi de feo --- ui/arduino/media/folder.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/arduino/media/folder.svg b/ui/arduino/media/folder.svg index 22a8362..68843f7 100644 --- a/ui/arduino/media/folder.svg +++ b/ui/arduino/media/folder.svg @@ -3,7 +3,7 @@ - + From 88fe9a2af3a357f483641fe2700bcdc709d74f31 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Wed, 11 Dec 2024 16:40:54 +0100 Subject: [PATCH 19/41] Implemented ALT option to run code selection. Signed-off-by: ubi de feo --- index.js | 4 ++++ ui/arduino/store.js | 13 +++++++++++-- ui/arduino/views/components/elements/button.js | 2 +- ui/arduino/views/components/toolbar.js | 8 +++++++- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 8c0409e..efb675e 100644 --- a/index.js +++ b/index.js @@ -90,6 +90,10 @@ function registerShortcuts() { console.log('Running Program') shortcutAction('r') }) + globalShortcut.register('CommandOrControl+Alt+R', () => { + console.log('Running Code Selection') + shortcutAction('_r') + }) globalShortcut.register('CommandOrControl+H', () => { console.log('Stopping Program (Halt)') shortcutAction('h') diff --git a/ui/arduino/store.js b/ui/arduino/store.js index fcaa0de..dca01bd 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -203,7 +203,7 @@ async function store(state, emitter) { }) // CODE EXECUTION - emitter.on('run', async () => { + emitter.on('run', async (selection = false) => { log('run') const openFile = state.openFiles.find(f => f.id == state.editingFile) let code = openFile.editor.editor.state.doc.toString() @@ -211,7 +211,7 @@ async function store(state, emitter) { // If there is a selection, run only the selected code const startIndex = openFile.editor.editor.state.selection.ranges[0].from const endIndex = openFile.editor.editor.state.selection.ranges[0].to - if (endIndex - startIndex > 0) { + if (endIndex - startIndex > 0 && selection) { selectedCode = openFile.editor.editor.state.doc.toString().substring(startIndex, endIndex) // Checking to see if the user accidentally double-clicked some whitespace // While a random selection would yield an error when executed, @@ -1430,6 +1430,10 @@ async function store(state, emitter) { if (state.view != 'editor') return runCode() } + if (key === '_r') { + if (state.view != 'editor') return + runCodeSelection() + } if (key === 'h') { if (state.view != 'editor') return stopCode() @@ -1451,6 +1455,11 @@ async function store(state, emitter) { emitter.emit('run') } } + function runCodeSelection() { + if (canExecute({ view: state.view, isConnected: state.isConnected })) { + emitter.emit('run', true) + } + } function stopCode() { if (canExecute({ view: state.view, isConnected: state.isConnected })) { emitter.emit('stop') diff --git a/ui/arduino/views/components/elements/button.js b/ui/arduino/views/components/elements/button.js index 7030d49..3d888dd 100644 --- a/ui/arduino/views/components/elements/button.js +++ b/ui/arduino/views/components/elements/button.js @@ -2,7 +2,7 @@ function Button(args) { const { size = '', icon = 'connect.svg', - onClick = () => false, + onClick = (e) => false, disabled = false, active = false, tooltip, diff --git a/ui/arduino/views/components/toolbar.js b/ui/arduino/views/components/toolbar.js index d5f6a7a..b63e243 100644 --- a/ui/arduino/views/components/toolbar.js +++ b/ui/arduino/views/components/toolbar.js @@ -26,7 +26,13 @@ function Toolbar(state, emit) { icon: 'run.svg', tooltip: `Run (${metaKeyString}+r)`, disabled: !_canExecute, - onClick: () => emit('run') + onClick: (e) => { + if (e.altKey) { + emit('run', true) + }else{ + emit('run') + } + } })} ${Button({ icon: 'stop.svg', From 9908d4a7789f0026f41adfa7f86f6e84fcaff9e0 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Thu, 12 Dec 2024 13:40:32 +0100 Subject: [PATCH 20/41] Refactored menu and global shortcuts into constants file. Signed-off-by: ubi de feo --- backend/menu.js | 38 ++++++---- backend/shortcuts.js | 23 ++++++ index.js | 88 +++++++++++++---------- preload.js | 6 +- ui/arduino/store.js | 20 +++--- ui/arduino/views/components/repl-panel.js | 2 +- 6 files changed, 114 insertions(+), 63 deletions(-) create mode 100644 backend/shortcuts.js diff --git a/backend/menu.js b/backend/menu.js index 6aa2051..e7ee2d9 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -1,6 +1,7 @@ const { app, Menu } = require('electron') const path = require('path') const openAboutWindow = require('about-window').default +const shortcuts = require('./shortcuts.js') module.exports = function registerMenu(win, state = {}) { const isMac = process.platform === 'darwin' @@ -54,32 +55,45 @@ module.exports = function registerMenu(win, state = {}) { submenu: [ { label: 'Connect', - accelerator: 'CmdOrCtrl+Shift+C', - click: () => win.webContents.send('shortcut-cmd', 'C') + accelerator: shortcuts.menu.CONNECT, + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.CONNECT) }, { label: 'Disconnect', - accelerator: 'CmdOrCtrl+Shift+D', - click: () => win.webContents.send('shortcut-cmd', 'D') + accelerator: shortcuts.menu.DISCONNECT, + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.DISCONNECT) }, - { role: 'separator' }, + { type: 'separator' }, { label: 'Run', - accelerator: 'CmdOrCtrl+R', + accelerator: shortcuts.menu.RUN, + enabled: state.isConnected && state.view === 'editor', + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.RUN) + }, + { + label: 'Run selection', + accelerator: shortcuts.menu.RUN_SELECTION, enabled: state.isConnected && state.view === 'editor', - click: () => win.webContents.send('shortcut-cmd', 'r') + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.RUN_SELECTION) }, { label: 'Stop', - accelerator: 'CmdOrCtrl+H', + accelerator: shortcuts.menu.STOP, enabled: state.isConnected && state.view === 'editor', - click: () => win.webContents.send('shortcut-cmd', 'h') + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.STOP) }, { label: 'Reset', - accelerator: 'CmdOrCtrl+Shift+R', + accelerator: shortcuts.menu.RESET, + enabled: state.isConnected && state.view === 'editor', + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.RESET) + }, + { type: 'separator' }, + { + label: 'Clear terminal', + accelerator: shortcuts.menu.CLEAR_TERMINAL, enabled: state.isConnected && state.view === 'editor', - click: () => win.webContents.send('shortcut-cmd', 'R') + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.CLEAR_TERMINAL) } ] }, @@ -100,7 +114,7 @@ module.exports = function registerMenu(win, state = {}) { } } }, - { role: 'toggleDevTools', accelerator: ''}, + { role: 'toggleDevTools'}, { type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, diff --git a/backend/shortcuts.js b/backend/shortcuts.js new file mode 100644 index 0000000..93026a2 --- /dev/null +++ b/backend/shortcuts.js @@ -0,0 +1,23 @@ +module.exports = { + global: { + CONNECT: 'CommandOrControl+Shift+C', + DISCONNECT: 'CommandOrControl+Shift+D', + SAVE: 'CommandOrControl+S', + RUN: 'CommandOrControl+R', + RUN_SELECTION: 'CommandOrControl+Alt+R', + STOP: 'CommandOrControl+H', + RESET: 'CommandOrControl+Shift+R', + CLEAR_TERMINAL: 'CommandOrControl+L', + ESC: 'Escape' + }, + menu: { + CONNECT: 'CmdOrCtrl+Shift+C', + DISCONNECT: 'CmdOrCtrl+Shift+D', + SAVE: 'CmdOrCtrl+S', + RUN: 'CmdOrCtrl+R', + RUN_SELECTION: 'CmdOrCtrl+Alt+R', + STOP: 'CmdOrCtrl+H', + RESET: 'CmdOrCtrl+Shift+R', + CLEAR_TERMINAL: 'CmdOrCtrl+L', + } +} diff --git a/index.js b/index.js index efb675e..b11d662 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ const { app, BrowserWindow, ipcMain, dialog, globalShortcut } = require('electron') const path = require('path') const fs = require('fs') +const shortcuts = require('./backend/shortcuts.js').global const registerIPCHandlers = require('./backend/ipc.js') const registerMenu = require('./backend/menu.js') @@ -86,47 +87,58 @@ function shortcutAction(key) { // Shortcuts function registerShortcuts() { - globalShortcut.register('CommandOrControl+R', () => { - console.log('Running Program') - shortcutAction('r') - }) - globalShortcut.register('CommandOrControl+Alt+R', () => { - console.log('Running Code Selection') - shortcutAction('_r') - }) - globalShortcut.register('CommandOrControl+H', () => { - console.log('Stopping Program (Halt)') - shortcutAction('h') - }) - globalShortcut.register('CommandOrControl+S', () => { - console.log('Saving File') - shortcutAction('s') + Object.entries(shortcuts).forEach(([command, shortcut]) => { + globalShortcut.register(shortcut, () => { + shortcutAction(shortcut) + }); }) + // shortcuts.forEach(element => { + // globalShortcut.register(element, () => { + + // shortcutAction(element) + // }); + // }); + // globalShortcut.register(shortcuts.RUN, () => { + // console.log('Running Program') + // shortcutAction(shortcuts.RUN) + // }) + // globalShortcut.register('CommandOrControl+Alt+R', () => { + // console.log('Running Code Selection') + // shortcutAction('meta_alt_r') + // }) + // globalShortcut.register('CommandOrControl+H', () => { + // console.log('Stopping Program (Halt)') + // shortcutAction('meta_h') + // }) + // globalShortcut.register('CommandOrControl+S', () => { + // console.log('Saving File') + // shortcutAction('meta_s') + // }) - globalShortcut.register('CommandOrControl+Shift+R', () => { - console.log('Resetting Board') - shortcutAction('R') - }) - globalShortcut.register('CommandOrControl+Shift+C', () => { - console.log('Connect to Board') - shortcutAction('C') - }) - globalShortcut.register('CommandOrControl+Shift+D', () => { - console.log('Disconnect from Board') - shortcutAction('D') - }), - globalShortcut.register('CommandOrControl+K', () => { - console.log('Clear Terminal') - shortcutAction('K') - }), - // Future: Toggle REPL Panel - // globalShortcut.register('CommandOrControl+T', () => { - // console.log('Toggle Terminal') - // shortcutAction('T') + // globalShortcut.register('CommandOrControl+Shift+R', () => { + // console.log('Resetting Board') + // shortcutAction('meta_shift_r') + // }) + // globalShortcut.register(shortcuts.CONNECT, () => { + // console.log('Connect to Board') + // shortcutAction(shortcuts.CONNECT) + // }) + // globalShortcut.register(shortcuts.DISCONNECT, () => { + // console.log('Disconnect from Board') + // shortcutAction(shortcuts.DISCONNECT) // }), - globalShortcut.register('Escape', () => { - shortcutAction('ESC') - }) + // globalShortcut.register('CommandOrControl+K', () => { + // console.log('Clear Terminal') + // shortcutAction('K') + // }), + // // Future: Toggle REPL Panel + // // globalShortcut.register('CommandOrControl+T', () => { + // // console.log('Toggle Terminal') + // // shortcutAction('T') + // // }), + // globalShortcut.register('Escape', () => { + // shortcutAction('ESC') + // }) } app.on('ready', () => { diff --git a/preload.js b/preload.js index 210d81f..bf78552 100644 --- a/preload.js +++ b/preload.js @@ -1,7 +1,7 @@ console.log('preload') const { contextBridge, ipcRenderer } = require('electron') const path = require('path') - +const shortcuts = require('./backend/shortcuts.js').global const MicroPython = require('micropython.js') const { emit, platform } = require('process') // const { platform } = requireprocess.platform @@ -196,8 +196,8 @@ const Window = { updateMenuState: (state) => { return ipcRenderer.invoke('update-menu-state', state) - } - + }, + getShortcuts: () => shortcuts } contextBridge.exposeInMainWorld('BridgeSerial', Serial) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index dca01bd..d10304d 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -3,6 +3,8 @@ const serial = window.BridgeSerial const disk = window.BridgeDisk const win = window.BridgeWindow +const shortcuts = window.BridgeWindow.getShortcuts() + const newFileContent = `# This program was created in Arduino Lab for MicroPython print('Hello, MicroPython!') @@ -1407,17 +1409,17 @@ async function store(state, emitter) { // }) win.onKeyboardShortcut((key) => { - if (key === 'C') { + if (key === shortcuts.CONNECT) { emitter.emit('open-connection-dialog') } - if (key === 'D') { + if (key === shortcuts.DISCONNECT) { emitter.emit('disconnect') } - if (key === 'R') { + if (key === shortcuts.RESET) { if (state.view != 'editor') return emitter.emit('reset') } - if (key === 'K') { + if (key === shortcuts.CLEAR_TERMINAL) { if (state.view != 'editor') return emitter.emit('clear-terminal') } @@ -1426,23 +1428,23 @@ async function store(state, emitter) { // if (state.view != 'editor') return // emitter.emit('clear-terminal') // } - if (key === 'r') { + if (key === shortcuts.RUN) { if (state.view != 'editor') return runCode() } - if (key === '_r') { + if (key === shortcuts.RUN_SELECTION) { if (state.view != 'editor') return runCodeSelection() } - if (key === 'h') { + if (key === shortcuts.STOP) { if (state.view != 'editor') return stopCode() } - if (key === 's') { + if (key === shortcuts.SAVE) { if (state.view != 'editor') return emitter.emit('save') } - if (key === 'ESC') { + if (key === shortcuts.ESC) { if (state.isConnectionDialogOpen) { emitter.emit('close-connection-dialog') } diff --git a/ui/arduino/views/components/repl-panel.js b/ui/arduino/views/components/repl-panel.js index b56c531..3974d50 100644 --- a/ui/arduino/views/components/repl-panel.js +++ b/ui/arduino/views/components/repl-panel.js @@ -50,7 +50,7 @@ function ReplOperations(state, emit) { Button({ icon: 'delete.svg', size: 'small', - tooltip: `Clean (${state.platform === 'darwin' ? 'Cmd' : 'Ctrl'}+k)`, + tooltip: `Clean (${state.platform === 'darwin' ? 'Cmd' : 'Ctrl'}+L)`, onClick: () => emit('clear-terminal') }) ] From 0fa99b18b97f80544d501d25c7ba63d0d50608ca Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Thu, 12 Dec 2024 14:03:51 +0100 Subject: [PATCH 21/41] Changed 'selection' to 'onlySelected'. Signed-off-by: ubi de feo --- ui/arduino/store.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index d10304d..aff39ce 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -205,7 +205,7 @@ async function store(state, emitter) { }) // CODE EXECUTION - emitter.on('run', async (selection = false) => { + emitter.on('run', async (onlySelected = false) => { log('run') const openFile = state.openFiles.find(f => f.id == state.editingFile) let code = openFile.editor.editor.state.doc.toString() @@ -213,7 +213,7 @@ async function store(state, emitter) { // If there is a selection, run only the selected code const startIndex = openFile.editor.editor.state.selection.ranges[0].from const endIndex = openFile.editor.editor.state.selection.ranges[0].to - if (endIndex - startIndex > 0 && selection) { + if (endIndex - startIndex > 0 && onlySelected) { selectedCode = openFile.editor.editor.state.doc.toString().substring(startIndex, endIndex) // Checking to see if the user accidentally double-clicked some whitespace // While a random selection would yield an error when executed, From 530dbc693156d36f8af01ec98dc2fa3328893c2c Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Thu, 12 Dec 2024 14:50:13 +0100 Subject: [PATCH 22/41] Moved registerMenu to file scope. Signed-off-by: ubi de feo --- backend/ipc.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/ipc.js b/backend/ipc.js index eefd348..12f2127 100644 --- a/backend/ipc.js +++ b/backend/ipc.js @@ -1,4 +1,6 @@ const fs = require('fs') +const registerMenu = require('./menu.js') + const { openFolderDialog, listFolder, @@ -130,7 +132,6 @@ module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) { }) ipcMain.handle('update-menu-state', (event, state) => { - const registerMenu = require('./menu.js') registerMenu(win, state) }) From a019c999ea42cac6d6c5bada02ab454dbdffbd7d Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Fri, 13 Dec 2024 07:42:37 +0100 Subject: [PATCH 23/41] Only save if openFile (tab) has changes. Signed-off-by: ubi de feo --- ui/arduino/store.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index aff39ce..93ed152 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -1599,6 +1599,7 @@ function pickRandom(array) { function canSave({ view, isConnected, openFiles, editingFile }) { const isEditor = view === 'editor' const file = openFiles.find(f => f.id === editingFile) + if (!file.hasChanges) return false // Can only save on editor if (!isEditor) return false // Can always save disk files From b0511201bb2b82add369ed5da02c1cef2d6a2e6d Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Fri, 13 Dec 2024 09:15:44 +0100 Subject: [PATCH 24/41] Added view switch, replaced editor icon, updated shortcuts. Signed-off-by: ubi de feo --- backend/menu.js | 32 ++++++++++++++++++-------- backend/shortcuts.js | 4 ++++ ui/arduino/media/code.svg | 3 +++ ui/arduino/store.js | 8 +++++++ ui/arduino/views/components/toolbar.js | 14 +++++------ 5 files changed, 44 insertions(+), 17 deletions(-) create mode 100644 ui/arduino/media/code.svg diff --git a/backend/menu.js b/backend/menu.js index e7ee2d9..e706a41 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -2,6 +2,7 @@ const { app, Menu } = require('electron') const path = require('path') const openAboutWindow = require('about-window').default const shortcuts = require('./shortcuts.js') +const { type } = require('os') module.exports = function registerMenu(win, state = {}) { const isMac = process.platform === 'darwin' @@ -99,6 +100,27 @@ module.exports = function registerMenu(win, state = {}) { }, { label: 'View', + submenu: [ + { + label: 'Editor', + accelerator: shortcuts.menu.EDITOR_VIEW, + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.EDITOR_VIEW,) + }, + { + label: 'Files', + accelerator: shortcuts.menu.FILES_VIEW, + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.FILES_VIEW) + }, + { type: 'separator' }, + { role: 'resetZoom' }, + { role: 'zoomIn' }, + { role: 'zoomOut' }, + { type: 'separator' }, + { role: 'togglefullscreen' }, + ] + }, + { + label: 'Window', submenu: [ { label: 'Reload', @@ -116,16 +138,6 @@ module.exports = function registerMenu(win, state = {}) { }, { role: 'toggleDevTools'}, { type: 'separator' }, - { role: 'resetZoom' }, - { role: 'zoomIn' }, - { role: 'zoomOut' }, - { type: 'separator' }, - { role: 'togglefullscreen' }, - ] - }, - { - label: 'Window', - submenu: [ { role: 'minimize' }, { role: 'zoom' }, ...(isMac ? [ diff --git a/backend/shortcuts.js b/backend/shortcuts.js index 93026a2..b496200 100644 --- a/backend/shortcuts.js +++ b/backend/shortcuts.js @@ -8,6 +8,8 @@ module.exports = { STOP: 'CommandOrControl+H', RESET: 'CommandOrControl+Shift+R', CLEAR_TERMINAL: 'CommandOrControl+L', + EDITOR_VIEW: 'CommandOrControl+Alt+1', + FILES_VIEW: 'CommandOrControl+Alt+2', ESC: 'Escape' }, menu: { @@ -19,5 +21,7 @@ module.exports = { STOP: 'CmdOrCtrl+H', RESET: 'CmdOrCtrl+Shift+R', CLEAR_TERMINAL: 'CmdOrCtrl+L', + EDITOR_VIEW: 'CmdOrCtrl+Alt+1', + FILES_VIEW: 'CmdOrCtrl+Alt+2' } } diff --git a/ui/arduino/media/code.svg b/ui/arduino/media/code.svg new file mode 100644 index 0000000..3b4303f --- /dev/null +++ b/ui/arduino/media/code.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 93ed152..fddea1a 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -1444,6 +1444,14 @@ async function store(state, emitter) { if (state.view != 'editor') return emitter.emit('save') } + if (key === shortcuts.EDITOR_VIEW) { + if (state.view != 'file-manager') return + emitter.emit('change-view', 'editor') + } + if (key === shortcuts.FILES_VIEW) { + if (state.view != 'editor') return + emitter.emit('change-view', 'file-manager') + } if (key === shortcuts.ESC) { if (state.isConnectionDialogOpen) { emitter.emit('close-connection-dialog') diff --git a/ui/arduino/views/components/toolbar.js b/ui/arduino/views/components/toolbar.js index b63e243..0e3d497 100644 --- a/ui/arduino/views/components/toolbar.js +++ b/ui/arduino/views/components/toolbar.js @@ -24,7 +24,7 @@ function Toolbar(state, emit) { ${Button({ icon: 'run.svg', - tooltip: `Run (${metaKeyString}+r)`, + tooltip: `Run (${metaKeyString}+R)`, disabled: !_canExecute, onClick: (e) => { if (e.altKey) { @@ -36,13 +36,13 @@ function Toolbar(state, emit) { })} ${Button({ icon: 'stop.svg', - tooltip: `Stop (${metaKeyString}+h)`, + tooltip: `Stop (${metaKeyString}+H)`, disabled: !_canExecute, onClick: () => emit('stop') })} ${Button({ icon: 'reboot.svg', - tooltip: `Reset (${metaKeyString}+Shift+r)`, + tooltip: `Reset (${metaKeyString}+Shift+R)`, disabled: !_canExecute, onClick: () => emit('reset') })} @@ -51,7 +51,7 @@ function Toolbar(state, emit) { ${Button({ icon: 'save.svg', - tooltip: `Save (${metaKeyString}+s)`, + tooltip: `Save (${metaKeyString}+S)`, disabled: !_canSave, onClick: () => emit('save') })} @@ -59,14 +59,14 @@ function Toolbar(state, emit) {
${Button({ - icon: 'editor.svg', - tooltip: 'Editor and REPL', + icon: 'code.svg', + tooltip: `Editor (${metaKeyString}+Alt+1)`, active: state.view === 'editor', onClick: () => emit('change-view', 'editor') })} ${Button({ icon: 'files.svg', - tooltip: 'File Manager', + tooltip: `Files (${metaKeyString}+Alt+2)`, active: state.view === 'file-manager', onClick: () => emit('change-view', 'file-manager') })} From bd3cc3b0e9d248493222e4614ec43afc2e484b45 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Fri, 13 Dec 2024 09:19:55 +0100 Subject: [PATCH 25/41] Moved 'Clear Terminal' to 'View' Signed-off-by: ubi de feo --- backend/menu.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/menu.js b/backend/menu.js index e706a41..337571a 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -89,13 +89,7 @@ module.exports = function registerMenu(win, state = {}) { enabled: state.isConnected && state.view === 'editor', click: () => win.webContents.send('shortcut-cmd', shortcuts.global.RESET) }, - { type: 'separator' }, - { - label: 'Clear terminal', - accelerator: shortcuts.menu.CLEAR_TERMINAL, - enabled: state.isConnected && state.view === 'editor', - click: () => win.webContents.send('shortcut-cmd', shortcuts.global.CLEAR_TERMINAL) - } + { type: 'separator' } ] }, { @@ -111,6 +105,12 @@ module.exports = function registerMenu(win, state = {}) { accelerator: shortcuts.menu.FILES_VIEW, click: () => win.webContents.send('shortcut-cmd', shortcuts.global.FILES_VIEW) }, + { + label: 'Clear terminal', + accelerator: shortcuts.menu.CLEAR_TERMINAL, + enabled: state.isConnected && state.view === 'editor', + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.CLEAR_TERMINAL) + }, { type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, From 923166149883f4de34f23e10bb4c4d84a9509c36 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Fri, 13 Dec 2024 11:40:54 +0100 Subject: [PATCH 26/41] Removed comments with dev notes. Signed-off-by: ubi de feo --- backend/menu.js | 1 - index.js | 47 ----------------------------------------------- preload.js | 13 +------------ 3 files changed, 1 insertion(+), 60 deletions(-) diff --git a/backend/menu.js b/backend/menu.js index 337571a..2d406f7 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -173,7 +173,6 @@ module.exports = function registerMenu(win, state = {}) { 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", diff --git a/index.js b/index.js index b11d662..aa5d989 100644 --- a/index.js +++ b/index.js @@ -92,53 +92,6 @@ function registerShortcuts() { shortcutAction(shortcut) }); }) - // shortcuts.forEach(element => { - // globalShortcut.register(element, () => { - - // shortcutAction(element) - // }); - // }); - // globalShortcut.register(shortcuts.RUN, () => { - // console.log('Running Program') - // shortcutAction(shortcuts.RUN) - // }) - // globalShortcut.register('CommandOrControl+Alt+R', () => { - // console.log('Running Code Selection') - // shortcutAction('meta_alt_r') - // }) - // globalShortcut.register('CommandOrControl+H', () => { - // console.log('Stopping Program (Halt)') - // shortcutAction('meta_h') - // }) - // globalShortcut.register('CommandOrControl+S', () => { - // console.log('Saving File') - // shortcutAction('meta_s') - // }) - - // globalShortcut.register('CommandOrControl+Shift+R', () => { - // console.log('Resetting Board') - // shortcutAction('meta_shift_r') - // }) - // globalShortcut.register(shortcuts.CONNECT, () => { - // console.log('Connect to Board') - // shortcutAction(shortcuts.CONNECT) - // }) - // globalShortcut.register(shortcuts.DISCONNECT, () => { - // console.log('Disconnect from Board') - // shortcutAction(shortcuts.DISCONNECT) - // }), - // globalShortcut.register('CommandOrControl+K', () => { - // console.log('Clear Terminal') - // shortcutAction('K') - // }), - // // Future: Toggle REPL Panel - // // globalShortcut.register('CommandOrControl+T', () => { - // // console.log('Toggle Terminal') - // // shortcutAction('T') - // // }), - // globalShortcut.register('Escape', () => { - // shortcutAction('ESC') - // }) } app.on('ready', () => { diff --git a/preload.js b/preload.js index bf78552..dd4f28f 100644 --- a/preload.js +++ b/preload.js @@ -4,7 +4,7 @@ const path = require('path') const shortcuts = require('./backend/shortcuts.js').global const MicroPython = require('micropython.js') const { emit, platform } = require('process') -// const { platform } = requireprocess.platform + const board = new MicroPython() board.chunk_size = 192 board.chunk_sleep = 200 @@ -159,18 +159,7 @@ const Window = { }, onKeyboardShortcut: (callback, key) => { ipcRenderer.on('shortcut-cmd', (event, k) => { - - - // Only trigger callback if terminal is not focused AND we're in editor view - // This has been deemed unnecessary since there are no real conflicts with the terminal - // The REPL shortcuts Ctrl+a|b|c|d are not used as application shortcuts and will - // only be triggered when the user has focused the REPL - // The code is left here for reference - // const activeElement = document.activeElement; - // const isTerminalFocused = activeElement.classList.contains('xterm-helper-textarea'); - // if (!isTerminalFocused) { callback(k); - // } }) }, From 9759ed752e82f4b308377c5d15d1e95664a57592 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Fri, 13 Dec 2024 12:38:24 +0100 Subject: [PATCH 27/41] Replaced Win/Linux Ctrl+Alt+R ignored shortcut with Ctrl+Alt+S Signed-off-by: ubi de feo --- backend/menu.js | 4 ++-- backend/shortcuts.js | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/menu.js b/backend/menu.js index 2d406f7..eb7810e 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -73,9 +73,9 @@ module.exports = function registerMenu(win, state = {}) { }, { label: 'Run selection', - accelerator: shortcuts.menu.RUN_SELECTION, + accelerator: isMac ? shortcuts.menu.RUN_SELECTION : shortcuts.menu.RUN_SELECTION_WL, enabled: state.isConnected && state.view === 'editor', - click: () => win.webContents.send('shortcut-cmd', shortcuts.global.RUN_SELECTION) + click: () => win.webContents.send('shortcut-cmd', (isMac ? shortcuts.global.RUN_SELECTION : shortcuts.global.RUN_SELECTION_WL)) }, { label: 'Stop', diff --git a/backend/shortcuts.js b/backend/shortcuts.js index b496200..e6b7159 100644 --- a/backend/shortcuts.js +++ b/backend/shortcuts.js @@ -5,6 +5,7 @@ module.exports = { SAVE: 'CommandOrControl+S', RUN: 'CommandOrControl+R', RUN_SELECTION: 'CommandOrControl+Alt+R', + RUN_SELECTION_WL: 'CommandOrControl+Alt+S', STOP: 'CommandOrControl+H', RESET: 'CommandOrControl+Shift+R', CLEAR_TERMINAL: 'CommandOrControl+L', @@ -18,6 +19,7 @@ module.exports = { SAVE: 'CmdOrCtrl+S', RUN: 'CmdOrCtrl+R', RUN_SELECTION: 'CmdOrCtrl+Alt+R', + RUN_SELECTION_WL: 'CmdOrCtrl+Alt+S', STOP: 'CmdOrCtrl+H', RESET: 'CmdOrCtrl+Shift+R', CLEAR_TERMINAL: 'CmdOrCtrl+L', From 6432226e5b70f5abf6370402e9de81c028e0eca1 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Fri, 13 Dec 2024 12:44:06 +0100 Subject: [PATCH 28/41] Amended shortcut handling in store.js Signed-off-by: ubi de feo --- ui/arduino/store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index fddea1a..f2bd1b0 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -1432,7 +1432,7 @@ async function store(state, emitter) { if (state.view != 'editor') return runCode() } - if (key === shortcuts.RUN_SELECTION) { + if (key === shortcuts.RUN_SELECTION || key === shortcuts.RUN_SELECTION_WL) { if (state.view != 'editor') return runCodeSelection() } From 5c153c008d65db85e73addfd34cc659a15a06877 Mon Sep 17 00:00:00 2001 From: ubi de feo Date: Fri, 13 Dec 2024 13:14:31 +0100 Subject: [PATCH 29/41] Update Board menu when opening files from File view. Signed-off-by: ubi de feo --- ui/arduino/store.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index f2bd1b0..c4de3ab 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -1198,6 +1198,7 @@ async function store(state, emitter) { state.openFiles = state.openFiles.concat(filesToOpen) state.view = 'editor' + updateMenu() emitter.emit('render') }) emitter.on('open-file', (source, file) => { From 48881ce6903b0238cc880eccdc4917fb2ada197e Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 11 Dec 2024 14:13:43 +0100 Subject: [PATCH 30/41] Use better name for callback --- ui/arduino/store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index c4de3ab..435066a 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -180,7 +180,7 @@ async function store(state, emitter) { term.write(data) term.scrollToBottom() }) - serial.onDisconnect(() => emitter.emit('disconnect')) + serial.onConnectionLost(() => emitter.emit('disconnect')) emitter.emit('close-connection-dialog') emitter.emit('refresh-files') From 42f8f9c5e5a06a445e89cbfb1df4625daa25e6d8 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 11 Dec 2024 14:14:25 +0100 Subject: [PATCH 31/41] Move serial logic to separate file --- backend/menu.js | 7 ++-- backend/serial.js | 102 ++++++++++++++++++++++++++++++++++++++++++++++ preload.js | 3 +- 3 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 backend/serial.js diff --git a/backend/menu.js b/backend/menu.js index eb7810e..b372527 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -1,5 +1,6 @@ const { app, Menu } = require('electron') const path = require('path') +const Serial = require('./serial.js') const openAboutWindow = require('about-window').default const shortcuts = require('./shortcuts.js') const { type } = require('os') @@ -127,10 +128,8 @@ module.exports = function registerMenu(win, state = {}) { accelerator: '', click: async () => { try { - win.webContents.send('cleanup-before-reload') - setTimeout(() => { - win.reload() - }, 500) + await Serial.disconnect() + win.reload() } catch(e) { console.error('Reload from menu failed:', e) } diff --git a/backend/serial.js b/backend/serial.js new file mode 100644 index 0000000..35a49a3 --- /dev/null +++ b/backend/serial.js @@ -0,0 +1,102 @@ +const MicroPython = require('micropython.js') + +const board = new MicroPython() +board.chunk_size = 192 +board.chunk_sleep = 200 + +const Serial = { + loadPorts: async () => { + let ports = await board.list_ports() + return ports.filter(p => p.vendorId && p.productId) + }, + connect: async (path) => { + return board.open(path) + }, + disconnect: async () => { + return board.close() + }, + run: async (code) => { + return board.run(code) + }, + execFile: async (path) => { + return board.execfile(path) + }, + getPrompt: async () => { + return board.get_prompt() + }, + keyboardInterrupt: async () => { + await board.stop() + return Promise.resolve() + }, + reset: async () => { + await board.stop() + await board.exit_raw_repl() + await board.reset() + return Promise.resolve() + }, + eval: (d) => { + return board.eval(d) + }, + onData: (fn) => { + board.serial.on('data', fn) + }, + listFiles: async (folder) => { + return board.fs_ls(folder) + }, + ilistFiles: async (folder) => { + return board.fs_ils(folder) + }, + loadFile: async (file) => { + const output = await board.fs_cat_binary(file) + return output || '' + }, + removeFile: async (file) => { + return board.fs_rm(file) + }, + saveFileContent: async (filename, content, dataConsumer) => { + return board.fs_save(content || ' ', filename, dataConsumer) + }, + uploadFile: async (src, dest, dataConsumer) => { + return board.fs_put(src, dest.replaceAll(path.win32.sep, path.posix.sep), dataConsumer) + }, + downloadFile: async (src, dest) => { + let contents = await Serial.loadFile(src) + return ipcRenderer.invoke('save-file', dest, contents) + }, + renameFile: async (oldName, newName) => { + return board.fs_rename(oldName, newName) + }, + onConnectionLost: async (fn) => { + board.serial.on('close', fn) + }, + createFolder: async (folder) => { + return await board.fs_mkdir(folder) + }, + removeFolder: async (folder) => { + return await board.fs_rmdir(folder) + }, + getNavigationPath: (navigation, target) => { + return path.posix.join(navigation, target) + }, + getFullPath: (root, navigation, file) => { + return path.posix.join(root, navigation, file) + }, + getParentPath: (navigation) => { + return path.posix.dirname(navigation) + }, + fileExists: async (filePath) => { + // !!!: Fix this on micropython.js level + // ???: Check if file exists is not part of mpremote specs + const output = await board.run(` +import os +try: + os.stat("${filePath}") + print(0) +except OSError: + print(1) +`) + return output[2] === '0' + } +} + +module.exports = Serial \ No newline at end of file diff --git a/preload.js b/preload.js index dd4f28f..a7c1361 100644 --- a/preload.js +++ b/preload.js @@ -1,6 +1,7 @@ console.log('preload') const { contextBridge, ipcRenderer } = require('electron') const path = require('path') +const Serial = require('./backend/serial.js') const shortcuts = require('./backend/shortcuts.js').global const MicroPython = require('micropython.js') const { emit, platform } = require('process') @@ -191,4 +192,4 @@ const Window = { contextBridge.exposeInMainWorld('BridgeSerial', Serial) contextBridge.exposeInMainWorld('BridgeDisk', Disk) -contextBridge.exposeInMainWorld('BridgeWindow', Window) +contextBridge.exposeInMainWorld('BridgeWindow', Window) \ No newline at end of file From cc5a8ee4cbffbeafc3fbe16f306454ae6c2a9371 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 11 Dec 2024 14:41:37 +0100 Subject: [PATCH 32/41] Decouple UI and logic of disconnection event --- backend/serial.js | 2 +- ui/arduino/store.js | 16 ++++++++++++---- ui/arduino/views/components/toolbar.js | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/backend/serial.js b/backend/serial.js index 35a49a3..ec0d6b7 100644 --- a/backend/serial.js +++ b/backend/serial.js @@ -66,7 +66,7 @@ const Serial = { renameFile: async (oldName, newName) => { return board.fs_rename(oldName, newName) }, - onConnectionLost: async (fn) => { + onConnectionClosed: async (fn) => { board.serial.on('close', fn) }, createFolder: async (folder) => { diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 435066a..4c18e7b 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -114,7 +114,8 @@ async function store(state, emitter) { // CONNECTION DIALOG emitter.on('open-connection-dialog', async () => { log('open-connection-dialog') - emitter.emit('disconnect') + // UI should be in disconnected state, no need to update + await serial.disconnect() state.availablePorts = await getAvailablePorts() state.isConnectionDialogOpen = true emitter.emit('render') @@ -180,14 +181,16 @@ async function store(state, emitter) { term.write(data) term.scrollToBottom() }) - serial.onConnectionLost(() => emitter.emit('disconnect')) + + // Update the UI when the conncetion is closed + // This may happen when unplugging the board + serial.onConnectionClosed(() => emitter.emit('disconnected')) emitter.emit('close-connection-dialog') emitter.emit('refresh-files') emitter.emit('render') }) - emitter.on('disconnect', async () => { - await serial.disconnect() + emitter.on('disconnected', () => { state.isConnected = false state.panelHeight = PANEL_CLOSED state.boardFiles = [] @@ -196,6 +199,11 @@ async function store(state, emitter) { emitter.emit('render') updateMenu() }) + emitter.on('disconnect', async () => { + await serial.disconnect() + // Update the UI after closing the connection + emitter.emit('disconnected') + }) emitter.on('connection-timeout', async () => { state.isConnected = false state.isConnecting = false diff --git a/ui/arduino/views/components/toolbar.js b/ui/arduino/views/components/toolbar.js index 0e3d497..70982b0 100644 --- a/ui/arduino/views/components/toolbar.js +++ b/ui/arduino/views/components/toolbar.js @@ -16,7 +16,7 @@ function Toolbar(state, emit) { ${Button({ icon: state.isConnected ? 'connect.svg' : 'disconnect.svg', tooltip: state.isConnected ? `Disconnect (${metaKeyString}+Shift+D)` : `Connect (${metaKeyString}+Shift+C)`, - onClick: () => emit('open-connection-dialog'), + onClick: () => state.isConnected ? emit('disconnect') : emit('open-connection-dialog'), active: state.isConnected })} From 88e0bda89b9f471a82e620bf05973378cc99e444 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 11 Dec 2024 15:40:13 +0100 Subject: [PATCH 33/41] Use serial bridge to execute serial commands --- backend/bridge/serial-bridge.js | 89 +++++++++++++++ backend/ipc.js | 9 ++ backend/serial.js | 191 +++++++++++++++++--------------- preload.js | 3 +- ui/arduino/store.js | 156 +++++++++++++------------- 5 files changed, 279 insertions(+), 169 deletions(-) create mode 100644 backend/bridge/serial-bridge.js diff --git a/backend/bridge/serial-bridge.js b/backend/bridge/serial-bridge.js new file mode 100644 index 0000000..54f7221 --- /dev/null +++ b/backend/bridge/serial-bridge.js @@ -0,0 +1,89 @@ +const { ipcRenderer } = require('electron') +const path = require('path') + +const SerialBridge = { + loadPorts: async () => { + return await ipcRenderer.invoke('serial', 'loadPorts') + }, + connect: async (path) => { + return await ipcRenderer.invoke('serial', 'connect', path) + }, + disconnect: async () => { + return await ipcRenderer.invoke('serial', 'disconnect') + }, + run: async (code) => { + return await ipcRenderer.invoke('serial', 'run', code) + }, + execFile: async (path) => { + return await ipcRenderer.invoke('serial', 'execFile', path) + }, + getPrompt: async () => { + return await ipcRenderer.invoke('serial', 'getPrompt') + }, + keyboardInterrupt: async () => { + await ipcRenderer.invoke('serial', 'keyboardInterrupt') + return Promise.resolve() + }, + reset: async () => { + await ipcRenderer.invoke('serial', 'reset') + return Promise.resolve() + }, + eval: (d) => { + return ipcRenderer.invoke('serial', 'eval', d) + }, + onData: (callback) => { + ipcRenderer.on('serial-on-data', (event, data) => { + callback(data) + }) + }, + listFiles: async (folder) => { + return await ipcRenderer.invoke('serial', 'listFiles', folder) + }, + ilistFiles: async (folder) => { + return await ipcRenderer.invoke('serial', 'ilistFiles', folder) + }, + loadFile: async (file) => { + return await ipcRenderer.invoke('serial', 'loadFile', file) + }, + removeFile: async (file) => { + return await ipcRenderer.invoke('serial', 'removeFile', file) + }, + saveFileContent: async (filename, content, dataConsumer) => { + return await ipcRenderer.invoke('serial', 'saveFileContent', filename, content, dataConsumer) + }, + uploadFile: async (src, dest, dataConsumer) => { + return await ipcRenderer.invoke('serial', 'uploadFile', src, dest, dataConsumer) + }, + downloadFile: async (src, dest) => { + let contents = await ipcRenderer.invoke('serial', 'loadFile', src) + return ipcRenderer.invoke('save-file', dest, contents) + }, + renameFile: async (oldName, newName) => { + return await ipcRenderer.invoke('serial', 'renameFile', oldName, newName) + }, + onConnectionClosed: async (callback) => { + ipcRenderer.on('serial-on-connection-closed', (event) => { + callback() + }) + }, + createFolder: async (folder) => { + return await ipcRenderer.invoke('serial', 'createFolder', folder) + }, + removeFolder: async (folder) => { + return await ipcRenderer.invoke('serial', 'removeFolder', folder) + }, + getNavigationPath: (navigation, target) => { + return path.posix.join(navigation, target) + }, + getFullPath: (root, navigation, file) => { + return path.posix.join(root, navigation, file) + }, + getParentPath: (navigation) => { + return path.posix.dirname(navigation) + }, + fileExists: async (filePath) => { + return await ipcRenderer.invoke('serial', 'fileExists', filePath) + } +} + +module.exports = SerialBridge \ No newline at end of file diff --git a/backend/ipc.js b/backend/ipc.js index 12f2127..f554010 100644 --- a/backend/ipc.js +++ b/backend/ipc.js @@ -1,4 +1,6 @@ const fs = require('fs') +const Serial = require('./serial.js') +let serial const registerMenu = require('./menu.js') const { @@ -9,6 +11,8 @@ const { } = require('./helpers.js') module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) { + serial = new Serial(win) + ipcMain.handle('open-folder', async (event) => { console.log('ipcMain', 'open-folder') const folder = await openFolderDialog(win) @@ -145,4 +149,9 @@ module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) { ipcMain.handle('prepare-reload', async (event) => { return win.webContents.send('before-reload') }) + + ipcMain.handle('serial', (event, command, ...args) => { + console.debug('Handling IPC serial command:', command, ...args) + return serial[command](...args) + }) } diff --git a/backend/serial.js b/backend/serial.js index ec0d6b7..72357c7 100644 --- a/backend/serial.js +++ b/backend/serial.js @@ -1,93 +1,104 @@ const MicroPython = require('micropython.js') -const board = new MicroPython() -board.chunk_size = 192 -board.chunk_sleep = 200 - -const Serial = { - loadPorts: async () => { - let ports = await board.list_ports() - return ports.filter(p => p.vendorId && p.productId) - }, - connect: async (path) => { - return board.open(path) - }, - disconnect: async () => { - return board.close() - }, - run: async (code) => { - return board.run(code) - }, - execFile: async (path) => { - return board.execfile(path) - }, - getPrompt: async () => { - return board.get_prompt() - }, - keyboardInterrupt: async () => { - await board.stop() - return Promise.resolve() - }, - reset: async () => { - await board.stop() - await board.exit_raw_repl() - await board.reset() - return Promise.resolve() - }, - eval: (d) => { - return board.eval(d) - }, - onData: (fn) => { - board.serial.on('data', fn) - }, - listFiles: async (folder) => { - return board.fs_ls(folder) - }, - ilistFiles: async (folder) => { - return board.fs_ils(folder) - }, - loadFile: async (file) => { - const output = await board.fs_cat_binary(file) - return output || '' - }, - removeFile: async (file) => { - return board.fs_rm(file) - }, - saveFileContent: async (filename, content, dataConsumer) => { - return board.fs_save(content || ' ', filename, dataConsumer) - }, - uploadFile: async (src, dest, dataConsumer) => { - return board.fs_put(src, dest.replaceAll(path.win32.sep, path.posix.sep), dataConsumer) - }, - downloadFile: async (src, dest) => { - let contents = await Serial.loadFile(src) - return ipcRenderer.invoke('save-file', dest, contents) - }, - renameFile: async (oldName, newName) => { - return board.fs_rename(oldName, newName) - }, - onConnectionClosed: async (fn) => { - board.serial.on('close', fn) - }, - createFolder: async (folder) => { - return await board.fs_mkdir(folder) - }, - removeFolder: async (folder) => { - return await board.fs_rmdir(folder) - }, - getNavigationPath: (navigation, target) => { - return path.posix.join(navigation, target) - }, - getFullPath: (root, navigation, file) => { - return path.posix.join(root, navigation, file) - }, - getParentPath: (navigation) => { - return path.posix.dirname(navigation) - }, - fileExists: async (filePath) => { - // !!!: Fix this on micropython.js level - // ???: Check if file exists is not part of mpremote specs - const output = await board.run(` +class Serial { + constructor(win) { + this.win = win + this.board = new MicroPython() + this.board.chunk_size = 192 + this.board.chunk_sleep = 200 + } + + async loadPorts() { + let ports = await this.board.list_ports() + return ports.filter(p => p.vendorId && p.productId) + } + + async connect(path) { + await this.board.open(path) + this.registerCallbacks() + } + + async disconnect() { + return await this.board.close() + } + + async run(code) { + return await this.board.run(code) + } + + async execFile(path) { + return await this.board.execfile(path) + } + + async getPrompt() { + return await this.board.get_prompt() + } + + async keyboardInterrupt() { + await this.board.stop() + return Promise.resolve() + } + + async reset() { + await this.board.stop() + await this.board.exit_raw_repl() + await this.board.reset() + return Promise.resolve() + } + + async eval(d) { + return await this.board.eval(d) + } + + registerCallbacks() { + this.board.serial.on('data', (data) => { + this.win.webContents.send('serial-on-data', data) + }) + + this.board.serial.on('close', () => { + this.win.webContents.send('serial-on-connection-closed') + }) + } + + async listFiles(folder) { + return await this.board.fs_ls(folder) + } + + async ilistFiles(folder) { + return await this.board.fs_ils(folder) + } + + async loadFile(file) { + const output = await this.board.fs_cat_binary(file) + return output || '' + } + + async removeFile(file) { + return await this.board.fs_rm(file) + } + + async saveFileContent(filename, content, dataConsumer) { + return await this.board.fs_save(content || ' ', filename, dataConsumer) + } + + async uploadFile(src, dest, dataConsumer) { + return await this.board.fs_put(src, dest.replaceAll(path.win32.sep, path.posix.sep), dataConsumer) + } + + async renameFile(oldName, newName) { + return await this.board.fs_rename(oldName, newName) + } + + async createFolder(folder) { + return await this.board.fs_mkdir(folder) + } + + async removeFolder(folder) { + return await this.board.fs_rmdir(folder) + } + + async fileExists(filePath) { + const output = await this.board.run(` import os try: os.stat("${filePath}") @@ -95,8 +106,8 @@ try: except OSError: print(1) `) - return output[2] === '0' - } + return output[2] === '0' + } } module.exports = Serial \ No newline at end of file diff --git a/preload.js b/preload.js index a7c1361..7549868 100644 --- a/preload.js +++ b/preload.js @@ -5,6 +5,7 @@ const Serial = require('./backend/serial.js') const shortcuts = require('./backend/shortcuts.js').global const MicroPython = require('micropython.js') const { emit, platform } = require('process') +const SerialBridge = require('./backend/bridge/serial-bridge.js') const board = new MicroPython() board.chunk_size = 192 @@ -190,6 +191,6 @@ const Window = { getShortcuts: () => shortcuts } -contextBridge.exposeInMainWorld('BridgeSerial', Serial) +contextBridge.exposeInMainWorld('BridgeSerial', SerialBridge) contextBridge.exposeInMainWorld('BridgeDisk', Disk) contextBridge.exposeInMainWorld('BridgeWindow', Window) \ No newline at end of file diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 4c18e7b..1e49661 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -1,5 +1,5 @@ const log = console.log -const serial = window.BridgeSerial +const serialBridge = window.BridgeSerial const disk = window.BridgeDisk const win = window.BridgeWindow @@ -115,7 +115,7 @@ async function store(state, emitter) { emitter.on('open-connection-dialog', async () => { log('open-connection-dialog') // UI should be in disconnected state, no need to update - await serial.disconnect() + await serialBridge.disconnect() state.availablePorts = await getAvailablePorts() state.isConnectionDialogOpen = true emitter.emit('render') @@ -149,14 +149,14 @@ async function store(state, emitter) { emitter.emit('connection-timeout') }, 3500) try { - await serial.connect(path) + await serialBridge.connect(path) } catch(e) { console.error(e) } // Stop whatever is going on // Recover from getting stuck in raw repl - await serial.getPrompt() + await serialBridge.getPrompt() clearTimeout(timeout_id) // Connected and ready state.isConnecting = false @@ -172,19 +172,19 @@ async function store(state, emitter) { if (!state.isTerminalBound) { state.isTerminalBound = true term.onData((data) => { - serial.eval(data) + serialBridge.eval(data) term.scrollToBottom() }) - serial.eval('\x02') + serialBridge.eval('\x02') } - serial.onData((data) => { + serialBridge.onData((data) => { term.write(data) term.scrollToBottom() }) // Update the UI when the conncetion is closed // This may happen when unplugging the board - serial.onConnectionClosed(() => emitter.emit('disconnected')) + serialBridge.onConnectionClosed(() => emitter.emit('disconnected')) emitter.emit('close-connection-dialog') emitter.emit('refresh-files') @@ -200,7 +200,7 @@ async function store(state, emitter) { updateMenu() }) emitter.on('disconnect', async () => { - await serial.disconnect() + await serialBridge.disconnect() // Update the UI after closing the connection emitter.emit('disconnected') }) @@ -236,8 +236,8 @@ async function store(state, emitter) { emitter.emit('open-panel') emitter.emit('render') try { - await serial.getPrompt() - await serial.run(code) + await serialBridge.getPrompt() + await serialBridge.run(code) } catch(e) { log('error', e) } @@ -249,7 +249,7 @@ async function store(state, emitter) { } emitter.emit('open-panel') emitter.emit('render') - await serial.getPrompt() + await serialBridge.getPrompt() }) emitter.on('reset', async () => { log('reset') @@ -258,7 +258,7 @@ async function store(state, emitter) { } emitter.emit('open-panel') emitter.emit('render') - await serial.reset() + await serialBridge.reset() emitter.emit('update-files') emitter.emit('render') }) @@ -334,9 +334,9 @@ async function store(state, emitter) { // Check if the current full path exists let fullPathExists = false if (openFile.source == 'board') { - await serial.getPrompt() - fullPathExists = await serial.fileExists( - serial.getFullPath( + await serialBridge.getPrompt() + fullPathExists = await serialBridge.fileExists( + serialBridge.getFullPath( state.boardNavigationRoot, openFile.parentFolder, openFile.fileName @@ -357,9 +357,9 @@ async function store(state, emitter) { if (openFile.source == 'board') { openFile.parentFolder = state.boardNavigationPath // Check for overwrite - await serial.getPrompt() - willOverwrite = await serial.fileExists( - serial.getFullPath( + await serialBridge.getPrompt() + willOverwrite = await serialBridge.fileExists( + serialBridge.getFullPath( state.boardNavigationRoot, openFile.parentFolder, openFile.fileName @@ -392,9 +392,9 @@ async function store(state, emitter) { const contents = openFile.editor.editor.state.doc.toString() try { if (openFile.source == 'board') { - await serial.getPrompt() - await serial.saveFileContent( - serial.getFullPath( + await serialBridge.getPrompt() + await serialBridge.saveFileContent( + serialBridge.getFullPath( state.boardNavigationRoot, openFile.parentFolder, openFile.fileName @@ -470,7 +470,7 @@ async function store(state, emitter) { if (state.isConnected) { try { state.boardFiles = await getBoardFiles( - serial.getFullPath( + serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, '' @@ -548,8 +548,8 @@ async function store(state, emitter) { } // TODO: Remove existing file } - await serial.saveFileContent( - serial.getFullPath( + await serialBridge.saveFileContent( + serialBridge.getFullPath( '/', state.boardNavigationPath, value @@ -619,15 +619,15 @@ async function store(state, emitter) { } // Remove existing folder await removeBoardFolder( - serial.getFullPath( + serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, value ) ) } - await serial.createFolder( - serial.getFullPath( + await serialBridge.createFolder( + serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, value @@ -709,7 +709,7 @@ async function store(state, emitter) { if (file.type == 'folder') { if (file.source === 'board') { await removeBoardFolder( - serial.getFullPath( + serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, file.fileName @@ -726,8 +726,8 @@ async function store(state, emitter) { } } else { if (file.source === 'board') { - await serial.removeFile( - serial.getFullPath( + await serialBridge.removeFile( + serialBridge.getFullPath( '/', state.boardNavigationPath, file.fileName @@ -795,15 +795,15 @@ async function store(state, emitter) { if (file.type == 'folder') { await removeBoardFolder( - serial.getFullPath( + serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, value ) ) } else if (file.type == 'file') { - await serial.removeFile( - serial.getFullPath( + await serialBridge.removeFile( + serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, value @@ -855,13 +855,13 @@ async function store(state, emitter) { try { if (state.renamingFile == 'board') { - await serial.renameFile( - serial.getFullPath( + await serialBridge.renameFile( + serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, file.fileName ), - serial.getFullPath( + serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, value @@ -932,8 +932,8 @@ async function store(state, emitter) { if (!isNewFile) { // Check if full path exists if (openFile.source == 'board') { - fullPathExists = await serial.fileExists( - serial.getFullPath( + fullPathExists = await serialBridge.fileExists( + serialBridge.getFullPath( state.boardNavigationRoot, openFile.parentFolder, oldName @@ -961,8 +961,8 @@ async function store(state, emitter) { // Check if it will overwrite let willOverwrite = false if (openFile.source == 'board') { - willOverwrite = await serial.fileExists( - serial.getFullPath( + willOverwrite = await serialBridge.fileExists( + serialBridge.getFullPath( state.boardNavigationRoot, openFile.parentFolder, openFile.fileName @@ -994,9 +994,9 @@ async function store(state, emitter) { const contents = openFile.editor.editor.state.doc.toString() try { if (openFile.source == 'board') { - await serial.getPrompt() - await serial.saveFileContent( - serial.getFullPath( + await serialBridge.getPrompt() + await serialBridge.saveFileContent( + serialBridge.getFullPath( state.boardNavigationRoot, openFile.parentFolder, oldName @@ -1023,13 +1023,13 @@ async function store(state, emitter) { // RENAME FILE try { if (openFile.source == 'board') { - await serial.renameFile( - serial.getFullPath( + await serialBridge.renameFile( + serialBridge.getFullPath( state.boardNavigationRoot, openFile.parentFolder, oldName ), - serial.getFullPath( + serialBridge.getFullPath( state.boardNavigationRoot, openFile.parentFolder, openFile.fileName @@ -1057,9 +1057,9 @@ async function store(state, emitter) { const contents = openFile.editor.editor.state.doc.toString() try { if (openFile.source == 'board') { - await serial.getPrompt() - await serial.saveFileContent( - serial.getFullPath( + await serialBridge.getPrompt() + await serialBridge.saveFileContent( + serialBridge.getFullPath( state.boardNavigationRoot, openFile.parentFolder, openFile.fileName @@ -1149,8 +1149,8 @@ async function store(state, emitter) { // load content and append it to the list of files to open let file = null if (selectedFile.source == 'board') { - const fileContent = await serial.loadFile( - serial.getFullPath( + const fileContent = await serialBridge.loadFile( + serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, selectedFile.fileName @@ -1230,7 +1230,7 @@ async function store(state, emitter) { const willOverwrite = await checkOverwrite({ source: 'board', fileNames: state.selectedFiles.map(f => f.fileName), - parentPath: serial.getFullPath( + parentPath: serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, '' @@ -1257,7 +1257,7 @@ async function store(state, emitter) { state.diskNavigationPath, file.fileName ) - const destPath = serial.getFullPath( + const destPath = serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, file.fileName @@ -1271,7 +1271,7 @@ async function store(state, emitter) { } ) } else { - await serial.uploadFile( + await serialBridge.uploadFile( srcPath, destPath, (progress) => { state.transferringProgress = `${file.fileName}: ${progress}` @@ -1317,7 +1317,7 @@ async function store(state, emitter) { for (let i in state.selectedFiles) { const file = state.selectedFiles[i] - const srcPath = serial.getFullPath( + const srcPath = serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, file.fileName @@ -1336,7 +1336,7 @@ async function store(state, emitter) { } ) } else { - await serial.downloadFile( + await serialBridge.downloadFile( srcPath, destPath, (e) => { state.transferringProgress = e @@ -1355,7 +1355,7 @@ async function store(state, emitter) { // NAVIGATION emitter.on('navigate-board-folder', (folder) => { log('navigate-board-folder', folder) - state.boardNavigationPath = serial.getNavigationPath( + state.boardNavigationPath = serialBridge.getNavigationPath( state.boardNavigationPath, folder ) @@ -1364,7 +1364,7 @@ async function store(state, emitter) { }) emitter.on('navigate-board-parent', () => { log('navigate-board-parent') - state.boardNavigationPath = serial.getNavigationPath( + state.boardNavigationPath = serialBridge.getNavigationPath( state.boardNavigationPath, '..' ) @@ -1394,7 +1394,7 @@ async function store(state, emitter) { win.onBeforeReload(async () => { // Perform any cleanup needed if (state.isConnected) { - await serial.disconnect() + await serialBridge.disconnect() state.isConnected = false state.panelHeight = PANEL_CLOSED state.boardFiles = [] @@ -1561,12 +1561,12 @@ function generateHash() { } async function getAvailablePorts() { - return await serial.loadPorts() + return await serialBridge.loadPorts() } async function getBoardFiles(path) { - await serial.getPrompt() - let files = await serial.ilistFiles(path) + await serialBridge.getPrompt() + let files = await serialBridge.ilistFiles(path) files = files.map(f => ({ fileName: f[0], type: f[1] === 0x4000 ? 'folder' : 'file' @@ -1584,9 +1584,9 @@ function checkDiskFile({ root, parentFolder, fileName }) { async function checkBoardFile({ root, parentFolder, fileName }) { if (root == null || parentFolder == null || fileName == null) return false - await serial.getPrompt() - return serial.fileExists( - serial.getFullPath(root, parentFolder, fileName) + await serialBridge.getPrompt() + return serialBridge.fileExists( + serialBridge.getFullPath(root, parentFolder, fileName) ) } @@ -1651,29 +1651,29 @@ function canEdit({ selectedFiles }) { async function removeBoardFolder(fullPath) { // TODO: Replace with getting the file tree from the board and deleting one by one - let output = await serial.execFile(await getHelperFullPath()) - await serial.run(`delete_folder('${fullPath}')`) + let output = await serialBridge.execFile(await getHelperFullPath()) + await serialBridge.run(`delete_folder('${fullPath}')`) } async function uploadFolder(srcPath, destPath, dataConsumer) { dataConsumer = dataConsumer || function() {} - await serial.createFolder(destPath) + await serialBridge.createFolder(destPath) let allFiles = await disk.ilistAllFiles(srcPath) for (let i in allFiles) { const file = allFiles[i] const relativePath = file.path.substring(srcPath.length) if (file.type === 'folder') { - await serial.createFolder( - serial.getFullPath( + await serialBridge.createFolder( + serialBridge.getFullPath( destPath, relativePath, '' ) ) } else { - await serial.uploadFile( + await serialBridge.uploadFile( disk.getFullPath(srcPath, relativePath, ''), - serial.getFullPath(destPath, relativePath, ''), + serialBridge.getFullPath(destPath, relativePath, ''), (progress) => { dataConsumer(progress, relativePath) } @@ -1685,8 +1685,8 @@ async function uploadFolder(srcPath, destPath, dataConsumer) { async function downloadFolder(srcPath, destPath, dataConsumer) { dataConsumer = dataConsumer || function() {} await disk.createFolder(destPath) - let output = await serial.execFile(await getHelperFullPath()) - output = await serial.run(`ilist_all('${srcPath}')`) + let output = await serialBridge.execFile(await getHelperFullPath()) + output = await serialBridge.run(`ilist_all('${srcPath}')`) let files = [] try { // Extracting the json output from serial response @@ -1706,9 +1706,9 @@ async function downloadFolder(srcPath, destPath, dataConsumer) { disk.getFullPath( destPath, relativePath, '') ) } else { - await serial.downloadFile( - serial.getFullPath(srcPath, relativePath, ''), - serial.getFullPath(destPath, relativePath, '') + await serialBridge.downloadFile( + serialBridge.getFullPath(srcPath, relativePath, ''), + serialBridge.getFullPath(destPath, relativePath, '') ) } } From 041468f796162cd0d8fa8f7d0896cfb4c4b9cdf0 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 11 Dec 2024 16:43:58 +0100 Subject: [PATCH 34/41] Use shared instance of Serial in menu handler --- backend/ipc.js | 5 ++--- backend/menu.js | 4 ++-- backend/serial.js | 6 ++++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/backend/ipc.js b/backend/ipc.js index f554010..91f352a 100644 --- a/backend/ipc.js +++ b/backend/ipc.js @@ -1,7 +1,6 @@ const fs = require('fs') -const Serial = require('./serial.js') -let serial const registerMenu = require('./menu.js') +const serial = require('./serial.js').sharedInstance const { openFolderDialog, @@ -11,7 +10,7 @@ const { } = require('./helpers.js') module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) { - serial = new Serial(win) + serial.win = win // Required to send callback messages to renderer ipcMain.handle('open-folder', async (event) => { console.log('ipcMain', 'open-folder') diff --git a/backend/menu.js b/backend/menu.js index b372527..932d704 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -1,6 +1,6 @@ const { app, Menu } = require('electron') const path = require('path') -const Serial = require('./serial.js') +const serial = require('./serial.js').sharedInstance const openAboutWindow = require('about-window').default const shortcuts = require('./shortcuts.js') const { type } = require('os') @@ -128,7 +128,7 @@ module.exports = function registerMenu(win, state = {}) { accelerator: '', click: async () => { try { - await Serial.disconnect() + await serial.disconnect() win.reload() } catch(e) { console.error('Reload from menu failed:', e) diff --git a/backend/serial.js b/backend/serial.js index 72357c7..1e13e6c 100644 --- a/backend/serial.js +++ b/backend/serial.js @@ -1,7 +1,7 @@ const MicroPython = require('micropython.js') class Serial { - constructor(win) { + constructor(win = null) { this.win = win this.board = new MicroPython() this.board.chunk_size = 192 @@ -110,4 +110,6 @@ except OSError: } } -module.exports = Serial \ No newline at end of file +const sharedInstance = new Serial() + +module.exports = {sharedInstance, Serial} \ No newline at end of file From 0792f2fc6f42dbeb13aee1fd930bea6b7852bf1d Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Fri, 13 Dec 2024 23:35:42 +0100 Subject: [PATCH 35/41] Remove residues from rebase --- preload.js | 101 ----------------------------------------------------- 1 file changed, 101 deletions(-) diff --git a/preload.js b/preload.js index 7549868..58cb5c3 100644 --- a/preload.js +++ b/preload.js @@ -1,111 +1,10 @@ console.log('preload') const { contextBridge, ipcRenderer } = require('electron') const path = require('path') -const Serial = require('./backend/serial.js') const shortcuts = require('./backend/shortcuts.js').global -const MicroPython = require('micropython.js') const { emit, platform } = require('process') const SerialBridge = require('./backend/bridge/serial-bridge.js') -const board = new MicroPython() -board.chunk_size = 192 -board.chunk_sleep = 200 - -const Serial = { - loadPorts: async () => { - let ports = await board.list_ports() - return ports.filter(p => p.vendorId && p.productId) - }, - connect: async (path) => { - return board.open(path) - }, - disconnect: async () => { - return board.close() - }, - run: async (code) => { - return board.run(code) - }, - execFile: async (path) => { - return board.execfile(path) - }, - getPrompt: async () => { - return board.get_prompt() - }, - keyboardInterrupt: async () => { - await board.stop() - return Promise.resolve() - }, - reset: async () => { - await board.stop() - await board.exit_raw_repl() - await board.reset() - return Promise.resolve() - }, - eval: (d) => { - return board.eval(d) - }, - onData: (fn) => { - board.serial.on('data', fn) - }, - listFiles: async (folder) => { - return board.fs_ls(folder) - }, - ilistFiles: async (folder) => { - return board.fs_ils(folder) - }, - loadFile: async (file) => { - const output = await board.fs_cat_binary(file) - return output || '' - }, - removeFile: async (file) => { - return board.fs_rm(file) - }, - saveFileContent: async (filename, content, dataConsumer) => { - return board.fs_save(content || ' ', filename, dataConsumer) - }, - uploadFile: async (src, dest, dataConsumer) => { - return board.fs_put(src, dest.replaceAll(path.win32.sep, path.posix.sep), dataConsumer) - }, - downloadFile: async (src, dest) => { - let contents = await Serial.loadFile(src) - return ipcRenderer.invoke('save-file', dest, contents) - }, - renameFile: async (oldName, newName) => { - return board.fs_rename(oldName, newName) - }, - onDisconnect: async (fn) => { - board.serial.on('close', fn) - }, - createFolder: async (folder) => { - return await board.fs_mkdir(folder) - }, - removeFolder: async (folder) => { - return await board.fs_rmdir(folder) - }, - getNavigationPath: (navigation, target) => { - return path.posix.join(navigation, target) - }, - getFullPath: (root, navigation, file) => { - return path.posix.join(root, navigation, file) - }, - getParentPath: (navigation) => { - return path.posix.dirname(navigation) - }, - fileExists: async (filePath) => { - // !!!: Fix this on micropython.js level - // ???: Check if file exists is not part of mpremote specs - const output = await board.run(` -import os -try: - os.stat("${filePath}") - print(0) -except OSError: - print(1) -`) - return output[2] === '0' - } -} - const Disk = { openFolder: async () => { return ipcRenderer.invoke('open-folder') From c3e2bff8b51c5bf8b4e38bfe87cbeb70fbdfe7f8 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Sat, 14 Dec 2024 00:03:05 +0100 Subject: [PATCH 36/41] Remove code to handle reload event --- backend/ipc.js | 5 ----- index.js | 18 ------------------ preload.js | 10 ---------- ui/arduino/store.js | 12 ------------ 4 files changed, 45 deletions(-) diff --git a/backend/ipc.js b/backend/ipc.js index 91f352a..67e20db 100644 --- a/backend/ipc.js +++ b/backend/ipc.js @@ -144,11 +144,6 @@ module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) { win.webContents.send('check-before-close') }) - // handle disconnection before reload - ipcMain.handle('prepare-reload', async (event) => { - return win.webContents.send('before-reload') - }) - ipcMain.handle('serial', (event, command, ...args) => { console.debug('Handling IPC serial command:', command, ...args) return serial[command](...args) diff --git a/index.js b/index.js index aa5d989..a6fcc04 100644 --- a/index.js +++ b/index.js @@ -50,24 +50,6 @@ function createWindow () { win.show() }) - win.webContents.on('before-reload', async (event) => { - // Prevent the default reload behavior - event.preventDefault() - - try { - // Tell renderer to do cleanup - win.webContents.send('cleanup-before-reload') - - // Wait for cleanup then reload - setTimeout(() => { - // This will trigger a page reload, but won't trigger 'before-reload' again - win.reload() - }, 500) - } catch(e) { - console.error('Reload preparation failed:', e) - } - }) - const initialMenuState = { isConnected: false, view: 'editor' diff --git a/preload.js b/preload.js index 58cb5c3..c13715d 100644 --- a/preload.js +++ b/preload.js @@ -64,16 +64,6 @@ const Window = { }) }, - onBeforeReload: (callback) => { - ipcRenderer.on('cleanup-before-reload', async () => { - try { - await callback() - } catch(e) { - console.error('Cleanup before reload failed:', e) - } - }) - }, - beforeClose: (callback) => ipcRenderer.on('check-before-close', callback), confirmClose: () => ipcRenderer.invoke('confirm-close'), isPackaged: () => ipcRenderer.invoke('is-packaged'), diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 1e49661..b8b8c23 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -1391,18 +1391,6 @@ async function store(state, emitter) { emitter.emit('render') }) - win.onBeforeReload(async () => { - // Perform any cleanup needed - if (state.isConnected) { - await serialBridge.disconnect() - state.isConnected = false - state.panelHeight = PANEL_CLOSED - state.boardFiles = [] - state.boardNavigationPath = '/' - } - // Any other cleanup needed - }) - win.beforeClose(async () => { const hasChanges = !!state.openFiles.find(f => f.hasChanges) if (hasChanges) { From 18c4929ebb5fdccfa471860463fcd366a457c685 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Sat, 14 Dec 2024 00:10:32 +0100 Subject: [PATCH 37/41] Rename folder --- backend/ipc.js | 2 +- backend/menu.js | 2 +- backend/{bridge => serial}/serial-bridge.js | 0 backend/{ => serial}/serial.js | 0 preload.js | 2 +- 5 files changed, 3 insertions(+), 3 deletions(-) rename backend/{bridge => serial}/serial-bridge.js (100%) rename backend/{ => serial}/serial.js (100%) diff --git a/backend/ipc.js b/backend/ipc.js index 67e20db..ab06335 100644 --- a/backend/ipc.js +++ b/backend/ipc.js @@ -1,6 +1,6 @@ const fs = require('fs') const registerMenu = require('./menu.js') -const serial = require('./serial.js').sharedInstance +const serial = require('./serial/serial.js').sharedInstance const { openFolderDialog, diff --git a/backend/menu.js b/backend/menu.js index 932d704..6db8023 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -1,6 +1,6 @@ const { app, Menu } = require('electron') const path = require('path') -const serial = require('./serial.js').sharedInstance +const serial = require('./serial/serial.js').sharedInstance const openAboutWindow = require('about-window').default const shortcuts = require('./shortcuts.js') const { type } = require('os') diff --git a/backend/bridge/serial-bridge.js b/backend/serial/serial-bridge.js similarity index 100% rename from backend/bridge/serial-bridge.js rename to backend/serial/serial-bridge.js diff --git a/backend/serial.js b/backend/serial/serial.js similarity index 100% rename from backend/serial.js rename to backend/serial/serial.js diff --git a/preload.js b/preload.js index c13715d..f67d43c 100644 --- a/preload.js +++ b/preload.js @@ -3,7 +3,7 @@ const { contextBridge, ipcRenderer } = require('electron') const path = require('path') const shortcuts = require('./backend/shortcuts.js').global const { emit, platform } = require('process') -const SerialBridge = require('./backend/bridge/serial-bridge.js') +const SerialBridge = require('./backend/serial/serial-bridge.js') const Disk = { openFolder: async () => { From 612d0b9306204a27ebc1fc00593a85aad99148ad Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Sat, 14 Dec 2024 10:51:44 +0100 Subject: [PATCH 38/41] Remove duplicated event handler --- ui/arduino/store.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index b8b8c23..2b00817 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -201,8 +201,6 @@ async function store(state, emitter) { }) emitter.on('disconnect', async () => { await serialBridge.disconnect() - // Update the UI after closing the connection - emitter.emit('disconnected') }) emitter.on('connection-timeout', async () => { state.isConnected = false From 4248c1eeb1c141d52e30f8f3999a03c24ddcfd6f Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Mon, 16 Dec 2024 09:46:02 +0100 Subject: [PATCH 39/41] Remove event handlers on connection close --- backend/serial/serial.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/serial/serial.js b/backend/serial/serial.js index 1e13e6c..e702b17 100644 --- a/backend/serial/serial.js +++ b/backend/serial/serial.js @@ -56,6 +56,8 @@ class Serial { }) this.board.serial.on('close', () => { + this.board.serial.removeAllListeners("data") + this.board.serial.removeAllListeners("close") this.win.webContents.send('serial-on-connection-closed') }) } From d7422b10b5f626eb2111b0fbbb31c0aceb8a47b0 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Mon, 16 Dec 2024 10:31:48 +0100 Subject: [PATCH 40/41] Replace existing event listeners in serial bridge --- backend/serial/serial-bridge.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/backend/serial/serial-bridge.js b/backend/serial/serial-bridge.js index 54f7221..715d21a 100644 --- a/backend/serial/serial-bridge.js +++ b/backend/serial/serial-bridge.js @@ -32,9 +32,13 @@ const SerialBridge = { return ipcRenderer.invoke('serial', 'eval', d) }, onData: (callback) => { - ipcRenderer.on('serial-on-data', (event, data) => { - callback(data) - }) + // Remove all previous listeners + if (ipcRenderer.listeners("serial-on-data").length > 0) { + ipcRenderer.removeAllListeners("serial-on-data") + } + ipcRenderer.on('serial-on-data', (event, data) => { + callback(data) + }) }, listFiles: async (folder) => { return await ipcRenderer.invoke('serial', 'listFiles', folder) @@ -62,9 +66,13 @@ const SerialBridge = { return await ipcRenderer.invoke('serial', 'renameFile', oldName, newName) }, onConnectionClosed: async (callback) => { - ipcRenderer.on('serial-on-connection-closed', (event) => { - callback() - }) + // Remove all previous listeners + if (ipcRenderer.listeners("serial-on-connection-closed").length > 0) { + ipcRenderer.removeAllListeners("serial-on-connection-closed") + } + ipcRenderer.on('serial-on-connection-closed', (event) => { + callback() + }) }, createFolder: async (folder) => { return await ipcRenderer.invoke('serial', 'createFolder', folder) From b5b52190f3d6e0f2362f8941230be18c9de90201 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Mon, 16 Dec 2024 10:31:57 +0100 Subject: [PATCH 41/41] Add code documentation --- ui/arduino/store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 2b00817..09a373e 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -175,7 +175,7 @@ async function store(state, emitter) { serialBridge.eval(data) term.scrollToBottom() }) - serialBridge.eval('\x02') + serialBridge.eval('\x02') // Send Ctrl+B to enter normal repl mode } serialBridge.onData((data) => { term.write(data) 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