Repository: JanLunge/pog Branch: main Commit: 9a7e910e8c26 Files: 98 Total size: 360.2 KB Directory structure: gitextract_3v1s9heo/ ├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .github/ │ └── workflows/ │ └── electron_build.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── LICENSE ├── README.md ├── build/ │ ├── entitlements.mac.plist │ ├── notarize.js │ └── resign.js ├── dev-app-update.yml ├── electron-builder.yml ├── electron.vite.config.ts ├── package.json ├── postcss.config.js ├── prettier.config.js ├── src/ │ ├── main/ │ │ ├── index.ts │ │ ├── keyboardDetector.ts │ │ ├── kmkUpdater.ts │ │ ├── pythontemplates/ │ │ │ ├── boot.ts │ │ │ ├── code.ts │ │ │ ├── coordmaphelper.ts │ │ │ ├── customkeys.ts │ │ │ ├── detection.ts │ │ │ ├── kb.ts │ │ │ ├── keymap.ts │ │ │ ├── pog.ts │ │ │ └── pog_serial.ts │ │ ├── saveConfig.ts │ │ ├── selectKeyboard.ts │ │ └── store.ts │ ├── preload/ │ │ ├── index.d.ts │ │ └── index.ts │ └── renderer/ │ ├── index.html │ └── src/ │ ├── App.vue │ ├── assets/ │ │ ├── css/ │ │ │ └── styles.less │ │ └── microcontrollers/ │ │ └── microcontrollers.json │ ├── components/ │ │ ├── AutomaticSetup.vue │ │ ├── BaseModal.vue │ │ ├── CircuitPythonSetup.vue │ │ ├── Community.vue │ │ ├── CoordMap.vue │ │ ├── EncoderLayer.vue │ │ ├── EncoderSetup.vue │ │ ├── HsvColorPicker.vue │ │ ├── KeyCap.vue │ │ ├── KeyLayoutInfo.vue │ │ ├── KeyPicker.vue │ │ ├── KeyboardLayout.vue │ │ ├── KeyboardName.vue │ │ ├── KeymapEditor.vue │ │ ├── KeymapLayer.vue │ │ ├── KmkInstaller.vue │ │ ├── LayoutEditor.vue │ │ ├── LoadingOverlay.vue │ │ ├── MacroModal.vue │ │ ├── MatrixSetup.vue │ │ ├── PinSetup.vue │ │ ├── RawKeymapEditor.vue │ │ ├── RgbSetup.vue │ │ ├── SetupMethodSelector.vue │ │ ├── VariantOption.vue │ │ ├── VariantSwitcher.vue │ │ ├── debug.vue │ │ ├── installPogFirmware.vue │ │ ├── picker-layouts/ │ │ │ ├── Colemak.vue │ │ │ ├── ColemakDH.vue │ │ │ ├── Dvorak.vue │ │ │ └── Qwerty.vue │ │ ├── setup/ │ │ │ └── Wizard.vue │ │ └── ui/ │ │ └── InputLabel.vue │ ├── composables/ │ │ └── useLoadingOverlay.ts │ ├── env.d.ts │ ├── helpers/ │ │ ├── colors.ts │ │ ├── index.ts │ │ ├── saveConfigurationWrapper.ts │ │ └── types.d.ts │ ├── main.ts │ ├── router/ │ │ └── index.ts │ ├── screens/ │ │ ├── AddKeyboard.vue │ │ ├── KeyboardConfigurator.vue │ │ ├── KeyboardSelector.vue │ │ ├── KeyboardSetup.vue │ │ ├── LaunchScreen.vue │ │ └── SetupWizard.vue │ ├── store/ │ │ ├── index.ts │ │ └── serial.ts │ └── style/ │ ├── index.css │ └── multiselect.css ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── tsconfig.web.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: .eslintignore ================================================ node_modules dist out .gitignore ================================================ FILE: .eslintrc.cjs ================================================ /* eslint-env node */ require('@rushstack/eslint-patch/modern-module-resolution') module.exports = { root: true, env: { browser: true, commonjs: true, es6: true, node: true, 'vue/setup-compiler-macros': true }, extends: [ 'plugin:vue/vue3-recommended', 'eslint:recommended', '@vue/eslint-config-typescript/recommended', '@vue/eslint-config-prettier' ], rules: { '@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow-with-description' }], '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-empty-function': ['error', { allow: ['arrowFunctions'] }], // '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-var-requires': 'off', 'vue/require-default-prop': 'off', 'vue/multi-word-component-names': 'off', '@typescript-eslint/no-explicit-any': 'off' }, overrides: [ { files: ['*.js'], rules: { '@typescript-eslint/explicit-function-return-type': 'off' } } ] } ================================================ FILE: .github/workflows/electron_build.yml ================================================ name: Build Electron App env: GH_TOKEN: ${{ secrets.GH_TOKEN }} on: push: tags: - 'v*' jobs: build-linux: runs-on: ubuntu-latest steps: - name: Create Draft Release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} with: tag_name: ${{ github.ref }} release_name: Release ${{ github.ref }} draft: true prerelease: false - name: Check out repository uses: actions/checkout@v2 - name: Set up Node.js uses: actions/setup-node@v2 with: node-version: '18' - name: Install dependencies run: npm install - name: Build Electron App for Windows run: npm run build && npx electron-builder --linux --config - name: Upload Linux Artifact uses: actions/upload-artifact@v4 with: name: linux-app path: dist/*.AppImage compression-level: 0 build-windows: runs-on: windows-latest steps: - name: Check out repository uses: actions/checkout@v2 - name: Set up Node.js uses: actions/setup-node@v2 with: node-version: '18' - name: Install dependencies run: npm install - name: Build Electron App for Windows run: npm run build && npx electron-builder --win --config - name: Upload Windows Artifact uses: actions/upload-artifact@v4 with: name: windows-app path: dist/*.exe compression-level: 0 build-mac: runs-on: macos-latest steps: - name: Check out repository uses: actions/checkout@v2 - name: Set up Node.js uses: actions/setup-node@v2 with: node-version: '18' - name: Import Apple Developer Certificate env: MAC_CERTIFICATE: ${{ secrets.MAC_CERTIFICATE }} CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }} run: | echo "$MAC_CERTIFICATE" | base64 --decode > certificate.p12 security create-keychain -p "" build.keychain security default-keychain -s build.keychain security unlock-keychain -p "" build.keychain security import certificate.p12 -k build.keychain -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain security find-identity -v - name: Install dependencies run: npm install - name: Resign modules run: npm run resign - name: Build Electron App for Mac run: npm run build && npx electron-builder --mac --config env: CSC_NAME: "Jan Vincent Lunge (BLH4PG2L7J)" CSC_KEYCHAIN: build.keychain - name: Upload Mac Artifact uses: actions/upload-artifact@v4 with: name: mac-app path: dist/*.dmg compression-level: 0 ================================================ FILE: .gitignore ================================================ node_modules dist out *.log* .idea .DS_Store .pog.code-workspace ================================================ FILE: .npmrc ================================================ ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ ================================================ FILE: .prettierignore ================================================ out dist pnpm-lock.yaml LICENSE.md tsconfig.json tsconfig.*.json ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": ["dbaeumer.vscode-eslint"] } ================================================ FILE: .vscode/launch.json ================================================ { "version": "0.2.0", "configurations": [ { "name": "Debug Main Process", "type": "node", "request": "launch", "cwd": "${workspaceRoot}", "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite", "windows": { "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" }, "runtimeArgs": ["--sourcemap"] } ] } ================================================ FILE: .vscode/settings.json ================================================ { "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" } } ================================================ FILE: LICENSE ================================================ MIT License (MIT) Copyright 2023 Jan Lunge Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ![logo](demo/pog-header.png?raw=true)

POG

KMK GUI, Layout Editor, Keymap Editor, Flashing Utility

Stars Badge Forks Badge

