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)