Full Code of JanLunge/pog for AI

main 9a7e910e8c26 cached
98 files
360.2 KB
103.3k tokens
50 symbols
1 requests
Download .txt
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
================================================
![logo](demo/pog-header.png?raw=true)
<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>

![preview](demo/pog-screenshot.png?raw=true)

# Documentation
the documentation is available [here](https://github.com/JanLunge/pog-docs). Feel free to contribute

# Installation
download the pre-built binaries for Windows, Mac and Linux are available in the [releases](https://github.com/JanLunge/pog/releases)

# Development Setup
## dependencies
* node 16
* yarn

install everything with
`yarn`
then just run it with dev to start
`yarn dev`

to release a new version use `npm version minor` or `major` or `patch` then just push and github actions will do the rest

# Tasks
## bugs
- [ ] maximum call stack error when closing the app
## urgent
- [x] check if a keyboard is connected (usb drive) in the keyboard selector preview
- [x] show serial output in the gui
- [ ] automatically get the correct serial device (by serial number)
- [ ] guides etc. for setup + split workflow | help menu + videos
- [ ] save wiring info in qr code or so
- [ ] share pog.json files
- [ ] check if the controller you use even has the pin you specified (controller lookup and serial to get pins )
- [ ] generate layout based on matrix + clear layout button / delete multiple
- [ ] features case-insensitive and via gui or pog json

## features wishlist
- [ ] bluetooth workflow
- [ ] language switcher for german and other layouts changing the labels on the keymap
- [ ] modtap/tapdance/macros/sequences
- [ ] encoder support direct pin click
- [ ] way to handle differences between pog.json to kmk code
- [ ] wiring preview


### Build

```bash
# For windows
$ npm run build:win

# For macOS
$ npm run build:mac

# For Linux
$ npm run build:linux
```


================================================
FILE: build/entitlements.mac.plist
================================================
<?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')">&lt;</div>
        <div class="key" @click="setKey('KC.RABK')">&gt;</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="
Download .txt
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
Download .txt
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": "![logo](demo/pog-header.png?raw=true)\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.

Copied to clipboard!