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
================================================

POG
KMK GUI, Layout Editor, Keymap Editor, Flashing Utility

# 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
================================================
Mapping your Pinout
Detected Configuration
Row Pins:
{{ detectionData.rows.join(', ') }}
Column Pins:
{{ detectionData.cols.join(', ') }}
Total Keys:
{{ detectionData.pressedKeys.length }}
Matrix Size:
{{ detectionData.rows.length }}x{{ detectionData.cols.length }}
Total Pins:
{{
detectionData.cols.length + detectionData.rows.length
}}
Instructions
To map your keyboard matrix, please:
1.
Press each key on your keyboard exactly once
2.
Make sure to hit all keys, including modifiers
3.
Watch the key preview below light up as you press
4.
Click "Done" when you've pressed all keys
Done pressing keys
================================================
FILE: src/renderer/src/components/BaseModal.vue
================================================
{{ props.title }}
{{ props.cancelText }}
{{ props.secondaryText }}
{{ props.confirmText }}
================================================
FILE: src/renderer/src/components/CircuitPythonSetup.vue
================================================
CircuitPython Setup Required
Before configuring your keyboard with Pog, CircuitPython needs to be installed on your board.
Download CircuitPython
for your board from circuitpython.org
Connect your keyboard while holding the RESET button
Copy the CircuitPython UF2 file to the mounted drive (the drive will be named RPI-RP2)
Wait for the board to restart (if will show up as a drive named CIRCUITPY)
Once CircuitPython is installed, your board will appear as a CIRCUITPY drive.
Select your CIRCUITPY drive:
{{ drive.name }}
{{ drive.path }}
No CIRCUITPY drives found.
Rescan
Back
Continue to Setup
Back
Continue
================================================
FILE: src/renderer/src/components/Community.vue
================================================
{{ renderedAccountAddress }}
sign
================================================
FILE: src/renderer/src/components/CoordMap.vue
================================================
Attention
Flashing the pog utilities on to the keyboard will delete the code.py and similar files from
the keyboard.
Be sure to backup your code if you still need any of it.
1. Install the debug code on the keyboard
Flash CoordMap Finder to keyboard
2. Click the text area and follow the guide below
3. Now press each key starting in the top left corner in the first row and moving to the
right when you reached the end press the last key once again to start with the next row
If nothing is happening first replug the board in case it hasnt started and wait 5 seconds.
If this did not help check the diode direction or pins.
the coordmap should be printed as a list of 3 digit numbers seperated by spaces.
eg 001 005 008 002 ...
it will print this via a hotkey on the number row so make sure to switch to something like
qwerty if you are using azerty or another layout that maps other keys to the number row. for
split keyboards try the coordmap with the type set to normal as depending on the split side
detection the secondary half might not output to usb.
new Row
add Space
remove last
clear
{{ initialSetup ? 'next' : 'save Coord Maping & create keyboard layout' }}
only save Coord Maping
note if your key indexes changed you need to rebuild your layout or adjust the indexes on the
on the layout editor, only saving the coord map is only advisable if you wanted to modify
spacings but not the order of the keys
================================================
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
================================================
Key #{{ [...selectedKeys][0] }}
{{ selectedKeys.size }} keys selected
Previous Key
Next Key
Basics
Matrix
Variant
Variant option
Rotation
deg
================================================
FILE: src/renderer/src/components/KeyPicker.vue
================================================
Empty
▽
=
,
~
!
@
#
$
%
^
&
*
(
)
_
+
{
}
<
>
:
|
?
"
Play Pause
Mute
Vol Down
Vol Up
next track (OSX)
prev track (OSX)
next track (win)
prev track (win)
stop track (win)
bright up
bright down
eject (OSX)
RGB Toggle
RGB Hue increase
RGB Hue decrease
you can build way more advanced things with custom keys here are some resources, for some you might need to edit the kb.py file or enable a keyboard feature. when testing these check the output of the REPL for errors
Macros KC.MACRO("send a string")
Keycodes List of all keys
Layers How layers work
Combos Multiple keys pressed at simultaneously output a different key
Holdtap Holding a key down for longer than a certain amount of time outputs a different key
Combo Layers pressing 2 layer keys at once opens a different layer
================================================
FILE: src/renderer/src/components/KeyboardLayout.vue
================================================
================================================
FILE: src/renderer/src/components/KeyboardName.vue
================================================
Keyboard Features
{{ formatFeatureName(option) }}
next
================================================
FILE: src/renderer/src/components/KeymapEditor.vue
================================================
Keycode Options for Selected Key(s)
{{ coordMapWarning }}
To add your own custom keycodes, edit the file `customkeys.py` to add your own and then use
them with `customkeys.MyKey` in your keymap
================================================
FILE: src/renderer/src/components/KeymapLayer.vue
================================================
{{ index }} {{ keyboardStore.layers[index].name }}
================================================
FILE: src/renderer/src/components/KmkInstaller.vue
================================================
Updating the POG files will overwrite all files on your keyboard generated by POG (e.g. kb.py,
code.py, customkeys.py, etc.)
Be sure to backup your code if you still need any of it.
Do you want to generate new ID for the restored configuration? This is only recommended if you
are restoring a configuration from another keyboard.
KMK is a capable firmware for
keyboards using the Rp2040.
Before you proceed make sure you installed
CircuitPython
on your controller
Info: This does not work when the controller is only connected via the serial port (and not as
mounted usb drive)
By clicking the button below, you can install KMK automatically to the following drive:
{{ keyboardStore.path }}
Next
{{ kmkInstallState || '' }}
{{ isNaN(progress) ? 'Done' : progress }}%
================================================
FILE: src/renderer/src/components/LayoutEditor.vue
================================================
Import from KLE
Import from Qmk info json
export from pog
you can import json files from the
keyboard layout editor
set the top left label to the matrix position eg '0,1' for row:0 col:1
set the bottom right label for a layout variant
eg. '0,1' this would be the first layout option using its other variant
this will overwrite your existing layout
convert to pog.json
================================================
FILE: src/renderer/src/components/LoadingOverlay.vue
================================================
{{ headline }}
{{ showLogs ? 'Hide logs' : 'Show logs' }}
{{ statusLine }}
{{ line }}
================================================
FILE: src/renderer/src/components/MacroModal.vue
================================================
Custom macro builder
Parse Error:
{{ parseError }}
Number of keys:
{{ num }} keys
{{ macroKeys[index].keycode }}
Key {{ index + 1 }}
+
Cancel
Apply
================================================
FILE: src/renderer/src/components/MatrixSetup.vue
================================================
Define the size of your keyboard matrix here, set it as big as you need. For easier wiring set
it to the max number of cols/rows on your keyboard
Wiring Method
Matrix
Direct Pins
Next
================================================
FILE: src/renderer/src/components/PinSetup.vue
================================================
Define the mapping for columns and rows to the microcontroller pins. For split keyboards
define the method for detecting the split side.
Next
Row Pins
{{ keyboardStore.rowPins.length }}
Direct Pins
{{
keyboardStore.directPins.length
}}
Split Pins {{ numberOfSplitPins }}
Pin Prefix
GP
none
board
quickpin
{{ pinPrefixHint }}
Diode Direction
COL2ROW
ROW2COL
Split Side Detection
Config (Left)
Config (Right)
VBUS
Drive Label (L/R)
{{ splitSideHint }}
Microcontroller
{{ microcontroller.name }}
other
Feel free to submit other microcontroller pinouts. Ensure you have the permission to use
the pinout image if it has not been created by you. In the meantime here are links to
other pinouts
Currently this tool works with any RP2040 controller.
Just look for a pinout and use any pin that is starting with GP.
================================================
FILE: src/renderer/src/components/RawKeymapEditor.vue
================================================
Keymap
================================================
FILE: src/renderer/src/components/RgbSetup.vue
================================================
================================================
FILE: src/renderer/src/components/SetupMethodSelector.vue
================================================
Choose Setup Method
{{
kmkInstallState !== 'done'
? 'Installing firmware... (should take up to 10 seconds)'
: 'Firmware installed'
}}
initial Firmware install complete, please unplug your keyboard and plug it back in to
continue
Serial Port
Select the serial port for your keyboard. We've automatically detected the most likely port.
Rescan
{{ port.port }}
{{ port.manufacturer || 'Unknown manufacturer' }}
{{ port.serialNumber ? `(${port.serialNumber})` : '' }}
Recommended
No serial ports found.
Rescan
Manual Setup
Configure your keyboard manually by specifying the pins, rows, columns, and diode direction.
Recommended for experienced users or if you have your keyboard's documentation.
Automatic Detection (Experimental)
Let Pog automatically detect your keyboard's configuration by pressing each key. We'll flash
a special firmware and guide you through the process.
================================================
FILE: src/renderer/src/components/VariantOption.vue
================================================
================================================
FILE: src/renderer/src/components/VariantSwitcher.vue
================================================
add Layout Option
================================================
FILE: src/renderer/src/components/debug.vue
================================================
Connect to your keyboard's serial console for debugging. Press the reload key on your
keyboard first.
Select a port
{{ port.manufacturer }} - {{ port.port }}
{{ isConnecting ? 'Connecting...' : 'Connect' }}
Disconnect
Send
Enter REPL
Exit REPL
================================================
FILE: src/renderer/src/components/installPogFirmware.vue
================================================
Installing Firmware
{{ kmkInstallState || 'Preparing...' }}
================================================
FILE: src/renderer/src/components/picker-layouts/Colemak.vue
================================================
ESC
F1
F2
F3
F4
F5
F6
F7
F8
F9
F10
F11
F12
Print Screen
Scroll Lock
Pause
~ `
! 1
@ 2
# 3
$ 4
% 5
^ 6
& 7
* 8
( 9
) 0
_ -
+ =
Bksp
Insert
Home
Page Up
Num Lock
/
*
-
Tab
Q
W
F
P
G
J
L
U
Y
: ;
{ [
} ]
| \
Del
End
Page Down
7
8
9
+
Caps Lock
A
R
S
T
D
H
N
E
I
O
" '
Enter
4
5
6
+
LShift
Z
X
C
V
B
K
M
< ,
> ,
? /
RShift
1
2
3
Num Enter
LCtrl
LGui
LAlt
Space
RAlt
RGui
Menu
RCTL
0
0
.
Num Enter
================================================
FILE: src/renderer/src/components/picker-layouts/ColemakDH.vue
================================================
ESC
F1
F2
F3
F4
F5
F6
F7
F8
F9
F10
F11
F12
Print Screen
Scroll Lock
Pause
~ `
! 1
@ 2
# 3
$ 4
% 5
^ 6
& 7
* 8
( 9
) 0
_ -
+ =
Bksp
Insert
Home
Page Up
Num Lock
/
*
-
Tab
Q
W
F
P
B
J
L
U
Y
: ;
{ [
} ]
| \
Del
End
Page Down
7
8
9
+
Caps Lock
A
R
S
T
G
M
N
E
I
O
" '
Enter
4
5
6
+
LShift
Z
X
C
D
V
K
H
< ,
> ,
? /
RShift
1
2
3
Num Enter
LCtrl
LGui
LAlt
Space
RAlt
RGui
Menu
RCTL
0
0
.
Num Enter
================================================
FILE: src/renderer/src/components/picker-layouts/Dvorak.vue
================================================
ESC
F1
F2
F3
F4
F5
F6
F7
F8
F9
F10
F11
F12
Print Screen
Scroll Lock
Pause
~ `
! 1
@ 2
# 3
$ 4
% 5
^ 6
& 7
* 8
( 9
) 0
{ [
} ]
Bksp
Insert
Home
Page Up
Num Lock
/
*
-
Tab
" '
< ,
> .
P
Y
F
G
C
R
L
? /
+ =
| \
Del
End
Page Down
7
8
9
+
Caps Lock
A
O
E
U
I
D
H
T
N
S
_ -
Enter
4
5
6
+
LShift
: ;
Q
J
K
X
B
M
W
V
Z
RShift
1
2
3
Num Enter
LCtrl
LGui
LAlt
Space
RAlt
RGui
Menu
RCTL
0
0
.
Num Enter
================================================
FILE: src/renderer/src/components/picker-layouts/Qwerty.vue
================================================
ESC
F1
F2
F3
F4
F5
F6
F7
F8
F9
F10
F11
F12
Print Screen
Scroll Lock
Pause
~ `
! 1
@ 2
# 3
$ 4
% 5
^ 6
& 7
* 8
( 9
) 0
_ -
+ =
Bksp
Insert
Home
Page Up
Num Lock
/
*
-
Tab
Q
W
E
R
T
Y
U
I
O
P
{ [
} ]
| \
Del
End
Page Down
7
8
9
+
Caps Lock
A
S
D
F
G
H
J
K
L
: ;
" '
Enter
4
5
6
+
LShift
Z
X
C
V
B
N
M
< ,
> ,
? /
RShift
1
2
3
Num Enter
LCtrl
LGui
LAlt
Space
RAlt
RGui
Menu
RCTL
0
0
.
Num Enter
================================================
FILE: src/renderer/src/components/setup/Wizard.vue
================================================
Before you start please check that your controller is using circuit python, if not please
download the matching firmware and drag it on the usb drive that appears when you enter its
bootloader mode
when you see the circuit python usb drive it is a good time to rename the drive to reflect
your keyboard name
With this done please select the drive so POG can start to flash the firmware, you will be
able to configure everything after the flashing is done
Select Drive
now unplug the keyboard and plug it back in to boot up the pog code
loading
define here how the pins are used
info prepare controller with circuit python
plugin controller and select the usb drive
flash kmk on drive + add pog files and an id in the pog.json
unplug and replug keyboard to boot pog
listen for serial ports with pog manufacturer then grab its info and compare with created id
open both connections to repl and data for debugging
name + matrix size + pins[rows,cols,directpins,rgb,encoders] (loadable from qr or link)
coordmap press each key
use generated layout or modify (or was loaded from link/qr)
================================================
FILE: src/renderer/src/components/ui/InputLabel.vue
================================================
{{ label }}
================================================
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
================================================
Create a Custom Keyboard firmware from scratch or choose a Keyboard from the community
from scratch
Select a Keyboard
{{ keyboardStore.path }}
{{ keyboardStore.keys.length }}
================================================
FILE: src/renderer/src/screens/KeyboardConfigurator.vue
================================================
================================================
FILE: src/renderer/src/screens/KeyboardSelector.vue
================================================
Back
Recent Keyboards
{{ keyboard.name }}
{{ keyboard.path }}
Set Up New Keyboard
New Keyboard
Add existing POG keyboard
================================================
FILE: src/renderer/src/screens/KeyboardSetup.vue
================================================
================================================
FILE: src/renderer/src/screens/LaunchScreen.vue
================================================
Easy Keyboard Configurator
Effortlessly customize your keyboard with Pog
add keyboard
Serial BETA
Serial available
USB Drive Mounted
USB Drive Disconnected
Read Only Serial
{{ keyboard.name }}
{{ keyboard.manufacturer }}
{{ keyboard.path }}
{{ keyboard.description }}
================================================
FILE: src/renderer/src/screens/SetupWizard.vue
================================================
Initial Keyboard Setup
Keyboard: {{ keyboardStore.path }}
Firmware
1 ? (currentStep = 1) : undefined"
>
Name
2 ? (currentStep = 2) : undefined"
>
Matrix
3 ? (currentStep = 3) : undefined"
>
Pins
4 ? (currentStep = 4) : undefined"
>
Coordmap
5 ? (currentStep = 5) : undefined"
>
Layout
================================================
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/*"
]
}
}
}