![preview](demo/pog-screenshot.png?raw=true) # Documentation the documentation is available [here](https://github.com/JanLunge/pog-docs). Feel free to contribute # Installation download the pre-built binaries for Windows, Mac and Linux are available in the [releases](https://github.com/JanLunge/pog/releases) # Development Setup ## dependencies * node 16 * yarn install everything with `yarn` then just run it with dev to start `yarn dev` to release a new version use `npm version minor` or `major` or `patch` then just push and github actions will do the rest # Tasks ## bugs - [ ] maximum call stack error when closing the app ## urgent - [x] check if a keyboard is connected (usb drive) in the keyboard selector preview - [x] show serial output in the gui - [ ] automatically get the correct serial device (by serial number) - [ ] guides etc. for setup + split workflow | help menu + videos - [ ] save wiring info in qr code or so - [ ] share pog.json files - [ ] check if the controller you use even has the pin you specified (controller lookup and serial to get pins ) - [ ] generate layout based on matrix + clear layout button / delete multiple - [ ] features case-insensitive and via gui or pog json ## features wishlist - [ ] bluetooth workflow - [ ] language switcher for german and other layouts changing the labels on the keymap - [ ] modtap/tapdance/macros/sequences - [ ] encoder support direct pin click - [ ] way to handle differences between pog.json to kmk code - [ ] wiring preview ### Build ```bash # For windows $ npm run build:win # For macOS $ npm run build:mac # For Linux $ npm run build:linux ``` ================================================ FILE: build/entitlements.mac.plist ================================================ com.apple.security.cs.allow-jit com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.cs.allow-dyld-environment-variables ================================================ FILE: build/notarize.js ================================================ const { notarize } = require('@electron/notarize') const path = require('path') const fs = require('fs') const { execSync } = require('child_process') module.exports = async (context) => { if (process.platform !== 'darwin') return console.log('aftersign hook triggered, start to notarize app.') if (!process.env.CI) { console.log(`skipping notarizing, not in CI.`) return } if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) { console.warn('skipping notarizing, APPLE_ID and APPLE_ID_PASS env variables must be set.') return } const appId = 'com.electron.app' const { appOutDir } = context const appName = context.packager.appInfo.productFilename try { await notarize({ appBundleId: appId, appPath: `${appOutDir}/${appName}.app`, appleId: process.env.APPLE_ID, appleIdPassword: process.env.APPLEIDPASS }) } catch (error) { console.error(error) } console.log(`done notarizing ${appId}.`) } ================================================ FILE: build/resign.js ================================================ const fs = require('fs') const path = require('path') const execSync = require('child_process').execSync function findNativeModules(dir, fileList = []) { const files = fs.readdirSync(dir) files.forEach((file) => { const filePath = path.join(dir, file) const fileStat = fs.lstatSync(filePath) if (fileStat.isDirectory()) { findNativeModules(filePath, fileList) } else if (filePath.endsWith('.node')) { fileList.push(filePath) } }) return fileList } const resign = () => { if (process.platform !== 'darwin') return const nativeModules = findNativeModules('./node_modules') console.log(nativeModules) nativeModules.forEach((module) => { // const fullPath = path.join(appOutDir, "pog.app", module) execSync(`codesign --deep --force --verbose --sign "BLH4PG2L7J" "${module}"`) }) console.log('signed all node modules') } resign() ================================================ FILE: dev-app-update.yml ================================================ provider: generic url: https://pog.heaper.de/auto-updates updaterCacheDirName: vue-vite-electron-updater ================================================ FILE: electron-builder.yml ================================================ appId: de.heaper.pog productName: pog directories: buildResources: build files: - '!**/.vscode/*' - '!src/*' - '!electron.vite.config.{js,ts,mjs,cjs}' - '!{.eslintignore,.eslintrc.cjs,.prettierignore,yulyuly.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' asarUnpack: - resources/* afterSign: build/notarize.js win: executableName: pog target: - target: portable arch: - x64 - target: nsis arch: - x64 nsis: artifactName: ${name}-${version}-${arch}-setup.${ext} shortcutName: ${productName} uninstallDisplayName: ${productName} createDesktopShortcut: always mac: entitlementsInherit: build/entitlements.mac.plist extendInfo: - NSCameraUsageDescription: Application requests access to the device's camera. - NSMicrophoneUsageDescription: Application requests access to the device's microphone. - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. # publish: # - generic target: - target: dmg arch: - x64 - arm64 - target: zip arch: - x64 - arm64 dmg: artifactName: ${name}-${version}-${arch}.${ext} linux: target: - target: AppImage arch: - x64 # - snap # - deb maintainer: Jan Lunge category: Utility appImage: artifactName: ${name}-${version}-${arch}.${ext} npmRebuild: false publish: provider: github repo: pog owner: janlunge releaseType: draft # url: https://pog.heaper.de/auto-updates ================================================ FILE: electron.vite.config.ts ================================================ import { resolve } from 'path' import { defineConfig, externalizeDepsPlugin } from 'electron-vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ main: { plugins: [externalizeDepsPlugin()] }, preload: { plugins: [externalizeDepsPlugin()] }, renderer: { resolve: { alias: { '@renderer': resolve('src/renderer/src') } }, plugins: [vue()] } }) ================================================ FILE: package.json ================================================ { "name": "pog", "version": "2.2.0", "license": "MIT", "description": "A KMK firmware configurator", "main": "./out/main/index.js", "author": "Jan Lunge", "homepage": "https://github.com/wlard/pog", "repository": { "type": "git", "url": "https://github.com/janlunge/pog.git" }, "scripts": { "format": "prettier --write .", "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix", "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", "typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false", "typecheck": "npm run typecheck:node && npm run typecheck:web", "start": "electron-vite preview", "dev": "electron-vite dev", "build": "npm run typecheck && electron-vite build", "resign": "node build/resign.js", "postinstall": "electron-builder install-app-deps", "build:win": "npm run build && electron-builder --win --config", "build:mac": "npm run build && electron-builder --mac --config", "build:linux": "npm run build && electron-builder --linux --config", "build:all": "npm run build && electron-builder --win --mac --linux --config", "devtools": "./node_modules/.bin/vue-devtools", "postversion": "git push --tags" }, "dependencies": { "@electron-toolkit/preload": "^1.0.3", "@electron-toolkit/utils": "^1.0.2", "@floating-ui/vue": "^1.1.9", "@mdi/font": "^7.1.96", "@viselect/vue": "^3.2.5", "@vueuse/core": "^9.12.0", "@wagmi/core": "^0.9.7", "@web3modal/ethereum": "^2.1.2", "@web3modal/html": "^2.1.2", "@wlard/vue-class-store": "^3.0.0", "@wlard/vue3-popper": "^1.3.1", "chroma-js": "^2.4.2", "daisyui": "^3.9.2", "dayjs": "^1.11.7", "decompress": "^4.2.1", "drivelist": "^12.0.2", "electron-updater": "^5.3.0", "ethers": "^5", "lighten-darken-color": "^1.0.0", "mini-svg-data-uri": "^1.4.4", "request": "^2.88.2", "sass": "^1.58.0", "serialport": "^10.5.0", "tailwindcss": "^3.3.3", "ulid": "^2.3.0", "vue-multiselect": "^3.0.0-beta.1", "vue-router": "^4.1.6", "vuedraggable": "^4.1.0" }, "devDependencies": { "@electron-toolkit/tsconfig": "^1.0.1", "@electron/notarize": "^1.2.3", "@rushstack/eslint-patch": "^1.2.0", "@types/node": "16.18.11", "@vitejs/plugin-vue": "^4.0.0", "@vue/devtools": "^6.5.0", "@vue/eslint-config-prettier": "^7.0.0", "@vue/eslint-config-typescript": "^11.0.2", "autoprefixer": "^10.4.13", "electron": "^21.3.3", "electron-builder": "^23.6.0", "electron-vite": "^1.0.17", "eslint": "^8.31.0", "eslint-plugin-vue": "^9.8.0", "less": "^4.1.3", "postcss": "^8.4.21", "prettier": "^2.8.2", "prettier-plugin-tailwindcss": "^0.2.3", "typescript": "5.5.4", "vite": "^4.0.4", "vue": "^3.2.45", "vue-tsc": "^1.0.22" } } ================================================ FILE: postcss.config.js ================================================ module.exports = { plugins: { tailwindcss: {}, autoprefixer: {} } } ================================================ FILE: prettier.config.js ================================================ // prettier.config.js module.exports = { plugins: [require('prettier-plugin-tailwindcss')], singleQuote: true, semi: false, printWidth: 100, trailingComma: 'none', } ================================================ FILE: src/main/index.ts ================================================ import { app, shell, BrowserWindow, ipcMain, Menu } from 'electron' import { join } from 'path' import { electronApp, optimizer, is } from '@electron-toolkit/utils' import icon from '../../resources/icon.png?asset' import drivelist from 'drivelist' import { flashFirmware } from './kmkUpdater' // import './saveConfig' import { checkForUSBKeyboards, handleSelectDrive, selectKeyboard } from './selectKeyboard' import { updateFirmware } from './kmkUpdater' import './keyboardDetector' // Import keyboard detector import serialPort from 'serialport' import { ReadlineParser } from '@serialport/parser-readline' import fs from 'fs' import { currentKeyboard } from './store' import { saveConfiguration } from './saveConfig' let mainWindow: BrowserWindow | null = null export { mainWindow } const isMac = process.platform === 'darwin' let triedToQuit = false const template: unknown = [ // { role: 'appMenu' } ...(isMac ? [ { label: app.name, submenu: [ { role: 'about' }, // { // label: 'Check for Updates...', // click: () => { // autoUpdater.checkForUpdates() // } // }, { type: 'separator' }, { role: 'services' }, { type: 'separator' }, { role: 'hide' }, { role: 'hideOthers' }, { role: 'unhide' }, { type: 'separator' }, { role: 'quit' } ] } ] : []), // { role: 'fileMenu' } { label: 'File', submenu: [isMac ? { role: 'close' } : { role: 'quit' }] }, // { role: 'editMenu' } { label: 'Edit', submenu: [ { role: 'undo' }, { role: 'redo' }, { type: 'separator' }, { role: 'cut' }, { role: 'copy' }, { role: 'paste' }, ...(isMac ? [ { role: 'pasteAndMatchStyle' }, { role: 'delete' }, { role: 'selectAll' }, { type: 'separator' }, { label: 'Speech', submenu: [{ role: 'startSpeaking' }, { role: 'stopSpeaking' }] } ] : [{ role: 'delete' }, { type: 'separator' }, { role: 'selectAll' }]) ] }, // { role: 'viewMenu' } { label: 'View', submenu: [ { role: 'reload' }, { role: 'forceReload' }, { role: 'toggleDevTools' }, { type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, { role: 'zoomOut' }, { type: 'separator' }, { role: 'togglefullscreen' } ] }, // { role: 'windowMenu' } { label: 'Window', submenu: [ { role: 'minimize' }, { role: 'zoom' }, ...(isMac ? [{ type: 'separator' }, { role: 'front' }, { type: 'separator' }, { role: 'window' }] : [{ role: 'close' }]) ] }, { role: 'help', submenu: [ { label: 'Learn More', click: async () => { const { shell } = require('electron') await shell.openExternal('https://electronjs.org') } } ] } ] const menu = Menu.buildFromTemplate(template as Electron.MenuItem[]) Menu.setApplicationMenu(menu) const createWindow = async () => { // Create the browser window. mainWindow = new BrowserWindow({ width: 900, height: 670, show: false, autoHideMenuBar: true, backgroundColor: '#000000', ...(process.platform === 'linux' ? { icon } : {}), webPreferences: { preload: join(__dirname, '../preload/index.js'), sandbox: false, nodeIntegration: true, contextIsolation: true, experimentalFeatures: true } }) mainWindow.on('ready-to-show', () => { mainWindow?.show() }) mainWindow.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url) return { action: 'deny' } }) // HMR for renderer base on electron-vite cli. // Load the remote URL for development or the local html file for production. if (is.dev && process.env['ELECTRON_RENDERER_URL']) { mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) } else { mainWindow.loadFile(join(__dirname, '../renderer/index.html')) } } // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.whenReady().then(() => { // Set app user model id for windows electronApp.setAppUserModelId('de.janlunge.pog') // Default open or close DevTools by F12 in development // and ignore CommandOrControl + R in production. // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils app.on('browser-window-created', (_, window) => { optimizer.watchWindowShortcuts(window) }) createWindow() app.on('activate', function () { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) createWindow() }) }) // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() } }) app.on('before-quit', (event) => { // Prevent the default behavior of this event if (debugPort !== undefined && !triedToQuit) { event.preventDefault() triedToQuit = true console.log('Preparing to quit...') debugPort.close(() => { console.log('Port closed') debugPort = undefined // Instead of app.quit(), directly exit the process process.exit(0) }) } else if (!triedToQuit) { // Now allow the app to continue quitting process.exit(0) } }) // In this file you can include the rest of your app"s specific main process // code. You can also put them in separate files and require them here. // select keyboard // update KMK // save keymap ipcMain.handle('selectDrive', () => handleSelectDrive()) ipcMain.handle('deselectKeyboard', () => deselectKeyboard()) ipcMain.handle('rescanKeyboards', () => scanForKeyboards()) ipcMain.handle('updateFirmware', () => updateFirmware()) ipcMain.on('saveConfiguration', (_event, data) => saveConfiguration(data)) ipcMain.handle('checkForUSBKeyboards', (_event, data) => checkForUSBKeyboards(data)) ipcMain.handle('selectKeyboard', (_event, data) => selectKeyboard(data)) ipcMain.handle('serialPorts', (_event, data) => checkSerialDevices()) ipcMain.on('serialSend', (_event, data) => sendSerial(data)) ipcMain.handle('serialConnect', (_event, data) => serialConnect(data)) ipcMain.handle('openExternal', (_event, data) => openExternal(data)) // autoUpdater.on('update-available', () => { // if (mainWindow) mainWindow.webContents.send('update_available') // }) // autoUpdater.on('update-downloaded', () => { // if (mainWindow) mainWindow.webContents.send('update_downloaded') // }) const baudRate = 9600 const startTime = new Date() let currentChunk = 0 let sendMode = '' export let pogconfigbuffer = '' export let keymapbuffer = '' let total_chunks = 0 const chunksize = 1200 const getBoardInfo = (port) => { return new Promise((res, rej) => { // connect to port and get the response just once const sport = new serialPort.SerialPort({ path: port.path, baudRate, autoOpen: true }, (e) => { // if the connection fails reject the promise if (e) return rej(e) }) const sparser = sport.pipe(new ReadlineParser({ delimiter: '\n' })) sparser.once('data', (data) => { sport.close() return res({ ...port, ...JSON.parse(data) }) }) // request the info sport.write('info_simple\n') }) } // timer helper function const timeout = (prom, time) => { let timer return Promise.race([prom, new Promise((_r, rej) => (timer = setTimeout(rej, time)))]).finally( () => clearTimeout(timer) ) } export const serialBoards: { value: any[] } = { value: [] } // TODO: resolve callbacks properly // https://stackoverflow.com/questions/69608234/get-promise-resolve-from-separate-callback const scanForKeyboards = async () => { console.log('checking for connected keyboards via serial') if (connectedKeyboardPort && connectedKeyboardPort.isOpen) connectedKeyboardPort.close() const ports = await serialPort.SerialPort.list() console.log('found the following raw ports:', ports) const circuitPythonPorts = ports.filter((port) => { // TODO: make sure the port is used for a pog keyboard // we dont want to send serial data to a REPL that is not a keyboard with pog firmware const manufacturer = port.manufacturer ? port.manufacturer.toLowerCase() : '' // if the manufactuer is pog or has pog as suffix or prefix with the - we assume its a pog keyboard return ( manufacturer.endsWith('-pog') || manufacturer.startsWith('pog-') || manufacturer === 'pog' ) }) const boards = (await Promise.allSettled( circuitPythonPorts.map(async (a) => await timeout(getBoardInfo(a), 2000)) )) as { status: 'fulfilled' | 'rejected' value: { name: string; id: string; path: string } }[] const filteredBoards: { name: string; id: string; path: string }[] = boards .filter((a) => a.value !== undefined) .map((a) => a.value) console.log('found the following boards:', filteredBoards) filteredBoards.map((a) => console.log(`${a.name} - ${a.id} | ${a.path}`)) mainWindow?.webContents.send('keyboardScan', { keyboards: filteredBoards }) serialBoards.value = filteredBoards return filteredBoards } let currentPackage = '' let addedChunks = 0 function crossSum(s: string) { // Compute the cross sum let total = 0 for (let i = 0; i < s.length; i++) { const c = s.charAt(i) total += c.charCodeAt(0) } return total } const sendConfigChunk = (port) => { port.write( JSON.stringify({ current_chunk: currentChunk, total_chunks, data: pogconfigbuffer.substring( chunksize * currentChunk, chunksize * currentChunk + chunksize ) }) + '\n' ) console.log('done sending next config chunk waiting for microcontroller') } const sendKeymapChunk = (port) => { port.write( JSON.stringify({ current_chunk: currentChunk, total_chunks, data: keymapbuffer.substring(chunksize * currentChunk, chunksize * currentChunk + chunksize) }) + '\n' ) console.log('done sending next keymap chunk waiting for microcontroller') } export let connectedKeyboardPort: any = null export const connectSerialKeyboard = async (keyboard) => { connectedKeyboardPort = new serialPort.SerialPort( { path: keyboard.path, baudRate, autoOpen: true }, (e) => {} ) const parser = connectedKeyboardPort.pipe(new ReadlineParser({ delimiter: '\n' })) // parser.once('data', (data) => { // sport.close() // res({ ...port, ...JSON.parse(data) }) // }) // port.write('info_simple\n'); parser.on('data', (data) => { try { const chunk = JSON.parse(data.toString()) if (chunk.type === 'pogconfig') { console.log('got chunk', chunk.current_chunk, 'of', chunk.total_chunks) const checksum = crossSum(chunk.data) console.log( 'checking cross sum', checksum, chunk.cross_sum, checksum === chunk.cross_sum ? 'valid' : 'invalid' ) // if(Math.random() > 0.8){ // console.error('fake invalid') // port.write('0\n') // return // } currentPackage += chunk.data addedChunks++ if (chunk.current_chunk === Math.ceil(chunk.total_chunks)) { const validated = Math.ceil(chunk.total_chunks) === addedChunks console.log('done', addedChunks, chunk.current_chunk, validated) addedChunks = 0 connectedKeyboardPort.write('y\n') const pogconfig = JSON.parse(currentPackage) // info was successfully queried push to frontend mainWindow?.webContents.send('serialKeyboardPogConfig', { pogconfig }) currentPackage = '' return } connectedKeyboardPort.write('1\n') return } else { console.log('keyboard info', chunk) } } catch (e) { // console.log('not a proper json command, moving to simple commands', e, data, data.toString()) } // pinging for next chunk if (data === '1') { if (sendMode === 'saveConfig') { total_chunks = Math.ceil(pogconfigbuffer.length / chunksize) console.log( 'got signal that last chunk came in fine, sending more if we have more', currentChunk, total_chunks, data ) if (currentChunk > total_chunks) { console.log('done sending') const dif = new Date().getTime() - startTime.getTime() console.log(`took ${dif / 1000}s`) } else { sendConfigChunk(connectedKeyboardPort) currentChunk += 1 } } else if (sendMode === 'saveKeymap') { total_chunks = Math.ceil(keymapbuffer.length / chunksize) console.log( 'last chunk came in fine, sending more if we have more', currentChunk, total_chunks, data ) if (currentChunk <= total_chunks) { sendKeymapChunk(connectedKeyboardPort) currentChunk += 1 } } } else if (data === 'y') { console.log('something else', data) // general reset sendMode = '' currentChunk = 0 } return }) } export const writePogConfViaSerial = (pogconfig) => { pogconfigbuffer = pogconfig currentChunk = 0 sendMode = 'saveConfig' if (!connectedKeyboardPort) { console.log('port not set') } else if (!connectedKeyboardPort.isConnected) { connectedKeyboardPort.open(() => { console.log('port open again') connectedKeyboardPort.write('save\n') }) } else { connectedKeyboardPort.write('save\n') } } export const writeKeymapViaSerial = (pogconfig) => { keymapbuffer = pogconfig currentChunk = 0 sendMode = 'saveKeymap' if (!connectedKeyboardPort) { console.log('port not set') } else if (!connectedKeyboardPort.isConnected) { connectedKeyboardPort.open(() => { console.log('port open again') connectedKeyboardPort.write('saveKeymap\n') }) } else { connectedKeyboardPort.write('saveKeymap\n') } } const deselectKeyboard = () => { if (connectedKeyboardPort && connectedKeyboardPort.isConnected) { connectedKeyboardPort.close() } } let debugPort: any = undefined const CONNECTION_TIMEOUT = 5000 // 5 seconds timeout const notifyConnectionStatus = (connected: boolean, error?: string) => { mainWindow?.webContents.send('serialConnectionStatus', { connected, error }) } const closeDebugPort = () => { return new Promise((resolve) => { if (debugPort?.isOpen) { debugPort.close(() => { debugPort = undefined resolve() }) } else { debugPort = undefined resolve() } }) } const serialConnect = async (port) => { console.log('connecting to serial port', port) try { // First ensure any existing connection is properly closed await closeDebugPort() // Create a promise that will reject after timeout const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Connection timeout')), CONNECTION_TIMEOUT) }) // Create the connection promise const connectionPromise = new Promise((resolve, reject) => { try { debugPort = new serialPort.SerialPort({ path: port, baudRate, autoOpen: true }, (err) => { if (err) { reject(err) return } const parser = debugPort.pipe(new ReadlineParser({ delimiter: '\n' })) parser.on('data', (data) => { console.log('got data from serial', data) mainWindow?.webContents.send('serialData', { message: data + '\n' }) }) debugPort.on('error', (error) => { console.error('Serial port error:', error) notifyConnectionStatus(false, error.message) closeDebugPort() }) debugPort.on('close', () => { console.log('Serial port closed') notifyConnectionStatus(false) }) resolve(true) }) } catch (err) { reject(err) } }) // Wait for either connection or timeout await Promise.race([connectionPromise, timeoutPromise]) console.log('Successfully connected to serial port') notifyConnectionStatus(true) } catch (error: any) { console.error('Failed to connect:', error) await closeDebugPort() notifyConnectionStatus(false, error instanceof Error ? error.message : 'Unknown error') throw error } } const sendSerial = (message) => { if (!debugPort?.isOpen) { console.error('Cannot send: port not open') return } console.log('sending serial', message) mainWindow?.webContents.send('serialData', { message: `> sent: ${message}\n` }) try { let buffer if (message === 'ctrlc') { buffer = Buffer.from('\x03\x03', 'utf8') } else if (message === 'ctrld') { buffer = Buffer.from('\x04', 'utf8') } else { buffer = Buffer.from(message + '\r\n', 'utf8') } debugPort.write(buffer, (err) => { if (err) { console.error('error sending serial', err) notifyConnectionStatus(false, 'Failed to send data') } }) } catch (error) { console.error('Error sending serial data:', error) notifyConnectionStatus(false, 'Failed to send data') } } // Add new IPC handler for disconnect ipcMain.handle('serialDisconnect', async () => { await closeDebugPort() }) const checkSerialDevices = async () => { try { console.log('checking serial devices') const ports = await serialPort.SerialPort.list() if (ports.length === 0) { console.log('No serial ports found') return [] } const returnPorts = ports.map((port) => { return { port: port.path, manufacturer: port.manufacturer, serialNumber: port.serialNumber // Add more attributes here if needed } }) console.log('found serial ports', returnPorts) return returnPorts } catch (error) { console.error('Error fetching the list of serial ports:', error) return [] } } const openExternal = (url) => { shell.openExternal(url) } // Drive and Firmware handlers ipcMain.handle('list-drives', async () => { try { const drives = await drivelist.list() const filteredDrives = drives .map((drive) => ({ path: drive.mountpoints[0]?.path || '', name: drive.mountpoints[0]?.label || drive.description || 'Unknown Drive', isReadOnly: drive.isReadOnly, isRemovable: drive.isRemovable, isSystem: drive.isSystem, isUSB: drive.isUSB, isCard: drive.isCard })) .filter((drive) => drive.path !== '' && drive.isUSB) console.log('drives', filteredDrives) return filteredDrives } catch (error) { console.error('Failed to list drives:', error) throw error } }) ipcMain.handle( 'flash-detection-firmware', async (_event, { drivePath, serialNumber }: { drivePath: string; serialNumber: string }) => { try { // Set the current keyboard path currentKeyboard.path = drivePath currentKeyboard.name = 'New Keyboard' currentKeyboard.id = Date.now().toString() currentKeyboard.serialNumber = serialNumber console.log('flashing detection firmware currentKeyboard', currentKeyboard) // Create necessary directories if they don't exist if (!fs.existsSync(drivePath)) { throw new Error(`Drive path ${drivePath} does not exist`) } // Flash the detection firmware await flashFirmware(drivePath) // setup both ports to listen for detection return { success: true } } catch (error) { console.error('Failed to flash firmware:', error) throw error } } ) // // Keyboard History handlers // ipcMain.handle('list-keyboards', () => { // try { // return listKeyboards() // } catch (error) { // console.error('Failed to list keyboards:', error) // throw error // } // }) // Serial Port handlers ipcMain.handle('serial-ports', async () => { try { console.log('checking serial devices') const ports = await serialPort.SerialPort.list() if (ports.length === 0) { console.log('No serial ports found') return [] } const returnPorts = ports.map((port) => ({ port: port.path, manufacturer: port.manufacturer, serialNumber: port.serialNumber })) console.log('found serial ports', returnPorts) return returnPorts } catch (error) { console.error('Error fetching the list of serial ports:', error) return [] } }) ipcMain.handle('serial-connect', async (_event, port: string) => { return serialConnect(port) }) ipcMain.handle('serial-disconnect', async () => { return closeDebugPort() }) ================================================ FILE: src/main/keyboardDetector.ts ================================================ import { SerialPort } from 'serialport' import { ReadlineParser } from '@serialport/parser-readline' import { BrowserWindow, ipcMain } from 'electron' import { flashFirmware } from './kmkUpdater' import path from 'path' import fs from 'fs' import { currentKeyboard } from './store' interface DetectionData { rows: string[] cols: string[] diodeDirection: 'COL2ROW' | 'ROW2COL' pressedKeys: { row: number; col: number }[] } export class KeyboardDetector { private port: SerialPort | null = null private parser: ReadlineParser | null = null private detectionData: DetectionData = { rows: [], cols: [], diodeDirection: 'COL2ROW', pressedKeys: [] } async startDetection(window: BrowserWindow) { try { // Flash detection firmware // const detectionFirmwarePath = path.join(__dirname, '../firmware/detection') // await flashFirmware(detectionFirmwarePath) // Wait for the board to restart // await new Promise(resolve => setTimeout(resolve, 2000)) // Find the data port (second port) const serialNumber = currentKeyboard.serialNumber console.log('Using serial number for detection:', serialNumber) // Find both serial ports for this serial number const ports = await SerialPort.list() const matchingPorts = ports .filter(port => port.serialNumber === serialNumber) .sort((a, b) => a.path.localeCompare(b.path)) if (matchingPorts.length < 2) { throw new Error('Could not find both serial ports for keyboard') } // Save ports to current keyboard, with lower numbered port as port A currentKeyboard.serialPortA = matchingPorts[0].path currentKeyboard.serialPortB = matchingPorts[1].path // Use port B (higher numbered port) for detection const dataPort = currentKeyboard.serialPortB if (!dataPort) { throw new Error('Data port not found. Make sure both serial ports are properly connected.') } // Open serial connection this.port = new SerialPort({ path: dataPort, baudRate: 115200 }) this.parser = this.port.pipe(new ReadlineParser()) // Handle incoming data this.parser.on('data', (data: string) => { this.handleDetectionData(data, window) }) // Start detection mode this.port.write('start_detection\n') } catch (error) { console.error('Detection failed:', error) throw error } } private handleDetectionData(data: string, window: BrowserWindow) { try { const message = JSON.parse(data) console.log('Received message:', message) switch (message.type) { case 'new_key_press': // Send update to renderer console.log('Sending new_key_press to renderer', message) window.webContents.send('detection-update', message) break case 'existing_key_press': // Send update to renderer console.log('Sending existing_key_press to renderer', message) window.webContents.send('detection-update', message) break case 'used_pins': this.detectionData.diodeDirection = message.direction console.log('Sending used_pins to renderer', message) window.webContents.send('detection-update', message) break } } catch (error) { console.error('Failed to handle detection data:', error) } } stopDetection() { if (this.port) { this.port.write('stop_detection\n') this.port.close() this.port = null this.parser = null } } getDetectionData(): DetectionData { return this.detectionData } } // Create detector instance const detector = new KeyboardDetector() // IPC handlers ipcMain.handle('start-detection', async (event) => { const window = BrowserWindow.fromWebContents(event.sender) if (window) { await detector.startDetection(window) return { success: true } } throw new Error('Window not found') }) ipcMain.handle('stop-detection', () => { detector.stopDetection() return { success: true } }) ipcMain.handle('get-detection-data', () => { return detector.getDetectionData() }) ================================================ FILE: src/main/kmkUpdater.ts ================================================ import { appDir, currentKeyboard } from './store' import * as fs from 'fs-extra' import request from 'request' import decompress from 'decompress' import { mainWindow } from './index' import { detectionFirmware } from './pythontemplates/detection' import { writeFile } from 'fs/promises' import { join } from 'path' import { app } from 'electron' import { bootpy } from './pythontemplates/boot' // downloads kmk to app storage export const updateFirmware = async () => { const versionSha = '5a6669d1da219444e027fb20f57d4f5b3ecdedfe' console.log('updating kmk firmware', appDir, versionSha) const file_url = `https://github.com/KMKfw/kmk_firmware/archive/${versionSha}.zip` const targetPath = appDir + 'kmk.zip' if (!fs.existsSync(appDir)) { fs.mkdirSync(appDir) } // Save variable to know progress let received_bytes = 0 let total_bytes = 0 mainWindow?.webContents.send('update-kmk-progress', { state: 'downloading', progress: received_bytes / total_bytes }) const out = fs.createWriteStream(targetPath) // download the newest version on await new Promise((resolve, reject): void => { request .get(file_url) .on('response', (data) => { // Change the total bytes value to get progress later. total_bytes = parseInt(data.headers['content-length']) || 1028312 console.log('updated total', total_bytes, data.headers, data.statusCode) }) .on('data', (chunk) => { // Update the received bytes received_bytes += chunk.length console.log(total_bytes, received_bytes) mainWindow?.webContents.send('onUpdateFirmwareInstallProgress', { state: 'downloading', progress: received_bytes / total_bytes }) }) .pipe(out) .on('finish', async () => { console.log('kmk downloaded') mainWindow?.webContents.send('onUpdateFirmwareInstallProgress', { state: 'unpacking', progress: 0 }) resolve() }).onerror = (err) => { console.error(err) reject() } }) // decompress the downloaded zip file await decompress(`${appDir}kmk.zip`, `${appDir}/kmk`) .then((files) => { console.log('kmk decompressed', files.length) mainWindow?.webContents.send('onUpdateFirmwareInstallProgress', { state: 'copying', progress: 0 }) }) .catch((error) => { console.log(error) }) // file copy needs to await the decompression try { console.log('moving kmk into keyboard') // write a file to the keyboard with the version sha if (fs.existsSync(`${currentKeyboard.path}/kmk`)) { console.log('removing old kmk folder') const countFilesRecursive = async (dir: string): Promise => { const files = await fs.readdir(dir, { withFileTypes: true }) let count = files.length for (const file of files) { if (file.isDirectory()) { count += await countFilesRecursive(`${dir}/${file.name}`) } } return count } const deleteWithProgress = async (dir: string) => { let processedFiles = 0 const totalFiles = await countFilesRecursive(dir) const deleteRecursive = async (currentDir: string) => { const currentFiles = await fs.readdir(currentDir, { withFileTypes: true }) for (const file of currentFiles) { const filePath = `${currentDir}/${file.name}` if (file.isDirectory()) { await deleteRecursive(filePath) } await fs.promises.rm(filePath, { force: true, recursive: true }) processedFiles++ mainWindow?.webContents.send('onUpdateFirmwareInstallProgress', { state: 'cleaning', progress: (processedFiles / totalFiles) * 100 }) } } await deleteRecursive(dir) } await deleteWithProgress(`${currentKeyboard.path}/kmk`) } if (!fs.existsSync(`${currentKeyboard.path}/kmk`)) { fs.mkdirSync(`${currentKeyboard.path}/kmk`) } console.log('writing version to keyboard', versionSha) fs.writeFileSync(`${currentKeyboard.path}/kmk/version`, versionSha) console.log('copying kmk to keyboard', `${currentKeyboard.path}/kmk`) const countFiles = async (src: string): Promise => { const files = await fs.readdir(src, { withFileTypes: true }) let count = files.length for (const file of files) { if (file.isDirectory()) { count += (await countFiles(`${src}/${file.name}`)) - 1 // subtract 1 to not count the directory itself twice } } return count } let processedFiles = 0 const copyWithProgress = async (src: string, dest: string, totalFiles: number) => { const files = await fs.readdir(src, { withFileTypes: true }) for (const file of files) { const srcPath = `${src}/${file.name}` const destPath = `${dest}/${file.name}` if (file.isDirectory()) { await fs.ensureDir(destPath) await copyWithProgress(srcPath, destPath, totalFiles) } else { console.log( 'copying file', destPath, processedFiles, totalFiles, processedFiles / totalFiles ) await fs.copy(srcPath, destPath) processedFiles++ mainWindow?.webContents.send('onUpdateFirmwareInstallProgress', { state: 'copying', progress: (processedFiles / totalFiles) * 100 }) } } } try { const sourcePath = `${appDir}/kmk/kmk_firmware-${versionSha}/kmk` const totalFiles = await countFiles(sourcePath) await copyWithProgress(sourcePath, `${currentKeyboard.path}/kmk`, totalFiles) mainWindow?.webContents.send('onUpdateFirmwareInstallProgress', { state: 'done', progress: 1, message: 'Firmware updated successfully, to version ' + versionSha }) } catch (err) { console.error('Error during copy:', err) mainWindow?.webContents.send('onUpdateFirmwareInstallProgress', { state: 'error', progress: 0 }) } } catch (err) { console.error(err) } } export async function flashFirmware(firmwarePath: string): Promise { try { console.log('flashing firmware initial', firmwarePath) // Create detection firmware file const detectionPath = join(firmwarePath, 'code.py') await writeFile(detectionPath, detectionFirmware) const bootPath = join(firmwarePath, 'boot.py') await writeFile(bootPath, bootpy) // Wait for the board to restart await new Promise(resolve => setTimeout(resolve, 2000)) mainWindow?.webContents.send('onUpdateFirmwareInstallProgress', { state: 'done', progress: 1, message: 'Initiated detection firmware flashed' }) } catch (error) { console.error('Failed to flash firmware:', error) throw error } } ================================================ FILE: src/main/pythontemplates/boot.ts ================================================ export const bootpy = `# boot.py - v1.0.5 import usb_cdc import supervisor import storage import microcontroller # optional # supervisor.set_next_stack_limit(4096 + 4096) usb_cdc.enable(console=True, data=True) # used to identify pog compatible keyboards while scanning com ports supervisor.set_usb_identification("Pog", "Pog Keyboard") # index configs # 0 - show usb drive | 0 false, 1 true if microcontroller.nvm[0] == 0: storage.disable_usb_drive() storage.remount("/", False) ` ================================================ FILE: src/main/pythontemplates/code.ts ================================================ export const codepy = `# Main Keyboard Configuration - v1.0.0 import board import pog # check if we just want to run the coord_mappping Assistant if pog.coordMappingAssistant: from coordmaphelper import CoordMapKeyboard if __name__ == '__main__': CoordMapKeyboard().go() print("Exiting Coord Mapping Assistant Because of an error") else: from kb import POGKeyboard # set the required features for you keyboard and keymap # add custom ones in the kb.py keyboard = POGKeyboard(features=pog.kbFeatures) # manage settings for our modules and extensions here keyboard.tapdance.tap_time = 200 # Keymap import keymap keyboard.keymap = keymap.keymap # Encoder Keymap if available if pog.hasEncoders: keyboard.encoder_handler.map = keymap.encoderKeymap # Execute the keyboard loop if __name__ == '__main__': keyboard.go() ` ================================================ FILE: src/main/pythontemplates/coordmaphelper.ts ================================================ export const coordmaphelperpy = `# coordmaphelper.py v1.0.1 import board import pog from kb import POGKeyboard from kmk.keys import KC from kmk.modules.macros import Press, Release, Tap, Macros class CoordMapKeyboard(POGKeyboard): def __init__(self): super().__init__(features=['basic', 'macros']) print("Running coord_mapping assistant") print("Press each key to get its coord_mapping value") if not hasattr(pog, 'keyCount') or pog.keyCount == 0: raise ValueError("pog.keyCount is not set or is zero") N = pog.keyCount * 2 coord_mapping = list(range(N)) layer = [] print(f"coord_mapping = {coord_mapping}") print(f"Total keys: {N}") for i in range(N): c, r = divmod(i, 100) d, u = divmod(r, 10) print(f"Adding key {i} ({c}{d}{u})") try: layer.append( KC.MACRO( Tap(getattr(KC, f"N{c}")), Tap(getattr(KC, f"N{d}")), Tap(getattr(KC, f"N{u}")), Tap(KC.SPC), ) ) except AttributeError as e: print(f"Error creating macro for key {i}: {e}") if not layer: raise ValueError("No keys were added to the layer") print(f"Layer created with {len(layer)} keys") self.keymap = [layer] self.coord_mapping = coord_mapping print(f"Keymap initialized with {len(self.keymap[0])} keys") ` ================================================ FILE: src/main/pythontemplates/customkeys.ts ================================================ export const customkeyspy = `# These are yous custom keycodes do any needed imports at the top - v1.0.0 # then you can reference them in your keymap with for example customkeys.MyKey from kmk.keys import KC from kmk.modules.macros import Tap, Release, Press import microcontroller # Here you can define your custom keys # MyKey = KC.X # Builtin macros for use in pog def next_boot_dfu(keyboard): print('setting next boot to dfu') #serial feedback microcontroller.on_next_reset(microcontroller.RunMode.UF2) DFUMODE = KC.MACRO(next_boot_dfu) def next_boot_safe(keyboard): print('setting next boot to safe') #serial feedback microcontroller.on_next_reset(microcontroller.RunMode.SAFE_MODE) SAFEMODE = KC.MACRO(next_boot_safe) def toggle_drive(keyboard): print('toggling usb drive') #serial feedback if microcontroller.nvm[0] == 0: microcontroller.nvm[0] = 1 else: microcontroller.nvm[0] = 0 ToggleDrive = KC.MACRO(toggle_drive)` ================================================ FILE: src/main/pythontemplates/detection.ts ================================================ export const detectionFirmware = `import board import digitalio import time import supervisor import usb_cdc import json # Initialize empty lists for pins and their IOs pin_names = [] ios = [] print("Scanning for available GPIO pins...") # Iterate over all attributes of the board module for pin_name in dir(board): if pin_name.startswith('GP'): # We are only interested in GPIO pins try: pin = getattr(board, pin_name) # Try to initialize the pin as a digital input io = digitalio.DigitalInOut(pin) io.switch_to_input(pull=digitalio.Pull.UP) pin_names.append(pin_name) # If successful, add it to the list ios.append(io) # Keep the IO object except (AttributeError, ValueError): print(f"Skipping {pin_name} (not available)") continue if not pin_names: print("No usable GPIO pins found!") while True: pass pin_names.sort() # Sort them for consistent ordering print(f"Found usable pins: {', '.join(pin_names)}") # Create pins list with references pins = list(zip([getattr(board, name) for name in pin_names], pin_names)) # Track connections for each pin row_connections = {} # pins that act as rows col_connections = {} # pins that act as columns # Initialize empty sets for each pin for pin_name in pin_names: row_connections[pin_name] = set() col_connections[pin_name] = set() def read_pin_reliable(pin): # Read the pin 3 times with a small delay between reads readings = [] for _ in range(3): readings.append(not pin.value) # not pin.value because True means pressed # time.sleep(0.001) # Return True only if all readings indicate pressed return all(readings) def print_connections(pin_name): print(f"Connections for {pin_name}:") if row_connections[pin_name]: # Only print if there are connections print(f"As Row -> Columns: {sorted(row_connections[pin_name])}") if col_connections[pin_name]: # Only print if there are connections print(f"As Column <- Rows: {sorted(col_connections[pin_name])}") print("Starting diode direction test") print("Press any key to exit") print("Connect switches between pins to test...") data_serial = usb_cdc.data if not data_serial: supervisor.reload() try: data_serial.write(json.dumps({ 'type': 'start_detection', 'pins': pin_names, }).encode() + b'\\n') while True: # Exit on any key press for row_idx in range(len(pins)): # Set current pin as row (output low) ios[row_idx].switch_to_output(value=False) # Test all other pins as columns for col_idx in range(len(pins)): if col_idx != row_idx: ios[col_idx].switch_to_input(pull=digitalio.Pull.UP) time.sleep(0.001) # Small delay for pin to settle if read_pin_reliable(ios[col_idx]): # Key is pressed (confirmed by 3 readings) row_pin = pins[row_idx][1] col_pin = pins[col_idx][1] print(f"New key press detected! Direction: ({row_pin}->{col_pin})") print_connections(row_pin) print_connections(col_pin) # Check if this is a new connection if col_pin not in row_connections[row_pin]: data_serial.write(json.dumps({ 'type': 'new_key_press', 'row': row_pin, 'col': col_pin, }).encode() + b'\\n') row_connections[row_pin].add(col_pin) col_connections[col_pin].add(row_pin) else: data_serial.write(json.dumps({ 'type': 'existing_key_press', 'row': row_pin, 'col': col_pin, }).encode() + b'\\n') data_serial.write(json.dumps({ 'type': 'used_pins', 'rows': sorted(row_connections[row_pin]), 'cols': sorted(col_connections[col_pin]), }).encode() + b'\\n') ios[col_idx].switch_to_input(pull=digitalio.Pull.UP) # Reset column pin # Reset row pin ios[row_idx].switch_to_input(pull=digitalio.Pull.UP) # time.sleep(0.001) # time.sleep(0.01) # Small delay between full matrix scans finally: # Clean up print("Cleaning up...") for io in ios: io.deinit() print("Final connection summary:") for pin_name in pin_names: if row_connections[pin_name] or col_connections[pin_name]: print_connections(pin_name) ` ================================================ FILE: src/main/pythontemplates/kb.ts ================================================ export const kbpy = `# kb.py KB base config - v1.0.0 import board import pog import microcontroller from kmk.kmk_keyboard import KMKKeyboard from kmk.scanners import DiodeOrientation from kmk.scanners.keypad import KeysScanner class POGKeyboard(KMKKeyboard): def __init__(self, features=['basic']): super().__init__() if "basic" in features: from kmk.modules.layers import Layers; combo_layers = { # combolayers can be added here # (1, 2): 3, } self.modules.append(Layers(combo_layers)) from kmk.extensions.media_keys import MediaKeys; self.extensions.append(MediaKeys()) if "international" in features: from kmk.extensions.international import International self.extensions.append(International()) if "serial" in features: from pog_serial import pogSerial; self.modules.append(pogSerial()) if "oneshot" in features: from kmk.modules.sticky_keys import StickyKeys sticky_keys = StickyKeys() # optional: set a custom release timeout in ms (default: 1000ms) # sticky_keys = StickyKeys(release_after=5000) self.modules.append(sticky_keys) if "tapdance" in features: from kmk.modules.tapdance import TapDance self.tapdance = TapDance() self.modules.append(self.tapdance) if "holdtap" in features: from kmk.modules.holdtap import HoldTap; self.modules.append(HoldTap()) if "mousekeys" in features: from kmk.modules.mouse_keys import MouseKeys; self.modules.append(MouseKeys()) if "combos" in features: from kmk.modules.combos import Combos, Chord, Sequence self.combos = Combos() self.modules.append(self.combos) # if "macros" in features: from kmk.modules.macros import Macros self.macros = Macros() self.modules.append(self.macros) # TODO: not tested yet if "capsword" in features: from kmk.modules.capsword import CapsWord self.capsword = CapsWord() self.modules.append(self.capsword) if pog.config['split']: from kmk.modules.split import Split, SplitSide, SplitType # Split Side Detection if pog.splitSide == "label": from storage import getmount side = SplitSide.RIGHT if str(getmount('/').label)[-1] == 'R' else SplitSide.LEFT if pog.splitSide == "vbus": import digitalio vbus = digitalio.DigitalInOut(pog.vbusPin) vbus.direction = digitalio.Direction.INPUT side = SplitSide.RIGHT if vbus.value == False else SplitSide.LEFT if pog.splitSide == "left" or pog.splitSide == "right": side = SplitSide.RIGHT if pog.splitSide == "right" else SplitSide.LEFT # Split Type Configuration if pog.keyboardType == "splitBLE": print("split with 2 pins") self.split = Split( split_type=SplitType.BLE, split_side=side, split_flip=pog.splitFlip) elif pog.keyboardType == "splitSerial": print("split with 2 pins (UART)") self.split = Split( split_side=side, split_target_left=pog.splitTargetLeft, split_type=SplitType.UART, data_pin=pog.splitPinA, data_pin2=pog.splitPinB, use_pio=pog.splitUsePio, split_flip=pog.splitFlip, uart_flip=pog.splitUartFlip) else: # Nested under pog.split == True => splitOnewire print('split with 1 pin') self.split = Split( split_side=side, split_target_left=pog.splitTargetLeft, data_pin=pog.splitPinA, use_pio=pog.splitUsePio, split_flip=pog.splitFlip) self.modules.append(self.split) # Add your own modules and extensions here # or sort them into the correct spot to have the correct import order # Encoders if pog.hasEncoders: from kmk.modules.encoder import EncoderHandler self.encoder_handler = EncoderHandler() self.encoder_handler.pins = pog.encoders self.modules.append(self.encoder_handler) if "rgb" in features: from kmk.extensions.RGB import RGB rgb = RGB( pixel_pin=eval(pog.rgbPin), num_pixels=pog.rgbNumLeds, rgb_order=(1, 0, 2), val_limit=40, # Maximum brightness level. Only change if you know what you are doing! hue_default=pog.rgbOptions["hueDefault"], sat_default=pog.rgbOptions["satDefault"], val_default=pog.rgbOptions["valDefault"], animation_speed=pog.rgbOptions["animationSpeed"], animation_mode=pog.rgbOptions["animationMode"], breathe_center=pog.rgbOptions["breatheCenter"], knight_effect_length=pog.rgbOptions["knightEffectLength"], ) self.extensions.append(rgb) # direct pin wiring # Must be set during init to override defaulting to matrix wiring if pog.directWiring: self.matrix = KeysScanner( pins=pog.pins_tuple, value_when_pressed=False, pull=True, interval=0.02, max_events=64 ) # matrix wiring if pog.matrixWiring: self.col_pins = pog.col_pins_tuple self.row_pins = pog.row_pins_tuple self.diode_orientation = DiodeOrientation.ROW2COL if pog.config["diodeDirection"] == "ROW2COL" else DiodeOrientation.COL2ROW # coord_mapping if len(pog.config["coordMap"]) != 0: self.coord_mapping = [int(val) for val in pog.coordMapping.split(",")[:-1]] ` ================================================ FILE: src/main/pythontemplates/keymap.ts ================================================ export const keymappy = `#keymap.py KB base config - v1.0.0 from kmk.keys import KC from kmk.modules.macros import Macros, Press, Release, Tap, Delay from kmk.modules.combos import Chord, Sequence import pog import customkeys keymap = [] for l, layer in enumerate(pog.config['keymap']): layerKeymap = [] for k, key in enumerate(layer): layerKeymap.append(eval(key)) keymap.append(tuple(layerKeymap)) encoderKeymap = [] for l, layer in enumerate(pog.config['encoderKeymap']): layerEncoders = [] for e, encoder in enumerate(layer): layerEncoders.append(tuple(map(eval, encoder))) encoderKeymap.append(tuple(layerEncoders)) ` ================================================ FILE: src/main/pythontemplates/pog.ts ================================================ export const pogpy = `# pog.py Import the pog config - v0.9.5 import json import board from kmk.keys import KC import microcontroller config = {} configbuffer = bytearray() configbufferlen = 0 try: with open("/pog.json", "r") as fp: x = fp.read() # parse x: config = json.loads(x) configbuffer = json.dumps(config) configbufferlen = len(configbuffer) except OSError as e: microcontroller.nvm[0] = 1 raise Exception("Could not read pog.json file. mounting drive") print("starting keyboard %s (%s)" % (config["name"], config["id"])) def pinValid(pin): if pin == "": return False if config["pinPrefix"] == "quickpin": pin = f'{eval(pin)}' if pin in [f'board.{alias}' for alias in dir(board)]: return True else: print(f'INVALID PIN FOUND {pin}') return False # Pin setup def renderPin(pin): pinLabel = '' if config["pinPrefix"] == "gp": pinLabel = "board.GP" + pin elif config["pinPrefix"] == "board": pinLabel = "board." + pin elif config["pinPrefix"] == "quickpin": pinLabel = "pins[" + pin + "]" else: pinLabel = pin if pinValid(pinLabel): return pinLabel colPinsArray = [] for i, item in enumerate(config["colPins"]): colPinsArray.append(renderPin(item)) # Remove the 'None's from the list of pins colPinsArray = [pin for pin in colPinsArray if pin is not None] colPins = ",".join(colPinsArray) if len(colPinsArray) == 1: colPins = colPins + "," # Create actual tuple of pin objects for direct use col_pins_tuple = tuple(eval(pin) for pin in colPinsArray) rowPinsArray = [] for i, item in enumerate(config["rowPins"]): rowPinsArray.append(renderPin(item)) # Remove the 'None's from the list of pins rowPinsArray = [pin for pin in rowPinsArray if pin is not None] rowPins = ",".join(rowPinsArray) if len(rowPinsArray) == 1: rowPins = rowPins + "," # Create actual tuple of pin objects for direct use row_pins_tuple = tuple(eval(pin) for pin in rowPinsArray) pinsArray = [] for i, item in enumerate(config["directPins"]): pinsArray.append(renderPin(item)) # Remove the 'None's from the list of pins pinsArray = [pin for pin in pinsArray if pin is not None] pins = ",".join(pinsArray) if len(pinsArray) == 1: pins = pins + "," # Create actual tuple of pin objects for direct use pins_tuple = tuple(eval(pin) for pin in pinsArray) kbFeatures = config.get('kbFeatures') rgbPin = config["rgbPin"] if pinValid(config["rgbPin"]) else None rgbNumLeds = config["rgbNumLeds"] rgbOptions = config["rgbOptions"] if not config["rgbOptions"] and "rgb" in kbFeatures: print("rgbOptions not set when rgb is needed") matrixWiring = False directWiring = False if config['wiringMethod'] == 'matrix': matrixWiring = True keyCount = len(rowPinsArray) * len(colPinsArray) print(f"Matrix wiring: rows={rowPins} cols={colPins}") else: directWiring = True keyCount = len(pinsArray) print(f"Direct wiring: pins={pins}") # encoders hasEncoders = len(config['encoders']) != 0 encoderArray = [] for i, item in enumerate(config["encoders"]): encoderArray.append([eval(renderPin(item['pad_a'])), eval(renderPin(item['pad_b'])), None]) encoderTupleArray = [] for i, item in enumerate(encoderArray): encoderTupleArray.append(tuple(item)) encoders = tuple(encoderTupleArray) # coord map coordMappingAssistant = config['coordMapSetup'] def convert_coord_mapping(): if not config.get("coordMap"): return "" str = "" for row in config["coordMap"]: str += " " + ",".join(val for val in row)+"," return str.replace("spc,", " ") coordMapping = convert_coord_mapping() keyboardType = None if config.get('keyboardType'): keyboardType = config['keyboardType'] splitSide = None if config.get('splitSide'): splitSide = config['splitSide'] splitTargetLeft = None if config.get('splitTargetLeft'): splitTargetLeft = config['splitTargetLeft'] splitPinA = None splitPinB = None if config.get('splitPinA'): splitPinA = eval(renderPin(config['splitPinA'])) if config.get('splitPinB'): splitPinB = eval(renderPin(config['splitPinB'])) splitUsePio = config.get('splitUsePio') splitFlip = config.get('splitFlip') splitUartFlip = config.get('splitUartFlip') vbusPin = None if config.get('vbusPin') and config.get('splitSide') == 'vbus' and pinValid("board." + config['vbusPin']): vbusPin = eval("board." + config['vbusPin']) # led pin without prefix for now if config.get('ledPin'): ledPin = eval(config.get('ledPin')) ledLength = int(config.get('ledLength')) ` ================================================ FILE: src/main/pythontemplates/pog_serial.ts ================================================ export const pog_serialpy = `# pog_serial module - v0.9.5 from usb_cdc import data from kmk.modules import Module from kmk.utils import Debug import pog import json import gc import time import microcontroller import os import supervisor import math import board debug = Debug(__name__) action = "" chunkindex = 0 def sendConfig(): def cross_sum(s): """ Returns the cross sum of a string, where each character is mapped to its Unicode code point. """ # Compute the cross sum total = 0 for c in s: total += ord(c) return total global action global chunkindex print('writing chunk', chunkindex) chunksize = 800 chunk_count = pog.configbufferlen / chunksize if chunkindex > chunk_count: return chunk = (json.dumps({ 'type': 'pogconfig', 'current_chunk': chunkindex + 1, # start at 1 for the first chunk 'total_chunks': math.ceil(chunk_count), # only show full chunks 'data': pog.configbuffer[chunksize*chunkindex:chunksize*chunkindex+chunksize], 'totalsize': pog.configbufferlen, 'cross_sum': cross_sum(pog.configbuffer[chunksize*chunkindex:chunksize*chunkindex+chunksize]) })+"\\n").encode() print(chunk) wrote = data.write(chunk) print('wrote', wrote) def readConfigChunk(line): global action global chunkindex lasttime = time.monotonic_ns() jsondata = json.loads(line) print('json loadin chunk', jsondata['current_chunk'] ,jsondata['total_chunks'],time.monotonic_ns() - lasttime) lasttime = time.monotonic_ns() #tmpConfigFile = open('received_file.json', 'a') #tmpConfigFile.write(jsondata['data']) # print(jsondata['current_chunk']) tmpConfigFile = open('received_file.json', 'a') print('open file',time.monotonic_ns() - lasttime) lasttime = time.monotonic_ns() # print('saving to file', line) tmpConfigFile.write(jsondata['data']) tmpConfigFile.close() print('writing', jsondata['current_chunk'], "of", jsondata['total_chunks']) if jsondata['total_chunks'] == jsondata['current_chunk']: print('done with reading the pog.config') data.write('y\\n'.encode()) action = "" chunkindex = 0 try: jsonfile = open('received_file.json', 'r') json.loads(jsonfile.read()) jsonfile.close() print('file close') # set as new pog.json os.rename('/pog.json','/pog.json.bk') os.rename('/received_file.json','/pog.json') except ValueError as err: print('sent file is not valid json', err) else: data.write('1\\n'.encode()) def readKeymapChunk(line): global action jsondata = json.loads(line) print('json loadin chunk', jsondata['current_chunk'] ,jsondata['total_chunks']) tmpConfigFile = open('received_file.py', 'a') print('open file') tmpConfigFile.write(jsondata['data']) tmpConfigFile.close() print('writing', jsondata['current_chunk'], "of", jsondata['total_chunks']) if jsondata['total_chunks'] == jsondata['current_chunk']: print('done with reading the pog.config') data.write('y\\n'.encode()) action = "" os.rename('/keymap.py','/keymap.py.bk') os.rename('/received_file.py','/keymap.py') else: data.write('1\\n'.encode()) class pogSerial(Module): buffer = bytearray() def during_bootup(self, keyboard): try: data.timeout = 0 except AttributeError: pass def before_matrix_scan(self, keyboard): pass def after_matrix_scan(self, keyboard): pass def process_key(self, keyboard, key, is_pressed, int_coord): return key def before_hid_send(self, keyboard): # Serial.data isn't initialized. if not data: return # Nothing to parse. if data.in_waiting == 0: return self.buffer.extend(data.read(64)) idx = self.buffer.find(b'\\n') # No full command yet. if idx == -1: return print('got serial request') try: line = (self.buffer[:idx]).decode('utf-8') self.buffer = self.buffer[idx + 1 :] global action global chunkindex if action == 'readConfig': print('data transmit mode: reading config file in chunks') readConfigChunk(line) elif action == 'readKeymap': print('data transmit mode: reading keymap file in chunks') readKeymapChunk(line) else: split = line.split() if split[0] == 'info': # print keyboard info action = 'info' chunkindex = 0 print('query keyboard info from serial') sendConfig() if split[0] == 'info_simple': # print basic keyboard info print('getting basic keyboard info') data.write((json.dumps({"driveMounted": microcontroller.nvm[0]!=0 ,"name": pog.config['name'], "manufacturer": pog.config['manufacturer'], "id": pog.config['id'], "board": dir(board) })+"\\n").encode()) if split[0] == 'save': # read chunks file_to_delete = open("received_file.json",'w') file_to_delete.close() print('start reading chunks') action = "readConfig" data.write('1\\n'.encode()) if split[0] == 'saveKeymap': # read chunks file_to_delete = open("received_file.py",'w') file_to_delete.close() print('start reading chunks') action = "readKeymap" data.write('1\\n'.encode()) if split[0] == 'reset': microcontroller.reset() if split[0] == 'drive': if microcontroller.nvm[0] == 0: microcontroller.nvm[0] = 1 else: microcontroller.nvm[0] = 0 print('toggling Drive to', microcontroller.nvm[0]) if split[0] == '1' or split[0] == '0': # contine chunk if split[0] == '1': chunkindex += 1 if action == 'info': sendConfig() if split[0] == 'y': print('resetting action') action = "" chunkindex = 0 except Exception as err: debug(f'error: {err}') def after_hid_send(self, keyboard): pass def on_powersave_enable(self, keyboard): pass def on_powersave_disable(self, keyboard): pass ` ================================================ FILE: src/main/saveConfig.ts ================================================ import * as fs from 'fs-extra' import { currentKeyboard } from './store' import { pogpy } from './pythontemplates/pog' import { coordmaphelperpy } from './pythontemplates/coordmaphelper' import { customkeyspy } from './pythontemplates/customkeys' import { kbpy } from './pythontemplates/kb' import { codepy } from './pythontemplates/code' import { bootpy } from './pythontemplates/boot' import { pog_serialpy } from './pythontemplates/pog_serial' import { keymappy } from './pythontemplates/keymap' import { writePogConfViaSerial, mainWindow } from './index' export const saveConfiguration = async (data: string) => { const { pogConfig, serial, writeFirmware } = JSON.parse(data) if (serial) { // write by serial to current keyboard console.log('writing firmware via usb serial') writePogConfViaSerial(JSON.stringify(pogConfig, null, 0)) return } // write via mounted USB drive console.log('writing firmware via usb files', 'overwriting Firmware:', writeFirmware) type WriteTask = { name: string; path: string; contents: string } const tasks: WriteTask[] = [] // Always write pog.json tasks.push({ name: 'pog.json', path: currentKeyboard.path + '/pog.json', contents: JSON.stringify(pogConfig, null, 4) }) const files = [ { name: 'pog.py', contents: pogpy }, { name: 'code.py', contents: codepy }, { name: 'coordmaphelper.py', contents: coordmaphelperpy }, { name: 'customkeys.py', contents: customkeyspy }, { name: 'boot.py', contents: bootpy }, { name: 'pog_serial.py', contents: pog_serialpy }, { name: 'keymap.py', contents: keymappy }, { name: 'kb.py', contents: kbpy } ] for (const file of files) { const targetPath = currentKeyboard.path + '/' + file.name if (!fs.existsSync(targetPath) || writeFirmware) { tasks.push({ name: file.name, path: targetPath, contents: file.contents }) } } const total = tasks.length let completed = 0 for (const task of tasks) { try { await fs.promises.writeFile(task.path, task.contents) completed += 1 mainWindow?.webContents.send('save-configuration-progress', { state: 'writing', filename: task.name, completed, total }) } catch (e) { console.error(`error writing ${task.name}`, e) mainWindow?.webContents.send('save-configuration-progress', { state: 'error', filename: task.name, completed, total }) } } mainWindow?.webContents.send('save-configuration-progress', { state: 'done', completed, total }) } ================================================ FILE: src/main/selectKeyboard.ts ================================================ import * as fs from 'fs-extra' import { currentKeyboard } from './store' import { dialog } from 'electron' import { connectedKeyboardPort, connectSerialKeyboard, serialBoards } from './index' import * as serialPort from 'serialport' // invoked from frontend to select a drive or folder load the conig from export const handleSelectDrive = async () => { const { canceled, filePaths } = await dialog.showOpenDialog({ properties: ['openDirectory'] }) if (canceled) return return await loadKeyboard(filePaths[0]) } const loadKeyboard = async (path) => { if (!fs.existsSync(`${path}`)) { return { error: 'pathNotFound' } } const folderContents = await fs.promises.readdir(`${path}`) // check for kmk, code.py and boot.py currentKeyboard.path = path let codeContents: string | undefined = undefined if (folderContents.includes('code.py')) { codeContents = await fs.promises.readFile(`${currentKeyboard.path}/code.py`, { encoding: 'utf8', flag: 'r' }) } let configContents = undefined if (folderContents.includes('pog.json')) { configContents = JSON.parse( await fs.promises.readFile(`${currentKeyboard.path}/pog.json`, { encoding: 'utf8', flag: 'r' }) ) } console.log('found something', folderContents) return { path, folderContents, codeContents, configContents } } export const selectKeyboard = async ({ path, id }: { path: string; id: string }) => { console.log('Selecting keyboard:', path, id) if (id) { // connect serial if available const port = serialBoards.value.find((a) => a.id === id) if (!port) return { error: 'not a serial keyboard' } console.log('Found serial board:', serialBoards, id) // Find both ports for this keyboard const allPorts = await serialPort.SerialPort.list() const keyboardPorts = allPorts .filter(p => p.serialNumber === port.serialNumber) .sort((a, b) => a.path.localeCompare(b.path)) if (keyboardPorts.length >= 2) { currentKeyboard.serialPortA = keyboardPorts[0].path currentKeyboard.serialPortB = keyboardPorts[1].path currentKeyboard.serialNumber = port.serialNumber console.log('Set keyboard ports:', currentKeyboard.serialPortA, currentKeyboard.serialPortB) } await connectSerialKeyboard(port) connectedKeyboardPort.write('info\n') } if (path) { console.log('checking keyboard files for', path) return await loadKeyboard(path) } else if (id) { console.log('connecting serial keyboard') return { success: true } } return { error: 'not all args provided' } } export const checkForUSBKeyboards = async (keyboardPaths: string[]) => { console.log('checking for usb keyboards', keyboardPaths) // check for each path in the filesystem if it exists const connectedKeyboards: { path: string; connected: boolean }[] = [] for (const path of keyboardPaths) { if (fs.existsSync(path)) { connectedKeyboards.push({ path, connected: true }) } } return connectedKeyboards } ================================================ FILE: src/main/store.ts ================================================ // Store for global variables import { app } from 'electron' export const appDir = app.getPath('appData') + '/pog/' interface Keyboard { path: string name: string id: string usingSerial?: boolean serialPortA?: string serialPortB?: string serialNumber?: string } export const currentKeyboard: Keyboard = { path: '', name: '', id: '', usingSerial: false, serialPortA: '', serialPortB: '', serialNumber: '' } ================================================ FILE: src/preload/index.d.ts ================================================ import { ElectronAPI } from '@electron-toolkit/preload' export interface IElectronAPI { // Keyboard History API listKeyboards: () => Promise< Array<{ id: string name: string path: string usingSerial?: boolean }> > keyboardScan: (callback: (event: Event, value: { keyboards: any[] }) => void) => void serialKeyboardPogConfig: (callback: (event: Event, value: { pogconfig: any }) => void) => void // Drive and Firmware API listDrives: () => Promise< Array<{ path: string name: string isReadOnly: boolean isRemovable: boolean isSystem: boolean isUSB: boolean isCard: boolean }> > flashDetectionFirmware: (params: { drivePath: string serialNumber?: string }) => Promise<{ success: boolean }> // Serial Port API serialPorts: () => Promise< Array<{ port: string manufacturer?: string serialNumber?: string }> > serialConnect: (port: string) => Promise serialDisconnect: () => Promise serialData: (callback: (event: any, data: { message: string }) => void) => void offSerialData: (callback: (event: any, data: { message: string }) => void) => void serialConnectionStatus: ( callback: (event: any, data: { connected: boolean; error?: string }) => void ) => void serialSend: (message: string) => void // Keyboard Detection API startDetection: () => Promise<{ success: boolean }> stopDetection: () => Promise<{ success: boolean }> getDetectionData: () => Promise<{ rows: string[] cols: string[] diodeDirection: 'COL2ROW' | 'ROW2COL' pressedKeys: { row: number; col: number }[] }> onDetectionUpdate: (callback: (data: any, event: any) => void) => void removeDetectionListeners: () => void onUpdateFirmwareInstallProgress: (callback: (data: any, event: any) => void) => void onSaveConfigurationProgress: ( callback: (event: any, data: { state: 'writing' | 'done' | 'error'; filename?: string; completed: number; total: number }) => void ) => void offSaveConfigurationProgress: ( callback: (event: any, data: { state: 'writing' | 'done' | 'error'; filename?: string; completed: number; total: number }) => void ) => void // Legacy API (to be migrated) selectKeyboard: (data: any) => Promise deselectKeyboard: () => Promise openExternal: (url: string) => Promise selectDrive: () => Promise updateFirmware: () => Promise saveConfiguration: (data: any) => Promise rescanKeyboards: () => Promise checkForUSBKeyboards: (keyboardPaths: string[]) => Promise } export declare global { interface Window { electron: ElectronAPI api: IElectronAPI } } ================================================ FILE: src/preload/index.ts ================================================ import { contextBridge, ipcRenderer } from 'electron' import { electronAPI } from '@electron-toolkit/preload' // Custom APIs for renderer const api = { selectDrive: () => ipcRenderer.invoke('selectDrive'), updateFirmware: () => ipcRenderer.invoke('updateFirmware'), saveConfiguration: (data) => ipcRenderer.send('saveConfiguration', data), selectKeyboard: (data) => ipcRenderer.invoke('selectKeyboard', data), onUpdateFirmwareInstallProgress: (callback) => ipcRenderer.on('onUpdateFirmwareInstallProgress', callback), onSaveConfigurationProgress: (callback) => ipcRenderer.on('save-configuration-progress', callback), offSaveConfigurationProgress: (callback) => ipcRenderer.removeListener('save-configuration-progress', callback), keyboardScan: (callback) => { ipcRenderer.on('keyboardScan', callback) }, serialKeyboardPogConfig: (callback) => { ipcRenderer.on('serialKeyboardPogConfig', callback) }, rescanKeyboards: () => ipcRenderer.invoke('rescanKeyboards'), checkForUSBKeyboards: (data) => ipcRenderer.invoke('checkForUSBKeyboards', data), deselectKeyboard: () => ipcRenderer.invoke('deselectKeyboard'), serialData: (callback) => ipcRenderer.on('serialData', callback), offSerialData: (callback) => ipcRenderer.removeListener('serialData', callback), serialConnectionStatus: (callback) => ipcRenderer.on('serialConnectionStatus', callback), serialPorts: () => ipcRenderer.invoke('serial-ports'), serialSend: (data) => ipcRenderer.send('serialSend', data), serialConnect: (port: string) => ipcRenderer.invoke('serial-connect', port), serialDisconnect: () => ipcRenderer.invoke('serial-disconnect'), openExternal: (data) => ipcRenderer.invoke('openExternal', data), // Keyboard Detection API startDetection: () => ipcRenderer.invoke('start-detection'), stopDetection: () => ipcRenderer.invoke('stop-detection'), getDetectionData: () => ipcRenderer.invoke('get-detection-data'), onDetectionUpdate: (callback) => ipcRenderer.on('detection-update', callback), // Keyboard History API listKeyboards: () => ipcRenderer.invoke('list-keyboards'), // Drive and Firmware API listDrives: () => ipcRenderer.invoke('list-drives'), flashDetectionFirmware: (drivePath: string) => ipcRenderer.invoke('flash-detection-firmware', drivePath) } // Use `contextBridge` APIs to expose Electron APIs to // renderer only if context isolation is enabled, otherwise // just add to the DOM global. if (process.contextIsolated) { try { contextBridge.exposeInMainWorld('electron', electronAPI) contextBridge.exposeInMainWorld('api', api) } catch (error) { console.error(error) } } else { // @ts-ignore (define in dts) window.electron = electronAPI // @ts-ignore (define in dts) window.api = api } ================================================ FILE: src/renderer/index.html ================================================ POG
================================================ FILE: src/renderer/src/App.vue ================================================ ================================================ FILE: src/renderer/src/assets/css/styles.less ================================================ body { display: flex; flex-direction: column; font-family: Roboto, -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Segoe UI', 'Oxygen', 'Ubuntu', 'Cantarell', 'Open Sans', sans-serif; color: #86a5b1; background-color: #2f3241; } * { padding: 0; margin: 0; } ul { list-style: none; } code { font-weight: 600; padding: 3px 5px; border-radius: 2px; background-color: #26282e; font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; font-size: 85%; } a { color: #9feaf9; font-weight: 600; cursor: pointer; text-decoration: none; outline: none; } a:hover { border-bottom: 1px solid; } #app { flex: 1; display: flex; flex-direction: column; max-width: 840px; margin: 0 auto; padding: 15px 30px 0 30px; } .versions { margin: 0 auto; float: none; clear: both; overflow: hidden; font-family: 'Menlo', 'Lucida Console', monospace; color: #c2f5ff; line-height: 1; transition: all 0.3s; li { display: block; float: left; border-right: 1px solid rgba(194, 245, 255, 0.4); padding: 0 20px; font-size: 13px; opacity: 0.8; &:last-child { border: none; } } } .hero-logo { margin-top: -0.4rem; transition: all 0.3s; } @media (max-width: 840px) { .versions { display: none; } .hero-logo { margin-top: -1.5rem; } } .hero-text { font-weight: 400; color: #c2f5ff; text-align: center; margin-top: -0.5rem; margin-bottom: 10px; } @media (max-width: 660px) { .hero-logo { display: none; } .hero-text { margin-top: 20px; } } .hero-tagline { text-align: center; margin-bottom: 14px; } .links { display: flex; align-items: center; justify-content: center; margin-bottom: 24px; font-size: 18px; font-weight: 500; a { font-weight: 500; } .link-item { padding: 0 4px; } } .features { display: flex; flex-wrap: wrap; margin: -6px; .feature-item { width: 33.33%; box-sizing: border-box; padding: 6px; } article { background-color: rgba(194, 245, 255, 0.1); border-radius: 8px; box-sizing: border-box; padding: 12px; height: 100%; } span { color: #d4e8ef; word-break: break-all; } .title { font-size: 17px; font-weight: 500; color: #c2f5ff; line-height: 22px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .detail { font-size: 14px; font-weight: 500; line-height: 22px; margin-top: 6px; } } @media (max-width: 660px) { .features .feature-item { width: 50%; } } @media (max-width: 480px) { .links { flex-direction: column; line-height: 32px; .link-dot { display: none; } } .features .feature-item { width: 100%; } } ================================================ FILE: src/renderer/src/assets/microcontrollers/microcontrollers.json ================================================ [ { "id": "0xcb-helios", "name": "0xCB Helios", "information": "The 0xCB Helios is an Elite-C compatible MicroController that is based on the RP2040.", "image": true, "imageUrl": "https://github.com/0xCB-dev/0xCB-Helios", "license": "CC BY-SA 4.0", "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0/" }, { "id": "pi-pico", "name": "Raspberry Pi Pico", "information": "The Raspberry Pi Pico is a low-cost, high-performance microcontroller board based on the RP2040 chip, designed for embedded projects and IoT applications.", "image": true, "imageUrl": "https://www.raspberrypi.com/documentation/microcontrollers/pico-series.html", "license": "CC BY-SA 4.0", "licenseUrl": "https://creativecommons.org/licenses/by-sa/4.0/" } ] ================================================ FILE: src/renderer/src/components/AutomaticSetup.vue ================================================ ================================================ FILE: src/renderer/src/components/BaseModal.vue ================================================ ================================================ FILE: src/renderer/src/components/CircuitPythonSetup.vue ================================================ ================================================ FILE: src/renderer/src/components/Community.vue ================================================ ================================================ FILE: src/renderer/src/components/CoordMap.vue ================================================ ================================================ FILE: src/renderer/src/components/EncoderLayer.vue ================================================ ================================================ FILE: src/renderer/src/components/EncoderSetup.vue ================================================ ================================================ FILE: src/renderer/src/components/HsvColorPicker.vue ================================================ ================================================ FILE: src/renderer/src/components/KeyCap.vue ================================================ ================================================ FILE: src/renderer/src/components/KeyLayoutInfo.vue ================================================ ================================================ FILE: src/renderer/src/components/KeyPicker.vue ================================================ ================================================ FILE: src/renderer/src/components/KeyboardLayout.vue ================================================ ================================================ FILE: src/renderer/src/components/KeyboardName.vue ================================================ ================================================ FILE: src/renderer/src/components/LoadingOverlay.vue ================================================ ================================================ FILE: src/renderer/src/components/MacroModal.vue ================================================ ================================================ FILE: src/renderer/src/components/MatrixSetup.vue ================================================ ================================================ FILE: src/renderer/src/components/PinSetup.vue ================================================ ================================================ FILE: src/renderer/src/components/RawKeymapEditor.vue ================================================ ================================================ FILE: src/renderer/src/components/RgbSetup.vue ================================================ ================================================ FILE: src/renderer/src/components/SetupMethodSelector.vue ================================================ ================================================ FILE: src/renderer/src/components/VariantOption.vue ================================================ ================================================ FILE: src/renderer/src/components/VariantSwitcher.vue ================================================ ================================================ FILE: src/renderer/src/components/debug.vue ================================================ ================================================ FILE: src/renderer/src/components/installPogFirmware.vue ================================================ ================================================ FILE: src/renderer/src/components/picker-layouts/Colemak.vue ================================================ ================================================ FILE: src/renderer/src/components/picker-layouts/ColemakDH.vue ================================================ ================================================ FILE: src/renderer/src/components/picker-layouts/Dvorak.vue ================================================ ================================================ FILE: src/renderer/src/components/picker-layouts/Qwerty.vue ================================================ ================================================ FILE: src/renderer/src/components/setup/Wizard.vue ================================================ ================================================ FILE: src/renderer/src/components/ui/InputLabel.vue ================================================ ================================================ FILE: src/renderer/src/composables/useLoadingOverlay.ts ================================================ import { ref, onMounted, onUnmounted } from 'vue' import { onLoadingChange, hideLoading } from '../helpers/saveConfigurationWrapper' export const useLoadingOverlay = () => { const isLoading = ref(false) let fallbackTimeout: number | null = null const hideLoadingOverlay = () => { if (fallbackTimeout) { clearTimeout(fallbackTimeout) fallbackTimeout = null } hideLoading() } const setupFallbackTimeout = (timeoutMs = 15000) => { if (fallbackTimeout) { clearTimeout(fallbackTimeout) } fallbackTimeout = setTimeout(() => { if (isLoading.value) { hideLoadingOverlay() } }, timeoutMs) as unknown as number } // Watch for loading state changes const unwatchLoading = onLoadingChange((loading) => { isLoading.value = loading }) onMounted(() => { // Set up fallback timeout when component mounts and loading is active if (isLoading.value) { setupFallbackTimeout() } }) onUnmounted(() => { unwatchLoading() if (fallbackTimeout) { clearTimeout(fallbackTimeout) fallbackTimeout = null } }) return { isLoading, hideLoadingOverlay, setupFallbackTimeout } } ================================================ FILE: src/renderer/src/env.d.ts ================================================ /// declare module '*.vue' { import type { DefineComponent } from 'vue' // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types const component: DefineComponent<{}, {}, any> export default component } ================================================ FILE: src/renderer/src/helpers/colors.ts ================================================ export const hexToHSL = (hex): {hue: number, sat: number, val: number} => { const result: RegExpExecArray | null = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) if (!result) return { hue: 0, sat: 0, val: 0 } let r = parseInt(result[1], 16) let g = parseInt(result[2], 16) let b = parseInt(result[3], 16) r /= 255 g /= 255 b /= 255 const max = Math.max(r, g, b) const min = Math.min(r, g, b) let h = 0 let s = 0 const l = (max + min) / 2 if (max == min) { h = s = 0 // achromatic } else { const d = max - min s = l > 0.5 ? d / (2 - max - min) : d / (max + min) switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0) break case g: h = (b - r) / d + 2 break case b: h = (r - g) / d + 4 break } h /= 6 } return { hue: Math.round(h * 255), sat: Math.round(s * 255), val: Math.round(l * 255) } } export function hslToHex(h, s, l): string { s /= 255 l /= 255 const c = (1 - Math.abs(2 * l - 1)) * s const x = c * (1 - Math.abs(((h / 60) % 2) - 1)) const m = l - c / 2 let r = 0 let g = 0 let b = 0 if (0 <= h && h < 60) { r = c g = x b = 0 } else if (60 <= h && h < 120) { r = x g = c b = 0 } else if (120 <= h && h < 180) { r = 0 g = c b = x } else if (180 <= h && h < 240) { r = 0 g = x b = c } else if (240 <= h && h < 300) { r = x g = 0 b = c } else if (300 <= h && h < 360) { r = c g = 0 b = x } // Having obtained RGB, convert channels to hex let r_str = Math.round((r + m) * 255).toString(16) let g_str = Math.round((g + m) * 255).toString(16) let b_str = Math.round((b + m) * 255).toString(16) // Prepend 0s, if necessary if (r_str.length == 1) r_str = '0' + r if (g_str.length == 1) g_str = '0' + g if (b_str.length == 1) b_str = '0' + b return '#' + r_str + g_str + b_str } ================================================ FILE: src/renderer/src/helpers/index.ts ================================================ import JSON5 from 'json5' import { keyboardStore, KeyInfo, selectedKeys } from '../store' export const matrixPositionToIndex = ({ pos, matrixWidth }: { pos: [number, number] matrixWidth: number }) => { if (!pos) return 0 return Number(pos[0]) * matrixWidth + Number(pos[1]) } const formatMatrixFromLabel = (label: string): [number, number] | false => { const matrix = label.split(',').map((a) => Number(a)) if (matrix.length !== 2) return false return [matrix[0], matrix[1]] } export const cleanupKeymap = () => { const filledKeymap = keyboardStore.keymap.map((layer) => { // replace empty keys with KC.TRNS const tmpLayer = layer.map((key: string | undefined) => { if (!key) return 'KC.TRNS' return key }) const matrixKeyCount = keyboardStore.physicalKeyCount() if (matrixKeyCount > tmpLayer.length) { while (matrixKeyCount > tmpLayer.length) { tmpLayer.push('KC.TRNS') } } else if (matrixKeyCount < tmpLayer.length) { while (matrixKeyCount < tmpLayer.length) { tmpLayer.pop() } } if (tmpLayer) return tmpLayer return [] }) if (filledKeymap) keyboardStore.keymap = filledKeymap console.log('fixed & set new keymap to ', filledKeymap) } const pickKeyAttributes = ({ w, w2, h, h2, x, x2, y2 }: { w: number w2: number h: number h2: number x: number x2: number y?: number y2: number }) => ({ w, w2, h, h2, x, x2, // y, y2 }) // convert a kle keymap to pog export const KleToPog = (kleString: string) => { let keymap = [] try { keymap = JSON5.parse(kleString) } catch (e) { console.log(e) try { keymap = JSON5.parse('[' + kleString + ']') } catch (e) { console.log(e) } } // parse Layout file const configContents: { layouts: { keymap: string[][]; labels: string[] | string[][] } } = { layouts: { keymap, labels: [] } } // const keyboardInfo = ref<{ // keys: KeyData[] // }>({ keys: [] }) // in place update kle to pog layout => iterate over rows let currentX = 0 let currentY = 0 let keydata: any = undefined // data to carry over to the next key until it is overwritten let firstKeyInRow = true const keys: KeyInfo[] = [] configContents.layouts.keymap.forEach((row) => { if (Array.isArray(row)) { // normal row row.forEach((keyOrData) => { // tmp key info let key: KeyInfo = { x: NaN, y: NaN } if (typeof keyOrData === 'string') { // this is a key const labels = keyOrData.split('\n') if (labels.length === 1) { // just the main label // labels = ["", "", "", "", "", "", "", "", "", keyOrData]; // key.matrixPos = keyOrData; const matrix = formatMatrixFromLabel(keyOrData) if (matrix !== false) key.matrix = matrix } else if (labels.length === 4) { // shortened labels top left and bottom right // labels = [keyOrData]; // key.matrixPos = labels[0]; const matrix = formatMatrixFromLabel(labels[0]) if (matrix !== false) key.matrix = matrix key.variant = labels[3].split(',').map((a) => Number(a)) as [number, number] } else { // all labels just keep split // key.matrixPos = keyOrData[0]; const matrix = formatMatrixFromLabel(keyOrData[0]) if (matrix !== false) key.matrix = matrix // key.variant = keyOrData[3] } // key.labels = labels; // Position data if (keydata) { key = { ...key, ...pickKeyAttributes(keydata) } if (keydata.y) currentY += keydata.y if (keydata.x) currentX = keydata.x + currentX if (firstKeyInRow) { key.x = currentX firstKeyInRow = false } else { key.x = currentX } } if (!key.y) key.y = currentY if (!key.x) key.x = currentX keydata = undefined if (!key.w || key.w === 1) { currentX++ } else { currentX = currentX + key.w } keys.push(key) } else { // this is just data for the next key keydata = keyOrData } // add 1 to left distance // next key }) // add 1 to top distance // next row currentX = 0 firstKeyInRow = true currentY++ } }) console.log('created layout', keys) return keys } export const selectNextKey = () => { selectedKeys.value = new Set([ Math.min([...selectedKeys.value][0] + 1, keyboardStore.keyCount() - 1) ]) } export const selectPrevKey = () => { selectedKeys.value = new Set([Math.max([...selectedKeys.value][0] - 1, 0)]) } export const renderLabel = (keycode: string) => { const keyLabels: { [key: string]: { label?: string; alt?: string; icon?: string; iconWindows?: string } } = { // Define labels for your keycodes here F1: { label: 'F1' }, F2: { label: 'F2' }, F3: { label: 'F3' }, F4: { label: 'F4' }, F5: { label: 'F5' }, F6: { label: 'F6' }, F7: { label: 'F7' }, F8: { label: 'F8' }, F9: { label: 'F9' }, F10: { label: 'F10' }, F11: { label: 'F11' }, F12: { label: 'F12' }, A: { label: 'A' }, B: { label: 'B' }, C: { label: 'C' }, D: { label: 'D' }, E: { label: 'E' }, F: { label: 'F' }, G: { label: 'G' }, H: { label: 'H' }, I: { label: 'I' }, J: { label: 'J' }, K: { label: 'K' }, L: { label: 'L' }, M: { label: 'M' }, N: { label: 'N' }, O: { label: 'O' }, P: { label: 'P' }, Q: { label: 'Q' }, R: { label: 'R' }, S: { label: 'S' }, T: { label: 'T' }, U: { label: 'U' }, V: { label: 'V' }, W: { label: 'W' }, X: { label: 'X' }, Y: { label: 'Y' }, Z: { label: 'Z' }, N1: { label: '1' }, N2: { label: '2' }, N3: { label: '3' }, N4: { label: '4' }, N5: { label: '5' }, N6: { label: '6' }, N7: { label: '7' }, N8: { label: '8' }, N9: { label: '9' }, N0: { label: '0' }, ESC: { icon: 'mdi-keyboard-esc' }, ENT: { icon: 'mdi-keyboard-return' }, SPC: { icon: 'mdi-keyboard-space' }, DOT: { label: '.' }, COMM: { label: ',' }, SLSH: { label: '/' }, KP_SLASH: { label: '/' }, SCLN: { label: ';' }, QUOT: { label: "'" }, LSFT: { icon: 'mdi-apple-keyboard-shift' }, RSFT: { icon: 'mdi-apple-keyboard-shift' }, LBRC: { label: '[' }, RBRC: { label: ']' }, LABK: { label: '<' }, RABK: { label: '>' }, LCBR: { label: '{' }, RCBR: { label: '}' }, LEFT_PAREN: { label: '(' }, RIGHT_PAREN: { label: ')' }, DQT: { label: '"' }, COLN: { label: ':' }, EXLM: { label: '!' }, PERCENT: { label: '%' }, AMPERSAND: { label: '&' }, TILDE: { label: '~' }, PIPE: { label: '|' }, DOLLAR: { label: '$' }, HASH: { label: '#' }, QUES: { label: '?' }, BSLS: { label: '\\' }, MINS: { label: '-' }, EQL: { label: '=' }, CAPS: { icon: 'mdi-apple-keyboard-caps' }, TAB: { icon: 'mdi-keyboard-tab' }, BSPC: { icon: 'mdi-backspace' }, DEL: { icon: 'mdi-backspace-reverse' }, LCTL: { icon: 'mdi-apple-keyboard-control' }, RCTL: { icon: 'mdi-apple-keyboard-control' }, LALT: { icon: 'mdi-apple-keyboard-option' }, RALT: { icon: 'mdi-apple-keyboard-option' }, LGUI: { icon: 'mdi-apple-keyboard-command' }, RGUI: { icon: 'mdi-apple-keyboard-command' }, HOME: { icon: 'mdi-arrow-top-left' }, END: { icon: 'mdi-arrow-bottom-right' }, PGDOWN: { icon: 'mdi-arrow-down' }, PGUP: { icon: 'mdi-arrow-up' }, UP: { icon: 'mdi-arrow-up-thin' }, LEFT: { icon: 'mdi-arrow-left-thin' }, DOWN: { icon: 'mdi-arrow-down-thin' }, RIGHT: { icon: 'mdi-arrow-right-thin' }, GRV: { label: '`' }, PLUS: { label: '+' }, AT: { label: '@' }, UNDERSCORE: { label: '_' }, CIRCUMFLEX: { label: '^' }, ASTERISK: { label: '*' }, // Layer MO: { label: 'MO' }, MT: { label: 'MT' }, LT: { label: 'LT' }, TT: { label: 'TT' }, TG: { label: 'TG' }, TO: { label: 'TO' }, TD: { label: 'TD' }, HT: { label: 'HT' }, OS: { label: 'OS' }, // Media MPLY: { label: 'Play/Pause', icon: 'mdi-play-pause' }, VOLU: { label: 'Vol up', icon: 'mdi-volume-plus' }, VOLD: { label: 'Vol down', icon: 'mdi-volume-minus' }, MEDIA_PLAY_PAUSE: { label: 'Play/Pause', icon: 'mdi-play-pause' }, MRWD: { label: 'Prev Track', icon: 'mdi-skip-previous' }, MFFD: { label: 'Next Track', icon: 'mdi-skip-next' }, send_string: { label: 'String' }, RESET: { label: 'Reset' }, RELOAD: { label: 'Reload' }, DEBUG: { label: 'Debug' }, RGB_TOG: { label: 'Toggle
RGB' }, RGB_HUI: { label: 'RGB
Hue +' }, RGB_HUD: { label: 'RGB
Hue -' }, RGB_SAI: { label: 'RGB
Sat +' }, RGB_SAD: { label: 'RGB
Sat -' }, RGB_ANI: { label: 'RGB
Animation +' }, RGB_AND: { label: 'RGB
Animation -' }, RGB_MODE_SWIRL: { label: 'RGB
Swirl' }, RGB_MODE_PLAIN: { label: 'RGB
Plain' }, RGB_MODE_KNIGHT: { label: 'RGB
Knight' }, RGB_MODE_RAINBOW: { label: 'RGB
Rainbow' } } const keylabel: { simple: boolean action: string main: string lower: string params: any[] layer: number | null layerNamePosition: string } = { simple: true, action: '', params: [], layer: null, main: '', lower: '', layerNamePosition: '' } // Check if the keycode is a sequence if (keycode.startsWith('KC.MACRO(') && keycode.endsWith(')')) { const reg = /^\s*KC\.MACRO\(\s*"([^"]*)"\s*\)\s*$/ if (reg.test(keycode)) { keylabel.action = '

String

' } else { keylabel.action = '

Macro

' } } else if (keycode.startsWith('customkeys.')) { keylabel.action = 'custom' keylabel.simple = false const customcode = keycode.substring(11) keylabel.main = `${customcode}` } else { // Check for modifier keys // if (keycode.includes('KC.LSHIFT') || keycode.includes('KC.RSHIFT') || // keycode.includes('KC.LCTL') || keycode.includes('KC.RCTL') || // keycode.includes('KC.LALT') || keycode.includes('KC.RALT')) { // label += '^ '; // } // Check for key presses const keyMatch = keycode.match(/KC\.(\w+)/) if (keyMatch) { const key = keyMatch[1] const foundKey = keyLabels[key] if (!foundKey) { keylabel.action = keycode } else if (foundKey.icon) { keylabel.action = `` } else if (foundKey.label) { keylabel.action += foundKey.label } // if it has arguments render them as keycode as well if (keycode.includes('(')) { const match = keycode.match(/^[^(]+\((.*)\)$/) // dont render options for some keys eg. MT if (match && match[1]) { const params = match[1].split(',').map((a) => renderLabel(a)) let maxParams = 10 if (['LT', 'MT'].includes(key)) { // keycodes that have a label at the top maxParams = 2 } switch (key) { case 'LT': keylabel.main = String(params[1].action) keylabel.lower = params[0].action keylabel.layer = Number(params[0].action) keylabel.simple = false keylabel.layerNamePosition = 'lower' break case 'OS': keylabel.main = String(params[0].action) keylabel.simple = false break case 'TD': keylabel.main = String(params.map((a) => a.action).join(' ')) keylabel.simple = false break case 'MT': keylabel.main = String(params[0].action) keylabel.lower = String(params[1].action) keylabel.simple = false break case 'HT': keylabel.main = String(params[0].action) keylabel.lower = String(params[1].action) keylabel.simple = false break case 'TT': keylabel.main = String(params[0].action) keylabel.layer = Number(params[0].action) keylabel.simple = false break case 'TO': keylabel.main = String(params[0].action) keylabel.layer = Number(params[0].action) keylabel.simple = false break case 'TG': keylabel.main = String(params[0].action) keylabel.layer = Number(params[0].action) keylabel.simple = false break case 'MO': keylabel.main = String(params[0].action) keylabel.layer = Number(params[0].action) keylabel.simple = false keylabel.layerNamePosition = 'main' break } keylabel.params = params.slice(0, maxParams) } } } else { // custom keycodes keylabel.action = keycode } } return keylabel } const controllers = { '0xcb_helios': ['GP29'] } export const microcontrollerPinValid = ({ controller, pin }: { controller: string pin: string }) => { // check if the controller has this pin return controllers[controller].includes(pin) } ================================================ FILE: src/renderer/src/helpers/saveConfigurationWrapper.ts ================================================ import { ref } from 'vue' // Global loading state const isLoading = ref(false) const loadingCallbacks = new Set<(loading: boolean) => void>() // Subscribe to loading state changes export const onLoadingChange = (callback: (loading: boolean) => void) => { const unwatch = () => { loadingCallbacks.delete(callback) } loadingCallbacks.add(callback) return unwatch } // Get current loading state export const getLoadingState = () => isLoading.value // Set loading state and notify all callbacks const setLoading = (loading: boolean) => { isLoading.value = loading loadingCallbacks.forEach((callback) => callback(loading)) } // Wrapper for saveConfiguration that automatically handles loading overlay export const saveConfigurationWithLoading = async (data: string) => { if (isLoading.value) { console.warn('Save operation already in progress, ignoring duplicate call') return } try { setLoading(true) await window.api.saveConfiguration(data) } catch (error) { console.error('Error in saveConfigurationWithLoading:', error) throw error } finally { // Don't set loading to false here - let the LoadingOverlay handle it // based on the progress events from the main process } } // Function to manually hide loading (for fallback timeouts) export const hideLoading = () => { setLoading(false) } ================================================ FILE: src/renderer/src/helpers/types.d.ts ================================================ ================================================ FILE: src/renderer/src/main.ts ================================================ import { createApp } from 'vue' import App from './App.vue' import router from './router' import '@mdi/font/css/materialdesignicons.css' import './style/index.css' // import '@vueform/multiselect/themes/default.css' import './style/multiselect.css' const app = createApp(App) app.use(router) app.mount('#app') ================================================ FILE: src/renderer/src/router/index.ts ================================================ import { createRouter, createWebHashHistory } from 'vue-router' import LaunchScreen from '../screens/LaunchScreen.vue' import AddKeyboard from '../screens/AddKeyboard.vue' import KeyboardConfigurator from '../screens/KeyboardConfigurator.vue' import KmkInstaller from '../components/KmkInstaller.vue' import SetupWizard from '../screens/SetupWizard.vue' import LayoutEditor from '../components/LayoutEditor.vue' import KeymapEditor from '../components/KeymapEditor.vue' import EncoderSetup from '../components/EncoderSetup.vue' import MatrixSetup from '../components/MatrixSetup.vue' import PinSetup from '../components/PinSetup.vue' import RawKeymapEditor from '../components/RawKeymapEditor.vue' import KeyboardName from '../components/KeyboardName.vue' import CoordMap from '../components/CoordMap.vue' import RgbSetup from '../components/RgbSetup.vue' import Community from '../components/Community.vue' // import Debug from '../components/debug.vue' import KeyboardSelector from '../screens/KeyboardSelector.vue' import CircuitPythonSetup from '../components/CircuitPythonSetup.vue' import SetupMethodSelector from '../components/SetupMethodSelector.vue' import AutomaticSetup from '../components/AutomaticSetup.vue' // import KeyboardSetup from '../screens/KeyboardSetup.vue' import InstallPogFirmware from '../components/installPogFirmware.vue' const routes = [ { path: '/', name: 'Launch', component: LaunchScreen }, { path: '/add-keyboard', name: 'Add Keyboard', component: AddKeyboard }, { // manual setup path: '/setup-wizard', name: 'Setup Wizard', component: SetupWizard }, { path: '/keyboard-selector', name: 'Keyboard Selector', component: KeyboardSelector }, { path: '/automatic-setup', name: 'Automatic Setup', children: [ { path: 'circuit-python', name: 'CircuitPython Setup', component: CircuitPythonSetup }, { path: 'method', name: 'Setup Method', component: SetupMethodSelector }, { path: 'mapping', name: 'Automatic Setup', component: AutomaticSetup }, { path: 'firmware', name: 'Pog Firmware', component: InstallPogFirmware } ] }, { path: '/configurator', name: 'Configurator', component: KeyboardConfigurator, children: [ { path: 'keymap', name: 'Keymap', component: KeymapEditor }, { path: 'layout-editor', name: 'Layout Editor', component: LayoutEditor }, { path: 'encoder', name: 'Encoder', component: EncoderSetup }, { path: 'info', name: 'Info', component: KeyboardName }, { path: 'matrix', name: 'Matrix', component: MatrixSetup }, { path: 'pins', name: 'Pins', component: PinSetup }, { path: 'coordmap', name: 'CoordMap', component: CoordMap }, { path: 'raw-keymap', name: 'Raw Keymap', component: RawKeymapEditor }, { path: 'firmware', name: 'Firmware', component: KmkInstaller }, { path: 'coordmap', component: CoordMap, name: 'CoordMap' }, { path: 'community', component: Community, name: 'Community' }, { path: 'rgb', name: 'RGB', component: RgbSetup } ] } ] export const router = createRouter({ history: createWebHashHistory(), routes }) export default router ================================================ FILE: src/renderer/src/screens/AddKeyboard.vue ================================================ ================================================ FILE: src/renderer/src/screens/KeyboardConfigurator.vue ================================================ ================================================ FILE: src/renderer/src/screens/KeyboardSelector.vue ================================================ ================================================ FILE: src/renderer/src/screens/KeyboardSetup.vue ================================================ ================================================ FILE: src/renderer/src/screens/LaunchScreen.vue ================================================ ================================================ FILE: src/renderer/src/screens/SetupWizard.vue ================================================ ================================================ FILE: src/renderer/src/store/index.ts ================================================ import { computed, ref } from 'vue' import VueStore from '@wlard/vue-class-store' import { ulid } from 'ulid' import { useRouter } from 'vue-router' import { matrixPositionToIndex } from '../helpers' import { useStorage } from '@vueuse/core' import dayjs from 'dayjs' const router = useRouter() // @ts-ignore will be used later type KeyActions = { type: 'chord' | 'tap' | 'short_hold' | 'hold' | 'macro' keycodes: string[] actions: KeyActions[] }[] export const keyboardHistory = useStorage('keyboardHistory', []) export const addToHistory = (keyboard) => { console.log('saving keyboard to history', keyboard) // to get rid of reactivity and proxys while deepcloning // does not work with structured clone const keyboardData = { ...JSON.parse(JSON.stringify(keyboard.serialize())), ...(keyboard.path && { path: keyboard.path }) } if (!keyboardHistory.value.find((board) => board.id === keyboard.id)) { keyboardHistory.value.unshift(keyboardData) } else { const index = keyboardHistory.value.findIndex((board) => board.id === keyboard.id) // push this version to the backups as well if (!keyboardHistory.value[index].backups) keyboardHistory.value[index].backups = [] const backups = [keyboardData, ...keyboardHistory.value[index].backups].slice(0, 100) keyboardHistory.value[index] = { ...keyboardData, backups } } } // list of key indexes that are selected export const selectedKeys = ref>(new Set()) // currently selected keymap layer export const selectedLayer = ref(0) type EncoderLayer = EncoderActions[] type EncoderActions = [string, string] export type BaseKeyInfo = { x: number y: number x2?: number y2?: number w?: number h?: number h2?: number w2?: number r?: number rx?: number ry?: number } export type KeyInfo = BaseKeyInfo & { matrix?: [number, number] variant?: [number, number] directPinIndex?: number coordMapIndex?: number idx?: number encoderIndex?: number keyboard?: any } export class Key { id = ulid() x = 0 x2?: number = undefined y = 0 y2?: number = undefined w = 1 w2?: number = undefined h = 1 h2?: number = undefined r = 0 rx = 0 ry = 0 matrix?: [number, number] = undefined coordMapIndex?: number = undefined encoderIndex?: number = undefined variant?: [number, number] = undefined keyboard?: any constructor({ x, y, matrix, variant, h, w, x2, y2, h2, w2, r, rx, ry, directPinIndex, // Todo: will be removed for idx in the future coordMapIndex, // Todo: will also be removed for idx idx, encoderIndex, keyboard }: KeyInfo) { this.x = x this.y = y if (matrix && matrix.length === 2) { this.matrix = matrix } if (variant && variant.length === 2) { this.variant = variant } // coordmap things if (!idx && typeof directPinIndex === 'number') idx = directPinIndex if (typeof idx === 'number') this.coordMapIndex = idx if (!idx && typeof coordMapIndex === 'number') this.coordMapIndex = coordMapIndex if (h) this.h = h if (w) this.w = w if (h2) this.h2 = h2 if (w2) this.w2 = w2 if (x2) this.x2 = x2 if (y2) this.y2 = y2 if (r) this.r = r if (rx) this.rx = rx if (ry) this.ry = ry if (typeof encoderIndex === 'number') this.encoderIndex = encoderIndex this.keyboard = keyboard } serialize() { const tmpKey: KeyInfo = { x: this.x, y: this.y } if (this.w !== 1) { tmpKey.w = this.w } if (this.h !== 1) { tmpKey.h = this.h } if (this.w2) { tmpKey.w2 = this.w2 } if (this.h2) { tmpKey.h2 = this.h2 } if (this.x2) { tmpKey.x2 = this.x2 } if (this.y2) { tmpKey.y2 = this.y2 } if (this.r) { tmpKey.r = this.r } if (this.ry) { tmpKey.ry = this.ry } if (this.rx) { tmpKey.rx = this.rx } if (Array.isArray(this.matrix) && this.matrix.length === 2) { tmpKey.matrix = this.matrix.map((n) => Number(n)) as [number, number] } if (Array.isArray(this.variant) && this.variant.length === 2) { tmpKey.variant = this.variant.map((n) => Number(n)) as [number, number] } if (typeof this.coordMapIndex === 'number') { tmpKey.idx = this.coordMapIndex } if (typeof this.encoderIndex === 'number') { tmpKey.encoderIndex = this.encoderIndex } return tmpKey } set({}) {} // happity hoppety property delta({ property, value }: { value: number; property: keyof BaseKeyInfo }) { console.log('writing delta', property, value, this[property]) if (this[property]) { // validate eg not less than 0 on w and h if (['w', 'h'].includes(property)) { if (this[property]! + value < 0.25) { return } } // @ts-ignore only using correct keys from type this[property] = this[property] + value } else { this[property] = value } } getKeymapIndex() { // if (!this.matrix && typeof this.coordMapIndex !== 'number') return undefined // if (this.keyboard.wiringMethod === 'matrix') { // return matrixPositionToIndex({ // pos: this.matrix || [0,0], // matrixWidth: keyboardStore.cols // }) // } else { return this.coordMapIndex // } } setOnKeymap(keyCode) { const keyIndex = this.getKeymapIndex() console.log('index', keyIndex, keyCode) if (typeof keyIndex !== 'number') return console.log('setting ', this.id, 'to', keyCode, 'at', keyIndex) if (!keyCode.includes('(')) { // TODO: could set this as arg in a key // if ( // currentKeyAction && // currentKeyAction.includes("(") && // selectedKey.value.args // ) { // // Validate for what args this function takes // // only set this as arg // // TODO: handle multiple args // let action = currentKeyAction.split("(")[0].replace(")", ""); // keymap.value[selectedLayer.value][keyIndex] = // action + "(" + keyCode + ")"; // return; // } } keyboardStore.keymap[selectedLayer.value][keyIndex] = keyCode } getMatrixLabel() { if (typeof this.coordMapIndex === 'number') return this.coordMapIndex if (this.matrix) { if ( typeof this.matrix[0] === 'number' && !isNaN(this.matrix[0]) && typeof this.matrix[1] === 'number' && !isNaN(this.matrix[1]) ) return `${this.matrix[0]} - ${this.matrix[1]}` if (typeof this.matrix[0] === 'number' && !isNaN(this.matrix[0])) return `${this.matrix[0]} - X` if (typeof this.matrix[1] === 'number' && !isNaN(this.matrix[1])) return `X - ${this.matrix[1]}` } return '' } getEncoderLabel() { if (typeof this.encoderIndex !== 'number') return { a: '', b: '' } return { a: this.keyboard.encoderKeymap[0][this.encoderIndex][0], b: this.keyboard.encoderKeymap[0][this.encoderIndex][1] } } } export type RgbOptions = { animationMode: number hueDefault: number satDefault: number valDefault: number animationSpeed: number breatheCenter: number knightEffectLength: number } export class Keyboard { id = ulid() path?: string = undefined name = '' manufacturer = '' tags: string[] = [] description = '' // serial interface port?: string = undefined // only set when serial is available usingSerial = false serialNumber = '' serialPortA?: string = undefined // Port with lower number serialPortB?: string = undefined // Port with higher number driveConnected = false driveContents: string[] = [] //manage the code.py yourself flashingMode: 'automatic' | 'manual' = 'automatic' pogConfigured = false firmwareInstalled = false // layout keys: Key[] = [] // Layout options layouts: { name: string; variants: string[]; selected: number }[] = [] coordMap: string[][] = [] // wiring controller = '' diodeDirection: 'ROW2COL' | 'COL2ROW' = 'COL2ROW' wiringMethod: 'matrix' | 'direct' = 'matrix' rows = 1 cols = 1 pins = 1 rowPins: string[] = [] colPins: string[] = [] directPins: string[] = [] coordMapSetup = false rgbPin = '' rgbNumLeds = 0 rgbOptions: RgbOptions = { animationMode: 0, hueDefault: 0, satDefault: 255, valDefault: 255, animationSpeed: 1, breatheCenter: 1, knightEffectLength: 3 } pinPrefix: 'board' | 'gp' | 'none' | 'quickpin' = 'gp' // features encoders: { pad_a: string; pad_b: string }[] = [] // split features keyboardType: 'normal' | 'splitBLE' | 'splitSerial' | 'splitOnewire' = 'normal' splitSide: 'left' | 'right' | 'vbus' | 'label' = 'left' // split = false splitPinA = '' splitPinB = '' vbusPin = 'VBUS_SENSE' splitUsePio = true splitFlip = false splitUartFlip = false // keymaps // layer > encoder index > encoder action index > keycode encoderKeymap: EncoderLayer[] = [] keymap: (string | undefined)[][] = [[]] layers: { name: string; color: string | undefined }[] = [] kbFeatures = [ 'basic', 'serial', 'oneshot', 'tapdance', 'holdtap', 'mousekeys', 'combos', 'macros' ] constructor() {} // Keys setKeys(keys: KeyInfo[]) { this.keys = [] if (!keys || keys.length === 0) return keys.forEach((key) => { const tmpKey = new Key({ ...key, keyboard: this }) this.keys.push(tmpKey) }) } getKeys() { return this.keys.map((key) => key.serialize()) } addKey(key) { this.keys.push(new Key({ ...key, keyboard: this })) } removeKeys({ ids }: { ids: string[] }) { this.keys = this.keys.filter((a) => !ids.includes(a.id)) } deltaForKeys({ keyIndexes, property, value }: { keyIndexes: number[] property: keyof BaseKeyInfo value: number }) { keyIndexes.forEach((keyIndex) => { this.keys[keyIndex].delta({ property, value }) }) } hasFile(filename) { return this.driveContents.includes(filename) } isSplit() { return this.keyboardType !== 'normal' } // count keys on the matrix physicalKeyCount() { let keycount = 0 if (this.wiringMethod === 'matrix') { keycount = this.rows * this.cols } else { keycount = this.pins } if (this.isSplit()) { keycount = keycount * 2 } return keycount } // count keys in the layout (including variant keys so duplicate physical keys) keyCount() { return this.keys.length } getMatrixWidth() { let width = 0 if (this.wiringMethod === 'matrix') { width = this.cols } else { width = this.pins } if (this.isSplit()) { width = width * 2 } return width } // get keymap index for matrix pos of a key getKeymapIndexForKey({ key }) { const keyIndex = matrixPositionToIndex({ pos: key.matrix, matrixWidth: this.getMatrixWidth() }) return keyIndex } getActionForKey({ key, layer }) { if (!this.keymap[layer]) return 'No layer' const keyCode = this.keymap[layer][key.getKeymapIndex()] // resolve readable character if (!keyCode || keyCode === 'KC.TRNS') return '▽' return keyCode } import({ path, configContents, folderContents, serial }: { path: string codeContents?: string configContents: any folderContents: string[] serial?: boolean }) { this.clear() this.id = ulid() this.path = path this.driveContents = folderContents this.usingSerial = serial === true this.pogConfigured = this.hasFile('pog.json') this.firmwareInstalled = this.hasFile('kmk') if (this.pogConfigured) { console.log('pog.json exists, importing keyboard features') this.setKeys(configContents.keys) if (configContents.id) this.id = configContents.id if (configContents.name) this.name = configContents.name if (configContents.description) this.description = configContents.description if (configContents.tags) this.tags = configContents.tags if (configContents.manufacturer) this.manufacturer = configContents.manufacturer if (configContents.keyboardType) this.keyboardType = configContents.keyboardType this.wiringMethod = configContents.wiringMethod || 'matrix' this.flashingMode = configContents.flashingMode || 'automatic' this.pinPrefix = configContents.pinPrefix || 'gp' this.coordMapSetup = configContents.coordMapSetup || false if (configContents.coordMap) this.coordMap = configContents.coordMap if (configContents.rows) this.rows = configContents.rows if (configContents.cols) this.cols = configContents.cols if (configContents.pins) this.pins = configContents.pins if (configContents.rowPins) this.rowPins = configContents.rowPins if (configContents.colPins) this.colPins = configContents.colPins if (configContents.diodeDirection) this.diodeDirection = configContents.diodeDirection if (configContents.directPins) this.directPins = configContents.directPins if (configContents.controller) this.controller = configContents.controller if (configContents.keymap) this.keymap = configContents.keymap if (configContents.layouts) this.layouts = configContents.layouts if (configContents.layers) this.layers = configContents.layers if (configContents.splitPinA) this.splitPinA = configContents.splitPinA if (configContents.splitPinB) this.splitPinB = configContents.splitPinB if (configContents.splitSide) this.splitSide = configContents.splitSide if (configContents.vbusPin) this.vbusPin = configContents.vbusPin if (configContents.splitUsePio) this.splitUsePio = configContents.splitUsePio if (configContents.splitFlip) this.splitFlip = configContents.splitFlip if (configContents.splitUartFlip) this.splitUartFlip = configContents.splitUartFlip // encoders if (configContents.encoders) this.encoders = configContents.encoders if (configContents.encoderKeymap) this.encoderKeymap = configContents.encoderKeymap //RGB if (configContents.rgbPin) this.rgbPin = configContents.rgbPin if (configContents.rgbNumLeds) this.rgbNumLeds = Number(configContents.rgbNumLeds) if (configContents.rgbOptions) this.rgbOptions = configContents.rgbOptions if (configContents.kbFeatures) this.kbFeatures = configContents.kbFeatures } } clear() { this.pogConfigured = false this.path = '' this.coordMap = [] this.layers = [] this.name = '' this.description = '' this.tags = [] this.manufacturer = '' this.keyboardType = 'normal' this.wiringMethod = 'matrix' this.flashingMode = 'automatic' this.pinPrefix = 'gp' this.coordMapSetup = false this.rows = 1 this.cols = 1 this.pins = 1 this.rowPins = [] this.colPins = [] this.directPins = [] this.diodeDirection = 'COL2ROW' this.controller = '' this.keymap = [] this.layouts = [] this.encoders = [] this.encoderKeymap = [] this.rgbPin = '' this.rgbNumLeds = 0 this.rgbOptions = { animationMode: 0, hueDefault: 0, satDefault: 255, valDefault: 255, animationSpeed: 1, breatheCenter: 1, knightEffectLength: 3 } // split pins this.splitPinA = '' this.splitPinB = '' this.splitSide = 'left' this.vbusPin = 'VBUS_SENSE' this.splitUsePio = true this.splitFlip = false this.splitUartFlip = false // Reset serial ports this.serialPortA = undefined this.serialPortB = undefined this.serialNumber = '' } serialize() { return { id: this.id, name: this.name, manufacturer: this.manufacturer, description: this.description, tags: this.tags, controller: this.controller, keyboardType: this.keyboardType, wiringMethod: this.wiringMethod, diodeDirection: this.wiringMethod == 'matrix' ? this.diodeDirection : '', rows: this.wiringMethod == 'matrix' ? this.rows : 0, cols: this.wiringMethod == 'matrix' ? this.cols : 0, pins: this.wiringMethod == 'direct' ? this.pins : 0, rowPins: this.wiringMethod == 'matrix' ? this.rowPins : [], colPins: this.wiringMethod == 'matrix' ? this.colPins : [], directPins: this.wiringMethod == 'direct' ? this.directPins : [], encoders: this.encoders, layouts: this.layouts, keys: this.getKeys(), keymap: this.keymap, encoderKeymap: this.encoderKeymap, layers: this.layers, split: this.keyboardType !== 'normal', splitPinA: this.splitPinA, splitPinB: this.splitPinB, splitSide: this.splitSide, vbusPin: this.vbusPin, splitUsePio: this.splitUsePio, splitFlip: this.splitFlip, splitUartFlip: this.splitUartFlip, coordMap: this.coordMap, pinPrefix: this.pinPrefix, coordMapSetup: this.coordMapSetup, rgbPin: this.rgbPin, rgbNumLeds: this.rgbNumLeds, rgbOptions: this.rgbOptions, kbFeatures: this.kbFeatures, flashingMode: this.flashingMode, lastEdited: dayjs().format('YYYY-MM-DD HH:mm') } } } @VueStore export class KeyboardStore extends Keyboard {} export const keyboardStore = new KeyboardStore() // [0,2,3] index of array is the selected layout and the value its option export const selectedVariants = ref([]) export const layoutVariants = ref<(string | string[])[]>([]) export const isNewKeyboardSetup = computed(() => { if (router) return router.currentRoute.value.path.startsWith('/setup-wizard') return false }) export const notifications = ref<{ label: string }[]>([]) export const pinPrefixHint = computed(() => { switch (keyboardStore.pinPrefix) { case 'gp': return 'Generates `board.GP1` like pins from numbers' break case 'board': return 'Generates `board.yourpin` like pins from text' break case 'none': return 'Generates `yourpin` like pins from text' break case 'quickpin': return 'Generates `pins[1]` like pins from numbers' default: return '' } }) export const splitSideHint = computed(() => { switch (keyboardStore.splitSide) { case 'label': return 'Detects split side by label on the microcontroller' break case 'vbus': return 'Detects split side using VBUS pin' break case 'left': return 'Detects split side using value in config file (left side)' break case 'right': return 'Detects split side using value in config file (right side)' break default: return '' } }) export const splitPinHint = computed(() => { switch (keyboardStore.keyboardType) { case 'splitSerial': return 'Defines the serial pins used to connect the two halves.' break case 'splitOnewire': return 'Defines the data pin used to connect the two halves' break default: return '' } }) export const vbusPinHint = computed(() => { return `Generates 'board.${keyboardStore.vbusPin}'` }) export const userSettings = useStorage('user-settings', { reduceKeymapColors: false, autoSelectNextKey: false }) export const serialKeyboards = ref([]) ================================================ FILE: src/renderer/src/store/serial.ts ================================================ import { ref } from 'vue' export const serialLogs = ref([]) const MAX_LOGS = 500 export function addSerialLine(raw: string) { const line = String(raw || '').trim() if (!line) return // ignore consecutive duplicates if (serialLogs.value[0] === line) return serialLogs.value.unshift(line) if (serialLogs.value.length > MAX_LOGS) serialLogs.value.length = MAX_LOGS } ================================================ FILE: src/renderer/src/style/index.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; ================================================ FILE: src/renderer/src/style/multiselect.css ================================================ .multiselect { @apply relative mx-auto w-full flex items-center justify-end box-border text-white cursor-pointer border border-white border-opacity-40 rounded-xl text-base leading-snug outline-none; } .multiselect.is-disabled { @apply cursor-default bg-gray-100; } .multiselect.is-open { @apply rounded-b-none; } .multiselect.is-open-top { @apply rounded-t-none; } /*.multiselect.is-active {*/ /* @apply ring ring-green-500 ring-opacity-30;*/ /*}*/ .multiselect-wrapper { @apply relative mx-auto w-full flex items-center justify-end box-border cursor-pointer outline-none; } .multiselect-single-label { @apply flex items-center h-full max-w-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5 pr-16 box-border rtl:left-auto rtl:right-0 rtl:pl-0 rtl:pr-3.5; } .multiselect-single-label-text { @apply overflow-ellipsis overflow-hidden block whitespace-nowrap max-w-full; } .multiselect-multiple-label { @apply flex items-center h-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5 rtl:left-auto rtl:right-0 rtl:pl-0 rtl:pr-3.5; } .multiselect-search { @apply w-full absolute inset-0 outline-none focus:ring-0 appearance-none box-border border-0 text-base font-sans bg-white rounded pl-3.5 rtl:pl-0 rtl:pr-3.5; } .multiselect-tags { @apply flex-grow flex-shrink flex flex-wrap items-center mt-1 pl-2 rtl:pl-0 rtl:pr-2; } .multiselect-tag { @apply bg-green-500 text-white text-sm font-semibold py-0.5 pl-2 rounded mr-1 mb-1 flex items-center whitespace-nowrap rtl:pl-0 rtl:pr-2 rtl:mr-0 rtl:ml-1; } .multiselect-tag.is-disabled { @apply pr-2 opacity-50 rtl:pl-2; } .multiselect-tag-remove { @apply flex items-center justify-center p-1 mx-0.5 rounded-sm hover:bg-black hover:bg-opacity-10; } .multiselect-tag-remove-icon { @apply bg-multiselect-remove bg-center bg-no-repeat opacity-30 inline-block w-3 h-3; } .multiselect-tag-remove:hover .multiselect-tag-remove-icon { @apply opacity-60; } .multiselect-tags-search-wrapper { @apply inline-block relative mx-1 mb-1 flex-grow flex-shrink h-full; } .multiselect-tags-search { @apply absolute inset-0 border-0 outline-none focus:ring-0 appearance-none p-0 text-base font-sans box-border w-full bg-transparent; } .multiselect-tags-search-copy { @apply invisible whitespace-pre-wrap inline-block h-px; } .multiselect-placeholder { @apply flex items-center h-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5 text-gray-400 rtl:left-auto rtl:right-0 rtl:pl-0 rtl:pr-3.5; } .multiselect-caret { @apply bg-multiselect-caret bg-center bg-no-repeat w-2.5 h-4 py-px box-content mr-3.5 relative z-10 opacity-40 flex-shrink-0 flex-grow-0 transition-transform transform pointer-events-none rtl:mr-0 rtl:ml-3.5; } .multiselect-caret.is-open { @apply rotate-180 pointer-events-auto; } .multiselect-clear { @apply pr-3.5 relative z-10 opacity-40 transition duration-300 flex-shrink-0 flex-grow-0 flex hover:opacity-80 rtl:pr-0 rtl:pl-3.5 ; } .multiselect-clear-icon { @apply bg-multiselect-remove bg-center bg-no-repeat w-2.5 h-4 py-px box-content inline-block; color: white; } .multiselect-spinner { @apply bg-multiselect-spinner bg-center bg-no-repeat w-4 h-4 z-10 mr-3.5 animate-spin flex-shrink-0 flex-grow-0 rtl:mr-0 rtl:ml-3.5; } .multiselect-inifite { @apply flex items-center justify-center w-full; } .multiselect-inifite-spinner { @apply bg-multiselect-spinner bg-center bg-no-repeat w-4 h-4 z-10 animate-spin flex-shrink-0 flex-grow-0 m-3.5; } .multiselect-dropdown { @apply max-h-60 absolute -left-px -right-px bottom-0 transform translate-y-full border border-gray-300 -mt-px overflow-y-scroll z-50 bg-white flex flex-col rounded-b; } .multiselect-dropdown.is-top { @apply -translate-y-full top-px bottom-auto rounded-b-none rounded-t; } .multiselect-dropdown.is-hidden { @apply hidden; } .multiselect-options { @apply flex flex-col p-0 m-0 list-none; } .multiselect-group { @apply p-0 m-0; } .multiselect-group-label { @apply flex text-sm box-border items-center justify-start text-left py-1 px-3 font-semibold bg-gray-200 cursor-default leading-normal; } .multiselect-group-label.is-pointable { @apply cursor-pointer; } .multiselect-group-label.is-pointed { @apply bg-gray-300 text-gray-700; } .multiselect-group-label.is-selected { @apply bg-green-600 text-white; } .multiselect-group-label.is-disabled { @apply bg-gray-100 text-gray-300 cursor-not-allowed; } .multiselect-group-label.is-selected.is-pointed { @apply bg-green-600 text-white opacity-90; } .multiselect-group-label.is-selected.is-disabled { @apply text-green-100 bg-green-600 bg-opacity-50 cursor-not-allowed; } .multiselect-group-options { @apply p-0 m-0; } .multiselect-option { @apply flex items-center justify-start box-border text-left cursor-pointer text-base leading-snug py-2 px-3; } .multiselect-option.is-pointed { @apply text-gray-800 bg-gray-100; } .multiselect-option.is-selected { @apply text-white bg-green-500; } .multiselect-option.is-disabled { @apply text-gray-300 cursor-not-allowed; } .multiselect-option.is-selected.is-pointed { @apply text-white bg-green-500 opacity-90; } .multiselect-option.is-selected.is-disabled { @apply text-green-100 bg-green-500 bg-opacity-50 cursor-not-allowed; } .multiselect-no-options { @apply py-2 px-3 text-gray-600 bg-white text-left rtl:text-right; } .multiselect-no-results { @apply py-2 px-3 text-gray-600 bg-white text-left rtl:text-right; } .multiselect-fake-input { @apply bg-transparent absolute left-0 right-0 -bottom-px w-full h-px border-0 p-0 appearance-none outline-none text-transparent; } .multiselect-assistive-text { @apply absolute -m-px w-px h-px overflow-hidden; clip: rect(0 0 0 0); } .multiselect-spacer { @apply h-9 py-px box-content; } ================================================ FILE: tailwind.config.js ================================================ /** @type {import('tailwindcss').Config} */ const svgToDataUri = require('mini-svg-data-uri') module.exports = { relative: true, content: ['./index.html', './src/**/*.vue'], theme: { extend: { backgroundImage: (theme) => ({ 'multiselect-caret': `url("${svgToDataUri( `` )}")`, 'multiselect-spinner': `url("${svgToDataUri( `` )}")`, 'multiselect-remove': `url("${svgToDataUri( `` )}")` }) } }, plugins: [require('daisyui')], daisyui: { // themes: false, // true: all themes | false: only light + dark | array: specific themes like this ["light", "dark", "cupcake"] darkTheme: 'worange', // name of one of the included themes for dark mode base: true, // applies background color and foreground color for root element by default styled: true, // include daisyUI colors and design decisions for all components utils: true, // adds responsive and modifier utility classes rtl: false, // rotate style direction from left-to-right to right-to-left. You also need to add dir="rtl" to your html tag and install `tailwindcss-flip` plugin for Tailwind CSS. prefix: '', // prefix for daisyUI classnames (components, modifiers and responsive class names. Not colors) logs: true, // Shows info about daisyU themes: [ { worange: { 'color-scheme': 'dark', 'primary-content': '#131616', secondary: '#6d3a9c', accent: '#51a800', 'accent-content': '#000000', neutral: '#2F1B05', info: '#2563eb', success: '#16a34a', warning: '#d97706', error: '#dc2626', primary: '#ef8f4c', 'base-100': '#171717', 'base-200': '#252525', 'base-300': '#383838' } } ] // darkTheme: 'halloween', // themes:[ // { // halloween:{ // ...require("daisyui/src/theming/themes")["[data-theme=halloween]"], // primary: '#ef8f4c', // 'base-100': '#171717', // 'base-200': '#252525', // 'base-300': '#383838', // } // } // ] } } ================================================ FILE: tsconfig.json ================================================ { "files": [], "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }], "compilerOptions": { "paths": { "@/*": [ "./*" ] } } } ================================================ FILE: tsconfig.node.json ================================================ { "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", "include": ["electron.vite.config.*", "src/main/*", "src/main/pythontemplates/*", "src/preload/*"], "compilerOptions": { "composite": true, "types": ["electron-vite/node"], "noUnusedLocals": false, "noUnusedParameters": false, } } ================================================ FILE: tsconfig.web.json ================================================ { "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", "include": [ "src/renderer/src/env.d.ts", "src/renderer/src/**/*", "src/renderer/src/**/*.vue", "src/preload/*.d.ts" ], "compilerOptions": { "experimentalDecorators": true, "composite": true, "baseUrl": ".", "paths": { "@renderer/*": [ "src/renderer/src/*" ] } } }