Last active
January 5, 2025 16:37
-
-
Save derekmc/822b2e11e55e9c2cf0138c2f584b21db to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<style> | |
/* CSS */ | |
#wrapper{ background: #fff; padding: 14px; } | |
#chordListOut{ user-select: auto; } | |
.mono{ font-family: monospace; } | |
h1, h2, h3{ font-family: sans-serif;} | |
body{ user-select: none; background: #ddd; margin:0; | |
font-family: sans-serif; max-width: 680px;} | |
p{ white-space: pre-wrap;} | |
th{ text-align: right; } | |
td{ vertical-align: bottom; font-family: monospace;} | |
h1{ | |
background: #000; | |
color: #fff; | |
padding: 16px 24px; | |
margin: -14px; | |
margin-bottom: 10px; | |
} | |
h3{ | |
padding: 9px 13px; | |
background: rgba(0,0,255,0.5); | |
border: none; | |
border-bottom: 3px solid rgba(0,0,255,0.75); | |
border-top: 3px solid rgba(255,255,255,0.25); | |
color: #fff; | |
margin: 8px -14px; | |
/* border-bottom: 2px solid black;*/ | |
} | |
h3::before{ | |
content: ""; | |
} | |
#textout, #codeout{ | |
border: 2px solid black; | |
padding: 4px; | |
width: 250px; | |
height: 180px; | |
word-break: break-all; | |
word-wrap: break-word; | |
overflow-wrap: break-word; | |
-webkit-hyphens: auto; | |
-moz-hyphens: auto; | |
-ms-hyphens: auto; | |
hyphens: auto; | |
} | |
#textout:focus{ | |
border: 4px solid #44f; | |
padding: 2px; | |
outline: none; | |
} | |
td{ | |
padding: 4px 8px; | |
line-height: 2.2em; | |
} | |
input.error{ | |
background-color: rgba(255,0,0,0.3); | |
} | |
button{ | |
background: #000; | |
color: #fff; | |
padding: 6px 10px; | |
font-weight: bold; | |
border: none; | |
border-radius: 16px; | |
} | |
.pane{ | |
display: none; | |
transition: height 0.6s; | |
} | |
.show1 > .pane.no1, | |
.show2 > .pane.no2, | |
.show3 > .pane.no3, | |
.show4 > .pane.no4 | |
{ | |
display: block; | |
} | |
/* END */ | |
</style> | |
</head> | |
<body> | |
<div class="markdown-src"> | |
<!-- HTML --> | |
<div id='wrapper'> | |
<h1> Chord2020 Test</h1> | |
<b>Chorded Typing With "Base 20" Notation</b> | |
<p id='noscript'> | |
Script still loading or not working. . .<br> | |
Wait a few seconds or | |
Try: <a href='./hybridkeyboard-babel.html'>./hybridkeyboard-babel.html</a> | |
</p> | |
<div id='accordianWrapper' class='show2'> | |
<h3 onclick='FocusPane(1)'> Options </h3> | |
<div class='pane no1'> | |
<table> | |
<tr> | |
<td></td> | |
<td> | |
<b>Set system keyboard to qwerty</b> | |
</td> | |
</tr> | |
<tr> | |
<th>Single Key Layout</th> | |
<td> | |
<select id='layoutSelect' size='4' | |
onchange="SelectLayout()" style='width: 100px; height: 80px;'> | |
<option selected name='qwerty'>qwerty</option> | |
<option name='dvorak'>dvorak</option> | |
<option name='colemak'>colemak</option> | |
<option name='colemakdh'>colemakdh</option> | |
<option name='custom'>custom</option> | |
</select> | |
<textarea id='customLayout' onchange="CustomizeLayout()" | |
rows=5 cols=20 spellcheck='false' | |
style='height: 75px'></textarea> | |
</td> | |
</tr> | |
<!--tr> | |
<th> Word Dictionary </th> | |
<td> | |
<textarea rows='10' cols='54' | |
spellcheck='false'></textarea> | |
</td> | |
</tr--> | |
<!--tr> | |
<th> Dictionary URL </th> | |
<td> <input size='35'></td> | |
<tr--> | |
<th> | |
Finger Keys<br> | |
<span style='font-weight: normal; font-size: 11px;'> | |
(qwerty) | |
</span> | |
</th> | |
<td> | |
   index, middle, ring, pinky | |
<br> L | |
<input size='2' id='fing2' title='left middle'> | |
<input size='2' id='fing1' title='left index'> | |
<input size='2' id='fing3' title='left ring'> | |
<input size='2' id='fing4' title='left pinky'> | |
<br> R | |
<input size='2' id='fing6' title='right middle'> | |
<input size='2' id='fing5' title='right index'> | |
<input size='2' id='fing7' title='right ring'> | |
<input size='2' id='fing8' title='right pinky'> | |
<br> | |
</td> | |
</tr> | |
<!--tr> | |
<th> Ignore Overlap </th> | |
<td> | |
<input type='number' value='10' step='5' min='0' max='60' | |
id='ignoreOverlapInput' style="width: 50px"> ms | |
</tr--> | |
<tr> | |
<th> Max Keys </th> | |
<td> | |
<input type='number' value='3' min='2' max='8' | |
id='maxKeysInput'> | |
<tr> | |
<td></td><td> <button onclick='UpdateOptions()'> Update </button> </td> | |
</tr> | |
<tr> | |
</tr> | |
</table> | |
</div> | |
<h3 onclick='FocusPane(2)'> Typing Demo</h3> | |
<div class='pane no2'> | |
<p id='typingstatus' class='mono'></p> | |
<textarea id='textout' readonly></textarea> | |
<textarea id='codeout' readonly onclick="enter('textout')"></textarea> | |
<p id='status' class='mono'></p> | |
</div> | |
<h3 onclick='FocusPane(3)'>Chord List </h3> | |
<div class='pane no3'> | |
<p class='mono' id='chordListOut'></p> | |
</div> | |
<h3 onclick='FocusPane(4)'>Other Tools </h3> | |
<div class='pane no4'> | |
No tools available. | |
<br><br><hr> | |
<b> TODO </b><br> | |
<ul> | |
<li><a href="https://gist.github.com/derekmc/158c0c4474a099bec653a10afc04155f">HybridChord20 Documentation</a></li> | |
<li><u>Multi-key Debugger (detect rollover ghosting etc)</u></li> | |
<!--<li><u> Chord Grid Browser </u></li> | |
<li><u> Chord Hero Music Game </u></li>--> | |
<li><a href='https://derekmc.gitlab.io/projects/adventureboard/adventureboard.html'> Adventure Keyboard </a></li> | |
<li><a href='https://derekmc.gitlab.io/snippets/layoutmap/layoutmap.html'>Layout Practice Tool</a></li> | |
</ul> | |
</div> | |
</div> | |
</div> | |
<!-- END --> | |
</div> | |
<div style="display: none;"></div> | |
<script> | |
let datakey = "__HTML_NOTES__:notedataid=7xXDURmDA3" | |
let inframe = (window!=window.top) | |
window.Note = {} | |
window.Note.data = | |
/* JSON */ | |
{"text":"To be or not to be that is the question.\nWhat is your name?","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":"colemakdh","fingerKeys":["de4","fgvc","sw3","q1","ki8","jhnm","lo9","p-"],"dictLoaded":true,"customLayout":"qwfpbjluy;[]\\arstgmneio'xcdvzkh,./","correctLastKey":false,"cursor":59}; | |
/* 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 */ | |
// 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,./`, | |
colemakdh: ` | |
qwfpb jluy;[]\\ | |
arstg mneio' | |
xcdvz kh,./`, | |
} | |
singleLayouts.custom = singleLayouts.qwerty | |
function UpdateLayout(){ | |
data.layoutName = id('layout_select').value | |
} | |
function NextLayout(){ | |
let names = Object.keys(singleLayouts) | |
let index = names.indexOf(data.layoutName) | |
if(index < 0) index = 0 | |
else index = (index + 1) % names.length | |
while(names[index].length == 0) | |
index = (index + 1) % names.length | |
data.layoutName = names[index] | |
id('layout_select').value = data.layoutName | |
status('Layout: ' + data.layoutName) | |
} | |
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', 'x', 'b', '\n', 'enter'] | |
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) | |
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 fingermap = x => [x[3], x[2], x[0], x[1], x[5], x[4], x[6], x[7]] | |
let header = ' #n keys fingers code word\n================================================\n' | |
let chordmap = chordOrder.map((x, i)=> '' + | |
('' + (i + 1)).padStart(6,' ') + ' ' | |
+ fingermap(fingerstates[i].map((x, j)=> | |
('_' + fingerKeys[j])[x])).join('') | |
+ ' ' + fingermap(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] | |
} | |
} | |
if(!data.customLayout) data.customLayout = singleLayouts.qwerty | |
//id('layout_select').value = data.layoutName | |
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 fs = fingerState | |
fs = '' + fs[3] + fs[2] + fs[0] + fs[1] + fs[5] + fs[4] + fs[6] + fs[7] | |
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 = '' | |
s += ' Fingers: ' + fs.substr(0,4) + ' ' + fs.substr(4) | |
s += ` Last word: [${LookupWordCode(lastword)} ${lastword}]` | |
s += ` Active word: [${code=='00-00'? '' : code} ${word}]` | |
id('typingstatus').innerHTML = s | |
let non_alphanum = /[^\p{N}\p{L}']/u | |
let count = | |
holdState.reduce(((x,y)=>x + (y>0? 1:0)), 0) + | |
fingerState.reduce(((x,y)=>x + (y>0? 1:0)), 0) | |
if(count > 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(e.ctrlKey && ctrlShortcuts.indexOf(key.toLowerCase()) != -1){ | |
return | |
} | |
if(immediate.indexOf(key) != -1 || !true){ | |
return Input(key) | |
} | |
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 | |
} | |
if(!true){ | |
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.text.length > 400) data.text = '' | |
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') | |
let out2 = id('codeout') | |
out2.innerHTML = out.innerHTML = '' | |
out.appendChild(document.createTextNode(text)) | |
out2.appendChild(document.createTextNode(text2)) | |
} | |
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) | |
shiftPress = false | |
key = MapLayout(data.layoutName, key) | |
if(key != ' ' && key.match(nonAlphanums) && | |
true && | |
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 && true && 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() | |
} | |
/*async function Main(){ | |
window.addEventListener('keydown', keydown) | |
window.addEventListener('keyup', keyup) | |
await Init() | |
FingerStatus() | |
id('textout').focus() | |
//id('noscript').innerHTML = '' | |
//console.info(MapUnshift('F')) | |
}*/ | |
async function Main(){ | |
id('textout').addEventListener('keydown', keydown) | |
id('textout').addEventListener('keyup', keyup) | |
await Init() | |
FingerStatus() | |
SelectLayout() | |
id('noscript').innerHTML = '' | |
console.info(MapUnshift('F')) | |
enter('textout') | |
} | |
function SelectLayout(){ | |
let name = id('layoutSelect').value | |
id('customLayout').value = singleLayouts[name]. | |
trim().split('\n').map(x=>x.trim()).join('\n') | |
} | |
function CustomizeLayout(){ | |
id('layoutSelect').value = 'custom' | |
singleLayouts['custom'] = id('customLayout').value | |
} | |
async function UpdateOptions(){ | |
let maxvalue = id('maxKeysInput').value | |
let custom = id('customLayout').value.replace(/\s/g, '') | |
if(custom.length) data.customLayout = custom | |
let layout = id('layoutSelect').value | |
if(singleLayouts.hasOwnProperty(layout)) | |
data.layoutName = layout | |
if(!isNaN(maxvalue)){ | |
maxKeys = maxvalue} | |
try{ | |
UpdateFingerKeys() | |
}catch(e){ | |
console.error(e) | |
} | |
await Init() | |
} | |
function UpdateFingerKeys(){ | |
try{ | |
for(let i=0; i<data.fingerKeys.length; ++i){ | |
let input = id('fing' + (i + 1)) | |
let val = input.value | |
if(val && val.length){ | |
data.fingerKeys[i] = val | |
input.classList.remove('error') | |
}else{ | |
input.classList.add('error') | |
} | |
} | |
}catch(e){ | |
console.error('could not update fingerKeys') | |
console.error(e) | |
} | |
//id('fingerKeyInput').classList.add('error') | |
} | |
function FocusPane(n){ | |
let list = id('accordianWrapper').classList | |
if(!list.contains('show' + n)){ | |
list.forEach(x=>{ | |
if(x.indexOf("show") == 0) | |
list.remove(x)}) | |
list.add('show' + n) | |
} else { | |
list.remove('show' + n) | |
} | |
if(n == 2 && list.contains('show2')){ | |
enter('textout') | |
} | |
} | |
/* END */ | |
//} | |
</script> | |
</body> | |
<html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment