Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
<div class="codepen" data-height="477" data-theme-id="dark" data-default-tab="result" data-user="mvaneijgen" data-slug-hash="LYEVQXp" data-prefill='{"title":"Key Combo Generator | QMK","description":"Each time I want to create a key combo in QMK, I get lost on how to build them exactly. The docs are fine, but I thought it could be simpler, so I&apos;ve build a simple web app that spits out the code you need based on your input.","tags":["vue","qmk"],"stylesheets":["https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/styles/tomorrow-night-blue.min.css"],"scripts":["https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js","https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/highlight.min.js"]}'>
<pre data-lang="html"><div id="app">
<h1>Key Combo Generator | QMK</h1>
<p>Looking for key codes? <a href="https://beta.docs.qmk.fm/features/keycodes_basic" target="_blank">Check the Basic
Keycodes docs</a></p>
<div class="alloy-user-input">
<div class="row" v-for="(item, index) in combos" :key="index">
<template v-for="(i, index) in item.combo">
<!-- <template v-for="i in item.combo"> -->
<input type="text" v-model="i.key">
<span class="plus">+</span>
</template>
<div class="btn-group">
<button @click="addKey(index)" v-if="item.combo.length <= comboKeyMax">+</button>
<button @click="removeKey(index)" v-if="item.combo.length >= 3">-</button>
</div>
<abbr title="Result of combination">=</abbr>
<input class="input-result" type="text" v-model="item.result">
<button @click="removeCombo(index)">-</button>
</div>
<div class="btn-group addCombo">
<button @click="addCombo">+ Add combo</button>
</div>
</div>
<pre v-highlightjs="sourcecode" @click="userIsReady" contenteditable="true"><code class="C#"></code></pre>
<transition name="scale">
<div v-if="isUserReady">
<h2>Important ⚠️ don't forget to update your config.h file!</h2>
<pre><code class="C#">#define COMBO_COUNT {{combos.length}} # Change this number.</code></pre>
</div>
</transition>
<p>Read the docs on <a href="https://beta.docs.qmk.fm/features/feature_combo" target="_blank">QMK Combos</a> | Made by <a href="//mvaneijgen.nl" target="_blank">@mvaneijgen</a></p>
</div></pre>
<pre data-lang="scss" >// * {box-sizing: border-box;}
body {
background-color: darken(#08254e, 5%);
max-width: 768px; // Look at that throw back!
margin: 0 auto;
}
p,
h1,
h2 {
display: block;
color: #fff;
text-align: center;
}
a {
color: #fff;
}
.alloy-user-input {
padding: 15px;
> .row {
padding-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
color: #fff;
abbr {
margin: 0 5px 0 10px;
max-width: 10px;
}
abbr,
span {
opacity: 0.3;
font-weight: 700;
}
.plus:last-of-type {
display: none;
}
.btn-group {
button {
&:first-of-type {
min-height: 30px !important;
}
&:last-of-type {
min-height: 15px;
}
}
}
button {
border: 0;
min-width: 30px;
max-width: 30px;
min-height: 30px;
height: auto;
font-size: 25px;
line-height: 0em;
padding: 0;
flex-grow: 1;
margin-top: 1px;
margin-bottom: 1px;
}
}
input {
padding: 10px 5px;
margin: 5px;
color: #fff;
width: 30%;
background-color: transparent;
border: 0;
border-bottom: 2px solid rgba(#fff, 0.2);
&:focus {
outline: none;
border-color: #fff;
}
}
.input-result {
min-width: 90px;
max-width: 90px;
}
}
.addCombo {
// display: block-inline;
margin-top: 15px;
}
pre {
padding: 30px !important;
background-color: #08254e;
color: #fff;
margin-bottom: 45px;
border-radius: 3px;
&:focus {
outline: 2px solid rgba(#fff, 0.2);
outline-offset: 5px;
}
code {
}
}
.scale-enter-active,
.scale-leave-active {
// transfrom: scaleX(1);
transition: opacity 300ms;
}
.scale-enter,
.scale-leave-to {
// transfrom: scaleX(0);
opacity: 0;
}
button {
color: #061a37;
font-weight: 700;
border: 0;
background-color: rgba(#fff, 0.2);
// display: block;
padding: 5px 10px;
border-radius: 3px;
transition: background-color;
transition-duration: 300ms;
transition-timing-function: ease;
cursor: pointer;
&:hover {
background-color: #fff;
}
&:active,
&:focus {
outline-width: 1px;
// outline-style: dashed;
outline-color: rgba(#fff, 0.2);
outline-offset: 1px;
border-radius: 3px;
}
}
.btn-group.addCombo {
text-align: center;
}
abbr[title] {
text-decoration: none;
}
</pre>
<pre data-lang="babel">Vue.directive("highlightjs", {
deep: true,
bind: function (el, binding) {
// on first bind, highlight all targets
let targets = el.querySelectorAll("code");
targets.forEach(target => {
// if a value is directly assigned to the directive, use this
// instead of the element content.
if (binding.value) {
target.textContent = binding.value;
}
hljs.highlightBlock(target);
});
},
componentUpdated: function (el, binding) {
// after an update, re-fill the content and then highlight
let targets = el.querySelectorAll("code");
targets.forEach(target => {
if (binding.value) {
target.textContent = binding.value;
hljs.highlightBlock(target);
}
});
}
});
var app = new Vue({
el: "#app",
data: {
combos: [
{
id: + new Date(),
combo: [{ key: "KC_A" }, { key: "KC_B" }],
result: "KC_ESC"
}
],
comboMax: null,
comboKeyMax: 8,
isUserReady: false
},
computed: {
sourcecode() {
return `enum combos {
${this.enum}
};
${this.uint16_t}
combo_t key_combos[COMBO_COUNT] = {
${this.combo_t}
};`;
},
enum() {
return this.combos.reduce((result, item) => {
const combo = item.combo.reduce((result, key) => {
return result += `${this.upperCase(this.replaceKC(key.key))}_`;
}, '');
return result += `${combo}${this.upperCase(this.replaceKC(item.result))},\n `;
}, '');
},
uint16_t() {
return this.combos.reduce((result, item) => {
const combo = item.combo.reduce((result, key) => {
return result += `${this.replaceKC(key.key)}_`;
}, '');
const keys = item.combo.reduce((result, key) => {
return result += `${this.upperCase(key.key)}, `;
}, '');
return result += `const uint16_t PROGMEM ${combo}${this.replaceKC(item.result)}[] = { ${keys}COMBO_END};\n`;
}, '');
},
combo_t() {
return this.combos.reduce((result, item) => {
const combo = item.combo.reduce((result, key) => {
return result += `${this.replaceKC(key.key)}_`;
}, '');
return result += `[${this.upperCase(combo)}${this.upperCase(this.replaceKC(item.result))}] = COMBO(${combo}${this.replaceKC(item.result)}_combo, ${this.upperCase(item.result)}),\n `;
}, '');
}
},
methods: {
replaceKC(str) {
return str.toLowerCase().replace("kc_", "");
},
upperCase(str) {
return str.toUpperCase();
},
userIsReady() {
this.isUserReady = true;
},
addCombo() {
const comboBase = { id: + new Date(), combo: [{ key: "KC_A" }, { key: "KC_B" }], result: "KC_BSPC" };
this.combos.push(comboBase);
},
removeCombo(index) {
this.combos.splice(index, 1);
},
addKey(index) {
this.combos[index].combo.push({ key: "KC_C" });
},
removeKey(index) {
this.combos[index].combo.pop();
}
},
mounted() {
console.log('App mounted!');
if (localStorage.getItem('combos')) this.combos = JSON.parse(localStorage.getItem('combos'));
},
watch: {
combos: {
handler() {
console.log('Combos changed!');
localStorage.setItem('combos', JSON.stringify(this.combos));
},
deep: true,
},
},
});</pre>
</div>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.