Showing preview only (387K chars total). Download the full file or copy to clipboard to get everything.
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
================================================

<h1 align="center">POG</h1>
<h4 align="center">
KMK GUI, Layout Editor, Keymap Editor, Flashing Utility
</h4>
<p align="center">
<a href="https://github.com/JanLunge/pog/stargazers"><img src="https://img.shields.io/github/stars/JanLunge/pog" alt="Stars Badge"/></a>
<a href="https://github.com/JanLunge/pog/network/members"><img src="https://img.shields.io/github/forks/JanLunge/pog" alt="Forks Badge"/></a>
<img src="https://badgen.net/badge/version/v1.4.4" alt="">
</p>

# 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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>
================================================
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<void>((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<void>((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<number> => {
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<number> => {
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<void> {
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<void>
serialDisconnect: () => Promise<void>
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<any>
deselectKeyboard: () => Promise<void>
openExternal: (url: string) => Promise<void>
selectDrive: () => Promise<any>
updateFirmware: () => Promise<void>
saveConfiguration: (data: any) => Promise<void>
rescanKeyboards: () => Promise<void>
checkForUSBKeyboards: (keyboardPaths: string[]) => Promise<any>
}
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
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>POG</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<!-- <meta-->
<!-- http-equiv="Content-Security-Policy"-->
<!-- content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"-->
<!-- />-->
<script src="http://localhost:8098"></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
================================================
FILE: src/renderer/src/App.vue
================================================
<script setup lang="ts">
import { computed, onMounted, onUnmounted } from 'vue'
import { addToHistory, keyboardStore, notifications, serialKeyboards } from './store'
import { addSerialLine } from './store/serial'
import { useRouter } from 'vue-router'
import LoadingOverlay from './components/LoadingOverlay.vue'
const router = useRouter()
const store = computed(() => {
return keyboardStore
})
console.log('store added to debug menu', store)
window.api.keyboardScan((_event: Event, value: { keyboards }) => {
console.log('found keyboards via serial', value)
serialKeyboards.value = value.keyboards.map((a) => {
const b = a
b.port = b.path
delete b.path
return b
})
})
window.api.serialKeyboardPogConfig((_event: Event, value: { pogconfig }) => {
console.log('loaded pog config', value)
keyboardStore.import({
path: '',
serial: true,
folderContents: ['pog.json', 'kmk'],
configContents: value.pogconfig
})
if (keyboardStore.pogConfigured) {
addToHistory(keyboardStore)
}
router.push('/configurator/keymap')
})
let serialHandler: ((event: any, data: { message: string }) => void) | null = null
onMounted(() => {
serialHandler = (_event, data) => addSerialLine(data.message)
window.api.serialData(serialHandler)
})
onUnmounted(() => {
if (serialHandler) window.api.offSerialData(serialHandler)
serialHandler = null
})
</script>
<template>
<div class="notifications">
<div v-for="(notification, nindex) in notifications" class="alert alert-error shadow-lg">
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 flex-shrink-0 stroke-current"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{{ notification.label }}</span>
<button class="btn btn-ghost btn-sm" @click="notifications.splice(nindex, 1)">
<i class="mdi mdi-close"></i>
</button>
</div>
</div>
</div>
<router-view></router-view>
<LoadingOverlay />
</template>
<style lang="scss">
html,
body,
#app {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen-Sans, Ubuntu, Cantarell,
Helvetica Neue, sans-serif;
}
.tooltip {
@apply rounded bg-base-300 p-4 shadow;
max-width: 300px;
}
.notifications {
position: absolute;
top: 0;
display: flex;
@apply z-20 flex-col gap-4 p-4;
}
</style>
================================================
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 <a class=\"link-primary link\" target=\"_blank\" href=\"https://keeb.supply/products/0xcb-helios\">0xCB Helios</a> 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 <a class=\"link-primary link\" target=\"_blank\" href=\"https://www.raspberrypi.com/products/raspberry-pi-pico/\">Raspberry Pi Pico</a> 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
================================================
<template>
<div class="min-h-screen bg-base-100 p-6">
<div class="mx-auto max-w-5xl space-y-8">
<div class="flex items-center justify-between">
<h2 class="text-center text-4xl font-bold text-base-content">Mapping your Pinout</h2>
<button class="btn btn-ghost" @click="toggleDebug">
<i class="mdi mdi-bug text-2xl" :class="{ 'text-primary': showDebug }"></i>
</button>
</div>
<div class="grid gap-6 md:grid-cols-2">
<!-- Detection Info Panel -->
<div class="card bg-base-200 shadow-lg">
<div class="card-body">
<h3 class="card-title text-xl font-bold text-base-content">Detected Configuration</h3>
<div class="space-y-4 text-base-content/80">
<div class="flex justify-between">
<span class="font-medium">Row Pins:</span>
<span class="font-mono text-primary">{{ detectionData.rows.join(', ') }}</span>
</div>
<div class="flex justify-between">
<span class="font-medium">Column Pins:</span>
<span class="font-mono text-primary">{{ detectionData.cols.join(', ') }}</span>
</div>
<div class="flex justify-between">
<span class="font-medium">Total Keys:</span>
<span class="font-mono text-primary">{{ detectionData.pressedKeys.length }}</span>
</div>
<div class="flex justify-between">
<span class="font-medium">Matrix Size:</span>
<span class="font-mono text-primary"
>{{ detectionData.rows.length }}x{{ detectionData.cols.length }}</span
>
</div>
<div class="flex justify-between">
<span class="font-medium">Total Pins:</span>
<span class="font-mono text-primary">{{
detectionData.cols.length + detectionData.rows.length
}}</span>
</div>
</div>
</div>
</div>
<!-- Instructions Panel -->
<div class="card bg-base-200 shadow-lg">
<div class="card-body">
<h3 class="card-title text-xl font-bold text-base-content">Instructions</h3>
<div class="space-y-4">
<p class="text-base-content/80">To map your keyboard matrix, please:</p>
<ol class="list-inside list-decimal space-y-3 text-base-content/80">
<li class="flex items-center space-x-2">
<span class="font-medium">1.</span>
<span>Press each key on your keyboard exactly once</span>
</li>
<li class="flex items-center space-x-2">
<span class="font-medium">2.</span>
<span>Make sure to hit all keys, including modifiers</span>
</li>
<li class="flex items-center space-x-2">
<span class="font-medium">3.</span>
<span>Watch the key preview below light up as you press</span>
</li>
<li class="flex items-center space-x-2">
<span class="font-medium">4.</span>
<span>Click "Done" when you've pressed all keys</span>
</li>
</ol>
</div>
</div>
</div>
</div>
<!-- Debug Panel -->
<div v-if="showDebug" class="card bg-base-200 shadow-lg">
<div class="card-body">
<Debug />
</div>
</div>
<!-- Key Preview -->
<div class="card bg-base-200 shadow-lg">
<div class="card-body">
<h3 class="card-title text-xl font-bold text-base-content">Key Press Preview</h3>
<div class="grid w-full grid-cols-[repeat(auto-fill,minmax(48px,1fr))] gap-2 p-4">
<div
v-for="(key, index) in detectionData.pressedKeys"
:key="index"
class="flex h-12 items-center justify-center rounded-lg font-medium transition-all duration-200"
:class="{
'bg-base-300 text-base-content': !(
detectionData.lastKeyPress &&
detectionData.lastKeyPress.row === key.row &&
detectionData.lastKeyPress.col === key.col
),
'scale-105 bg-primary text-primary-content shadow-md':
detectionData.lastKeyPress &&
detectionData.lastKeyPress.row === key.row &&
detectionData.lastKeyPress.col === key.col
}"
>
{{ index + 1 }}
</div>
</div>
</div>
</div>
<!-- Action Button -->
<div class="flex justify-center">
<button class="btn btn-primary" @click="proceed">Done pressing keys</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import router from '@renderer/router'
import { keyboardStore } from '@renderer/store'
import { ref, onMounted, onUnmounted } from 'vue'
import Debug from './debug.vue'
const showDebug = ref(false)
const detectionData = ref<{
rows: string[]
cols: string[]
pressedKeys: { row: number; col: number }[]
lastKeyPress: { row: number; col: number } | null
}>({
rows: [],
cols: [],
pressedKeys: [],
lastKeyPress: null
})
onMounted(async () => {
await window.api.startDetection()
})
function handleDetectionUpdate(data: any, event: any) {
// detectionData.value = data
console.log('handleDetectionUpdate', data, event)
switch (event.type) {
case 'new_key_press':
detectionData.value.pressedKeys.push({ row: event.row, col: event.col })
detectionData.value.lastKeyPress = { row: event.row, col: event.col }
break
case 'existing_key_press':
detectionData.value.lastKeyPress = { row: event.row, col: event.col }
if (
!detectionData.value.pressedKeys.some(
(key) => key.row === event.row && key.col === event.col
)
) {
detectionData.value.pressedKeys.push({ row: event.row, col: event.col })
}
break
case 'used_pins':
detectionData.value.rows = event.rows
detectionData.value.cols = event.cols
break
}
}
function proceed() {
// Emit completion event with configuration
// window.api.stopDetection()
// detectionData.value
keyboardStore.rowPins = detectionData.value.rows
keyboardStore.colPins = detectionData.value.cols
keyboardStore.rows = detectionData.value.rows.length
keyboardStore.cols = detectionData.value.cols.length
keyboardStore.diodeDirection = 'ROW2COL'
keyboardStore.coordMapSetup = true
keyboardStore.pinPrefix = 'board'
router.push('/automatic-setup/firmware')
}
defineEmits(['setup-complete'])
onMounted(() => {
console.log('onMounted')
window.api.onDetectionUpdate((data: any, event: any) => handleDetectionUpdate(data, event))
})
onUnmounted(() => {
// window.api.removeDetectionListeners()
// window.api.stopDetection()
})
function toggleDebug() {
showDebug.value = !showDebug.value
}
</script>
================================================
FILE: src/renderer/src/components/BaseModal.vue
================================================
<template>
<Transition
enter-active-class="transition duration-300 ease-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition duration-200 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div
v-if="props.open"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
@click.self="$emit('close')"
>
<div
class="relative w-full max-w-md rounded-2xl border border-gray-200/20 bg-base-100/80 p-8 shadow-2xl backdrop-blur-md"
>
<h3 v-if="props.title" class="mb-2 text-xl font-semibold">{{ props.title }}</h3>
<div class="py-2">
<slot />
</div>
<div class="mt-6 flex items-center justify-end gap-2">
<button class="btn justify-self-start" @click="$emit('close')">
{{ props.cancelText }}
</button>
<button
v-if="props.secondaryText"
class="btn btn-primary justify-self-center"
@click="$emit('secondary')"
>
{{ props.secondaryText }}
</button>
<div v-else></div>
<button
v-if="props.showConfirm && props.confirmText"
class="btn btn-primary justify-self-end"
@click="$emit('confirm')"
>
{{ props.confirmText }}
</button>
</div>
</div>
</div>
</Transition>
</template>
<script setup lang="ts">
interface Props {
open: boolean
title?: string
confirmText?: string
cancelText?: string
secondaryText?: string
showConfirm?: boolean
}
const props = withDefaults(defineProps<Props>(), {
title: '',
confirmText: '',
cancelText: 'Cancel',
secondaryText: '',
showConfirm: true
})
defineEmits<{
close: []
confirm: []
secondary: []
}>()
</script>
================================================
FILE: src/renderer/src/components/CircuitPythonSetup.vue
================================================
<template>
<div class="flex flex-col items-center justify-center p-8">
<h2 class="mb-6 text-2xl font-bold">CircuitPython Setup Required</h2>
<div class="max-w-2xl rounded-lg bg-base-100 p-6">
<p class="mb-4">
Before configuring your keyboard with Pog, CircuitPython needs to be installed on your board.
</p>
<ol class="mb-4 list-inside list-decimal space-y-2">
<li>Download CircuitPython
for your board from circuitpython.org</li>
<li>Connect your keyboard while holding the RESET button</li>
<li>Copy the CircuitPython UF2 file to the mounted drive (the drive will be named RPI-RP2)</li>
<li>Wait for the board to restart (if will show up as a drive named CIRCUITPY)</li>
</ol>
<p class="mb-4">
Once CircuitPython is installed, your board will appear as a CIRCUITPY drive.
</p>
<!-- Drive Selection -->
<div v-if="showDriveSelect" class="mt-6 space-y-4">
<p class="font-medium">Select your CIRCUITPY drive:</p>
<div v-if="drives.length > 0" class="space-y-2">
<div
v-for="drive in drives"
:key="drive.path"
class="cursor-pointer rounded-lg border p-3 transition-colors"
:class="{
'border-primary bg-primary/10': selectedDrive === drive.path
}"
@click="selectedDrive = drive.path"
>
<div class="flex items-center">
<i class="mdi mdi-usb mr-2 text-xl"></i>
<div>
<p class="font-medium">{{ drive.name }}</p>
<p class="text-sm text-gray-600">{{ drive.path }}</p>
</div>
</div>
</div>
</div>
<div v-else class="text-center text-gray-600">
<p>No CIRCUITPY drives found.</p>
<button
class="mt-2 rounded border px-4 py-2 text-sm hover:bg-gray-50"
@click="scanDrives"
>
Rescan
</button>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="mt-6 flex gap-4">
<button v-if="!showDriveSelect" class="btn" @click="router.back()">Back</button>
<button v-if="!showDriveSelect" class="btn btn-primary" @click="showDriveSelect = true">
Continue to Setup
</button>
<template v-else>
<button class="btn" @click="showDriveSelect = false">Back</button>
<button class="btn btn-primary" :disabled="!selectedDrive" @click="handleContinue">
Continue
</button>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import router from '../router'
import { keyboardStore } from '../store'
const showDriveSelect = ref(false)
const selectedDrive = ref('')
const drives = ref<{ path: string; name: string }[]>([])
async function scanDrives() {
try {
const result = await window.api.listDrives()
// drives.value = result.filter(drive =>
// drive.name.toLowerCase().includes('circuitpy') ||
// drive.path.toLowerCase().includes('circuitpy')
// )
drives.value = result
} catch (error) {
console.error('Failed to scan drives:', error)
}
}
function handleContinue() {
if (selectedDrive.value) {
// emit('continue', selectedDrive.value)
keyboardStore.path = selectedDrive.value
router.push('/automatic-setup/method')
}
}
// const emit = defineEmits(['continue'])
onMounted(() => {
scanDrives()
})
</script>
================================================
FILE: src/renderer/src/components/Community.vue
================================================
<template>
<div>
<w3m-core-button label="Login"></w3m-core-button>
<div v-if="accountAddress">
{{ renderedAccountAddress }}
<div class="btn" @click="sign">sign</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { EthereumClient, modalConnectors, walletConnectProvider } from '@web3modal/ethereum'
import { Web3Modal } from '@web3modal/html'
import { configureChains, createClient, signMessage } from '@wagmi/core'
import { arbitrum, mainnet, polygon } from '@wagmi/core/chains'
import { watchAccount } from '@wagmi/core'
import { computed, ref } from 'vue'
const chains = [arbitrum, mainnet, polygon]
// Wagmi Core Client
const { provider } = configureChains(chains, [
walletConnectProvider({ projectId: '7f2678536a5b6b5643d94a6428e341a1' })
])
const wagmiClient = createClient({
autoConnect: true,
connectors: modalConnectors({
projectId: '7f2678536a5b6b5643d94a6428e341a1',
version: '1', // or "2"
appName: 'web3Modal',
chains
}),
provider
})
// Web3Modal and Ethereum Client
const ethereumClient = new EthereumClient(wagmiClient, chains)
const web3modal = new Web3Modal(
{
projectId: '7f2678536a5b6b5643d94a6428e341a1',
themeColor: 'orange',
themeBackground: 'themeColor',
enableAccountView: false
},
ethereumClient
)
console.log(web3modal)
// const unsubscribe = web3modal.subscribeModal((newState) =>
// console.log(newState)
// );
const accountAddress = ref('')
watchAccount((account) => {
if (account.address) {
console.log('account changed', account.address)
accountAddress.value = account.address
} else {
accountAddress.value = ''
console.log('account disconnected')
}
})
const sign = async () => {
const signature = await signMessage({
message: 'pog wants authorisation'
})
console.log(signature)
}
const renderedAccountAddress = computed(() => {
return accountAddress.value.slice(0, 4) + '...' + accountAddress.value.slice(-4)
})
</script>
<style lang="scss" scoped></style>
================================================
FILE: src/renderer/src/components/CoordMap.vue
================================================
<template>
<dialog id="flash_modal" class="modal">
<div class="modal-box">
<h3 class="text-lg font-bold">Attention</h3>
<p class="py-4">
Flashing the pog utilities on to the keyboard will delete the code.py and similar files from
the keyboard.
</p>
<p class="py-4">Be sure to backup your code if you still need any of it.</p>
<div class="flex justify-between">
<div class="btn">Abort</div>
<div class="btn btn-warning" @click="flashCoordMapping({ overwrite: true })">Flash POG</div>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<div>
<p class="py-4">1. Install the debug code on the keyboard</p>
<button class="btn btn-primary btn-sm" @click="promptFlashing">
Flash CoordMap Finder to keyboard
</button>
<div>
<p class="py-4">2. Click the text area and follow the guide below</p>
<p class="mb-4">
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
</p>
<p class="py-4">
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.
</p>
<p class="py-4">
the coordmap should be printed as a list of 3 digit numbers seperated by spaces.<br />
eg 001 005 008 002 ... <br />
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.
</p>
<textarea
id="keycapture"
v-model="coordmap"
class="textarea textarea-bordered w-full font-mono"
></textarea>
</div>
<div class="flex gap-2 py-4">
<button class="btn btn-primary" @click="addRow">new Row</button>
<button class="btn btn-primary" @click="addSpc">add Space</button>
<button class="btn btn-primary" @click="rmLast">remove last</button>
<button class="btn btn-primary" @click="clear">clear</button>
</div>
<div>
<KeyboardLayout
:key-layout="keyboardlayout"
:keymap="[]"
:layouts="[]"
mode="layout"
></KeyboardLayout>
</div>
<div>
<pre class="my-2 rounded bg-base-300 p-4">{{ coordmapstring }}</pre>
</div>
<div class="flex gap-2">
<button class="btn btn-primary mt-2" @click="done">
{{ initialSetup ? 'next' : 'save Coord Maping & create keyboard layout' }}
</button>
<button v-if="!initialSetup" class="btn btn-primary mt-2" @click="onlySave">
only save Coord Maping
</button>
</div>
<p class="my-4">
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
</p>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
import KeyboardLayout from './KeyboardLayout.vue'
import { Key, keyboardStore, KeyInfo } from '../store'
const coordmap = ref('')
const props = defineProps<{ initialSetup: boolean }>()
const emits = defineEmits(['next'])
console.log('loading coordmap', keyboardStore.coordMap)
coordmap.value = keyboardStore.coordMap
.map((row) => {
return row.join(' ')
})
.join(' row ')
const addRow = () => {
coordmap.value = coordmap.value + '\n'
;(document.querySelector('#keycapture') as HTMLInputElement).focus()
}
const addSpc = () => {
coordmap.value += 'spc '
;(document.querySelector('#keycapture') as HTMLInputElement).focus()
}
const done = () => {
keyboardStore.setKeys(keyboardlayout.value as KeyInfo[])
keyboardStore.coordMap = keys.value
emits('next')
}
const onlySave = () => {
keyboardStore.coordMap = keys.value
}
const promptFlashing = () => {
if (props.initialSetup) {
// after valid ok then flash file with overwrite on
;(document.getElementById('flash_modal') as HTMLDialogElement).showModal()
} else {
flashCoordMapping({ overwrite: false })
}
}
const flashCoordMapping = async ({ overwrite }: { overwrite: boolean }) => {
console.log('flashCoordMapping with overwrite:', overwrite)
keyboardStore.coordMapSetup = true
;(document.getElementById('flash_modal') as HTMLDialogElement).close()
overwrite = Boolean(overwrite)
await window.api.saveConfiguration(
JSON.stringify({
pogConfig: keyboardStore.serialize(),
writeFirmware: overwrite,
writeCoordMapHelper: true
})
)
}
const keys = computed(() => {
// array of rows
const tmpKeys = coordmap.value.replaceAll(/\n|\r\n|\r/gi, ' row ')
const rows: any[] = []
let rowIndex = 0
let lastkey = ''
tmpKeys.split(' ').forEach((key) => {
if (key === '' || key.length !== 3) return
// next row
if (key === lastkey && !['row', 'spc'].includes(key)) {
if (!coordmap.value.endsWith(' ')) return
coordmap.value = coordmap.value.slice(0, -4)
addRow()
return
}
if (key === 'row') {
rowIndex++
return
}
if (!rows[rowIndex]) rows[rowIndex] = []
rows[rowIndex].push(key)
lastkey = key
})
return rows
})
const coordmapstring = computed(() => {
let str = 'coord_mapping = [\n'
keys.value.forEach((row) => {
str += row.join(',') + ',\n'
})
str += ']'
return str.replaceAll(/spc,/gi, ' ')
})
const keyboardlayout = computed(() => {
const realKeys: Key[] = []
let globalkeyindex = 0
keys.value.forEach((row, rowindex) => {
row.forEach((key, kindex) => {
if (key === 'spc') return
const keyToAdd = new Key({
x: kindex,
y: rowindex,
idx: globalkeyindex
})
realKeys.push(keyToAdd)
globalkeyindex++
})
})
console.log(realKeys)
return realKeys
})
const rmLast = () => {
coordmap.value = coordmap.value.split(' ').slice(0, -1).join(' ')
}
const clear = () => {
coordmap.value = ''
}
</script>
<style lang="scss" scoped></style>
================================================
FILE: src/renderer/src/components/EncoderLayer.vue
================================================
<template>
<div
v-if="keyboardStore.encoderKeymap[lindex] && keyboardStore.encoderKeymap[lindex][eindex]"
class="mb-4 flex items-center gap-4"
>
<p class="w-24">Layer {{ lindex }}</p>
<input
v-model="keyboardStore.encoderKeymap[lindex][eindex][0]"
type="text"
class="input input-bordered input-sm"
@blur="handleBlur(0)"
/>
<input
v-model="keyboardStore.encoderKeymap[lindex][eindex][1]"
type="text"
class="input input-bordered input-sm"
@blur="handleBlur(1)"
/>
</div>
</template>
<script lang="ts" setup>
import { keyboardStore } from '../store'
const props = defineProps(['lindex', 'layer', 'eindex'])
if (!keyboardStore.encoderKeymap[props.lindex]) {
// create the layer
keyboardStore.encoderKeymap[props.lindex] = []
}
if (!keyboardStore.encoderKeymap[props.lindex][props.eindex]) {
keyboardStore.encoderKeymap[props.lindex][props.eindex] = ['KC.TRNS', 'KC.TRNS']
}
const handleBlur = (index: number) => {
const value = keyboardStore.encoderKeymap[props.lindex][props.eindex][index]
if (!value || value === '▽') {
keyboardStore.encoderKeymap[props.lindex][props.eindex][index] = 'KC.TRNS'
}
}
</script>
<style lang="scss" scoped></style>
================================================
FILE: src/renderer/src/components/EncoderSetup.vue
================================================
<template>
<div>
<div
v-for="(encoder, eindex) in keyboardStore.encoders"
class="my-2 grid gap-4 bg-base-300 p-4"
>
<div class="flex justify-between gap-4">
<p class="text-lg font-bold">Encoder {{ eindex }}</p>
<button class="btn btn-error btn-xs" @click="removeEncoder(eindex)">
<i class="mdi mdi-delete"></i> remove encoder
</button>
</div>
<p>Prefix: {{ keyboardStore.pinPrefix }} - {{ pinPrefixHint }}</p>
<div class="mb-2 flex items-center gap-4">
<label>Pad A</label>
<input
v-model="encoder.pad_a"
type="text"
class="input input-bordered input-sm"
placeholder="14"
/>
</div>
<div class="flex items-center gap-4">
<label>Pad B</label>
<input
v-model="encoder.pad_b"
type="text"
class="input input-bordered input-sm"
placeholder="14"
/>
</div>
<div>
Keymap
<EncoderLayer
v-for="(_layer, lindex) in keyboardStore.keymap"
:lindex="lindex"
:eindex="eindex"
></EncoderLayer>
</div>
</div>
<div class="btn btn-primary btn-sm mt-2" @click="addEncoder">
<i class="mdi mdi-plus"></i>add Encoder
</div>
</div>
</template>
<script lang="ts" setup>
import { keyboardStore, pinPrefixHint } from '../store'
import EncoderLayer from './EncoderLayer.vue'
const cleanEncoders = () => {
if (keyboardStore.encoderKeymap.length !== keyboardStore.keymap.length) {
// add or remove encoder layers to match the keymap layer count
while (keyboardStore.encoderKeymap.length <= keyboardStore.keymap.length) {
// add an empty layer
keyboardStore.encoderKeymap.push([
// selectedConfig.value.encoders.map((a) => ["KC.TRNS", "KC.TRNS"]),
])
}
while (keyboardStore.encoderKeymap.length > keyboardStore.keymap.length) {
// remove a layer
keyboardStore.encoderKeymap.pop()
}
}
}
cleanEncoders()
const addEncoder = () => {
const encoder = { pad_a: '', pad_b: '' }
// TODO: initialize encoder keymap according to layers and encoders
// check the amount of layers
// add one encoder to each layer (push)
// cleanEncoders();
keyboardStore.encoderKeymap.forEach((layer) => {
layer.push(['KC.TRNS', 'KC.TRNS'])
})
keyboardStore.encoders.push(encoder)
}
const removeEncoder = (index: number) => {
// remove the encoder
// cleanEncoders();
keyboardStore.encoders = keyboardStore.encoders.filter((_e, eindex) => {
return eindex !== index
})
// remove that index from each keymap layer
keyboardStore.encoderKeymap.forEach((layer, lindex) => {
keyboardStore.encoderKeymap[lindex] = layer.filter((_l, eindex) => eindex !== index)
})
}
</script>
<style lang="scss" scoped></style>
================================================
FILE: src/renderer/src/components/HsvColorPicker.vue
================================================
<template>
<div class="mb-2 flex items-center gap-4">
<div class="flex flex-col gap-2">
<h2 class="font-bold">Color & Brightness</h2>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-2 gap-2">
<label>Hue:</label>
<input
v-model="hsvColor.hue"
class="input input-bordered input-sm"
type="number"
max="255"
min="0"
@input="onInput"
/>
<label>Saturation:</label>
<input
v-model="hsvColor.sat"
class="input input-bordered input-sm"
type="number"
max="255"
min="0"
@input="onInput"
/>
<label>Brightness:</label>
<input
v-model="hsvColor.val"
class="input input-bordered input-sm"
type="number"
max="255"
min="0"
@input="onInput"
/>
</div>
<input
v-model="rgbColor"
type="color"
class="input input-sm row-span-1 h-full w-full"
placeholder="14"
@change="onColorPicker"
/>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, Ref, ref } from 'vue'
import { keyboardStore } from '../store'
import { hexToHSL, hslToHex } from '../helpers/colors'
const emit = defineEmits(['change'])
const rgbColor = ref('')
const hsvColor: Ref<{ hue: number; sat: number; val: number }> = ref({ hue: 0, sat: 0, val: 0 })
onMounted(() => {
if (!keyboardStore.rgbOptions) return
hsvColor.value.hue = keyboardStore.rgbOptions.hueDefault
hsvColor.value.sat = keyboardStore.rgbOptions.satDefault
hsvColor.value.val = keyboardStore.rgbOptions.valDefault
rgbColor.value = hslToHex(hsvColor.value.hue, hsvColor.value.sat, hsvColor.value.val)
})
const onInput = () => {
rgbColor.value = hslToHex(hsvColor.value.hue, hsvColor.value.sat, hsvColor.value.val)
emit('change', hsvColor.value)
}
const onColorPicker = () => {
hsvColor.value = hexToHSL(rgbColor.value)
emit('change', hsvColor.value)
}
</script>
================================================
FILE: src/renderer/src/components/KeyCap.vue
================================================
<template>
<div
v-if="visible"
ref="keyElem"
class="keycap"
style="user-select: none"
:data-index="keyIndex"
:style="{
left: keyData.x * (baseKeyWidth + keyGap) + 'px',
top: keyData.y * (baseKeyWidth + keyGap) + 'px',
width: keyWidth + 'px',
height: keyHeight + 'px',
transform: `rotate(${keyData.r}deg)`,
transformOrigin: rotationOrigin
}"
:class="{
selected: mainSelected,
'is-trns': isTRNS,
encoder: typeof keyData.encoderIndex === 'number'
}"
>
<div
v-if="keyData.w2 || keyData.h2"
class="keyborder-blocker"
:style="{
left: '1px',
top: '4px',
width: keyWidth - 2 + 'px',
height: keyHeight - 8 + 'px'
}"
></div>
<div
class="keyborder"
:style="{
width: keyWidth + 'px',
height: keyHeight + 'px',
backgroundColor: keyColorDark
}"
></div>
<div
v-if="keyData.w2 || keyData.h2"
class="keyborder"
:class="{ selected: mainSelected }"
:style="{
left: keyData.x2 * baseKeyWidth - 1 + 'px',
width: keyWidth2 + 'px',
height: keyHeight2 + 'px'
}"
></div>
<div
v-if="keyData.w2 || keyData.h2"
class="keytop"
:style="{
height: keyTopHeight2 + 'px',
left: keyData.x2 * (baseKeyWidth + keyGap) + 1 + 'px',
backgroundColor: keyColor
}"
></div>
<!-- <div-->
<!-- class="keytop"-->
<!-- @click="bgClick"-->
<!-- ></div>-->
<div
v-else
class="keytop"
:style="{
top: !mainLabel || isSimple ? '4px' : '14px',
height: keyTopHeight + 'px',
background: keyColor
}"
></div>
<div v-if="!isSimple && mode !== 'layout'" class="keylabel-action">
<div
v-if="typeof keyData.encoderIndex === 'number' && mode !== 'layout'"
class="encoder-labels"
>
<div v-html="encoderActionA"></div>
<div v-html="encoderActionB"></div>
</div>
<span v-else>
{{ mainLabel.action }}
</span>
</div>
<!-- <div class="keylabel-action"></div>-->
<div
class="keylabels"
:class="{ 'has-args': !isSimple }"
:style="{ height: keyTopHeight + 'px', top: !mainLabel || isSimple ? '4px' : '14px' }"
>
<!-- <div class="keylabel" :class="['keylabel-'+index]" v-for="(label,index) in keyData.labels">-->
<!-- <div class="keylabel-inner">-->
<!-- {{label}}-->
<!-- </div>-->
<!-- </div>-->
<div v-if="mainLabel" class="keylabel keylabel-center">
<span
v-if="isSimple || typeof keyData.encoderIndex === 'number'"
class="keylabel-main"
v-html="mainLabel.action"
></span>
<div v-else class="flex h-full flex-col justify-between p-1">
<span
class="keylabel-main"
v-html="
mainLabel.layerNamePosition === 'main'
? mainLabel.main + ' ' + layerName
: mainLabel.main
"
></span>
<span
class="keylabel-lower"
v-html="
mainLabel.layerNamePosition === 'lower'
? mainLabel.lower + ' ' + layerName
: mainLabel.lower
"
></span>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, nextTick, onMounted, ref, VNodeRef, watch } from 'vue'
import { selectedLayer, selectedKeys, userSettings } from '../store'
import { renderLabel } from '../helpers'
import chroma from 'chroma-js'
const props = defineProps([
'keyData',
'keyIndex',
'mode',
'keymap',
'matrixWidth',
'layouts',
'wiringMethod'
])
defineEmits(['selected'])
const keyGap = 4
// hide normal labels and show the keymap thing
const action = computed(() => {
if (props.mode === 'layout') return //String(props.keyData.matrix)
let keyIndex = 0
keyIndex = props.keyData.coordMapIndex
if (!props.keymap[selectedLayer.value]) return 'No layer'
const keyCode = props.keymap[selectedLayer.value][keyIndex]
// resolve readable character
if (!keyCode || keyCode === 'KC.TRNS') return '▽'
return keyCode
})
const isTRNS = computed(() => {
return action.value === '▽'
})
const visible = computed(() => {
// hide decal keys
if (props.keyData.d) {
return false
}
// show correct variant
const variant: number[] = props.keyData.variant
if (variant) {
if (variant.length !== 2) return false
if (props.layouts[variant[0]]) return props.layouts[variant[0]].selected === variant[1]
}
// show keys that don't have variant
return true
})
const baseKeyWidth = ref(54)
const keyWidthU = computed(() => {
// if(props.keyData.w2) return props.keyData.w2
return props.keyData.w || 1
})
const keyHeightU = computed(() => {
return props.keyData.h || 1
})
const keyWidth2U = computed(() => {
return props.keyData.w2 || 1
})
const keyHeight2U = computed(() => {
return props.keyData.h2 || 1
})
const keyWidth = computed(() => {
return keyWidthU.value * baseKeyWidth.value + (keyWidthU.value - 1) * keyGap
})
const keyHeight = computed(() => {
return keyHeightU.value * baseKeyWidth.value + (keyHeightU.value - 1) * keyGap
})
const keyWidth2 = computed(() => {
return keyWidth2U.value * baseKeyWidth.value + (keyWidth2U.value - 1) * keyGap
})
const keyHeight2 = computed(() => {
return keyHeight2U.value * baseKeyWidth.value + (keyHeight2U.value - 1) * keyGap
})
// const hasArguments = computed(() => {
// if (!action.value) return false
// return action.value.includes(')')
// })
// const keyTopWidth = computed(() => {
// return keyWidth.value - keyGap * 2 - 4 //+ ((keyWidthU.value-1)*keyGap))
// })
const keyTopHeight = computed(() => {
let padding = 3
if (mainLabel.value && !mainLabel.value.simple) padding += 10
return keyHeight.value - padding * keyHeightU.value - keyGap + (keyHeightU.value - 1) * keyGap
})
// const keyTopWidth2 = computed(() => {
// const padding = 0
// return keyWidth2.value - padding * keyWidth2U.value - keyGap - 2 + (keyWidth2U.value - 1) * keyGap
// })
const keyTopHeight2 = computed(() => {
const padding = 3
return keyHeight2.value - padding * keyHeight2U.value - keyGap + (keyHeight2U.value - 1) * keyGap
})
const mainLabel = computed(() => {
// in Layout Mode show the matrix pos
if (props.mode === 'layout') {
return {
simple: true,
action: props.keyData.getMatrixLabel(),
layer: null,
lower: '',
main: '',
layerNamePosition: ''
}
}
// otherwise show the action from the keymap
// if (!action.value) return {simple: true,action: '',}
// render readable label
return renderLabel(action.value)
})
// const argLabel = computed(() => {
// if (hasArguments.value && action.value) {
// const argAction = action.value.split('(')[1].replace(')', '')
// if (argAction.startsWith('KC.')) {
// return argAction.split('.')[1]
// }
// return argAction
// }
// return
// })
const mainSelected = ref(false)
const argsSelected = ref(false)
// const bgClick = (e:MouseEvent) => {
// mainSelected.value = true;
// argsSelected.value = false;
// emit("selected", {
// key: props.keyData.matrix,
// args: argsSelected.value,
// keyIndex: props.keyIndex,
// added: e.shiftKey
// });
// };
// const argClick = () => {
// argsSelected.value = true;
// mainSelected.value = false;
// emit("selected", {
// key: props.keyData.matrix,
// args: argsSelected.value,
// keyIndex: props.keyIndex,
// });
// };
// watch(
// () => selectedKey.value.key,
// (newValue) => {
// if (selectedKey.value.key !== props.keyData.matrix) {
// mainSelected.value = false;
// argsSelected.value = false;
// }
// }
// );
watch(
() => [...selectedKeys.value],
(_newValue) => {
if (selectedKeys.value.has(props.keyIndex)) {
mainSelected.value = true
argsSelected.value = false
} else {
mainSelected.value = false
argsSelected.value = false
}
}
)
const rotationOrigin = computed(() => {
if (typeof props.keyData.rx !== 'number' || typeof props.keyData.ry !== 'number') return '0 0'
const x = props.keyData.rx * 58 - props.keyData.x * (baseKeyWidth.value + keyGap)
const y = props.keyData.ry * 58 - props.keyData.y * (baseKeyWidth.value + keyGap)
return `${x}px ${y}px` // return "xpx ypx"
})
const keyColor = computed(() => {
if (userSettings.value.reduceKeymapColors) return undefined
if (mainLabel.value && mainLabel.value.layer && props.keyData.keyboard) {
if (props.keyData.keyboard.layers[mainLabel.value.layer]) {
return props.keyData.keyboard.layers[mainLabel.value.layer].color
}
} else if (mainLabel.value && mainLabel.value.action === 'MT') {
return '#592424'
}
return undefined
})
const keyColorDark = computed(() => {
if (keyColor.value) {
return chroma(keyColor.value).darken(2).hex()
}
return undefined
})
const layerName = computed(() => {
if (!props.keyData.keyboard) return ''
if (!mainLabel.value.layer) return ''
const layer = props.keyData.keyboard.layers[mainLabel.value.layer]
return layer ? layer.name : ''
})
const encoderActionA = computed(() => {
// get encoder index then lookup current keycode
if (!props.keyData.getEncoderLabel) return
return renderLabel(props.keyData.getEncoderLabel().a).action
})
const encoderActionB = computed(() => {
// get encoder index then lookup current keycode
if (!props.keyData.getEncoderLabel) return
return renderLabel(props.keyData.getEncoderLabel().b).action
})
const isSimple = computed(() => {
if (typeof props.keyData.encoderIndex === 'number') return false
return mainLabel.value.simple
})
const keyElem = ref<VNodeRef | null>(null)
const fixLabelWidth = () => {
// key is eventually hidden with a layout variant
if (!keyElem.value) return
const label = keyElem.value.querySelector('.keylabel-main')
const labels = keyElem.value.querySelector('.keylabels')
if (label) {
console.log('fixing label width')
label.style.transform = `scale(1)`
const labelWidth = label.getBoundingClientRect().width
const wrapperWidth = labels.getBoundingClientRect().width
const scaling = Math.min(wrapperWidth / labelWidth, 1)
label.style.transform = `scale(${scaling})`
}
}
watch(mainLabel, async () => {
console.log('watching cap layer')
await nextTick()
fixLabelWidth()
})
onMounted(() => {
fixLabelWidth()
})
</script>
<style lang="scss" scoped>
.keyborder {
// outer key outline and background
background: #171717;
background-image: url('../assets/keycaptophighlight.png');
background-repeat: repeat-x;
position: absolute;
width: 54px;
height: 54px;
cursor: pointer;
z-index: 0;
border-radius: 10px;
.encoder & {
border-bottom-left-radius: 50%;
border-bottom-right-radius: 50%;
}
.selected & {
border-color: white;
z-index: 4;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 1);
}
}
.encoder-labels {
position: absolute;
top: 2px;
z-index: 10;
@apply flex w-full justify-between;
& > div {
//background: #646464;
line-height: 15px;
@apply rounded px-1.5;
}
}
.keyborder-blocker {
background: #3e3e3e;
position: absolute;
width: 52px;
height: 52px;
cursor: pointer;
border-radius: 12px;
z-index: 5;
}
.keytop {
position: absolute;
height: 42px;
width: calc(100% - 2px);
left: 1px;
top: 4px;
right: 1px;
background: #3e3e3e;
cursor: pointer;
border-radius: 12px;
z-index: 6;
.encoder & {
border-radius: 50%;
}
.selected.encoder & {
border-bottom: 1px solid white;
}
}
.keylabels {
position: absolute;
pointer-events: none;
width: calc(100% - 12px);
left: 6px;
top: 4px;
right: 6px;
line-height: 1rem;
//z-index: 3;
z-index: 7;
.selected & {
}
}
.keylabel {
position: absolute;
width: 100%;
height: 100%; //calc(48px - 5px);
@apply gap-1;
&-0 {
left: 8px;
top: 2px;
@apply flex items-start justify-start text-center;
}
&-3 {
right: 8px;
bottom: 2px;
@apply flex items-end justify-end text-center;
}
&-center {
@apply flex items-center justify-center text-center;
flex-wrap: wrap;
}
.arg-top {
@apply text-center;
position: absolute;
top: 0px;
left: 6px;
right: 6px;
font-size: 10px;
}
.arg-bottom {
@apply flex items-center justify-center rounded text-center;
position: absolute;
//border: 1px solid #666;
left: 6px;
right: 6px;
bottom: 2px;
height: 28px;
pointer-events: all;
cursor: pointer;
&.selected {
border-color: white;
}
}
}
.keycap {
position: absolute;
@apply transition-all;
.dragging & {
transition: all 0.08s ease-out;
}
&.is-trns {
opacity: 0.3;
}
}
//.keycap {
// width: 50px;
// height: 50px;
// position: absolute;
// background: #333;
// @apply rounded;
// &::after{
// @apply absolute;
// background: #red;
// width: 100px;
// height: 100px;
// content: '';
// }
//}
</style>
<style lang="scss">
.keylabel {
font-weight: bold;
font-size: 18px;
//text-shadow: 1px 2px 6px rgba(0, 0, 0, 0.6);
i.mdi {
font-size: 18px;
}
}
.keylabel-main {
white-space: nowrap;
.has-args & {
i.mdi {
font-size: 14px;
}
}
}
.keylabel-lower {
font-size: 10px;
font-weight: bold;
font-style: italic;
width: 100%;
i.mdi {
font-size: 12px;
}
}
.keylabel-action {
font-size: 10px;
font-weight: bold;
//font-style: italic;
width: 100%;
position: absolute;
z-index: 10;
text-align: center;
}
</style>
================================================
FILE: src/renderer/src/components/KeyLayoutInfo.vue
================================================
<template>
<div class="flex justify-between items-center h-10">
<div v-if="selectedKeys.size === 0">
<p class="font-bold">No keys selected</p>
</div>
<div v-else>
<p class="font-bold">
<template v-if="selectedKeys.size === 1">
Key #{{ [...selectedKeys][0] }}
</template>
<template v-else>
{{ selectedKeys.size }} keys selected
</template>
</p>
</div>
<div v-if="selectedKeys.size === 1" class="flex gap-2">
<button
class="btn btn-sm"
:disabled="[...selectedKeys][0] === 0"
@click="selectPreviousKey"
>
Previous Key
</button>
<button
class="btn btn-sm"
:disabled="[...selectedKeys][0] === layout.length - 1"
@click="selectNextKey"
>
Next Key
</button>
</div>
</div>
<hr class="border-base-300">
<p class="mt-2 text-sm">Basics</p>
<div class="flex flex-col gap-2">
<div class="grid grid-cols-4 gap-2 text-right">
<div class="keydata-input-group">
<span>x</span>
<input
v-model="tmpKey.x"
type="text"
placeholder="x"
class="keyinfo-input"
@change="updateKey"
/>
</div>
<div class="keydata-input-group">
<span>y</span>
<input
v-model="tmpKey.y"
type="text"
placeholder="y"
class="keyinfo-input"
@change="updateKey"
/>
</div>
<div class="keydata-input-group">
<span>x2</span>
<input
v-model="tmpKey.x2"
type="text"
placeholder="x2"
class="keyinfo-input"
@change="updateKey"
/>
</div>
<div class="keydata-input-group">
<span>y2</span>
<input
v-model="tmpKey.y2"
type="text"
placeholder="y2"
class="keyinfo-input"
@change="updateKey"
/>
</div>
</div>
<div class="grid grid-cols-4 gap-2 text-right">
<div class="keydata-input-group">
<span>w</span>
<input
v-model="tmpKey.w"
placeholder="w"
type="text"
class="keyinfo-input"
@change="updateKey"
/>
</div>
<div class="keydata-input-group">
<span>h</span>
<input
v-model="tmpKey.h"
placeholder="h"
type="text"
class="keyinfo-input"
@change="updateKey"
/>
</div>
<div class="keydata-input-group">
<span>w2</span>
<input
v-model="tmpKey.w2"
placeholder="w2"
type="text"
class="keyinfo-input"
@change="updateKey"
/>
</div>
<div class="keydata-input-group">
<span>h2</span>
<input
v-model="tmpKey.h2"
placeholder="h2"
type="text"
class="keyinfo-input"
@change="updateKey"
/>
</div>
</div>
</div>
<template v-if="keyboardStore.wiringMethod === 'matrix' && false">
<p class="mt-2 text-sm" :class="{ 'text-error': matrixValid }">Matrix</p>
<div class="flex gap-2">
<div class="keydata-input-group">
<span>row</span>
<input
v-model="tmpKey.matrix[0]"
type="text"
class="keyinfo-input w-1/2"
placeholder="row"
@change="updateKey"
/>
</div>
<div class="keydata-input-group">
<span>col</span>
<input
v-model="tmpKey.matrix[1]"
type="text"
class="keyinfo-input w-1/2"
placeholder="col"
@change="updateKey"
/>
</div>
</div>
</template>
<div>
<p>Key Index <span class="text-xs">(from CoordMap)</span></p>
<input v-model="tmpKey.coordMapIndex" type="text" class="keyinfo-input" @change="updateKey" />
</div>
<div>
<p>Encoder Index</p>
<input v-model="tmpKey.encoderIndex" type="text" class="keyinfo-input" @change="updateKey" />
</div>
<div v-if="keyboardStore.layouts.length !== 0" class="flex gap-1">
<label>
<span>Variant</span>
<input v-model="tmpKey.variant[0]" type="text" class="keyinfo-input" @change="updateKey" />
</label>
<label>
<span>Variant option</span>
<input v-model="tmpKey.variant[1]" type="text" class="keyinfo-input" @change="updateKey" />
</label>
</div>
<span>Rotation</span>
<div class="flex gap-1">
<input
v-model="tmpKey.r"
type="number"
step="1"
class="keyinfo-input w-1/3"
@change="updateKey"
/>deg
<input
v-model="tmpKey.rx"
type="number"
step="1"
class="keyinfo-input w-1/3"
placeholder="rotation x"
@change="updateKey"
/>
<input
v-model="tmpKey.ry"
type="number"
step="1"
class="keyinfo-input w-1/3"
placeholder="rotation y"
@change="updateKey"
/>
</div>
</template>
<script lang="ts" setup>
import { Key, selectedKeys } from '../store'
import { computed, ref, watch } from 'vue'
import { isNumber } from '@vueuse/core'
import { keyboardStore } from '../store'
const props = defineProps<{
layout: Key[]
}>()
const tmpKey = ref<{
x: number | ''
y: number | ''
x2: number | ''
y2: number | ''
w: number | ''
h: number | ''
w2: number | ''
h2: number | ''
matrix: (number | '')[]
variant: (number | '')[]
coordMapIndex?: number | ''
encoderIndex?: number | ''
r: number | ''
rx: number | ''
ry: number | ''
}>({
x: 0,
y: 0,
x2: 0,
y2: 0,
w: 1,
h: 1,
w2: 0,
h2: 0,
matrix: ['', ''],
variant: ['', ''],
coordMapIndex: '',
encoderIndex: '',
r: 0,
rx: 0,
ry: 0
})
const isAttrSame = (keys, attr) => {
return keys.reduce((acc, val) => acc.add(val[attr]), new Set()).size === 1
}
const getSameKeyAttrs = (keys) => {
console.log(keys)
const sameAttrs = new Map()
;['y', 'y2', 'x', 'x2', 'w', 'w2', 'h', 'h2', 'r', 'ry', 'rx', 'encoderIndex'].forEach((attr) => {
if (isAttrSame(keys, attr) && keys[0][attr] !== undefined) {
console.log('attr is same', attr)
sameAttrs.set(attr, keys[0][attr])
}
})
const returnObj = Object.fromEntries(sameAttrs)
console.log(returnObj)
return returnObj
}
const updateSelectedKey = () => {
console.log('updating selected keys')
console.log(props.layout)
console.log(selectedKeys.value)
if ([...selectedKeys.value].length === 1 && props.layout?.length > 0) {
const keyToLoad = props.layout[[...selectedKeys.value][0]]
// only load overlapping data from all selected keys
tmpKey.value = {
x: keyToLoad.x,
y: keyToLoad.y,
x2: keyToLoad.x2 ?? '',
y2: keyToLoad.y2 ?? '',
w: keyToLoad.w,
h: keyToLoad.h,
w2: keyToLoad.w2 ?? '',
h2: keyToLoad.h2 ?? '',
r: keyToLoad.r,
rx: keyToLoad.rx,
ry: keyToLoad.ry,
matrix: keyToLoad.matrix ?? ['', ''],
variant: keyToLoad.variant ?? ['', ''],
coordMapIndex: keyToLoad.coordMapIndex ?? '',
encoderIndex: keyToLoad.encoderIndex ?? ''
}
} else {
// set every property that has different values to ""
tmpKey.value = {
matrix: ['', ''],
variant: ['', ''],
coordMapIndex: '',
encoderIndex: '',
x2: '',
x: '',
y2: '',
y: '',
w: '',
h: '',
w2: '',
h2: '',
r: '',
rx: '',
ry: ''
}
const attrs = getSameKeyAttrs(props.layout.filter((_a, i) => selectedKeys.value.has(i)))
tmpKey.value = { ...tmpKey.value, ...attrs }
}
}
updateSelectedKey()
watch(
() => new Set(selectedKeys.value),
(newVal) => {
console.log('selected keys changed', newVal)
updateSelectedKey()
},
{ deep: true }
)
const emit = defineEmits(['update:layout'])
const updateKey = () => {
// Create a new copy of the layout to modify
const newLayout = [...props.layout]
selectedKeys.value.forEach((keyIndex) => {
// Create a new object with only the modified properties
const updates: Partial<(typeof props.layout)[0]> = {}
// Only add properties to updates if they have a non-empty value
if (tmpKey.value.x !== '') updates.x = Number(tmpKey.value.x)
if (tmpKey.value.x2 !== '') updates.x2 = Number(tmpKey.value.x2)
if (tmpKey.value.y2 !== '') updates.y2 = Number(tmpKey.value.y2)
if (tmpKey.value.y !== '') updates.y = Number(tmpKey.value.y)
if (tmpKey.value.w !== '') updates.w = Number(tmpKey.value.w)
if (tmpKey.value.h !== '') updates.h = Number(tmpKey.value.h)
if (tmpKey.value.w2 !== '') updates.w2 = Number(tmpKey.value.w2)
if (tmpKey.value.h2 !== '') updates.h2 = Number(tmpKey.value.h2)
if (tmpKey.value.r !== '') updates.r = Number(tmpKey.value.r)
if (tmpKey.value.rx !== '') updates.rx = Number(tmpKey.value.rx)
if (tmpKey.value.ry !== '') updates.ry = Number(tmpKey.value.ry)
// Handle coordMapIndex only if it was explicitly changed
if (tmpKey.value.coordMapIndex !== '') {
updates.coordMapIndex = Number(tmpKey.value.coordMapIndex)
}
// Handle encoderIndex only if it was explicitly changed
if (tmpKey.value.encoderIndex !== '') {
updates.encoderIndex = Number(tmpKey.value.encoderIndex)
}
// Handle matrix updates
if (tmpKey.value.matrix) {
if (tmpKey.value.matrix[0] !== '') {
if (!Array.isArray(newLayout[keyIndex].matrix)) {
updates.matrix = [Number(tmpKey.value.matrix[0]), NaN]
} else {
updates.matrix = [...newLayout[keyIndex].matrix]
updates.matrix[0] = Number(tmpKey.value.matrix[0])
}
}
if (tmpKey.value.matrix[1] !== '') {
updates.matrix = Array.isArray(updates.matrix)
? updates.matrix
: Array.isArray(newLayout[keyIndex].matrix)
? [...newLayout[keyIndex].matrix]
: [NaN, NaN]
updates.matrix[1] = Number(tmpKey.value.matrix[1])
}
}
// Handle variant updates
if (tmpKey.value.variant) {
if (tmpKey.value.variant[0] !== '') {
updates.variant = Array.isArray(newLayout[keyIndex].variant)
? [...newLayout[keyIndex].variant]
: [NaN, NaN]
updates.variant[0] = Number(tmpKey.value.variant[0])
}
if (tmpKey.value.variant[1] !== '') {
updates.variant = Array.isArray(updates.variant)
? updates.variant
: Array.isArray(newLayout[keyIndex].variant)
? [...newLayout[keyIndex].variant]
: [NaN, NaN]
updates.variant[1] = Number(tmpKey.value.variant[1])
}
}
// Update the key in our new layout copy
newLayout[keyIndex] = Object.assign(newLayout[keyIndex], updates)
savePartialLayout(newLayout)
})
// Emit the entire updated layout once
emit('update:layout', newLayout)
}
const matrixValid = computed(() => {
return !isNumber(tmpKey.value.matrix[0]) || !isNumber(tmpKey.value.matrix[1])
})
const savePartialLayout = (newLayout: Key[]) => {
console.log('saving partial layout', newLayout)
newLayout.forEach((key, i) => {
// Only update specific properties that can be changed in the layout editor
const keyProps = ['x', 'y', 'x2', 'y2', 'w', 'h', 'w2', 'h2', 'r', 'rx', 'ry', 'matrix', 'variant', 'coordMapIndex', 'encoderIndex']
keyProps.forEach(prop => {
if (key[prop] !== undefined && key[prop] !== keyboardStore.keys[i][prop]) {
keyboardStore.keys[i][prop] = key[prop]
}
})
})
}
const selectNextKey = () => {
const currentKey = [...selectedKeys.value][0]
if (currentKey < props.layout.length - 1) {
selectedKeys.value = new Set([currentKey + 1])
}
}
const selectPreviousKey = () => {
const currentKey = [...selectedKeys.value][0]
if (currentKey > 0) {
selectedKeys.value = new Set([currentKey - 1])
}
}
</script>
<style lang="scss" scoped>
.keyinfo-input {
@apply input input-bordered input-sm w-full flex-shrink;
padding-left: 3px;
padding-right: 3px;
}
.keydata-input-group {
@apply flex items-center gap-1;
}
</style>
================================================
FILE: src/renderer/src/components/KeyPicker.vue
================================================
<template>
<div class="tabs tabs-boxed my-4">
<a class="tab" :class="{ 'tab-active': layout === 'qwerty' }" @click="layout = 'qwerty'"
>QWERTY</a
>
<a class="tab" :class="{ 'tab-active': layout === 'colemak' }" @click="layout = 'colemak'"
>Colemak</a
>
<a class="tab" :class="{ 'tab-active': layout === 'colemak-dh' }" @click="layout = 'colemak-dh'"
>Colemak DH</a
>
<a class="tab" :class="{ 'tab-active': layout === 'dvorak' }" @click="layout = 'dvorak'"
>Dvorak</a
>
</div>
<div id="keyboard-picker" :style="{ transform: `scale(${scale})` }">
<Qwerty v-if="layout === 'qwerty'" @key="setKey" />
<Colemak v-if="layout === 'colemak'" @key="setKey" />
<ColemakDH v-if="layout === 'colemak-dh'" @key="setKey" />
<Dvorak v-if="layout === 'dvorak'" @key="setKey" />
</div>
<div v-show="showSecondary" class="secondary mb-4">
<div class="tabs tabs-boxed mt-4">
<a class="tab" :class="{ 'tab-active': category === 'basic' }" @click="category = 'basic'"
>Basic</a
>
<a class="tab" :class="{ 'tab-active': category === 'layers' }" @click="category = 'layers'"
>Layers</a
>
<a class="tab" :class="{ 'tab-active': category === 'kmk' }" @click="category = 'kmk'">KMK</a>
<a class="tab" :class="{ 'tab-active': category === 'app' }" @click="category = 'app'"
>App/Media/Mouse</a
>
<a class="tab" :class="{ 'tab-active': category === 'rgb' }" @click="category = 'rgb'">RGB</a>
<a class="tab" :class="{ 'tab-active': category === 'advanced' }" @click="category = 'advanced'"
>Advanced & Help</a
>
</div>
<div class="key-chooser">
<div v-if="category === 'basic'" class="bonus">
<div class="key" @click="setKey('KC.NO')">Empty</div>
<div class="key" @click="setKey('KC.TRNS')">▽</div>
<div class="key" @click="setKey('KC.KP_EQUAL')">=</div>
<div class="key" @click="setKey('KC.KP_COMMA')">,</div>
<div class="key" @click="setKey('KC.TILDE')">~</div>
<div class="key" @click="setKey('KC.EXLM')">!</div>
<div class="key" @click="setKey('KC.AT')">@</div>
<div class="key" @click="setKey('KC.HASH')">#</div>
<div class="key" @click="setKey('KC.DOLLAR')">$</div>
<div class="key" @click="setKey('KC.PERCENT')">%</div>
<div class="key" @click="setKey('KC.CIRCUMFLEX')">^</div>
<div class="key" @click="setKey('KC.AMPERSAND')">&</div>
<div class="key" @click="setKey('KC.ASTERISK')">*</div>
<div class="key" @click="setKey('KC.LEFT_PAREN')">(</div>
<div class="key" @click="setKey('KC.RIGHT_PAREN')">)</div>
<div class="key" @click="setKey('KC.UNDERSCORE')">_</div>
<div class="key" @click="setKey('KC.PLUS')">+</div>
<div class="key" @click="setKey('KC.LCBR')">{</div>
<div class="key" @click="setKey('KC.RCBR')">}</div>
<div class="key" @click="setKey('KC.LABK')"><</div>
<div class="key" @click="setKey('KC.RABK')">></div>
<div class="key" @click="setKey('KC.COLN')">:</div>
<div class="key" @click="setKey('KC.PIPE')">|</div>
<div class="key" @click="setKey('KC.QUES')">?</div>
<div class="key" @click="setKey('KC.DQT')">"</div>
</div>
</div>
<div v-if="category === 'layers'" class="key-chooser flex">
<div class="bonus">
<div class="group">
<div
v-for="(_layer, index) in keyboardStore.keymap"
class="key"
@click="setKey(`KC.MO(${index})`)"
>
MO({{ index }})
</div>
</div>
<!-- <div class="key" @click="setKey('KC.LM()')">LM(l, mod)</div>-->
<!-- <div class="key" @click="setKey('KC.LT()')">LT(l, kc)</div>-->
<div class="group">
<div
v-for="(_layer, index) in keyboardStore.keymap"
class="key"
@click="setKey(`KC.TG(${index})`)"
>
TG({{ index }})
</div>
</div>
<div class="group">
<div
v-for="(_layer, index) in keyboardStore.keymap"
class="key"
@click="setKey(`KC.TO(${index})`)"
>
TO({{ index }})
</div>
</div>
<div class="group">
<div
v-for="(_layer, index) in keyboardStore.keymap"
class="key"
@click="setKey(`KC.TT(${index})`)"
>
TT({{ index }})
</div>
</div>
<div class="group">
<div
v-for="(_layer, index) in keyboardStore.keymap"
class="key"
@click="setKey(`KC.LM(${index},KC.LGUI)`)"
>
LM({{ index }}, mod)
</div>
</div>
</div>
<div class="bonus"></div>
</div>
<div v-if="category === 'kmk'" class="key-chooser flex">
<div class="bonus">
<div class="key" @click="setKey('KC.RESET')">Reset</div>
<div class="key" @click="setKey('KC.RELOAD')">Reload</div>
<div class="key" @click="setKey('KC.DEBUG')">Debug</div>
<div class="key" @click="setKey('KC.BKDL')">BKDL</div>
</div>
</div>
<div v-if="category === 'app'" class="key-chooser flex">
<div class="bonus">
<div class="key" @click="setKey('KC.MPLY')">Play Pause</div>
<div class="key" @click="setKey('KC.MUTE')">Mute</div>
<div class="key" @click="setKey('KC.VOLD')">Vol Down</div>
<div class="key" @click="setKey('KC.VOLU')">Vol Up</div>
<div class="key" @click="setKey('KC.MFFD')">next track (OSX)</div>
<div class="key" @click="setKey('KC.MRWD')">prev track (OSX)</div>
<div class="key" @click="setKey('KC.MNXT')">next track (win)</div>
<div class="key" @click="setKey('KC.MPRV')">prev track (win)</div>
<div class="key" @click="setKey('KC.MSTP')">stop track (win)</div>
<div class="key" @click="setKey('KC.BRIU')">bright up</div>
<div class="key" @click="setKey('KC.BRID')">bright down</div>
<div class="key" @click="setKey('KC.EJCT')">eject (OSX)</div>
</div>
</div>
<div v-if="category === 'rgb'" class="key-chooser flex">
<div class="bonus">
<div class="key" @click="setKey('KC.RGB_TOG')">RGB Toggle</div>
<div class="key" @click="setKey('KC.RGB_HUI')">RGB Hue increase</div>
<div class="key" @click="setKey('KC.RGB_HUD')">RGB Hue decrease</div>
</div>
</div>
<div v-if="category === 'advanced'">
<p>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
</p>
<ul>
<li><a class="text-primary" href="https://github.com/KMKfw/kmk_firmware/blob/main/docs/en/macros.md">Macros</a> KC.MACRO("send a string")</li>
<li><a class="text-primary" href="https://github.com/KMKfw/kmk_firmware/blob/main/docs/en/keycodes.md">Keycodes</a> List of all keys</li>
<li><a class="text-primary" href="https://github.com/KMKfw/kmk_firmware/blob/main/docs/en/layers.md">Layers</a> How layers work</li>
<li><a class="text-primary" href="https://github.com/KMKfw/kmk_firmware/blob/main/docs/en/combos.md">Combos</a> Multiple keys pressed at simultaneously output a different key</li>
<li><a class="text-primary" href="https://github.com/KMKfw/kmk_firmware/blob/main/docs/en/holdtap.md">Holdtap</a> Holding a key down for longer than a certain amount of time outputs a different key</li>
<li><a class="text-primary" href="https://github.com/KMKfw/kmk_firmware/blob/main/docs/en/combo_layers.md">Combo Layers</a> pressing 2 layer keys at once opens a different layer</li>
</ul>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { keyboardStore } from '../store'
import Qwerty from './picker-layouts/Qwerty.vue'
import Colemak from './picker-layouts/Colemak.vue'
import ColemakDH from './picker-layouts/ColemakDH.vue'
import Dvorak from './picker-layouts/Dvorak.vue'
const layout = ref('qwerty')
const category = ref('basic')
const emit = defineEmits(['setKey'])
const props = defineProps<{
showSecondary?: boolean
}>()
const showSecondary = computed(() => props.showSecondary !== false)
// set the currently selected key to keycode
const setKey = (key: string | number) => {
emit('setKey', String(key))
}
const scale = ref(1)
const scaleKeyboard = () => {
const wrapper = document.querySelector('#keyboard-picker')
const keyboard = document.querySelector('#keyboard-picker .key-chooser')
if (!wrapper || !keyboard) return
const ww = wrapper.getBoundingClientRect().width
const wk = keyboard.getBoundingClientRect().width
scale.value = Math.min(ww / wk, 1)
}
onMounted(() => {
window.addEventListener('resize', () => scaleKeyboard())
scaleKeyboard()
})
</script>
<style lang="scss">
:root {
--key-size: 35px;
}
#keyboard-picker {
@apply flex flex-wrap justify-center;
transform-origin: center top;
}
.key-chooser {
gap: 4px;
@apply flex flex-col flex-wrap;
.row {
@apply flex;
gap: 4px;
}
.bonus {
margin-top: 20px;
gap: 4px;
@apply flex;
flex-wrap: wrap;
.key {
width: calc(var(--key-size) * 1.4);
height: calc(var(--key-size) * 1.4);
}
}
.group {
@apply mr-2 flex gap-1;
}
.blocker-half {
width: calc(var(--key-size) / 2);
height: var(--key-size);
@apply shrink-0;
}
.blocker-full {
height: var(--key-size);
width: var(--key-size);
@apply shrink-0;
}
.key {
width: var(--key-size);
height: var(--key-size);
background: #444444;
@apply flex shrink-0 flex-col items-center justify-center rounded text-center transition-all;
font-size: 14px;
line-height: 16px;
border: 1px solid #555;
cursor: pointer;
&.sm {
font-size: 10px;
}
i.mdi {
font-size: 18px;
}
&:hover {
background: #555;
}
&-2u {
width: calc(var(--key-size) * 2 + 8px);
}
&-1-25u {
width: calc(var(--key-size) * 1.25 + 2px);
}
&-1-5u {
width: calc(var(--key-size) * 1.5 + 4px);
}
&-1-75u {
width: calc(var(--key-size) * 1.75 + 4px);
}
&-2-25u {
width: calc(var(--key-size) * 2.25 + 8px);
}
&-2-5u {
width: calc(var(--key-size) * 2.5 + 8px);
}
&-6u {
width: calc(var(--key-size) * 6.25 + 18px);
}
}
}
</style>
================================================
FILE: src/renderer/src/components/KeyboardLayout.vue
================================================
<template>
<SelectionArea
ref="keyboardContainer"
class="keyboard-container container"
:class="{ 'fixed-height': fixedHeight }"
:options="{ selectables: ['.keycap'] }"
:on-move="onMove"
:on-start="onStart"
>
<div
id="keyboardlayout-wrapper"
class="relative flex justify-center"
:style="{
height: keyboardHeight + 'px',
width: keyboardScale * (keyboardWidth * 58) + 'px'
}"
>
<div
id="keyboardlayout"
class="relative w-full"
:style="{
width: keyboardWidth * 58 + 'px',
// height: keyboardHeight * 58 + 'px',
transform: `scale( ${keyboardScale})`,
'transform-origin': 'left top'
}"
:class="{ dragging: moving }"
>
<div
v-if="mode === 'layout' && selectedKeys.size !== 0"
class="rotation-origin-helper"
:style="{ left: rotationOriginX, top: rotationOriginY }"
></div>
<div class="wire-preview">
<!-- for each key show 2 wires o the next keys-->
</div>
<key-cap
v-for="(key, keyIndex) in keyLayout"
:key="key.id"
:key-data="key"
:key-index="keyIndex"
:mode="mode"
:keymap="keymap"
:matrix-width="matrixWidth"
:layouts="layouts"
>
</key-cap>
</div>
</div>
</SelectionArea>
</template>
<script lang="ts" setup>
import KeyCap from './KeyCap.vue'
import { computed, nextTick, onMounted, onUnmounted, ref, VNodeRef, watch } from 'vue'
import { keyboardStore, selectedKeys } from '../store'
import { SelectionArea } from '@viselect/vue'
import type { SelectionEvent } from '@viselect/vue'
import { isNumber } from '@vueuse/core'
import { useDebounceFn } from '@vueuse/core'
const props = defineProps(['keyLayout', 'keymap', 'mode', 'matrixWidth', 'layouts', 'fixedHeight'])
// mode can be layout or keymap
const keyboardContainer = ref<VNodeRef | null>(null)
// find right edge
const keyboardWidth = computed(() => {
let maxW = 0
props.keyLayout.forEach((k) => {
const width = k.w || 1
const rightEdge = k.x + width
if (rightEdge > maxW) {
maxW = rightEdge
}
})
return maxW
})
// find bottom edge
const keyboardKeyHeight = computed(() => {
let maxH = 0
props.keyLayout.forEach((k) => {
const height = k.h || 1
const bottomEdge = k.y + height
if (bottomEdge > maxH) {
maxH = bottomEdge
}
})
return maxH
})
const keyboardScale = ref(1)
const keyboardHeight = ref(200)
const updateHeight = () => {
// check all keys
const wrapper = keyboardContainer.value.$el
const keys = wrapper.querySelectorAll('.keycap')
let lowestKey = 0
keys.forEach((key) => {
const box = key.getBoundingClientRect()
const height = box.height + box.top - key.parentNode.getBoundingClientRect().top
if (height > lowestKey) {
lowestKey = height
}
})
// if(props.fixedHeight && keyboardHeight.value < lowestKey ){
// console.log('lowest key ignored', lowestKey,keyboardHeight.value)
// return
// }
keyboardHeight.value = lowestKey
}
const updateScale = () => {
// updateHeight()
if (!keyboardContainer.value) return
const wrapper = keyboardContainer.value.$el
let heightScale = 1
let widthScale = 1
if (wrapper) {
if (keyboardWidth.value === 0) return
if (keyboardWidth.value / keyboardKeyHeight.value > 2.71) {
const wrapperWidth = wrapper.getBoundingClientRect().width
widthScale = Math.min(wrapperWidth / (keyboardWidth.value * 58), 1)
} else {
const wrapperHeight = wrapper.getBoundingClientRect().height
heightScale = Math.min(wrapperHeight / (keyboardKeyHeight.value * 58), 1)
}
// if (props.fixedHeight) {
// const wrapperHeight = wrapper.parentNode.getBoundingClientRect().height
// heightScale = Math.min(wrapperHeight / keyboardHeight.value, 1)
// if (heightScale < 1){
// console.log('needed scale', heightScale, wrapperHeight, keyboardHeight.value)
// }
// }
keyboardScale.value = Math.min(heightScale, widthScale)
}
updateHeight()
}
onMounted(async () => {
// adjust keyboard size to fit
// TODO: figure out why i need to apply it 3 times to work
updateScale()
await nextTick()
updateScale()
setTimeout(async () => {
updateHeight()
}, 100)
window.addEventListener('resize', updateScale)
})
onUnmounted(() => {
window.removeEventListener('resize', updateScale)
})
watch(props.keyLayout, () => {
console.log('keylayout changed')
updateScale()
updateHeight()
})
const rotationOriginX = computed(() => {
if (!selectedKeys.value.size) return 0
const firstSelectedKeyIndex = [...selectedKeys.value][0]
if (!props.keyLayout[firstSelectedKeyIndex]) return '0'
const x = props.keyLayout[firstSelectedKeyIndex].rx * 58
return `${x}px` // return "xpx ypx"
})
const rotationOriginY = computed(() => {
if (!selectedKeys.value.size) return 0
const firstSelectedKeyIndex = [...selectedKeys.value][0]
if (!props.keyLayout[firstSelectedKeyIndex]) return '0'
const y = props.keyLayout[firstSelectedKeyIndex].ry * 58
return `${y}px` // return "xpx ypx"
})
// const deselectKey = (e: MouseEvent) => {
// console.log(e);
// if (
// e.target &&
// (e.target as unknown as { id: string }).id === "keyboardlayout-wrapper"
// ) {
// selectedKeys.value.clear()
// selectedKey.value = { keyIndex: NaN, key: [], args: false };
// }
// };
const extractIndexes = (els: Element[]): number[] => {
return els
.map((v) => v.getAttribute('data-index'))
.filter((a) => !isNumber(a))
.map(Number)
}
const moving = ref(false)
const moveStart = ref({ x: 0, y: 0 })
const writtenDelta = ref({ x: 0, y: 0 })
const onStart = ({ event, selection }: SelectionEvent) => {
if (props.mode === 'static') {
selection.cancel()
return
}
if (event?.shiftKey && props.mode === 'layout') {
if (event instanceof MouseEvent) {
// save start point
moving.value = true
moveStart.value.x = event.clientX
moveStart.value.y = event.clientY
writtenDelta.value.x = 0
writtenDelta.value.y = 0
selection.getSelectionArea().classList.add('hidden')
}
return
}
selection.getSelectionArea().classList.remove('hidden')
if (!event?.ctrlKey && !event?.metaKey) {
selection.clearSelection()
selectedKeys.value.clear()
}
}
const roundNearQtr = (number: number) => {
return Math.round(number * 4) / 4
}
const onMove = ({
store: {
changed: { added, removed }
},
event
}: SelectionEvent) => {
if (props.mode === 'static') {
return
}
if (event?.shiftKey && props.mode === 'layout') {
if (event instanceof MouseEvent) {
// console.log(event, selection);
moving.value = true
// move keys by start distance
const delta = { x: 0, y: 0 }
delta.x = (event.clientX - moveStart.value.x) * (1 / keyboardScale.value)
delta.y = (event.clientY - moveStart.value.y) * (1 / keyboardScale.value)
// snap in every 0.25 of a key width 58
const deltaTmp = {
x: roundNearQtr(delta.x / 58),
y: roundNearQtr(delta.y / 58)
}
// subtract already written distance
const writableDelta = {
x: deltaTmp.x - writtenDelta.value.x,
y: deltaTmp.y - writtenDelta.value.y
}
writtenDelta.value.x = deltaTmp.x
writtenDelta.value.y = deltaTmp.y
// write to each key
selectedKeys.value.forEach((keyIndex) => {
keyboardStore.keys[keyIndex].delta({ property: 'x', value: writableDelta.x })
keyboardStore.keys[keyIndex].delta({ property: 'y', value: writableDelta.y })
})
}
resetMoving()
return
}
extractIndexes(added).forEach((id) => selectedKeys.value.add(id))
extractIndexes(removed).forEach((id) => selectedKeys.value.delete(id))
}
const resetMoving = useDebounceFn(() => {
moving.value = false
}, 1000)
defineExpose({ keyboardContainer })
</script>
<style lang="scss" scoped>
.rotation-origin-helper {
width: 5px;
height: 5px;
background: red;
position: absolute;
z-index: 10;
border-radius: 5px;
transform: translate(-50%, -50%);
}
.container {
user-select: none;
height: 100%;
width: 100%;
@apply flex items-center justify-center p-4;
}
.keyboard-layout {
}
</style>
<style lang="scss">
.selection-area {
background: rgba(152, 90, 19, 0.2);
border: 2px solid rgb(242, 140, 24);
border-radius: 0.1em;
z-index: 100;
&.hidden {
opacity: 0;
}
}
</style>
================================================
FILE: src/renderer/src/components/KeyboardName.vue
================================================
<template>
<div class="mt-8">
<div class="mb-4">
<p class="mb-2 text-sm">Name</p>
<input v-model="keyboardStore.name" type="text" class="input input-bordered w-full" />
</div>
<div class="mb-4">
<p class="mb-2 text-sm">Manufacturer (optional)</p>
<input v-model="keyboardStore.manufacturer" type="text" class="input input-bordered w-full" />
</div>
<div class="mb-4">
<p class="mb-2 text-sm">Description (optional)</p>
<textarea
v-model="keyboardStore.description"
type="text"
class="textarea textarea-bordered w-full"
/>
</div>
<div class="mb-4">
<p class="mb-2 text-sm">Tags (optional)</p>
<VueMultiselect
v-model="keyboardStore.tags"
:options="keyboardTags"
:multiple="true"
:taggable="true"
class="w-full"
@tag="addTag"
>
</VueMultiselect>
</div>
<div class="mb-4">
<p class="mb-2 text-sm">Keyboard Features</p>
<VueMultiselect
v-model="keyboardStore.kbFeatures"
:options="availableFeatures"
:multiple="true"
class="w-full"
>
<template #option="{ option }">
<span>{{ formatFeatureName(option) }}</span>
</template>
</VueMultiselect>
</div>
<div class="mt-8 flex justify-center">
<button v-if="initialSetup" class="btn btn-primary" @click="$emit('next')">next</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { keyboardStore } from '../store'
import VueMultiselect from 'vue-multiselect'
defineProps(['initialSetup'])
const keyboardTags = ['65%']
const addTag = (tag) => {
console.log(tag)
keyboardStore.tags.push(tag)
}
const availableFeatures = [
'basic',
'serial',
'oneshot',
'tapdance',
'holdtap',
'mousekeys',
'combos',
'macros',
'capsword',
'international'
]
const formatFeatureName = (feature: string) => {
return feature.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
</script>
<style lang="scss">
@import 'vue-multiselect/dist/vue-multiselect.css';
.multiselect__tags {
width: 100%;
background: transparent;
border: none;
}
.multiselect__placeholder {
background: transparent;
}
.multiselect__option--selected.multiselect__option {
background: #674848;
}
.multiselect__option {
background: #252525;
color: #fff;
&:hover {
background: #353535;
}
}
</style>
================================================
FILE: src/renderer/src/components/KeymapEditor.vue
================================================
<template>
<div class="relative">
<!-- <h2 class="mb-2 inline-block absolute top-6" style="transform: rotate(-90deg); left: -15px">Layers</h2>-->
<div class="mb-2 flex gap-2">
<button class="btn btn-sm" @click="addLayer"><i class="mdi mdi-plus"></i>add Layer</button>
<button class="btn btn-sm" :disabled="keyboardStore.keymap.length === 1" @click="removeLayer">
<i class="mdi mdi-trash-can"></i>remove Layer
</button>
<button class="btn btn-sm" @click="duplicateLayer">
<i class="mdi mdi-content-duplicate"></i>Duplicate Layer
</button>
<button class="btn btn-sm" @click="toggleSettings">
<i class="mdi mdi-cog"></i>
</button>
</div>
<div v-if="settingsOpen" class="mb-4 flex gap-2">
<label class="flex items-center gap-2">
<input v-model="userSettings.reduceKeymapColors" type="checkbox" class="checkbox" />
<span>Reduce keymap colors</span>
</label>
<label class="flex items-center gap-2">
<input v-model="userSettings.autoSelectNextKey" type="checkbox" class="checkbox" />
<span>Auto-select next key</span>
</label>
</div>
<div class="mt-4 flex items-center">
<div class="flex gap-2">
<KeymapLayer
v-for="(layer, index) in keyboardStore.keymap"
:key="index"
:layer="layer"
:index="index"
/>
</div>
</div>
</div>
<div class="my-12">
<keyboard-layout
:key-layout="keyboardStore.keys"
:keymap="keyboardStore.keymap"
:matrix-width="keyboardStore.cols"
:layouts="keyboardStore.layouts"
/>
</div>
<div class="my-4">
<p class="mb-2 text-sm font-bold">
Keycode Options for Selected Key(s)
<span class="text-sm text-warning">{{ coordMapWarning }}</span>
</p>
<div class="flex gap-2">
<div class="flex-grow">
<input
v-model="currentKeyCode"
:disabled="selectedKeys.size === 0"
type="text"
class="input input-bordered input-sm w-full"
/>
</div>
<button v-if="selectedKeys.size > 0" class="btn btn-primary btn-sm" @click="openMacroModal">
Custom Macro
</button>
<!-- Templates Dropdown with Floating UI -->
<div v-if="selectedKeys.size > 0" class="relative">
<button ref="templatesButtonRef" class="btn btn-sm" @click="toggleTemplatesDropdown">
<i class="mdi mdi-file-document-outline mr-1"></i>Templates
<i class="mdi mdi-chevron-down ml-1"></i>
</button>
<Teleport to="body">
<div
v-if="templatesDropdownOpen"
class="fixed inset-0 z-40"
@click="closeTemplatesDropdown"
></div>
<ul
v-if="templatesDropdownOpen"
ref="templatesDropdownRef"
:style="floatingStyles"
class="menu rounded-box z-50 w-52 bg-base-300 p-2 shadow-lg"
>
<li class="menu-title"><span>Insert Template</span></li>
<li>
<a @click="insertTemplate('macro'), closeTemplatesDropdown()"
><i class="mdi mdi-keyboard"></i> Macro</a
>
</li>
<li>
<a @click="insertTemplate('string'), closeTemplatesDropdown()"
><i class="mdi mdi-format-text"></i> String</a
>
</li>
<li>
<a @click="insertTemplate('tapdance'), closeTemplatesDropdown()"
><i class="mdi mdi-gesture-double-tap"></i> Tap Dance</a
>
</li>
<li>
<a @click="insertTemplate('custom'), closeTemplatesDropdown()"
><i class="mdi mdi-cog"></i> Custom Key</a
>
</li>
<li class="my-1 h-px bg-base-content/20"></li>
<li class="menu-title"><span>Documentation</span></li>
<li>
<a
href="https://github.com/KMKfw/kmk_firmware/blob/main/docs/en/macros.md"
target="_blank"
>
<i class="mdi mdi-open-in-new"></i> Macros Guide
</a>
</li>
<li>
<a
href="https://github.com/KMKfw/kmk_firmware/blob/main/docs/en/tapdance.md"
target="_blank"
>
<i class="mdi mdi-open-in-new"></i> Tap Dance Guide
</a>
</li>
<li>
<a
href="https://github.com/KMKfw/kmk_firmware/blob/main/docs/en/keycodes.md"
target="_blank"
>
<i class="mdi mdi-open-in-new"></i> Keycodes Reference
</a>
</li>
<li>
<a
href="https://github.com/KMKfw/kmk_firmware/blob/main/docs/en/layers.md"
target="_blank"
>
<i class="mdi mdi-open-in-new"></i> Layers Guide
</a>
</li>
</ul>
</Teleport>
</div>
</div>
<div v-if="keycodeModeForSelection === 'custom'" class="p-2 text-sm italic">
<p>
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
</p>
</div>
</div>
<KeyPicker :show-secondary="true" @set-key="setKey"></KeyPicker>
<!-- Macro Modal -->
<MacroModal
:is-open="macroModal.isOpen"
:initial-macro-code="macroModal.initialCode"
@close="closeMacroModal"
@apply="applyMacroCode"
/>
</template>
<script lang="ts" setup>
import { keyboardStore, selectedKeys, selectedLayer, userSettings } from '../store'
import KeyboardLayout from './KeyboardLayout.vue'
import KeyPicker from './KeyPicker.vue'
import MacroModal from './MacroModal.vue'
import { cleanupKeymap, selectNextKey } from '../helpers'
import { computed, ref, watch } from 'vue'
import KeymapLayer from './KeymapLayer.vue'
import { useFloating, offset, flip, shift, autoUpdate } from '@floating-ui/vue'
selectedKeys.value.clear()
type KeycodeMode = 'simple' | 'combo' | 'macro' | 'custom' | 'tapdance' | 'string'
const keycodeModeForSelection = ref<KeycodeMode>('simple')
const detectKeycodeType = (keycode: string): KeycodeMode => {
if (!keycode || keycode === 'No key selected' || keycode === '▽') return 'simple'
if (keycode.includes('KC.MACRO("') || keycode.includes("KC.MACRO('")) return 'string'
if (keycode.includes('KC.MACRO(')) return 'macro'
if (keycode.includes('KC.TD(')) return 'tapdance'
if (keycode.includes('customkeys.')) return 'custom'
if (keycode.includes('KC.COMBO(')) return 'combo'
return 'simple'
}
const setKey = (keyCode: string) => {
selectedKeys.value.forEach((index) => {
keyboardStore.keys[index].setOnKeymap(keyCode)
})
// if one key is selected select the next
// TODO: only select visible keys
if (selectedKeys.value.size === 1 && userSettings.value.autoSelectNextKey) {
selectNextKey()
}
}
cleanupKeymap()
const addLayer = () => {
if (!keyboardStore.keymap[0]) {
keyboardStore.keymap.push(Array(keyboardStore.cols * keyboardStore.rows).fill('KC.TRNS'))
}
const tmpKeymap = [...keyboardStore.keymap[0]]
tmpKeymap.fill('KC.TRNS')
keyboardStore.keymap.push(tmpKeymap)
// if needed also add an encoder layer
const encoderCount = keyboardStore.encoders.length
if (encoderCount !== 0) {
keyboardStore.encoderKeymap.push(Array(encoderCount).fill(['KC.TRNS', 'KC.TRNS']))
}
}
const removeLayer = () => {
if (keyboardStore.keymap.length <= 1) return
// if needed also remove the encoder layer
const encoderCount = keyboardStore.encoders.length
if (encoderCount !== 0) {
keyboardStore.encoderKeymap.splice(selectedLayer.value, 1)
}
keyboardStore.layers.splice(selectedLayer.value, 1)
keyboardStore.keymap.splice(selectedLayer.value, 1)
if (selectedLayer.value === keyboardStore.keymap.length - 1 && selectedLayer.value !== 0) {
selectedLayer.value = keyboardStore.keymap.length - 2
}
}
const duplicateLayer = () => {
keyboardStore.keymap.push([...keyboardStore.keymap[selectedLayer.value]])
}
const currentKeyCode = computed({
get() {
const keys = keyboardStore.keys.filter((_k, index) => selectedKeys.value.has(index))
if (keys.length === 0) return 'No key selected'
const actions: string[] = []
keys.forEach((key) => {
actions.push(keyboardStore.getActionForKey({ key, layer: selectedLayer.value }))
})
return actions[0]
},
set(newVal) {
if (newVal === '▽') return
let setNewVal = newVal
if (!newVal || selectedKeys.value.size === 0) {
setNewVal = 'KC.TRNS'
}
const keys = keyboardStore.keys.filter((_k, index) => selectedKeys.value.has(index))
keys.forEach((key) => {
key.setOnKeymap(setNewVal)
})
}
})
const insertTemplate = (templateType: KeycodeMode) => {
const keys = keyboardStore.keys.filter((_k, index) => selectedKeys.value.has(index))
keys.forEach((key) => {
if (templateType === 'macro') {
key.setOnKeymap('KC.MACRO(Press(KC.LCTL),Tap(KC.A),Release(KC.LCTL))')
} else if (templateType === 'string') {
key.setOnKeymap('KC.MACRO("Sample string")')
} else if (templateType === 'tapdance') {
key.setOnKeymap('KC.TD(KC.A,KC.B)')
} else if (templateType === 'custom') {
key.setOnKeymap('customkeys.MyKey')
}
})
}
const settingsOpen = ref(false)
const toggleSettings = () => {
settingsOpen.value = !settingsOpen.value
}
// Floating UI for Templates dropdown
const templatesButtonRef = ref<HTMLElement | null>(null)
const templatesDropdownRef = ref<HTMLElement | null>(null)
const templatesDropdownOpen = ref(false)
const { floatingStyles } = useFloating(templatesButtonRef, templatesDropdownRef, {
placement: 'bottom-end',
middleware: [offset(4), flip(), shift({ padding: 8 })],
whileElementsMounted: autoUpdate
})
const toggleTemplatesDropdown = () => {
templatesDropdownOpen.value = !templatesDropdownOpen.value
}
const closeTemplatesDropdown = () => {
templatesDropdownOpen.value = false
}
const macroModal = ref({
isOpen: false,
initialCode: ''
})
const openMacroModal = () => {
macroModal.value = { isOpen: true, initialCode: currentKeyCode.value }
}
const closeMacroModal = () => {
macroModal.value.isOpen = false
}
const applyMacroCode = (macroCode: string) => {
currentKeyCode.value = macroCode
closeMacroModal()
}
const coordMapWarning = computed(() => {
// show if any of the selected keys does not have and idx
const keys = keyboardStore.keys.filter((_k, index) => selectedKeys.value.has(index))
if (keys.length === 0) return ''
console.log(keys, keys[0].coordMapIndex)
if (keys.some((key) => typeof key.coordMapIndex !== 'number')) {
return '⚠️ no coordmap index set in the layout for this key'
}
return ''
})
watch(
() => currentKeyCode.value,
(newKeyCode) => {
if (newKeyCode && newKeyCode !== 'No key selected') {
const detectedType = detectKeycodeType(newKeyCode)
keycodeModeForSelection.value = detectedType
}
},
{ immediate: true }
)
</script>
================================================
FILE: src/renderer/src/components/KeymapLayer.vue
================================================
<template>
<div
class="tab font-bold"
:class="{ 'tab-active': index === selectedLayer }"
:style="{
background: keyboardStore.layers[index].color || '#434343',
color: 'white'
}"
@click="selectedLayer = index"
>
{{ index }} {{ keyboardStore.layers[index].name }}
<Popper>
<span class="edit-btn ml-4 px-1"><i class="mdi mdi-cog"></i></span>
<template #content>
<div class="popover text-left">
<span>Name</span>
<input v-model="keyboardStore.layers[index].name" class="input input-bordered input-sm" />
<span>Color</span>
<label class="relative">
<div
class="h-8 w-full cursor-pointer rounded border border-white border-opacity-40"
:style="{ background: keyboardStore.layers[index].color }"
></div>
<input
v-model="keyboardStore.layers[index].color"
type="color"
style="visibility: hidden; position: absolute"
/>
</label>
<div class="mt-2 flex gap-2">
<div
class="colorswatch"
style="background: #333"
@click="keyboardStore.layers[index].color = undefined"
></div>
<div
class="colorswatch"
style="background: #0ca508"
@click="keyboardStore.layers[index].color = '#0ca508'"
></div>
<div
class="colorswatch"
style="background: #259eb9"
@click="keyboardStore.layers[index].color = '#259eb9'"
></div>
<div
class="colorswatch"
style="background: #f28c18"
@click="keyboardStore.layers[index].color = '#f28c18'"
></div>
</div>
</div>
</template>
</Popper>
</div>
</template>
<script setup lang="ts">
import Popper from '@wlard/vue3-popper'
import { keyboardStore, selectedLayer } from '../store'
const props = defineProps(['layer', 'index'])
if (!keyboardStore.layers[props.index])
keyboardStore.layers[props.index] = { name: '', color: undefined }
</script>
<style lang="scss" scoped>
.tab {
@apply rounded pr-1;
background: #434343;
border: 2px solid transparent;
opacity: 0.6;
.edit-btn {
opacity: 0.5;
@apply rounded transition-all;
}
&:hover .edit-btn {
opacity: 1;
background: rgba(0, 0, 0, 0.2);
}
}
.tab-active {
@apply bg-primary font-bold text-black;
border: white 2px solid;
opacity: 1;
}
.popover {
@apply rounded border border-white border-opacity-40 bg-base-100 p-2 shadow-2xl;
}
.colorswatch {
height: 28px;
width: 28px;
@apply cursor-pointer rounded;
}
</style>
================================================
FILE: src/renderer/src/components/KmkInstaller.vue
================================================
<template>
<BaseModal
:open="isUpdateOpen"
title="Attention"
confirm-text="Update POG files"
cancel-text="Abort"
@close="isUpdateOpen = false"
@confirm="updatePOG"
>
<p>
Updating the POG files will overwrite all files on your keyboard generated by POG (e.g. kb.py,
code.py, customkeys.py, etc.)
</p>
<p class="pt-2">Be sure to backup your code if you still need any of it.</p>
</BaseModal>
<BaseModal
:open="isRestoreOpen"
title="Restore Configuration"
cancel-text="Cancel"
secondary-text="Keep ID"
confirm-text="Generate New ID"
@close="isRestoreOpen = false"
@secondary="restoreConfig(false)"
@confirm="restoreConfig(true)"
>
<p>
Do you want to generate new ID for the restored configuration? This is only recommended if you
are restoring a configuration from another keyboard.
</p>
</BaseModal>
<div class="mt-4 p-4 text-left">
<p>
<a href="https://kmkfw.io/" target="_blank" class="link">KMK</a> is a capable firmware for
keyboards using the Rp2040.
</p>
<p>
Before you proceed make sure you installed
<a class="link" href="https://circuitpython.org/downloads" target="_blank">CircuitPython</a>
on your controller
</p>
<p>
Info: This does not work when the controller is only connected via the serial port (and not as
mounted usb drive)
</p>
<br />
<p v-if="!keyboardStore.firmwareInstalled">
By clicking the button below, you can install KMK automatically to the following drive:
<span class="font-mono">{{ keyboardStore.path }}</span>
</p>
<div class="holder">
<div class="flex justify-center">
<div v-if="keyboardStore.firmwareInstalled" class="stats mt-8 shadow-xl">
<div class="stat text-left">
<div class="stat-figure text-primary">
<i class="mdi mdi-check text-3xl"></i>
</div>
<div class="stat-title">Firmware Installed</div>
<div class="stat-value text-primary">KMK</div>
<!-- <div class="stat-desc">modified on</div>-->
</div>
</div>
</div>
<div
v-if="['', 'done'].includes(kmkInstallState)"
class="mt-8 flex flex-col items-center justify-center"
>
<div
class="mt-8 grid justify-center gap-4"
:class="{
'grid-cols-1': !keyboardStore.firmwareInstalled,
'grid-cols-2': keyboardStore.firmwareInstalled
}"
>
<button class="btn btn-primary" @click="updateKMK">
{{ keyboardStore.firmwareInstalled ? 'update' : 'install' }} KMK
</button>
<button v-if="!initialSetup" class="btn btn-primary" @click="isUpdateOpen = true">
update Firmware
</button>
<button v-if="!initialSetup" class="btn btn-primary" @click="backupConfiguration">
Backup Config
</button>
<button
v-if="!initialSetup || keyboardStore.firmwareInstalled"
class="btn btn-primary"
@click="restoreConfiguration"
>
Restore Config
</button>
</div>
<input
ref="fileInput"
type="file"
accept=".json"
style="display: none"
@change="handleFileUpload"
/>
</div>
<div v-if="initialSetup" class="mt-8 flex justify-center">
<button
v-if="keyboardStore.firmwareInstalled"
class="btn btn-primary mt-4 block"
@click="$emit('next')"
>
Next
</button>
</div>
<div
v-if="['downloading', 'copying', 'unpacking'].includes(kmkInstallState)"
class="mt-4 flex flex-col items-center justify-center"
>
<p class="m-4 mt-8">{{ kmkInstallState || '' }}</p>
<progress class="progress progress-primary w-56" :value="progress" max="100"></progress>
<span v-if="progress !== 0"> {{ isNaN(progress) ? 'Done' : progress }}% </span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import BaseModal from './BaseModal.vue'
import { keyboardStore, addToHistory } from '../store'
import dayjs from 'dayjs'
import { ulid } from 'ulid'
import { saveConfigurationWithLoading } from '../helpers/saveConfigurationWrapper'
const progress = ref(0)
const kmkInstallState = ref('')
const isUpdateOpen = ref(false)
const isRestoreOpen = ref(false)
const fileInput = ref<HTMLInputElement>()
const pendingConfigData = ref<any>(null)
const props = defineProps<{ initialSetup: boolean }>()
const emit = defineEmits(['next', 'done'])
const startTime = ref(dayjs())
const endTime = ref(dayjs())
const updateKMK = async () => {
await window.api.updateFirmware()
kmkInstallState.value = 'downloading'
startTime.value = dayjs()
}
const updatePOG = async () => {
await saveConfigurationWithLoading(
JSON.stringify({ pogConfig: keyboardStore.serialize(), writeFirmware: true })
)
isUpdateOpen.value = false
}
const backupConfiguration = async () => {
try {
const configData = keyboardStore.serialize()
const jsonString = JSON.stringify(configData, null, 2)
const blob = new Blob([jsonString], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = 'pog_backup.json'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
} catch (error) {
console.error('Error downloading configuration:', error)
}
}
const restoreConfiguration = () => {
fileInput.value?.click()
}
const handleFileUpload = async (event: Event) => {
const target = event.target as HTMLInputElement
const file = target.files?.[0]
if (!file) return
try {
const configData = JSON.parse(await file.text())
pendingConfigData.value = configData
isRestoreOpen.value = true
} catch (error) {
console.error('Error reading or parsing configuration file:', error)
}
target.value = ''
}
const restoreConfig = async (generateNewIds: boolean) => {
if (!pendingConfigData.value) return
const configData = generateNewIds ? { ...pendingConfigData.value } : pendingConfigData.value
if (generateNewIds) {
configData.id = ulid()
}
try {
isRestoreOpen.value = false
await saveConfigurationWithLoading(
JSON.stringify({ pogConfig: configData, serial: false, writeFirmware: true })
)
if (keyboardStore.path) {
const keyboardData = await window.api.selectKeyboard({ path: keyboardStore.path })
if (keyboardData && !keyboardData.error) {
keyboardStore.import(keyboardData)
}
}
if (keyboardStore.keymap.length === 0) keyboardStore.keymap = [[]]
keyboardStore.coordMapSetup = false
if (props.initialSetup) {
addToHistory(keyboardStore)
}
emit('done')
} catch (e) {
console.error('restore failed', e)
}
}
window.api.onUpdateFirmwareInstallProgress(
(_event: Event, value: { state: string; progress: number }) => {
console.log('kmk progress', value)
// don't go back from done
if (kmkInstallState.value !== 'done') {
kmkInstallState.value = value.state
console.log('progress', value.progress)
progress.value = Math.round(value.progress)
if (value.state === 'done') {
keyboardStore.firmwareInstalled = true
endTime.value = dayjs()
console.log(startTime.value, endTime.value)
}
}
}
)
</script>
================================================
FILE: src/renderer/src/components/LayoutEditor.vue
================================================
<template>
<div class="flex gap-2">
<div class="btn btn-sm mb-4 p-2" @click="showConverter">
<i class="mdi mdi-import"></i>Import from KLE
</div>
<div class="btn btn-sm mb-4 p-2" @click="showQmkConverter">
<i class="mdi mdi-import"></i>Import from Qmk info json
</div>
<div class="btn btn-sm mb-4 p-2" @click="showRawPogOutput">
<i class="mdi mdi-export"></i>export from pog
</div>
</div>
<div v-if="converterVisible">
<div class="flex gap-2">
<div class="text-left">
<p>
you can import json files from the
<a class="link" href="http://keyboard-layout-editor.com" target="
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
SYMBOL INDEX (50 symbols across 10 files)
FILE: build/resign.js
function findNativeModules (line 5) | function findNativeModules(dir, fileList = []) {
FILE: src/main/index.ts
function crossSum (line 306) | function crossSum(s: string) {
constant CONNECTION_TIMEOUT (line 473) | const CONNECTION_TIMEOUT = 5000 // 5 seconds timeout
FILE: src/main/keyboardDetector.ts
type DetectionData (line 9) | interface DetectionData {
class KeyboardDetector (line 16) | class KeyboardDetector {
method startDetection (line 26) | async startDetection(window: BrowserWindow) {
method handleDetectionData (line 81) | private handleDetectionData(data: string, window: BrowserWindow) {
method stopDetection (line 107) | stopDetection() {
method getDetectionData (line 116) | getDetectionData(): DetectionData {
FILE: src/main/kmkUpdater.ts
function flashFirmware (line 184) | async function flashFirmware(firmwarePath: string): Promise<void> {
FILE: src/main/saveConfig.ts
type WriteTask (line 25) | type WriteTask = { name: string; path: string; contents: string }
FILE: src/main/store.ts
type Keyboard (line 6) | interface Keyboard {
FILE: src/preload/index.d.ts
type IElectronAPI (line 4) | interface IElectronAPI {
type Window (line 83) | interface Window {
FILE: src/renderer/src/helpers/colors.ts
function hslToHex (line 44) | function hslToHex(h, s, l): string {
FILE: src/renderer/src/store/index.ts
type KeyActions (line 11) | type KeyActions = {
type EncoderLayer (line 45) | type EncoderLayer = EncoderActions[]
type EncoderActions (line 46) | type EncoderActions = [string, string]
type BaseKeyInfo (line 48) | type BaseKeyInfo = {
type KeyInfo (line 61) | type KeyInfo = BaseKeyInfo & {
class Key (line 71) | class Key {
method constructor (line 90) | constructor({
method serialize (line 135) | serialize() {
method set (line 182) | set({}) {}
method delta (line 185) | delta({ property, value }: { value: number; property: keyof BaseKeyInf...
method getKeymapIndex (line 201) | getKeymapIndex() {
method setOnKeymap (line 213) | setOnKeymap(keyCode) {
method getMatrixLabel (line 238) | getMatrixLabel() {
method getEncoderLabel (line 256) | getEncoderLabel() {
type RgbOptions (line 265) | type RgbOptions = {
class Keyboard (line 275) | class Keyboard {
method constructor (line 363) | constructor() {}
method setKeys (line 366) | setKeys(keys: KeyInfo[]) {
method getKeys (line 375) | getKeys() {
method addKey (line 379) | addKey(key) {
method removeKeys (line 383) | removeKeys({ ids }: { ids: string[] }) {
method deltaForKeys (line 387) | deltaForKeys({
method hasFile (line 401) | hasFile(filename) {
method isSplit (line 405) | isSplit() {
method physicalKeyCount (line 410) | physicalKeyCount() {
method keyCount (line 424) | keyCount() {
method getMatrixWidth (line 428) | getMatrixWidth() {
method getKeymapIndexForKey (line 442) | getKeymapIndexForKey({ key }) {
method getActionForKey (line 450) | getActionForKey({ key, layer }) {
method import (line 458) | import({
method clear (line 526) | clear() {
method serialize (line 577) | serialize() {
class KeyboardStore (line 632) | class KeyboardStore extends Keyboard {}
FILE: src/renderer/src/store/serial.ts
constant MAX_LOGS (line 5) | const MAX_LOGS = 500
function addSerialLine (line 7) | function addSerialLine(raw: string) {
Condensed preview — 98 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (393K chars).
[
{
"path": ".editorconfig",
"chars": 154,
"preview": "root = true\r\n\r\n[*]\r\ncharset = utf-8\r\nindent_style = space\r\nindent_size = 2\r\nend_of_line = lf\r\ninsert_final_newline = tru"
},
{
"path": ".eslintignore",
"chars": 33,
"preview": "node_modules\ndist\nout\n.gitignore\n"
},
{
"path": ".eslintrc.cjs",
"chars": 1096,
"preview": "/* eslint-env node */\nrequire('@rushstack/eslint-patch/modern-module-resolution')\n\nmodule.exports = {\n root: true,\n en"
},
{
"path": ".github/workflows/electron_build.yml",
"chars": 3005,
"preview": "name: Build Electron App\nenv:\n GH_TOKEN: ${{ secrets.GH_TOKEN }}\non:\n push:\n tags:\n - 'v*'\n\njobs:\n build-linu"
},
{
"path": ".gitignore",
"chars": 65,
"preview": "node_modules\ndist\nout\n*.log*\n.idea\n.DS_Store\n.pog.code-workspace\n"
},
{
"path": ".npmrc",
"chars": 56,
"preview": "ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/\n"
},
{
"path": ".prettierignore",
"chars": 65,
"preview": "out\ndist\npnpm-lock.yaml\nLICENSE.md\ntsconfig.json\ntsconfig.*.json\n"
},
{
"path": ".vscode/extensions.json",
"chars": 52,
"preview": "{\n \"recommendations\": [\"dbaeumer.vscode-eslint\"]\n}\n"
},
{
"path": ".vscode/launch.json",
"chars": 410,
"preview": "{\n \"version\": \"0.2.0\",\n \"configurations\": [\n {\n \"name\": \"Debug Main Process\",\n \"type\": \"node\",\n \"req"
},
{
"path": ".vscode/settings.json",
"chars": 240,
"preview": "{\n \"[typescript]\": {\n \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n },\n \"[javascript]\": {\n \"editor.defau"
},
{
"path": "LICENSE",
"chars": 1068,
"preview": "MIT License (MIT)\n\nCopyright 2023 Jan Lunge\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
},
{
"path": "README.md",
"chars": 2159,
"preview": "\n<h1 align=\"center\">POG</h1>\n<h4 align=\"center\">\nKMK GUI, Layout Editor, Keymap Edi"
},
{
"path": "build/entitlements.mac.plist",
"chars": 415,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "build/notarize.js",
"chars": 993,
"preview": "const { notarize } = require('@electron/notarize')\nconst path = require('path')\nconst fs = require('fs')\nconst { execSyn"
},
{
"path": "build/resign.js",
"chars": 893,
"preview": "const fs = require('fs')\nconst path = require('path')\nconst execSync = require('child_process').execSync\n\nfunction findN"
},
{
"path": "dev-app-update.yml",
"chars": 105,
"preview": "provider: generic\nurl: https://pog.heaper.de/auto-updates\nupdaterCacheDirName: vue-vite-electron-updater\n"
},
{
"path": "electron-builder.yml",
"chars": 1684,
"preview": "appId: de.heaper.pog\nproductName: pog\ndirectories:\n buildResources: build\nfiles:\n - '!**/.vscode/*'\n - '!src/*'\n - '"
},
{
"path": "electron.vite.config.ts",
"chars": 414,
"preview": "import { resolve } from 'path'\nimport { defineConfig, externalizeDepsPlugin } from 'electron-vite'\nimport vue from '@vit"
},
{
"path": "package.json",
"chars": 2904,
"preview": "{\n \"name\": \"pog\",\n \"version\": \"2.2.0\",\n \"license\": \"MIT\",\n \"description\": \"A KMK firmware configurator\",\n \"main\": \""
},
{
"path": "postcss.config.js",
"chars": 80,
"preview": "module.exports = {\n plugins: {\n tailwindcss: {},\n autoprefixer: {}\n }\n}\n"
},
{
"path": "prettier.config.js",
"chars": 176,
"preview": "// prettier.config.js\nmodule.exports = {\n plugins: [require('prettier-plugin-tailwindcss')],\n singleQuote: true,\n sem"
},
{
"path": "src/main/index.ts",
"chars": 21257,
"preview": "import { app, shell, BrowserWindow, ipcMain, Menu } from 'electron'\nimport { join } from 'path'\nimport { electronApp, op"
},
{
"path": "src/main/keyboardDetector.ts",
"chars": 4205,
"preview": "import { SerialPort } from 'serialport'\nimport { ReadlineParser } from '@serialport/parser-readline'\nimport { BrowserWin"
},
{
"path": "src/main/kmkUpdater.ts",
"chars": 7050,
"preview": "import { appDir, currentKeyboard } from './store'\nimport * as fs from 'fs-extra'\nimport request from 'request'\nimport de"
},
{
"path": "src/main/pythontemplates/boot.ts",
"chars": 492,
"preview": "export const bootpy = `# boot.py - v1.0.5\nimport usb_cdc\nimport supervisor\nimport storage\nimport microcontroller\n\n# opti"
},
{
"path": "src/main/pythontemplates/code.ts",
"chars": 908,
"preview": "export const codepy = `# Main Keyboard Configuration - v1.0.0\nimport board\nimport pog\n# check if we just want to run the"
},
{
"path": "src/main/pythontemplates/coordmaphelper.ts",
"chars": 1567,
"preview": "export const coordmaphelperpy = `# coordmaphelper.py v1.0.1\nimport board\nimport pog\nfrom kb import POGKeyboard\nfrom kmk."
},
{
"path": "src/main/pythontemplates/customkeys.ts",
"chars": 977,
"preview": "export const customkeyspy = `# These are yous custom keycodes do any needed imports at the top - v1.0.0\n# then you can r"
},
{
"path": "src/main/pythontemplates/detection.ts",
"chars": 5124,
"preview": "export const detectionFirmware = `import board\nimport digitalio\nimport time\nimport supervisor\nimport usb_cdc\nimport json"
},
{
"path": "src/main/pythontemplates/kb.ts",
"chars": 6243,
"preview": "export const kbpy = `# kb.py KB base config - v1.0.0\nimport board\nimport pog\nimport microcontroller\n\nfrom kmk.kmk_keyboa"
},
{
"path": "src/main/pythontemplates/keymap.ts",
"chars": 665,
"preview": "export const keymappy = `#keymap.py KB base config - v1.0.0\nfrom kmk.keys import KC\nfrom kmk.modules.macros import Macro"
},
{
"path": "src/main/pythontemplates/pog.ts",
"chars": 4632,
"preview": "export const pogpy = `# pog.py Import the pog config - v0.9.5\nimport json\nimport board\nfrom kmk.keys import KC\nimport mi"
},
{
"path": "src/main/pythontemplates/pog_serial.ts",
"chars": 7007,
"preview": "export const pog_serialpy = `# pog_serial module - v0.9.5\nfrom usb_cdc import data\nfrom kmk.modules import Module\nfrom k"
},
{
"path": "src/main/saveConfig.ts",
"chars": 2602,
"preview": "import * as fs from 'fs-extra'\nimport { currentKeyboard } from './store'\nimport { pogpy } from './pythontemplates/pog'\ni"
},
{
"path": "src/main/selectKeyboard.ts",
"chars": 3070,
"preview": "import * as fs from 'fs-extra'\nimport { currentKeyboard } from './store'\nimport { dialog } from 'electron'\nimport { conn"
},
{
"path": "src/main/store.ts",
"chars": 437,
"preview": "// Store for global variables\nimport { app } from 'electron'\n\nexport const appDir = app.getPath('appData') + '/pog/'\n\nin"
},
{
"path": "src/preload/index.d.ts",
"chars": 2730,
"preview": "import { ElectronAPI } from '@electron-toolkit/preload'\n\n\nexport interface IElectronAPI {\n // Keyboard History API\n li"
},
{
"path": "src/preload/index.ts",
"chars": 2789,
"preview": "import { contextBridge, ipcRenderer } from 'electron'\nimport { electronAPI } from '@electron-toolkit/preload'\n\n// Custom"
},
{
"path": "src/renderer/index.html",
"chars": 495,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"UTF-8\" />\n <title>POG</title>\n <!-- https://developer.mozilla.o"
},
{
"path": "src/renderer/src/App.vue",
"chars": 2606,
"preview": "<script setup lang=\"ts\">\nimport { computed, onMounted, onUnmounted } from 'vue'\nimport { addToHistory, keyboardStore, no"
},
{
"path": "src/renderer/src/assets/css/styles.less",
"chars": 2821,
"preview": "body {\n display: flex;\n flex-direction: column;\n font-family: Roboto, -apple-system, BlinkMacSystemFont, 'Helvetica N"
},
{
"path": "src/renderer/src/assets/microcontrollers/microcontrollers.json",
"chars": 1086,
"preview": "[\n {\n \"id\": \"0xcb-helios\",\n \"name\": \"0xCB Helios\",\n \"information\": \"The <a class=\\\"link-primary "
},
{
"path": "src/renderer/src/components/AutomaticSetup.vue",
"chars": 7134,
"preview": "<template>\n <div class=\"min-h-screen bg-base-100 p-6\">\n <div class=\"mx-auto max-w-5xl space-y-8\">\n <div class=\""
},
{
"path": "src/renderer/src/components/BaseModal.vue",
"chars": 1883,
"preview": "<template>\n <Transition\n enter-active-class=\"transition duration-300 ease-out\"\n enter-from-class=\"opacity-0\"\n "
},
{
"path": "src/renderer/src/components/CircuitPythonSetup.vue",
"chars": 3533,
"preview": "<template>\n <div class=\"flex flex-col items-center justify-center p-8\">\n <h2 class=\"mb-6 text-2xl font-bold\">Circuit"
},
{
"path": "src/renderer/src/components/Community.vue",
"chars": 2014,
"preview": "<template>\n <div>\n <w3m-core-button label=\"Login\"></w3m-core-button>\n <div v-if=\"accountAddress\">\n {{ render"
},
{
"path": "src/renderer/src/components/CoordMap.vue",
"chars": 6378,
"preview": "<template>\n <dialog id=\"flash_modal\" class=\"modal\">\n <div class=\"modal-box\">\n <h3 class=\"text-lg font-bold\">Att"
},
{
"path": "src/renderer/src/components/EncoderLayer.vue",
"chars": 1243,
"preview": "<template>\n <div\n v-if=\"keyboardStore.encoderKeymap[lindex] && keyboardStore.encoderKeymap[lindex][eindex]\"\n clas"
},
{
"path": "src/renderer/src/components/EncoderSetup.vue",
"chars": 2864,
"preview": "<template>\n <div>\n <div\n v-for=\"(encoder, eindex) in keyboardStore.encoders\"\n class=\"my-2 grid gap-4 bg-ba"
},
{
"path": "src/renderer/src/components/HsvColorPicker.vue",
"chars": 2153,
"preview": "<template>\n <div class=\"mb-2 flex items-center gap-4\">\n <div class=\"flex flex-col gap-2\">\n <h2 class=\"font-bold"
},
{
"path": "src/renderer/src/components/KeyCap.vue",
"chars": 13790,
"preview": "<template>\n <div\n v-if=\"visible\"\n ref=\"keyElem\"\n class=\"keycap\"\n style=\"user-select: none\"\n :data-index="
},
{
"path": "src/renderer/src/components/KeyLayoutInfo.vue",
"chars": 12192,
"preview": "<template>\n \n <div class=\"flex justify-between items-center h-10\">\n <div v-if=\"selectedKeys.size === 0\">\n "
},
{
"path": "src/renderer/src/components/KeyPicker.vue",
"chars": 10644,
"preview": "<template>\n <div class=\"tabs tabs-boxed my-4\">\n <a class=\"tab\" :class=\"{ 'tab-active': layout === 'qwerty' }\" @click"
},
{
"path": "src/renderer/src/components/KeyboardLayout.vue",
"chars": 8603,
"preview": "<template>\n <SelectionArea\n ref=\"keyboardContainer\"\n class=\"keyboard-container container\"\n :class=\"{ 'fixed-he"
},
{
"path": "src/renderer/src/components/KeyboardName.vue",
"chars": 2467,
"preview": "<template>\n <div class=\"mt-8\">\n <div class=\"mb-4\">\n <p class=\"mb-2 text-sm\">Name</p>\n <input v-model=\"keyb"
},
{
"path": "src/renderer/src/components/KeymapEditor.vue",
"chars": 11195,
"preview": "<template>\n <div class=\"relative\">\n <!-- <h2 class=\"mb-2 inline-block absolute top-6\" style=\"transform: rotate(-9"
},
{
"path": "src/renderer/src/components/KeymapLayer.vue",
"chars": 2752,
"preview": "<template>\n <div\n class=\"tab font-bold\"\n :class=\"{ 'tab-active': index === selectedLayer }\"\n :style=\"{\n b"
},
{
"path": "src/renderer/src/components/KmkInstaller.vue",
"chars": 7626,
"preview": "<template>\n <BaseModal\n :open=\"isUpdateOpen\"\n title=\"Attention\"\n confirm-text=\"Update POG files\"\n cancel-te"
},
{
"path": "src/renderer/src/components/LayoutEditor.vue",
"chars": 9463,
"preview": "<template>\n <div class=\"flex gap-2\">\n <div class=\"btn btn-sm mb-4 p-2\" @click=\"showConverter\">\n <i class=\"mdi m"
},
{
"path": "src/renderer/src/components/LoadingOverlay.vue",
"chars": 8999,
"preview": "<template>\n <Transition\n enter-active-class=\"transition duration-300 ease-out\"\n enter-from-class=\"opacity-0 -tran"
},
{
"path": "src/renderer/src/components/MacroModal.vue",
"chars": 6822,
"preview": "<template>\n <Transition\n enter-active-class=\"transition duration-300 ease-out\"\n enter-from-class=\"opacity-0\"\n "
},
{
"path": "src/renderer/src/components/MatrixSetup.vue",
"chars": 3878,
"preview": "<template>\n <div>\n <p class=\"max-w-xl py-4\">\n Define the size of your keyboard matrix here, set it as big as yo"
},
{
"path": "src/renderer/src/components/PinSetup.vue",
"chars": 11210,
"preview": "<template>\n <div class=\"flex items-center justify-center\">\n <p class=\"max-w-md py-4\">\n Define the mapping for c"
},
{
"path": "src/renderer/src/components/RawKeymapEditor.vue",
"chars": 715,
"preview": "<template>\n <p class=\"font-bold\">Keymap</p>\n <div>\n <div v-for=\"(layer, layerindex) in keyboardStore.keymap\">\n "
},
{
"path": "src/renderer/src/components/RgbSetup.vue",
"chars": 5200,
"preview": "<template>\n <div>\n <label class=\"flex gap-4\">\n <input v-model=\"rgbEnabled\" class=\"checkbox\" type=\"checkbox\" @ch"
},
{
"path": "src/renderer/src/components/SetupMethodSelector.vue",
"chars": 7318,
"preview": "<template>\n <div class=\"flex flex-col items-center justify-center p-8\">\n <h2 class=\"mb-6 text-2xl font-bold\">Choose "
},
{
"path": "src/renderer/src/components/VariantOption.vue",
"chars": 2097,
"preview": "<template>\n <div class=\"flex items-center pr-4\">\n <div class=\"mr-2\">\n <button class=\"btn btn-primary btn-xs\" @c"
},
{
"path": "src/renderer/src/components/VariantSwitcher.vue",
"chars": 592,
"preview": "<template>\n <div>\n <button class=\"btn btn-primary btn-sm mb-4\" @click=\"addLayoutOption\">\n <i class=\"mdi mdi-plu"
},
{
"path": "src/renderer/src/components/debug.vue",
"chars": 7014,
"preview": "<template>\n <div class=\"flex h-full flex-col\">\n <div class=\"mb-2 text-sm\">\n <div class=\"mb-2 flex items-center "
},
{
"path": "src/renderer/src/components/installPogFirmware.vue",
"chars": 2090,
"preview": "<template>\n <div class=\"min-h-screen bg-base-100 p-6\">\n <div class=\"mx-auto max-w-2xl space-y-8\">\n <div class=\""
},
{
"path": "src/renderer/src/components/picker-layouts/Colemak.vue",
"chars": 8951,
"preview": "<template>\n <div class=\"key-chooser flex\">\n <div class=\"row\">\n <div class=\"key\" @click=\"emit('key', 'KC.ESC')\">"
},
{
"path": "src/renderer/src/components/picker-layouts/ColemakDH.vue",
"chars": 8951,
"preview": "<template>\n <div class=\"key-chooser flex\">\n <div class=\"row\">\n <div class=\"key\" @click=\"emit('key', 'KC.ESC')\">"
},
{
"path": "src/renderer/src/components/picker-layouts/Dvorak.vue",
"chars": 8951,
"preview": "<template>\n <div class=\"key-chooser flex\">\n <div class=\"row\">\n <div class=\"key\" @click=\"emit('key', 'KC.ESC')\">"
},
{
"path": "src/renderer/src/components/picker-layouts/Qwerty.vue",
"chars": 8951,
"preview": "<template>\n <div class=\"key-chooser flex\">\n <div class=\"row\">\n <div class=\"key\" @click=\"emit('key', 'KC.ESC')\">"
},
{
"path": "src/renderer/src/components/setup/Wizard.vue",
"chars": 1801,
"preview": "<template>\n <div>\n <p>\n Before you start please check that your controller is using circuit python, if not plea"
},
{
"path": "src/renderer/src/components/ui/InputLabel.vue",
"chars": 992,
"preview": "<template>\n <div class=\"form-control\">\n <label class=\"label\">\n <span class=\"label-text\">{{ label }}</span>\n "
},
{
"path": "src/renderer/src/composables/useLoadingOverlay.ts",
"chars": 1205,
"preview": "import { ref, onMounted, onUnmounted } from 'vue'\nimport { onLoadingChange, hideLoading } from '../helpers/saveConfigura"
},
{
"path": "src/renderer/src/env.d.ts",
"chars": 281,
"preview": "/// <reference types=\"vite/client\" />\n\ndeclare module '*.vue' {\n import type { DefineComponent } from 'vue'\n // eslint"
},
{
"path": "src/renderer/src/helpers/colors.ts",
"chars": 1954,
"preview": "export const hexToHSL = (hex): {hue: number, sat: number, val: number} => {\n const result: RegExpExecArray | null = /^#"
},
{
"path": "src/renderer/src/helpers/index.ts",
"chars": 13620,
"preview": "import JSON5 from 'json5'\nimport { keyboardStore, KeyInfo, selectedKeys } from '../store'\nexport const matrixPositionToI"
},
{
"path": "src/renderer/src/helpers/saveConfigurationWrapper.ts",
"chars": 1356,
"preview": "import { ref } from 'vue'\n\n// Global loading state\nconst isLoading = ref(false)\nconst loadingCallbacks = new Set<(loadin"
},
{
"path": "src/renderer/src/helpers/types.d.ts",
"chars": 0,
"preview": ""
},
{
"path": "src/renderer/src/main.ts",
"chars": 312,
"preview": "import { createApp } from 'vue'\nimport App from './App.vue'\nimport router from './router'\n\nimport '@mdi/font/css/materia"
},
{
"path": "src/renderer/src/router/index.ts",
"chars": 3691,
"preview": "import { createRouter, createWebHashHistory } from 'vue-router'\nimport LaunchScreen from '../screens/LaunchScreen.vue'\ni"
},
{
"path": "src/renderer/src/screens/AddKeyboard.vue",
"chars": 906,
"preview": "<template>\n <div class=\"btn\" @click=\"$router.push('/')\"><i class=\"mdi mdi-close\"></i></div>\n <p>Create a Custom Keyboa"
},
{
"path": "src/renderer/src/screens/KeyboardConfigurator.vue",
"chars": 5818,
"preview": "<template>\n <div class=\"flex h-screen\">\n <ul\n class=\"menu flex-shrink-0 bg-base-100\"\n :class=\"{\n 'm"
},
{
"path": "src/renderer/src/screens/KeyboardSelector.vue",
"chars": 2988,
"preview": "<template>\n <div class=\"min-h-screen bg-base-200 flex items-center justify-center\">\n <div class=\"max-w-2xl w-full p-"
},
{
"path": "src/renderer/src/screens/KeyboardSetup.vue",
"chars": 676,
"preview": "<template>\n <div class=\"min-h-screen bg-base-200\">\n <div class=\"container mx-auto py-8\">\n <router-view\n "
},
{
"path": "src/renderer/src/screens/LaunchScreen.vue",
"chars": 7446,
"preview": "<template>\n <div class=\"flex flex-col p-4\">\n <div class=\"flex items-center justify-between px-12\">\n <div class="
},
{
"path": "src/renderer/src/screens/SetupWizard.vue",
"chars": 3293,
"preview": "<template>\n <div class=\"flex h-full w-full flex-col items-center\">\n <div class=\"flex-grow-0\">\n <h1 class=\"my-4 "
},
{
"path": "src/renderer/src/store/index.ts",
"chars": 19385,
"preview": "import { computed, ref } from 'vue'\nimport VueStore from '@wlard/vue-class-store'\nimport { ulid } from 'ulid'\nimport { u"
},
{
"path": "src/renderer/src/store/serial.ts",
"chars": 391,
"preview": "import { ref } from 'vue'\n\nexport const serialLogs = ref<string[]>([])\n\nconst MAX_LOGS = 500\n\nexport function addSerialL"
},
{
"path": "src/renderer/src/style/index.css",
"chars": 59,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"
},
{
"path": "src/renderer/src/style/multiselect.css",
"chars": 5851,
"preview": ".multiselect {\n @apply relative mx-auto w-full flex items-center justify-end box-border text-white cursor-pointer borde"
},
{
"path": "tailwind.config.js",
"chars": 3393,
"preview": "/** @type {import('tailwindcss').Config} */\nconst svgToDataUri = require('mini-svg-data-uri')\nmodule.exports = {\n relat"
},
{
"path": "tsconfig.json",
"chars": 193,
"preview": "{\n \"files\": [],\n \"references\": [{ \"path\": \"./tsconfig.node.json\" }, { \"path\": \"./tsconfig.web.json\" }],\n \"compilerOpt"
},
{
"path": "tsconfig.node.json",
"chars": 317,
"preview": "{\n \"extends\": \"@electron-toolkit/tsconfig/tsconfig.node.json\",\n \"include\": [\"electron.vite.config.*\", \"src/main/*\", \"s"
},
{
"path": "tsconfig.web.json",
"chars": 392,
"preview": "{\n \"extends\": \"@electron-toolkit/tsconfig/tsconfig.web.json\",\n \"include\": [\n \"src/renderer/src/env.d.ts\",\n \"src/"
}
]
About this extraction
This page contains the full source code of the JanLunge/pog GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 98 files (360.2 KB), approximately 103.3k tokens, and a symbol index with 50 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.