diff --git a/backend/ipc.js b/backend/ipc.js index 8bace22..ab06335 100644 --- a/backend/ipc.js +++ b/backend/ipc.js @@ -1,4 +1,7 @@ const fs = require('fs') +const registerMenu = require('./menu.js') +const serial = require('./serial/serial.js').sharedInstance + const { openFolderDialog, listFolder, @@ -7,6 +10,8 @@ const { } = require('./helpers.js') module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) { + serial.win = win // Required to send callback messages to renderer + ipcMain.handle('open-folder', async (event) => { console.log('ipcMain', 'open-folder') const folder = await openFolderDialog(win) @@ -129,9 +134,18 @@ module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) { return response != opt.cancelId }) + ipcMain.handle('update-menu-state', (event, state) => { + registerMenu(win, state) + }) + win.on('close', (event) => { console.log('BrowserWindow', 'close') event.preventDefault() win.webContents.send('check-before-close') }) + + ipcMain.handle('serial', (event, command, ...args) => { + console.debug('Handling IPC serial command:', command, ...args) + return serial[command](...args) + }) } diff --git a/backend/menu.js b/backend/menu.js index 6b62cdf..6db8023 100644 --- a/backend/menu.js +++ b/backend/menu.js @@ -1,8 +1,11 @@ const { app, Menu } = require('electron') const path = require('path') +const serial = require('./serial/serial.js').sharedInstance const openAboutWindow = require('about-window').default +const shortcuts = require('./shortcuts.js') +const { type } = require('os') -module.exports = function registerMenu(win) { +module.exports = function registerMenu(win, state = {}) { const isMac = process.platform === 'darwin' const template = [ ...(isMac ? [{ @@ -10,9 +13,8 @@ module.exports = function registerMenu(win) { submenu: [ { role: 'about'}, { type: 'separator' }, - { role: 'services' }, { type: 'separator' }, - { role: 'hide' }, + { role: 'hide', accelerator: 'CmdOrCtrl+Shift+H' }, { role: 'hideOthers' }, { role: 'unhide' }, { type: 'separator' }, @@ -35,7 +37,6 @@ module.exports = function registerMenu(win) { { role: 'copy' }, { role: 'paste' }, ...(isMac ? [ - { role: 'pasteAndMatchStyle' }, { role: 'selectAll' }, { type: 'separator' }, { @@ -51,11 +52,66 @@ module.exports = function registerMenu(win) { ]) ] }, + { + label: 'Board', + submenu: [ + { + label: 'Connect', + accelerator: shortcuts.menu.CONNECT, + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.CONNECT) + }, + { + label: 'Disconnect', + accelerator: shortcuts.menu.DISCONNECT, + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.DISCONNECT) + }, + { type: 'separator' }, + { + label: 'Run', + accelerator: shortcuts.menu.RUN, + enabled: state.isConnected && state.view === 'editor', + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.RUN) + }, + { + label: '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', (isMac ? shortcuts.global.RUN_SELECTION : shortcuts.global.RUN_SELECTION_WL)) + }, + { + label: 'Stop', + accelerator: shortcuts.menu.STOP, + enabled: state.isConnected && state.view === 'editor', + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.STOP) + }, + { + label: 'Reset', + accelerator: shortcuts.menu.RESET, + enabled: state.isConnected && state.view === 'editor', + click: () => win.webContents.send('shortcut-cmd', shortcuts.global.RESET) + }, + { type: 'separator' } + ] + }, { label: 'View', submenu: [ - { role: 'reload' }, - { role: 'toggleDevTools' }, + { + 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) + }, + { + 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' }, @@ -67,6 +123,20 @@ module.exports = function registerMenu(win) { { label: 'Window', submenu: [ + { + label: 'Reload', + accelerator: '', + click: async () => { + try { + await serial.disconnect() + win.reload() + } catch(e) { + console.error('Reload from menu failed:', e) + } + } + }, + { role: 'toggleDevTools'}, + { type: 'separator' }, { role: 'minimize' }, { role: 'zoom' }, ...(isMac ? [ @@ -75,7 +145,7 @@ module.exports = function registerMenu(win) { { type: 'separator' }, { role: 'window' } ] : [ - { role: 'close' } + ]) ] }, @@ -102,7 +172,6 @@ module.exports = function registerMenu(win) { 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/backend/serial/serial-bridge.js b/backend/serial/serial-bridge.js new file mode 100644 index 0000000..715d21a --- /dev/null +++ b/backend/serial/serial-bridge.js @@ -0,0 +1,97 @@ +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) => { + // 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) + }, + 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) => { + // 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) + }, + 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/serial/serial.js b/backend/serial/serial.js new file mode 100644 index 0000000..e702b17 --- /dev/null +++ b/backend/serial/serial.js @@ -0,0 +1,117 @@ +const MicroPython = require('micropython.js') + +class Serial { + constructor(win = null) { + 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.board.serial.removeAllListeners("data") + this.board.serial.removeAllListeners("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}") + print(0) +except OSError: + print(1) +`) + return output[2] === '0' + } +} + +const sharedInstance = new Serial() + +module.exports = {sharedInstance, Serial} \ No newline at end of file diff --git a/backend/shortcuts.js b/backend/shortcuts.js new file mode 100644 index 0000000..e6b7159 --- /dev/null +++ b/backend/shortcuts.js @@ -0,0 +1,29 @@ +module.exports = { + global: { + CONNECT: 'CommandOrControl+Shift+C', + DISCONNECT: 'CommandOrControl+Shift+D', + 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', + EDITOR_VIEW: 'CommandOrControl+Alt+1', + FILES_VIEW: 'CommandOrControl+Alt+2', + ESC: 'Escape' + }, + menu: { + CONNECT: 'CmdOrCtrl+Shift+C', + DISCONNECT: 'CmdOrCtrl+Shift+D', + 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', + EDITOR_VIEW: 'CmdOrCtrl+Alt+1', + FILES_VIEW: 'CmdOrCtrl+Alt+2' + } +} diff --git a/index.js b/index.js index 57eba4c..a6fcc04 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ -const { app, BrowserWindow, ipcMain, dialog } = require('electron') +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') @@ -49,12 +50,41 @@ 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() }) } -app.on('ready', createWindow) +function shortcutAction(key) { + win.webContents.send('shortcut-cmd', key); +} + +// Shortcuts +function registerShortcuts() { + Object.entries(shortcuts).forEach(([command, shortcut]) => { + globalShortcut.register(shortcut, () => { + shortcutAction(shortcut) + }); + }) +} + +app.on('ready', () => { + createWindow() + registerShortcuts() + + win.on('focus', () => { + registerShortcuts() + }) + win.on('blur', () => { + globalShortcut.unregisterAll() + }) + +}) \ No newline at end of file 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" diff --git a/preload.js b/preload.js index ddcb8aa..f67d43c 100644 --- a/preload.js +++ b/preload.js @@ -1,106 +1,9 @@ console.log('preload') const { contextBridge, ipcRenderer } = require('electron') const path = require('path') - -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) - }, - 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 shortcuts = require('./backend/shortcuts.js').global +const { emit, platform } = require('process') +const SerialBridge = require('./backend/serial/serial-bridge.js') const Disk = { openFolder: async () => { @@ -155,13 +58,28 @@ const Window = { setWindowSize: (minWidth, minHeight) => { ipcRenderer.invoke('set-window-size', minWidth, minHeight) }, + onKeyboardShortcut: (callback, key) => { + ipcRenderer.on('shortcut-cmd', (event, k) => { + 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', + updateMenuState: (state) => { + return ipcRenderer.invoke('update-menu-state', state) + }, + getShortcuts: () => shortcuts +} -contextBridge.exposeInMainWorld('BridgeSerial', Serial) +contextBridge.exposeInMainWorld('BridgeSerial', SerialBridge) contextBridge.exposeInMainWorld('BridgeDisk', Disk) -contextBridge.exposeInMainWorld('BridgeWindow', Window) +contextBridge.exposeInMainWorld('BridgeWindow', Window) \ No newline at end of file diff --git a/ui/arduino/main.js b/ui/arduino/main.js index bf693df..ce52be1 100644 --- a/ui/arduino/main.js +++ b/ui/arduino/main.js @@ -46,11 +46,9 @@ 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') } }) - }) 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/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/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/media/folder.svg b/ui/arduino/media/folder.svg index 87e8555..68843f7 100644 --- a/ui/arduino/media/folder.svg +++ b/ui/arduino/media/folder.svg @@ -1,3 +1,15 @@ - - + + + + + + + + + + + + + + diff --git a/ui/arduino/store.js b/ui/arduino/store.js index c748905..09a373e 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -1,8 +1,10 @@ const log = console.log -const serial = window.BridgeSerial +const serialBridge = 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!') @@ -24,6 +26,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() @@ -80,6 +83,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() @@ -97,12 +108,14 @@ async function store(state, emitter) { emitter.emit('refresh-files') } emitter.emit('render') + updateMenu() }) // 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 serialBridge.disconnect() state.availablePorts = await getAvailablePorts() state.isConnectionDialogOpen = true emitter.emit('render') @@ -136,17 +149,19 @@ 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 state.isConnected = true + updateMenu() if (state.view === 'editor' && state.panelHeight <= PANEL_CLOSED) { state.panelHeight = state.savedPanelHeight } @@ -157,29 +172,35 @@ 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') // Send Ctrl+B to enter normal repl mode } - serial.onData((data) => { + serialBridge.onData((data) => { term.write(data) term.scrollToBottom() }) - serial.onDisconnect(() => emitter.emit('disconnect')) + + // Update the UI when the conncetion is closed + // This may happen when unplugging the board + serialBridge.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 = [] state.boardNavigationPath = '/' emitter.emit('refresh-files') emitter.emit('render') + updateMenu() + }) + emitter.on('disconnect', async () => { + await serialBridge.disconnect() }) emitter.on('connection-timeout', async () => { state.isConnected = false @@ -190,15 +211,31 @@ async function store(state, emitter) { }) // CODE EXECUTION - emitter.on('run', async () => { + emitter.on('run', async (onlySelected = false) => { 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 && 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, + // 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 { - await serial.getPrompt() - await serial.run(code) + await serialBridge.getPrompt() + await serialBridge.run(code) } catch(e) { log('error', e) } @@ -210,7 +247,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') @@ -219,7 +256,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') }) @@ -295,9 +332,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 @@ -318,9 +355,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 @@ -353,9 +390,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 @@ -431,7 +468,7 @@ async function store(state, emitter) { if (state.isConnected) { try { state.boardFiles = await getBoardFiles( - serial.getFullPath( + serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, '' @@ -509,8 +546,8 @@ async function store(state, emitter) { } // TODO: Remove existing file } - await serial.saveFileContent( - serial.getFullPath( + await serialBridge.saveFileContent( + serialBridge.getFullPath( '/', state.boardNavigationPath, value @@ -580,15 +617,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 @@ -670,7 +707,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 @@ -687,8 +724,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 @@ -756,15 +793,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 @@ -816,13 +853,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 @@ -893,8 +930,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 @@ -922,8 +959,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 @@ -955,9 +992,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 @@ -984,13 +1021,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 @@ -1018,9 +1055,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 @@ -1110,8 +1147,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 @@ -1167,6 +1204,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) => { @@ -1190,7 +1228,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, '' @@ -1217,7 +1255,7 @@ async function store(state, emitter) { state.diskNavigationPath, file.fileName ) - const destPath = serial.getFullPath( + const destPath = serialBridge.getFullPath( state.boardNavigationRoot, state.boardNavigationPath, file.fileName @@ -1231,7 +1269,7 @@ async function store(state, emitter) { } ) } else { - await serial.uploadFile( + await serialBridge.uploadFile( srcPath, destPath, (progress) => { state.transferringProgress = `${file.fileName}: ${progress}` @@ -1277,7 +1315,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 @@ -1296,7 +1334,7 @@ async function store(state, emitter) { } ) } else { - await serial.downloadFile( + await serialBridge.downloadFile( srcPath, destPath, (e) => { state.transferringProgress = e @@ -1315,7 +1353,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 ) @@ -1324,7 +1362,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, '..' ) @@ -1360,6 +1398,78 @@ async function store(state, emitter) { await win.confirmClose() }) + // win.shortcutCmdR(() => { + // // Only run if we can execute + + // }) + + win.onKeyboardShortcut((key) => { + if (key === shortcuts.CONNECT) { + emitter.emit('open-connection-dialog') + } + if (key === shortcuts.DISCONNECT) { + emitter.emit('disconnect') + } + if (key === shortcuts.RESET) { + if (state.view != 'editor') return + emitter.emit('reset') + } + if (key === shortcuts.CLEAR_TERMINAL) { + 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 === shortcuts.RUN) { + if (state.view != 'editor') return + runCode() + } + if (key === shortcuts.RUN_SELECTION || key === shortcuts.RUN_SELECTION_WL) { + if (state.view != 'editor') return + runCodeSelection() + } + if (key === shortcuts.STOP) { + if (state.view != 'editor') return + stopCode() + } + if (key === shortcuts.SAVE) { + 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') + } + } + + }) + + function runCode() { + if (canExecute({ view: state.view, isConnected: state.isConnected })) { + 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') + } + } function createFile(args) { const { source, @@ -1437,12 +1547,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' @@ -1460,9 +1570,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) ) } @@ -1492,6 +1602,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 @@ -1526,29 +1637,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) } @@ -1560,8 +1671,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 @@ -1581,9 +1692,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, '') ) } } @@ -1604,4 +1715,5 @@ async function getHelperFullPath() { '' ) } + } 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/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)} diff --git a/ui/arduino/views/components/repl-panel.js b/ui/arduino/views/components/repl-panel.js index ac1760c..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', + tooltip: `Clean (${state.platform === 'darwin' ? 'Cmd' : 'Ctrl'}+L)`, onClick: () => emit('clear-terminal') }) ] 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..70982b0 100644 --- a/ui/arduino/views/components/toolbar.js +++ b/ui/arduino/views/components/toolbar.js @@ -9,13 +9,14 @@ 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', - onClick: () => emit('open-connection-dialog'), + tooltip: state.isConnected ? `Disconnect (${metaKeyString}+Shift+D)` : `Connect (${metaKeyString}+Shift+C)`, + onClick: () => state.isConnected ? emit('disconnect') : emit('open-connection-dialog'), active: state.isConnected })} @@ -23,19 +24,25 @@ function Toolbar(state, emit) { ${Button({ icon: 'run.svg', - tooltip: 'Run', + tooltip: `Run (${metaKeyString}+R)`, disabled: !_canExecute, - onClick: () => emit('run') + onClick: (e) => { + if (e.altKey) { + emit('run', true) + }else{ + 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 +51,7 @@ function Toolbar(state, emit) { ${Button({ icon: 'save.svg', - tooltip: 'Save', + tooltip: `Save (${metaKeyString}+S)`, disabled: !_canSave, onClick: () => emit('save') })} @@ -52,14 +59,14 @@ function Toolbar(state, emit) {
${Button({ - icon: 'console.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') })} 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}
pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy