Skip to content

Instantly share code, notes, and snippets.

@derekmc
Last active March 5, 2025 22:17
Show Gist options
  • Save derekmc/9f6c4fd784de9296183bf972889a607b to your computer and use it in GitHub Desktop.
Save derekmc/9f6c4fd784de9296183bf972889a607b to your computer and use it in GitHub Desktop.
A basic editor for the "simple chord" chorded keyboard.
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
/* CSS */
/* The switch - the box around the slider */
h1, h2, h3, h4{
display: inline-block;
}
.h4{
font-weight: bold;
font-size: 18px;
height: 200px;
}
button{
border: 2px solid #888;
background: #ccc;
font-size: 14px;
padding: 3px 10px;
font-weight: bold;
}
body{
font-family: sans-serif;
overflow: hidden;
user-select:none;
}
label{
cursor: pointer;
}
#textout::selection{
background: #fff;
color: #008;
}
#textout::-webkit-scrollbar{
display: none;
}
#textout:focus{
border-top: 3px solid #bdf;
border-bottom: 3px solid #bdf;
outline: none;
}
#textout{
margin-top: 10px;
border: none;
border-top: 3px solid #bdf;
border-bottom: 3px solid #bdf;
background: #46f;
color: #fff;
padding: 6px calc(2vw + 8px);
margin-left: -9px;
margin-right: -12px;
width: calc(100vw + 8px);
box-sizing: border-box;
height: calc(98vh - 64px);
}
@media only screen and (max-width: 580px) {
#textout{
height: calc(98vh - 108px);
}
}
:root{
--switch-width: 31px;
--switch-height: 19px;
--switch-slide: 13px;
--switch-slide-neg: -13px;
--switch-inner: 13px;
}
var
body{
font-family: sans-serif;
}
.switch {
position: relative;
display: inline-block;
width: var(--switch-width);
height: var(--switch-height);
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: var(--switch-inner);
width: var(--switch-inner);
left: 3px;
bottom: 3px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #2196F3;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .slider:before {
-webkit-transform: translateX(var(--switch-slide));
-ms-transform: translateX(var(--switch-slide));
transform: translateX(var(--switch-slide));
}
/* Rounded sliders */
.slider.round {
border-radius: var(--switch-inner);
}
.slider.round:before {
border-radius: 50%;
}
/* END */
</style>
</head>
<body>
<div class="markdown-src">
<!-- HTML -->
<span class='h4'>KeyChord20 Editor</span>
&emsp;
<span title='"Ctrl + Space"'>
<label class="switch">
<input id="show_codes" type="checkbox" checked onchange="WriteText('');
document.getElementById('textout').focus()">
<span class="slider round"></span>
</label>
<label for="show_codes">Show Codes</label>
</span>
<span style="float: right">
<a href="https://gist.github.com/derekmc/158c0c4474a099bec653a10afc04155f">Documentation</a> &emsp;
</span>
<textarea id='textout' readonly spellcheck="false"></textarea>
<button onclick="CutAction()">Cut</button>
<button onclick="CopyAction()">Copy</button>
<button onclick="PasteAction()">Paste</button>
&nbsp;
<span id='status' style="font-size: 14px;"></span>
<span id='typingstatus' style="float: right; font-size: 12px;"></span>
<!-- END -->
</div>
<div style="display: none;"></div>
<script>
let datakey = "__HTML_NOTES__:notedataid=HG0rfajDWP"
let inframe = (window!=window.top)
window.Note = {}
window.Note.data =
/* JSON */
{"text":"Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal.\r\n\r\nNow we are engaged in a great civil war, testing whether that nation, or any nation so conceived and so dedicated, can long endure. We are met on a great battle-field of that war. We have come to dedicate a portion of that field, as a final resting place for those who here gave their lives that that nation might live. It is altogether fitting and proper that we should do this.\r\n\r\nBut, in a larger sense, we can not dedicate—we can not consecrate—we can not hallow—this ground. The brave men, living and dead, who struggled here, have consecrated it, far above our poor power to add or detract. The world will little note, nor long remember what we say here, but it can never forget what they did here. It is for us the living, rather, to be dedicated here to the unfinished work which they who fought here have thus far so nobly advanced. It is rather for us to be here dedicated to the great task remaining before us—that from these honored dead we take increased devotion to that cause for which they gave the last full measure of devotion—that we here highly resolve that these dead shall not have died in vain—that this nation, under God, shall have a new birth of freedom—and that government of the people, by the people, for the people, shall not perish from the earth.\r\n\r\n—Abraham Lincoln\n","cursor":1479,"dictionary":"a from look say time about get make see to all give man she two also go many so up and have me some use as he more take very at her my tell want be here new than we because him no that well but his not the what by how now their when can I of them which come if on then who could in one there why day into onto these will do it or they with even its other thing would find just our think year first know out this you for like people those your is was are had word were said an each way write long has did number sound most over water call may down side been any work part place made live where after back little only round came show every good under name through form sentence great help low line differ turn cause much mean before move right boy old too same does set three air play small end put home read hand port large spell add land must big high such follow act ask men change went light kind off need house picture try us again animal point mother world near build self earth father head stand own page should country found answer school grow study still learn plant cover food sun four between state keep eye never last let thought city tree cross farm hard start might story saw far sea draw left late run don't while press close night real life few north open seem together next white children begin got walk example ease paper group always music both mark often letter until mile river car feet care second book carry took science eat room friend began idea fish mountain stop once base hear horse cut sure watch color face wood main enough plain girl usual young ready above ever red list though feel talk bird soon body dog family direct pose leave song measure door product black short numeral class wind question happen complete ship area half rock order fire south problem piece told knew pass since top whole king space heard best hour better true during hundred five remember step early hold west ground interest reach fast verb sing listen six table travel less morning ten simple several vowel toward war lay against pattern slow center love person money serve appear road map rain rule govern pull cold notice voice unit power town fine certain fly fall lead cry dark machine note wait plan figure star box noun field rest correct able pound done beauty drive stood contain front teach week final gave green oh quick develop ocean warm free minute strong special mind behind clear tail produce fact street inch multiply nothing course stay wheel full force blue object decide surface deep moon island foot system busy test record boat common gold possible plane stead dry wonder laugh thousand ago ran check game shape equate hot miss brought heat snow tire bring yes distant fill east paint language among grand ball yet wave drop heart am present heavy dance engine position arm wide sail material size vary settle speak weight general ice matter circle pair include divide syllable felt perhaps pick sudden count square reason length represent art subject region energy hunt probable bed brother egg ride cell believe fraction forest sit race window store summer train sleep prove lone leg exercise wall catch mount wish sky board joy winter sat written wild instrument kept glass grass cow job edge sign visit past soft fun bright gas weather month million bear finish happy hope flower clothe strange gone jump baby eight village meet root buy raise solve metal whether push seven paragraph third shall held hair describe cook floor either result burn hill safe cat century consider type law bit coast copy phrase silent tall sand soil roll temperature finger industry value fight lie beat excite natural view sense ear else quite broke case middle kill son lake moment scale loud spring observe child straight consonant nation dictionary milk speed method organ pay age section dress cloud surprise quiet stone tiny climb cool design poor lot experiment bottom key iron single stick flat twenty skin smile crease hole trade melody trip office receive row mouth exact symbol die least trouble shout except wrote seed tone join suggest clean break lady yard rise bad blow oil blood touch grew cent mix team wire cost lost brown wear garden equal sent choose fell fit flow fair bank collect save control decimal gentle woman captain practice separate difficult doctor please protect noon whose locate ring character insect caught period indicate radio spoke atom human history effect electric expect crop modern element hit student corner party supply bone rail imagine provide agree thus capital won't chair danger fruit rich thick soldier process operate guess necessary sharp wing create neighbor wash bat rather crowd corn compare poem string bell depend meat rub tube famous dollar stream fear sight thin triangle planet hurry chief colony clock mine tie enter major fresh search send yellow gun allow print dead spot desert suit current lift rose continue block chart hat sell success company subtract event particular deal swim term opposite wife shoe shoulder spread arrange camp invent cotton born determine quart nine truck noise level chance gather shop stretch throw shine property column molecule select wrong gray repeat require broad prepare salt nose plural anger claim continent oxygen sugar death pretty skill women season solution magnet silver thank branch match suffix especially fig afraid huge sister steel discuss forward similar guide experience score apple bought led pitch coat mass card band rope slip win dream evening condition feed tool total basic smell valley nor double seat arrive master track parent shore division sheet substance favor connect post spend chord fat glad original share station dad bread charge proper bar offer segment slave duck instant market degree populate chick dear enemy reply drink occur support speech nature range steam motion path liquid log meant quotient teeth shell neck information site news contact business web pm online services click service price date email health re used products data policy available copyright message software jan video info rights public books links review years privacy items user de research university january mail reviews program games days management united hotel item international ebay comments development report member details terms hotels local using results education national posted internet address community within states phone dvd shipping reserved forum based code prices website index being file link today technology project pages uk version sports related security county american photo members network computer systems following download without per access resources posts media pictures personal including directory location text rating rate government usa return students shopping account times sites digital profile previous events john hours image department title description non insurance another cd quality listing content private tools customer december movies college article york jobs source author different sale around canada teen stock training credit categories advanced sales english estate conditions windows photos gay thread category gallery register however june october november library really action series model features provided tv required accessories movie forums march la september questions july yahoo going medical dec server pc application cart staff articles san feedback looking issues april users topic comment financial things working standard tax below mobile blog payment equipment login programs offers legal recent park stores memory performance social august quote options rates america important ii activities club girls additional password latest something gift changes ca texas oct poker status browse issue building seller court february audio nov groups al easy given files release analysis request fax china making needs professional areas future committee cards problems london washington meeting rss become id california schools added reference companies listed learning delivery net popular film stories computers journal reports co welcome central images president council away includes australia discussion archive others entertainment agreement format society months safety friends faq edition cars messages marketing further updated association having provides david already studies specific feb living sep collection called arts display limited powered solutions means director daily beach due et electronics upon planning database says official mar average technical france pro microsoft conference environment records st district calendar costs style url statement update parts aug downloads miles resource applications document works bill apr federal hosting rules adult tickets centre requirements via cheap kids finance minutes gifts europe reading topics individual tips plus auto usually edit videos percent function getting global tech economic en player projects lyrics subscribe submit germany amount included risk thanks everything deals various words linux jul production commercial james advertising received treatment newsletter archives points knowledge magazine error camera jun currently construction toys registered golf domain methods chapter makes protection policies loan manager india taken sort listings models michael known cases engineering florida none wireless license paul friday annual published later sony shows corporate google church purchase customers active response hardware materials holiday chat designed along writing html countries loss brand discount higher effects created standards political increase advertise kingdom environmental stuff french storage japan doing loans shoes entry orders availability africa summary growth notes agency monday european activity although drug pics western income cash employment overall bay commission ad package contents seen players album regional supplies started administration institute views plans screen exchange types sponsored lines electronic across benefits needed apply someone ny anything printer effective organization asked eur sunday selection casino pdf tour menu volume anyone mortgage corporation inside mature role weeks addition usr executive running lower union jewelry according dc clothing mon com names robert homepage skills bush islands advice career military rental decision british teens pre facilities zip bid sellers cable opportunities taking values coming tuesday lesbian appropriate logo actually nice statistics client ok returns sample investment shown saturday christmas england culture flash ms george choice starting registration fri thursday courses consumer hi airport foreign artist outside furniture levels channel mode phones ideas wednesday structure fund contract button releases wed homes super male custom virginia almost located multiple asian distribution editor inn industrial potential cnet ltd los hp focus featured rooms female responsible inc communications associated thomas primary cancer numbers browser foundation eg friendly schedule documents communication purpose feature comes police everyone independent ip approach cameras physical operating maps medicine ratings chicago forms tue smith wanted developed unique survey prior telephone sport sources mexico population pa regular secure","layoutName":"qwerty","fingerKeys":["de4","fgvc","sw3","q1","ki8","jhnm","lo9","p-"],"dictLoaded":true,"correctLastKey":false};
/* END */
(function(){
window.Note.save = saveData
window.Note.load = loadData
window.Note.autoSave = autoSave
function getNoteModule(){
if(!window['Note']) window.Note = {}
return window.Note
}
function loadData(){
let note = getNoteModule()
if(!inframe){
try{
let savedata = localStorage.getItem(datakey)
if(savedata && savedata.length){
note.data = JSON.parse(savedata)
}
window.addEventListener("message", onMessage)
} catch(e){
console.log("No local data")
console.log(e)
}
} else {
return note.data
}
}
let firstLogCall = true
let originalLogFunc = console.log
function log(x, ...rest){
if(firstLogCall)
document.body.appendChild(document.createElement("hr"))
else
document.body.appendChild(document.createElement("br"))
document.body.appendChild(
document.createTextNode(x + " " + rest.join(" ")))
firstLogCall = false
originalLogFunc(x, ...rest)
}
console.log = log
let SaveInterval = 500
window.addEventListener("load", ()=>{
convertMarkdown()
autoSave()
loadData() // dont wait for async
})
let saveIntervalRef = null
function autoSave(enable){
if(enable === undefined) enable = true
clearInterval(saveIntervalRef)
if(enable){
saveIntervalRef = window.setInterval(saveData, SaveInterval)
}
}
// this is preferred over a 'localstorage' polyfill for a frame,
// so the programmer doesn't assume this is the browsers localstorage
function saveData(){
let note = getNoteModule()
if(inframe){
let msg_obj = {
action: "saveData",
data: JSON.stringify(note['data'])
}
let message = JSON.stringify(msg_obj)
window.parent.postMessage(message)
} else {
localStorage.setItem(datakey, JSON.stringify(note['data']))
}
}
let md_subs = [
/(\n|^)\s*######\s([^\s#].*)\n*/g, "\n<h6 id=\"$2\">$2</h6>\n",
/(\n|^)\s*#####\s([^\s#].*)\n*/g, "\n<h5 id=\"$2\">$2</h5>\n",
/(\n|^)\s*####\s*([^\s#].*)\n*/g, "\n<h4 id=\"$2\">$2</h4>\n",
/(\n|^)\s*###\s*([^\s#].*)\n*/g, "\n<h3 id=\"$2\">$2</h3>\n",
/(\n|^)\s*##\s*([^\s#].*)\n*/g, "\n<h2 id=\"$2\">$2</h2>\n",
/(\n|^)\s*#\s*([^\s#].*)\n*/g, "\n<h1 id=\"$2\">$2</h1>\n",
/\n(\s*)[\*\-](.*)/g, '\n<ul><li>$2</li></ul>',
/\n+\n(?=[^#\n])/g, "\n\n<br><br>",
/\n+\n/g, "\n",
/__([^_\n]*)__/g, "<b>$1</b>",
/\*\*([^_\n]*)\*\*/g, "<b>$1</b>",
/_([^_\n]*)_/g, "<i>$1</i>",
/\*([^_\n]*)\*/g, "<i>$1</i>",
/\!\[([^\]\n]*)\]\(([^\)\n]*)\)/g, "<img src=\"$2\" alt=\"$1\"></img>",
/\[([^\]\n]*)\]\(([^\)\n]*)\)/g, "<a href=\"$2\" target=\"_blank\">$1</a>",
]
function onMessage(){
try{
let message = JSON.parse(e.data)
// console.log('received message', message)
if(message.hasOwnProperty("event")){
if(message.event == "keydown" && typeof keydown != "undefined"){
keydown(message)
}
if(message.event == "keyup" && typeof keyup != "undefined"){
keydown(message)
}
}
} catch(e){
console.warn("error processing message: " + e.data)
}
}
function convertMarkdown(){
let containers = document.getElementsByClassName("markdown-src")
for(let j=0; j<containers.length; ++j){
let container = containers[j]
let src = container.innerHTML
for(var i=0; i<md_subs.length-1; i += 2){
var search = md_subs[i]
var replace = md_subs[i+1]
src = src.replace(search, replace)
}
container.innerHTML = src
}
}
})()
//window.addEventListener('load', htmlNotesMainFunc)
//function htmlNotesMainFunc(){
/* JS */
window.addEventListener('keydown', keydownWindow)
async function CopyAction(){
localClipboard = data.text
if(navigator.clipboard){
await navigator.clipboard.writeText(localClipboard)
}
status('Copy')
}
async function PasteAction(){
if(navigator.clipboard){
localClipboard = await navigator.clipboard.readText()
}
WriteText(localClipboard)
status('Paste')
}
async function CutAction(){
CopyAction()
data.text = ''
WriteText('')
}
async function keydownWindow(e){
if(e.ctrlKey && e.key == ' '){
let checkbox = id('show_codes')
checkbox.checked = !checkbox.checked
WriteText('')
e.preventDefault()
e.stopPropagation()
}
if(e.ctrlKey && e.key.toLowerCase() == 'x'){
await CutAction()
}
if(e.ctrlKey && e.key.toLowerCase() == 'c'){
await CopyAction()
}
if(e.ctrlKey && e.key.toLowerCase() == 'v'){
await PasteAction()
}
}
// start with this dictionary
let dict0 = qw`
a from look say time about get make see to
all give man she two also go many so up
and have me some use as he more take very
at her my tell want be here new than we
because him no that well but his not the what
by how now their when can I of them which
come if on then who could in one there why
day into onto these will do it or they with
even its other thing would find just our think year
first know out this you for like people those your
`
// additional dictionaries
// the first few dictionaries are sorted by frequency (deekayen, first20hours)
// the later dictionaries should be sorted by word length
// sort mode
let SORT_NONE = 0, SORT_LENGTH = 1
let DICT_CACHE_KEY_PREFIX = "__DICTIONARY_CACHE__"
let dictionaryURLs = [
[SORT_NONE, 'https://gist.githubusercontent.com/deekayen/4148741/raw/98d35708fa344717d8eee15d11987de6c8e26d7d/1-1000.txt'],
[SORT_NONE, 'https://raw.githubusercontent.com/first20hours/google-10000-english/refs/heads/master/google-10000-english-no-swears.txt'],
[SORT_NONE, 'https://raw.githubusercontent.com/first20hours/google-10000-english/refs/heads/master/20k.txt'],
[SORT_LENGTH, 'https://raw.githubusercontent.com/dolph/dictionary/refs/heads/master/popular.txt'],
[SORT_LENGTH, 'https://raw.githubusercontent.com/dolph/dictionary/refs/heads/master/ospd.txt'],
]
let shiftMap = `\`~1!2@3#4$5%6^7&8*9(0)-_=+
[{]}\\|;:'",<.>/?`.replaceAll(/\s+/g, '')
let singleLayouts = {
qwerty: `
qwert yuiop[]\\
asdfg hjkl;'
zxcvb nm,./
`,
dvorak: `
',.py fgcrl/=\\
aoeui dhtns-
;qjkx bmwvz`,
colemak: `
qwfpg jluy;[]\\
arstd hneio'
zxcvb km,./`,
}
singleLayouts.custom = singleLayouts.qwerty
function MapLayout(name, ch){
if(name == 'qwerty')
return ch
if(ch.length > 1)
return ch
let layout
if(data.layoutName =="custom")
layout = data.customLayout.replace(/\s/g, '')
else
layout = singleLayouts[name].replace(/\s/g, '')
let qwerty = singleLayouts.qwerty.replace(/\s/g, '')
if(!layout) return ch
let shiftCh = MapUnshift(ch)
if(shiftCh != ch){
let index = qwerty.indexOf(shiftCh)
return MapShift(layout[index])
}
let index = qwerty.indexOf(ch)
if(index < 0) return ch
return layout[index]
}
function MapUnshift(ch){
let index = shiftMap.indexOf(ch)
if(index < 0) return ch.toLowerCase()
if(index%2 == 0) return ch
return shiftMap[index - 1]
}
function MapShift(ch){
let index = shiftMap.indexOf(ch)
if(index < 0) return ch.toUpperCase()
if(index % 2==1) return ch
return shiftMap[index+1]
}
//let data = {}
let data = Note.data
if(!data.text) data.text = ''
if(!data.cursor) data.cursor = data.text.length
if(!data.dictionary) data.dictionary = []
if(!data.layoutName) data.layoutName = 'qwerty'
if(!data.fingerKeys) data.fingerKeys = qw`de4 fgvc sw3 q1 ki8 jhnm lo9 p-`
if(!data.dictLoaded) data.dictLoaded = false
if(data.customLayout) singleLayouts.custom = data.customLayout
if(!data.hasOwnProperty('correctLastKey')) data.correctLastKey = false
let dict = dict0.slice()
let dictLookup = {}
let chordOrder = []
// keyAddress is how chords are made,
// an address is the possible finger positions for
// a given finger.
let keyAddress = {}
let fingerCount = 0
let fingerState = [0,0,0,0, 0,0,0,0]
let holdState = [0,0,0,0, 0,0,0,0] // to enable rolling through chords
let freezeState = [0,0,0,0, 0,0,0,0]
let shiftPress = false
let maxFingers = 0
let lastKey = null
let ctrlShortcuts = [' ', 'c', 'v']
let nonAlphanums = /[^\p{N}\p{L}']+/u
let localClipboard = 'clipboard'
let textPreview = ''
let textCodes = ''
let base36 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
// the 2 and 3 key combos are created first,
// so that you can still use on limited keyboards.
let maxKeys = 5
//extra banned chords
let banList = qw``
window.addEventListener("load", Main)
async function Main(){
window.addEventListener('keydown', keydown)
window.addEventListener('keyup', keyup)
await Init()
FingerStatus()
id('textout').focus()
//id('noscript').innerHTML = ''
//console.info(MapUnshift('F'))
}
function wordFilter(word){
if(word.length == 1){
word = word.toLowerCase()
return word=='i' || word=='a'
}
return true
}
function AddDictLookup(dict, start){
if(!start) start = 0
for(let i=start; i<dict.length; ++i){
dictLookup[dict[i].toLowerCase()] = i
}
}
// functions that access global variables (or singleton class variables)
// should begin with a capital letter.
// TODO fallback on cached dictionaries
async function LoadDictionaries(){
dict = dict0.slice()
dictLookup = {} // clear dict lookup
AddDictLookup(dict, 0)
let actions = []
let loaded_dicts = []
// try to load dictionaries from localStorage cache first
for(let i=0; i<dictionaryURLs.length; ++i){
try{
let url = dictionaryURLs[i][1]
let cache_key = DICT_CACHE_KEY_PREFIX + url
let dict_text = localStorage.getItem(cache_key)
if(!dict_text) continue
let d = dict_text.trim().split(nonAlphanums)
d = d.filter(wordFilter)
let sortlength = dictionaryURLs[i][0] == SORT_LENGTH
if(sortlength)
d = d.sort((a,b)=>a.length - b.length)
loaded_dicts[i] = d
}catch(e){
console.error(e)
}
}
// load dictionaries from remote source
for(let i=0; i<dictionaryURLs.length; ++i){
actions.push(fetch(dictionaryURLs[i][1]).then(res=>res.text()))
}
let textvals = await Promise.all(actions)
for(let i=0; i<textvals.length; ++i){
let text = textvals[i]
let sortlength = dictionaryURLs[i][0] == SORT_LENGTH
if(text){
try{
let d;
let n = dict.length
let url = dictionaryURLs[i][1]
let cache_key = DICT_CACHE_KEY_PREFIX + url
localStorage.setItem(cache_key, text)
d = text.trim().split(nonAlphanums)
d = d.filter(wordFilter)
if(sortlength)
d = d.sort((a,b)=>a.length - b.length)
loaded_dicts[i] = d
}catch(e){
console.error(e)
}
}
}
for(let i=0; i<loaded_dicts.length; ++i){
let sortlength = dictionaryURLs[i][0] == SORT_LENGTH
let n = dict.length
let d = loaded_dicts[i]
if(d && d.length > 0){
for(let j=0; j<d.length; ++j){
let word = d[j]
if(!dictLookup.hasOwnProperty(word.toLowerCase())){
dict.push(word)}
}
AddDictLookup(dict, n)
}
}
data.dictLoaded = true
if(data.hasOwnProperty('dictionary') && typeof(data.dictionary) == 'string'){
let saved = data.dictionary.split(' ')
if(saved.length > dict.length){
dict = saved }
}
}
function EnumerateChordOrder(){
console.info("Enumerating Chords")
let bans = banList
chordOrder = []
let threechords = 999 // count the number of 2 and 3 length chords
let fingerstates = []
let {fingerKeys} = data
let finger_base = fingerKeys.map(x=>x.length + 1)
// max dictionary length
let n0 = finger_base.reduce(((x,y)=>x*y),1)
- finger_base.reduce((x,y)=>x+y-1) - 1
n0 = Math.min(n0, dict.length)
let maxscore = 1 +
finger_base[0]*finger_base[1]*finger_base[2]*finger_base[3] +
finger_base[4]*finger_base[5]*finger_base[6]*finger_base[7]
let m0 = 10*n0 // prevent infinite loop
// create 2 and 3 key combinations in the first pass,
// 4 key in the second pass, etc.
for(let mink=2, maxk=3; maxk <= maxKeys; mink = ++maxk){
//console.info(`Enumerating chords ${mink} to ${maxk}`)
let n = n0 - chordOrder.length, m = m0
// goes over chords diagonally
// counting up on the left hand and down on the right
let diagscore = 2
let diagindex = -1
while(n>0 && m > 0){
if(++diagindex == diagscore+1){
if(++diagscore > maxscore) break
diagindex = 0
}
let b = diagindex
let a = diagscore - b
let state = fingerKeys.map(x=>0)
let f = Math.floor
for(let j=0; j<4; ++j){
let c = finger_base[j]
let d = finger_base[j + 4]
state[j] += a % c
state[j + 4] += b % d
a = f(a/c)
b = f(b/d)
}
// the
let excess = a > 0 || b > 0
let keycount = state.reduce((x,y)=>x + (y? 1 : 0), 0)
if(!excess && keycount >= mink && keycount <= maxk){
fingerstates.push(state)
let code = ChordCode(state)
chordOrder.push(code)
--n
}
else --m
}
if(maxk == 3) threechords = chordOrder.length
console.info(`${chordOrder.length} <= ${maxk}-key chords.`)
}
//console.info('chord order fingers', debugStates)
//console.info('chord order', chordOrder)
let header = '| #n keys fingers code word\n|================================================\n'
let chordmap = chordOrder.map((x, i)=> '|' +
('' + (i + 1)).padStart(6,' ') + ' '
+ fingerstates[i].map((x, j)=>
('_' + fingerKeys[j])[x]).join('')
+ ' ' + fingerstates[i].map(
(x,i)=>(i==4?' ':'') + x).join('')
+ ' ' + x + (x.length ==2? ' ' : '') + ' ' + dict[i])
let maptext = header + chordmap.join('\n')
//console.info('chord map\n' + maptext)
//let out = id('chordListOut')
//out.innerHTML = ''
//out.appendChild(document.createTextNode(maptext))
// only save threechord words to local dictionary.
data.dictionary = dict.slice(0, threechords).join(' ')
}
async function Init(){
let {fingerKeys} = data
for(let i=0; i<fingerKeys.length; ++i){
//id('fing' + (i+1)).value = fingerKeys[i]
}
//id('maxKeysInput').value = maxKeys
if(true || !data.dictLoaded)
await LoadDictionaries()
EnumerateChordOrder()
keyAddress = {}
for(let i=0; i<fingerKeys.length; ++i){
let s = fingerKeys[i]
for(let j=0; j<s.length; ++j){
keyAddress[s[j]] = [i, j+1]
}
}
status('init')
}
function ChordCode(state){
let {fingerKeys} = data
assert(fingerKeys.length >= 8, 'need 8 fingerkey sets')
let result = ""
let r = fingerKeys[0].length + 1,
s = fingerKeys[2].length + 1,
t = fingerKeys[4].length + 1,
u = fingerKeys[6].length + 1;
let a = base36[state[0] + r*state[1]]
let b = base36[state[2] + s*state[3]]
let c = base36[state[4] + t*state[5]]
let d = base36[state[6] + u*state[7]]
if(b=='0' && d=='0')
return a + c
else
return a + b + c + d
}
function TranslateTextToCodes(text){
let alphanum = /[\p{N}\p{L}']/u
let result = ''
let word = ''
for(let i=0; i<text.length; ++i){
let ch = text[i]
if(ch.match(alphanum)) word += ch
else{
if(word.length){
result += TranslateWordToCodes(word)
word = ''
}
result += ch
}
}
if(word.length){
result += TranslateWordToCodes(word)
}
return result
}
//translates a word to one or more codes
function TranslateWordToCodes(word){
if(word == 'i') return word // dont translate a lowercase i to a code.
let compoundWords = false
let getIndex = (w)=>{
let i = dictLookup[w.toLowerCase()]
return i < chordOrder.length? i : -1
}
let getChord = (i)=>chordOrder[i]
let i = getIndex(word)
if(i > -1) return getChord(i)
if(!compoundWords) return word
let result = word
let score = word.length + 2
// try split
for(let j=1; j<word.length - 1; ++j){
let a = word.substring(0, j)
let b = word.substring(j)
let k = getIndex(a)
let l = getIndex(b)
if(k > -1 && l < 0){
if(score > b.length + 4){
score = b.length + 4
result = getChord(k) + b
}
}
if(k < 0 && l > -1){
if(score > a.length + 4){
score = a.length + 4
result = a + getChord(l)
}
}
if(k > -1 && l > -1){
if(score >= 8){
score = 8
result = getChord(k) + "_" + getChord(l)
}
}
}
return result
}
function FingerStatus(){
let s = fingerState.join('')
s = 'Fingers: ' + s.substr(0,4) + ' ' + s.substr(4)
let code = ChordCode(fingerState.map((x,i)=>x>0? x : holdState[i]))
let freezecode = ChordCode(freezeState)
let i = chordOrder.indexOf(code)
let j = chordOrder.indexOf(freezecode)
let word = i >= 0 ? dict[i] : ''
if(word.length && shiftPress)
word = word[0].toUpperCase() + word.slice(1)
let freezeword = j >= 0? dict[j] : ''
if(freezeword.length && shiftPress)
freezeword = freezeword[0].toUpperCase() + freezeword.slice(1)
let lastword = GetLastWord()
s += `;&emsp;Active word: [${code=='00-00'? '' : code} ${word}]`
s += `&emsp;Last word: [${LookupWordCode(lastword)} ${lastword}]`
id('typingstatus').innerHTML = s
let non_alphanum = /[^\p{N}\p{L}']/u
if(fingerState.reduce((x,y)=>x+(y?1:0), 0) > 1){
let lastspace = data.text.length == 0 || data.text[data.text.length - 1].match(non_alphanum)
textPreview = (lastspace? ' ' : '') + '[' + freezecode + ' ' + freezeword + ']'
} else {
textPreview = '|'
}
WriteText('')
}
function keydown(e){
let {key} = e
let immediate = ["Backspace", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"]
if(immediate.indexOf(key) != -1){
return Input(key)
}
if(e.ctrlKey && ctrlShortcuts.indexOf(key.toLowerCase()) != -1){
return
}
let upper = key.length == 1 && key.match(/[A-Z]/)
if(upper) key = key.toUpperCase()
if(e.shiftKey){
shiftPress = true
status('^^^')
}
let k = key.toLowerCase()
lastKey = k
if(k in keyAddress){
let [i, j] = keyAddress[k]
if(fingerState[i] == 0) ++fingerCount
fingerState[i] = j
}
let holdcount = holdState.reduce(
((x,y)=>x + (y>0? 1:0)), 0)
if(fingerCount + holdcount >= maxFingers){
freezeState = fingerState.map((x,i)=>
x>0? x : holdState[i])
maxFingers = fingerCount + holdcount
}
FingerStatus()
}
let statusTimeout = 8000
let clearStatus = null
function status(x){
clearTimeout(clearStatus)
let y = id('status')
y.innerHTML = ''
y.appendChild(document.createTextNode(x))
clearStatus = setTimeout(()=>status(''), statusTimeout)
}
function keyup(e){
let {key} = e
if(e.ctrlKey && ctrlShortcuts.indexOf(key.toLowerCase()) != -1){
return
}
let upper = key.length == 1 && key.match(/[A-Z]/)
if(upper) key = key.toUpperCase()
if(e.shiftKey){
shiftPress = true
status('^^^')
}
if(key == 'Shift') return
let k = key.toLowerCase()
if(k in keyAddress){
let [i, j] = keyAddress[k]
fingerState[i] = 0
if(!data.correctLastKey || k != lastKey){
holdState[i] = j}
--fingerCount
if(fingerCount == 0){
if(maxFingers > 1){
Emit(freezeState, shiftPress)
freezeState = fingerState.slice(0)
shiftPress = false
status('')
}else{
Input(key)
shiftPress = false
status('')
}
holdState = fingerState.slice(0)
maxFingers = 0
}
} else {
let exclude = ["Backspace", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"]
if(fingerCount == 0 && maxFingers <= 1 && exclude.indexOf(key) == -1)
Input(key)
shiftPress = false
status('')
}
FingerStatus()
}
function WriteText(x){
if(data.cursor > data.text.length)
data.cursor = data.text.length
if(data.cursor < 0)
data.cursor = 0
let before = data.text.substr(0, data.cursor)
let after = data.text.substr(data.cursor)
data.text = before + x + after
data.cursor += x.length
before += x
let text = before + textPreview + after
let translate = TranslateTextToCodes
let preword = '' // before and after cursor words
let postword = ''
for(let i=before.length - 1; i>=0; --i){
if(before[i].match(nonAlphanums)) break
preword = before[i] + preword
before = before.substr(0, i)
}
while(after.length > 0){
if(after[0].match(nonAlphanums)) break
postword += after[0]
after = after.substr(1)
}
let text2 = translate(before) + preword + textPreview + postword + translate(after)
let out = id('textout')
out.innerHTML = ''
let showCodes = id('show_codes').checked
out.appendChild(document.createTextNode(showCodes? text2 : text))
}
function Emit(state, shift){
textPreview = ''
let a = ChordCode(state)
let i = chordOrder.indexOf(a)
if(i<0) return
let b = dict[i]
if(shift)
b = b[0].toUpperCase() + b.slice(1)
let alphanum = /[\p{N}\p{L}']/u
if(data.cursor > 0 && !data.text[data.cursor - 1].match(/[\s]/))
b = ' ' + b
WriteText(b + ' ')
}
function Input(key){
if(key.length == 1){
if(shiftPress) key = MapShift(key)
key = MapLayout(data.layoutName, key)
if(key != ' ' && key.match(nonAlphanums) &&
data.cursor > 0 && data.text[data.cursor - 1] == ' '){
Input('Backspace')
}
WriteText(key)
}else{
if(key == 'Home'){
data.cursor = 0
WriteText('')
}
if(key == 'End'){
data.cursor = data.text.length
WriteText('')
}
if(key == 'ArrowUp'){
if(data.cursor > 0)
for(let i=0; i<10 && data.cursor > 0; ++i)
--data.cursor
WriteText('')
}
if(key == 'ArrowDown'){
if(data.cursor < data.text.length)
for(let i=0; i<10 && data.cursor < data.text.length; ++i)
++data.cursor
WriteText('')
}
if(key == 'ArrowLeft'){
if(data.cursor > 0) --data.cursor
WriteText('')
}
if(key == 'ArrowRight'){
if(data.cursor < data.text.length) ++data.cursor
WriteText('')
}
if(key == 'Backspace'){
let n = data.cursor
let before = data.text.substr(0, n)
let after = data.text.substr(n)
let remove = 1
let alphanum = /[\p{N}\p{L}']/u
if(n > 0 && data.text[n - 1].match(alphanum))
while(n - remove > 0 && data.text[n - remove - 1].match(alphanum))
++remove
n = Math.max(0, n - remove)
data.cursor -= remove
data.text = before.substring(0, n) + after
WriteText('')
}
if(key=='Enter'){
WriteText('\n')
}
if(key=="Shift"){
shiftPress = true
status('^^^')
}
}
//console.log(key)
}
function assert(x, msg){
if(x) return
if(!msg) msg = "Assertion failed."
else msg = "Assertion failed: " + msg
console.error(msg)
throw new Error(msg)
}
function GetLastWord(){
let b = data.text.length
let alphanum = /[\p{N}\p{L}']/u
while(--b >= 0 && !data.text[b].match(alphanum));
let a = b
while(--a >= 0 && data.text[a].match(alphanum));
return data.text.substring(a+1, b+1)
}
function LookupWordCode(s){
//console.log(dictLookup)
let i = dictLookup[s.toLowerCase()]
if(isNaN(i) || i >=chordOrder.length) return ''
return chordOrder[i]
}
function qw(parts){
return parts.join('').trim().split(/\s+/)
}
function id(x){
return document.getElementById(x)
}
function enter(x){
id(x).focus()
}
/* END */
//}
</script>
</body>
<html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment