aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--api/routes/github.js9
-rw-r--r--api/services/github/files.js17
-rw-r--r--api/services/github/index.js2
-rw-r--r--api/services/zmk/keymap.js2
-rw-r--r--application/components/app.vue196
-rw-r--r--application/components/key.vue7
-rw-r--r--application/components/keymap.vue2
-rw-r--r--application/components/modal.vue11
-rw-r--r--application/components/value-picker.vue88
-rw-r--r--application/github.js15
-rw-r--r--application/style.css1
11 files changed, 287 insertions, 63 deletions
diff --git a/api/routes/github.js b/api/routes/github.js
index 5c4456e..a1f23b4 100644
--- a/api/routes/github.js
+++ b/api/routes/github.js
@@ -10,7 +10,8 @@ const {
fetchKeyboardFiles,
createOauthFlowUrl,
createOauthReturnUrl,
- commitChanges
+ commitChanges,
+ InvalidRepoError,
} = require('../services/github')
const { parseKeymap } = require('../services/zmk/keymap')
@@ -88,6 +89,12 @@ const getKeyboardFiles = async (req, res, next) => {
keyboardFiles.keymap = parseKeymap(keyboardFiles.keymap)
res.json(keyboardFiles)
} catch (err) {
+ if (err instanceof InvalidRepoError) {
+ return res.status(400).json({
+ error: 'InvalidRepoError'
+ })
+ }
+
next(err)
}
}
diff --git a/api/services/github/files.js b/api/services/github/files.js
index 5f5837f..40bcb41 100644
--- a/api/services/github/files.js
+++ b/api/services/github/files.js
@@ -4,12 +4,22 @@ const zmk = require('../zmk')
const MODE_FILE = '100644'
+class InvalidRepoError extends Error {}
+
async function fetchKeyboardFiles (installationId, repository) {
const { data: { token: installationToken } } = await auth.createInstallationToken(installationId)
- const { data: info } = await fetchFile(installationToken, repository, 'config/info.json', true)
- const { data: keymap } = await fetchFile(installationToken, repository, 'config/keymap.json', true)
+ try {
+ const { data: info } = await fetchFile(installationToken, repository, 'config/info.json', true)
+ const { data: keymap } = await fetchFile(installationToken, repository, 'config/keymap.json', true)
+
+ return { info, keymap }
+ } catch (err) {
+ if (err.response && err.response.status === 404) {
+ throw new InvalidRepoError()
+ }
- return { info, keymap }
+ throw err
+ }
}
function fetchFile (installationToken, repository, path, raw = false) {
@@ -76,6 +86,7 @@ async function commitChanges (installationId, repository, layout, keymap) {
}
module.exports = {
+ InvalidRepoError,
fetchKeyboardFiles,
commitChanges
}
diff --git a/api/services/github/index.js b/api/services/github/index.js
index 34a0654..8443dd7 100644
--- a/api/services/github/index.js
+++ b/api/services/github/index.js
@@ -13,6 +13,7 @@ const {
} = require('./installations')
const {
+ InvalidRepoError,
fetchKeyboardFiles,
commitChanges
} = require('./files')
@@ -26,6 +27,7 @@ module.exports = {
verifyUserToken,
fetchInstallation,
fetchInstallationRepos,
+ InvalidRepoError,
fetchKeyboardFiles,
commitChanges
}
diff --git a/api/services/zmk/keymap.js b/api/services/zmk/keymap.js
index 011f486..a8837ea 100644
--- a/api/services/zmk/keymap.js
+++ b/api/services/zmk/keymap.js
@@ -108,7 +108,7 @@ function generateKeymapCode (layout, keymap, encoded) {
})
return `
- ${name} {
+ ${name.replace(/[^a-zA-Z0-9_]/g, '_')} {
bindings = <
${rendered}
>;
diff --git a/application/components/app.vue b/application/components/app.vue
index 2092b74..ac4348d 100644
--- a/application/components/app.vue
+++ b/application/components/app.vue
@@ -2,6 +2,8 @@
import Initialize from './initialize.vue'
import Keymap from './keymap.vue'
+import Loader from './loader.vue'
+import Modal from './modal.vue'
import * as config from '../config'
import * as github from '../github'
@@ -9,12 +11,31 @@ import * as github from '../github'
export default {
components: {
keymap: Keymap,
- Initialize
+ Initialize,
+ Loader,
+ Modal
+ },
+ provide() {
+ return {
+ keycodes: this.keycodes,
+ behaviours: this.behaviours,
+ indexedKeycodes: this.indexedKeycodes,
+ indexedBehaviours: this.indexedBehaviours
+ }
},
data() {
return {
config,
+ keycodes: [],
editingKeymap: {},
+ indexedKeycodes: {},
+ behaviours: [],
+ indexedBehaviours: {},
+ tooManyRepos: false,
+ loadKeyboardError: null,
+ keymap: {},
+ layout: [],
+ layers: [],
terminalOpen: false,
socket: null
}
@@ -25,6 +46,57 @@ export default {
}
},
methods: {
+ async loadData() {
+ await github.init()
+ if (config.enableGitHub && github.isGitHubAuthorized() && github.repositories.length > 1) {
+ this.tooManyRepos = true
+ return
+ }
+ const loadKeyboardData = async () => {
+ if (config.enableGitHub && github.isGitHubAuthorized()) {
+ const response = await github.fetchLayoutAndKeymap()
+ if (response.error) {
+ this.loadKeyboardError = response.error
+ return { layout: [], keymap: { layers: [] } }
+ }
+
+ return response
+ } else if (config.enableLocal) {
+ const [layout, keymap] = await Promise.all([
+ loadLayout(),
+ loadKeymap()
+ ])
+ return { layout, keymap }
+ } else {
+ return { layout: [], keymap: { layers: [] } }
+ }
+ }
+
+ const [
+ keycodes,
+ behaviours,
+ { layout, keymap }
+ ] = await Promise.all([
+ loadKeycodes(),
+ loadBehaviours(),
+ loadKeyboardData()
+ ])
+
+ this.keycodes.splice(0, this.keycodes.length, ...keycodes)
+ this.behaviours.splice(0, this.behaviours.length, ...behaviours)
+ Object.assign(this.indexedKeycodes, keyBy(this.keycodes, 'code'))
+ Object.assign(this.indexedBehaviours, keyBy(this.behaviours, 'code'))
+
+ this.layout.splice(0, this.layout.length, ...layout.map(key => (
+ { ...key, u: key.u || key.w || 1, h: key.h || 1 }
+ )))
+
+ const layerNames = keymap.layer_names || keymap.layers.map((_, i) => `Layer ${i}`)
+ Object.assign(this.layers, keymap.layers)
+ Object.assign(this.keymap, keymap, {
+ layer_names: layerNames
+ })
+ },
handleUpdateKeymap(keymap) {
Object.assign(this.editingKeymap, keymap)
},
@@ -32,7 +104,7 @@ export default {
github.beginLoginFlow()
},
handleCommitChanges() {
- github.commitChanges(this.layout, this.keymap)
+ github.commitChanges(this.layout, this.editingKeymap)
},
handleCompile() {
fetch('/keymap', {
@@ -40,8 +112,15 @@ export default {
headers: {
'Content-Type': 'application/json'
},
- body: JSON.stringify(this.keymap)
+ body: JSON.stringify(this.editingKeymap)
})
+ },
+ getInstallationUrl() {
+ return `https://github.com/settings/installations/${github.installation.id}`
+ },
+ async doReadyCheck() {
+ await healthcheck()
+ await this.loadData()
}
}
}
@@ -49,37 +128,70 @@ export default {
<template>
<initialize v-slot="{ keymap, layout }">
+ <div v-if="tooManyRepos">
+ <modal>
+ <div class="dialog">
+ <h2>Hold up a second!</h2>
+ <p>The Keymap Editor app has been installed for more than one GitHub repository.</p>
+ <p>
+ I'm still working on things, including the ability to pick a specific
+ repo, but in the meantime you should go back to your <a :href="getInstallationUrl()">app configuration</a>
+ and select a single repository containing your keyboard's zmk-config.
+ </p>
+ </div>
+ </modal>
+ </div>
- <keymap
- :layout="layout"
- :keymap="editingKeymap.keyboard ? editingKeymap : keymap"
- @update="handleUpdateKeymap"
- />
-
- <div id="actions">
- <button
- v-if="config.enableLocal"
- v-text="`Save Local`"
- id="compile"
- @click="handleCompile"
+ <div v-else-if="loadKeyboardError === 'InvalidRepoError'">
+ <modal>
+ <div class="dialog">
+ <h2>Hold up a second!</h2>
+ <p>The selected repository does not contain <code>info.json</code> or <code>keymap.json</code>.</p>
+ <p>
+ This app depends on some additional metadata to render the keymap.
+ For an example repository ready to use now or metadata you can apply
+ to your own keyboard repo, have a look at <a href="https://github.com/nickcoutsos/zmk-config-corne-demo/">zmk-config-corne-demo</a>.
+ </p>
+ </div>
+ </modal>
+ </div>
+
+ <template v-else>
+ <keymap
+ :layout="layout"
+ :keymap="editingKeymap.keyboard ? editingKeymap : keymap"
+ @update="handleUpdateKeymap"
/>
+ <div id="actions">
+ <button
+ v-if="config.enableLocal"
+ v-text="`Save Local`"
+ id="compile"
+ :disabled="!this.editingKeymap.keyboard"
+ @click="handleCompile"
+ />
- <button
- v-if="config.enableGitHub && !githubAuthorized"
- v-text="`Authorize GitHub`"
- @click="handleGithubAuthorize"
- title="Install as a GitHub app to edit a zmk-config repository."
+ <button
+ v-if="config.enableGitHub && !githubAuthorized"
+ v-text="`Authorize GitHub`"
+ @click="handleGithubAuthorize"
+ title="Install as a GitHub app to edit a zmk-config repository."
- />
+ />
- <button
- v-if="config.enableGitHub && githubAuthorized"
- v-text="`Commit Changes`"
- @click="handleCommitChanges"
- title="Commit keymap changes to GitHub repository"
- />
- </div>
+ <button
+ v-if="config.enableGitHub && githubAuthorized"
+ v-text="`Commit Changes`"
+ @click="handleCommitChanges"
+ :disabled="!this.editingKeymap.keyboard"
+ title="Commit keymap changes to GitHub repository"
+ />
+ </div>
+ </template>
+ <a class="github-link" href="https://github.com/nickcoutsos/keymap-editor">
+ <i class="fab fa-github" />/nickcoutsos/keymap-editor
+ </a>
</initialize>
</template>
@@ -96,4 +208,32 @@ button {
margin: 2px;
}
+button[disabled] {
+ background-color: #ccc;
+ cursor: not-allowed;
+}
+
+.dialog {
+ background-color: white;
+ padding: 40px;
+ margin: 40px;
+ max-width: 500px;
+}
+
+.github-link {
+ display: inline-block;
+ position: absolute;
+ z-index: 100;
+ bottom: 5px;
+ left: 5px;
+ font-size: 110%;
+ font-style: italic;
+ background-color: white;
+ border-radius: 20px;
+ padding: 5px 10px;
+ text-decoration: none;
+
+ color: royalblue;
+}
+
</style>
diff --git a/application/components/key.vue b/application/components/key.vue
index f958c88..dcc6972 100644
--- a/application/components/key.vue
+++ b/application/components/key.vue
@@ -24,9 +24,8 @@
:values="normalized.params"
:onSelect="handleSelectCode"
/>
- <teleport to="body">
+ <modal v-if="editing">
<value-picker
- v-if="editing"
:target="editing.target"
:value="editing.code"
:param="editing.param"
@@ -36,7 +35,7 @@
@select="handleSelectValue"
@cancel="editing = null"
/>
- </teleport>
+ </modal>
</div>
</template>
@@ -51,6 +50,7 @@ import { getKeyStyles } from '../key-units'
import KeyValue from './key-value.vue'
import KeyParamlist from './key-paramlist.vue'
+import Modal from './modal.vue'
import ValuePicker from './value-picker.vue'
function makeIndex (tree) {
@@ -77,6 +77,7 @@ export default {
components: {
'key-value': KeyValue,
'key-paramlist': KeyParamlist,
+ Modal,
ValuePicker
},
data () {
diff --git a/application/components/keymap.vue b/application/components/keymap.vue
index a370a79..5d5c878 100644
--- a/application/components/keymap.vue
+++ b/application/components/keymap.vue
@@ -81,7 +81,7 @@ export default {
case 'mod':
return filter(this.keycodes, 'isModifier')
case 'command':
- get(this.sources, ['behaviours', behaviour, 'commands'], [])
+ return get(this.sources, ['behaviours', behaviour, 'commands'], [])
case 'kc':
default:
return this.keycodes
diff --git a/application/components/modal.vue b/application/components/modal.vue
index 2eb5bf3..65d10f2 100644
--- a/application/components/modal.vue
+++ b/application/components/modal.vue
@@ -8,7 +8,9 @@ export default {
<template>
<teleport to="body">
<div class="wrapper">
- <slot />
+ <div class="content">
+ <slot />
+ </div>
</div>
</teleport>
</template>
@@ -16,12 +18,17 @@ export default {
<style scoped>
.wrapper {
position: absolute;
+ top: 0;
+ left: 0;
width: 100vw;
height: 100vh;
- background-color: rgba(0, 0, 0, 0.75);
+ background-color: rgba(0, 0, 0, 0.25);
z-index: 100;
display: flex;
justify-content: center;
align-items: center;
}
+.content {
+ display: block;
+}
</style> \ No newline at end of file
diff --git a/application/components/value-picker.vue b/application/components/value-picker.vue
index fca91f7..3769a39 100644
--- a/application/components/value-picker.vue
+++ b/application/components/value-picker.vue
@@ -15,16 +15,30 @@ export default {
param: [String, Object],
value: String,
prompt: String,
- searchKey: String
+ searchKey: String,
+ searchThreshold: {
+ type: Number,
+ default: 10
+ },
+ showAllThreshold: {
+ type: Number,
+ default: 50,
+ validator: value => value >= 0
+ }
},
data() {
return {
query: null,
- highlighted: null
+ highlighted: null,
+ showAll: false
}
},
mounted() {
document.body.addEventListener('click', this.handleClickOutside, true)
+
+ if (this.$refs.searchBox) {
+ this.$refs.searchBox.focus()
+ }
},
unmounted() {
document.body.removeEventListener('click', this.handleClickOutside, true)
@@ -34,18 +48,32 @@ export default {
const { query, choices } = this
const options = { key: this.searchKey, limit: 30 }
const filtered = fuzzysort.go(query, choices, options)
+ const showAll = this.showAll || this.searchThreshold > choices.length
- return choices.length <= 10 ? choices : filtered.map(result => ({
+ if (showAll) {
+ return choices
+ } else if (!query) {
+ return choices.slice(0, this.searchThreshold)
+ }
+
+ return filtered.map(result => ({
...result.obj,
search: result
}))
},
+ enableShowAllButton() {
+ return (
+ !this.showAll &&
+ this.choices.length > this.searchThreshold &&
+ this.choices.length <= this.showAllThreshold
+ )
+ },
style() {
const rect = this.target.getBoundingClientRect()
return {
- display: 'block',
- top: `${window.scrollY + (rect.top + rect.bottom) / 2}px`,
- left: `${window.scrollX + (rect.left + rect.right) / 2}px`
+ // display: 'block',
+ // top: `${window.scrollY + (rect.top + rect.bottom) / 2}px`,
+ // left: `${window.scrollX + (rect.left + rect.right) / 2}px`
}
}
},
@@ -103,7 +131,6 @@ export default {
element.scrollIntoView(alignToTop)
}
}
-
}
}
</script>
@@ -119,7 +146,8 @@ export default {
>
<p>{{prompt}}</p>
<input
- v-if="choices.length > 10"
+ v-if="choices.length > searchThreshold"
+ ref="searchBox"
type="text"
:value="query !== null ? query : value"
@keypress="handleKeyPress"
@@ -138,15 +166,24 @@ export default {
<span v-else v-text="result[searchKey]" />
</li>
</ul>
+ <div
+ v-if="choices.length > searchThreshold"
+ class="choices-counter"
+ >
+ Total choices: {{choices.length}}.
+ <a
+ v-if="enableShowAllButton"
+ v-text="`Show all`"
+ @click.stop="showAll = true"
+ />
+ </div>
</div>
</template>
-<style>
+<style scoped>
.dialog {
- position: absolute;
- transform: translate(-60px, -30px);
- z-index: 2;
+ width: 300px;
}
.dialog p {
margin: 0;
@@ -155,16 +192,16 @@ export default {
}
.dialog input {
display: block;
- padding: 0;
- margin: 0;
- width: 120px;
- height: 60px;
+ width: 100%;
+ height: 30px;
+ line-height: 30px;
+
font-size: 120%;
- border: 2px solid steelblue;
+ margin: 0;
+ padding: 4px;
+ border: none;
border-radius: 4px;
-
- text-align: center;
- line-height: 60px;
+ box-sizing: border-box;
}
ul.results {
font-family: monospace;
@@ -173,6 +210,7 @@ ul.results {
max-height: 200px;
overflow: scroll;
padding: 4px;
+ margin: 4px 0;
background: rgba(0, 0, 0, 0.8);
border-radius: 4px;
}
@@ -187,4 +225,14 @@ ul.results {
}
.results li b { color: red; }
+.choices-counter {
+ font-size: 10px;
+}
+
+.choices-counter a {
+ color: var(--selection);
+ border-bottom: 1px dotted var(--selection);
+ cursor: pointer;
+}
+
</style> \ No newline at end of file
diff --git a/application/github.js b/application/github.js
index 51dafea..08fd664 100644
--- a/application/github.js
+++ b/application/github.js
@@ -1,8 +1,8 @@
import * as config from './config'
let token
-let installation
-let repositories
+export let installation
+export let repositories
function request (...args) {
return fetch(...args).then(res => {
@@ -48,10 +48,17 @@ export function isGitHubAuthorized() {
}
export async function fetchLayoutAndKeymap() {
- const data = await request(
+ const response = await request(
`${config.apiBaseUrl}/github/keyboard-files/${encodeURIComponent(installation.id)}/${encodeURIComponent(repositories[0].full_name)}`,
{ headers: { Authorization: `Bearer ${localStorage.auth_token}`} }
- ).then(res => res.json())
+ )
+
+ if (response.status === 400) {
+ console.error('Failed to load keymap and layout from github')
+ return response.json()
+ }
+
+ const data = await response.json()
const defaultLayout = data.info.layouts.default || data.info.layouts[Object.keys(data.info.layouts)[0]]
return {
layout: defaultLayout.layout,
diff --git a/application/style.css b/application/style.css
index daca53b..5619cb4 100644
--- a/application/style.css
+++ b/application/style.css
@@ -1,6 +1,7 @@
:root {
--dark-red: #910e0e;
--dark-blue: #6d99c6;
+ --selection: rgb(60, 179, 113);
--hover-selection: rgba(60, 179, 113, 0.85);
}