diff options
-rw-r--r-- | api/routes/github.js | 9 | ||||
-rw-r--r-- | api/services/github/files.js | 17 | ||||
-rw-r--r-- | api/services/github/index.js | 2 | ||||
-rw-r--r-- | api/services/zmk/keymap.js | 2 | ||||
-rw-r--r-- | application/components/app.vue | 196 | ||||
-rw-r--r-- | application/components/key.vue | 7 | ||||
-rw-r--r-- | application/components/keymap.vue | 2 | ||||
-rw-r--r-- | application/components/modal.vue | 11 | ||||
-rw-r--r-- | application/components/value-picker.vue | 88 | ||||
-rw-r--r-- | application/github.js | 15 | ||||
-rw-r--r-- | application/style.css | 1 |
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); } |