Last active
April 16, 2025 11:21
-
-
Save derekmc/c9318322a5d0dd9451260296868f8af3 to your computer and use it in GitHub Desktop.
This file contains hidden or 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 */ | |
body{whitespace:pre; font-family: monospace; } | |
/* END */ | |
</style> | |
</head> | |
<body> | |
<div class="markdown-src"> | |
<!-- HTML --> | |
<button onclick='ref.interpret()'>Interpret Log</button> | |
<!-- END --> | |
</div> | |
<div style="display: none;"></div> | |
<script> | |
let datakey = "__HTML_NOTES__:notedataid=tZJSX5lupP" | |
let inframe = (window!=window.top) | |
window.Note = {} | |
window.Note.data = | |
/* JSON */ | |
{"fulllog":""}; | |
/* 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 */ | |
let data = Note.data | |
data.fulllog = '' | |
let ref = {} | |
let env = {} // constants | |
env.maxdt = 5000 | |
env.mindt = 18 | |
env.maxkeyhold = 15*1000 | |
env.keyChars = { | |
65: "abcdefghijklmnopqrstuvwxyz", | |
'-65': "ABCDEFGHIJKLMNOPQRSTUVWXYZ", | |
48: "0123456789", | |
'-48':")!@#$%^&*(", | |
187: "=,-./`", | |
'-187': "+<_>?~", | |
219: "[\\]'", | |
'-219': "{|}\"", | |
596: "0123456789*+", | |
609: "-./" | |
} | |
env.keyNames = { | |
8: "Backspace Tab", | |
13: "Enter", | |
16: "Shift Ctrl Alt Break CapsLock", | |
27: "Escape", | |
32: "Space PageUp PageDown End Home ArrowLeft ArrowUp ArrowRight ArrowDown", | |
44: "PrintScreen Insert Delete", | |
91: "MetaLeft MetaRight ContextMenu", | |
144: "NumLock ScrollLock", | |
112: "F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12" | |
} | |
window.addEventListener('load', main) | |
function getCode(e){ | |
return e.keyCode | |
} | |
function json(x){ | |
return JSON.stringify(x) | |
} | |
function init(){ | |
env.keyCodes = {} | |
for(let code in env.keyChars){ | |
let pre = "" | |
let chars = env.keyChars[code] | |
if(typeof code == "string"){ | |
code = parseInt(code)} | |
if(code > 500) pre = "Numpad_" | |
if(code < 0) code = -code + 1000 | |
for(let i=0; i<chars.length; ++i){ | |
env.keyCodes[pre + chars[i]] = code + i } | |
} | |
for(let code in env.keyNames){ | |
let pre = "" | |
let names = env.keyNames[code].split(/\s+/) | |
if(typeof code == "string"){ | |
code = parseInt(code)} | |
if(code > 500) pre = "Numpad_" | |
if(code < 0) code = -code + 1000 | |
for(let i=0; i<names.length; ++i){ | |
env.keyCodes[pre + names[i]] = code + i } | |
} | |
console.log(json(env.keyCodes)) | |
} | |
function main(){ | |
init() | |
console.log('main') | |
let buffer = "" | |
ref.interpret = interpret | |
let realtime = { //timer | |
now: ()=>Date.now(), | |
timeout: (f, dt)=>setTimeout(f,dt), | |
} | |
let buff = {write: bufferWrite, flush: bufferFlush} | |
let rawhandler = rawKeyAction(buff) | |
let rawkey = keyActionWrapper(rawhandler)(realtime) | |
let keys = rawkey | |
window.addEventListener('keydown', keys.keydown) | |
window.addEventListener('keyup', keys.keyup) | |
setInterval(bufferFlush, 1000) | |
function bufferWrite(s){ | |
if(buffer.length) buffer += " " | |
buffer += s | |
} | |
function bufferFlush(){ | |
if(buffer.length == 0) return | |
console.log("ready") | |
console.info(buffer) | |
data.fulllog += buffer + " " | |
buffer = "" | |
} | |
function interpret(){ | |
console.log('===== raw log', data.fulllog) | |
let log = readLog(data.fulllog) | |
replay(log) | |
data.fulllog = "" | |
} | |
function replay(log){ | |
let buffer2 = "" | |
let buff = {write: bufferWrite, flush: bufferFlush} | |
let chordAction = chordKeyAction(buff) | |
let chordkey = keyActionWrapper(chordAction) | |
replayLog(log, chordkey) | |
let log2 = readLog(buffer2) | |
console.info('log2', json(log2)) | |
console.log('=== chord log', writeLog(log2)) | |
sandwhichOrderLog(log2) | |
console.log('sandwhich log', writeLog(log2)) | |
function bufferWrite(s){ | |
if(buffer2.length) buffer2 += " " | |
buffer2 += s | |
} | |
function bufferFlush(){ | |
if(buffer2.length == 0) return | |
console.log("ready") | |
console.info(buffer) | |
data.fulllog += buffer2 + " " | |
buffer2 = "" | |
} | |
} | |
} | |
function readLog(s){ | |
let list = s.split(/\s+/) | |
let result = [] | |
for(let i=0; i<list.length; ++i){ | |
let e = list[i] | |
let dt = 0 | |
let j = e.indexOf(':') | |
if(j > -1){ | |
dt = parseInt(e.substring(j+1)) | |
e = e.substring(0, j) | |
} | |
let keys = e.split(',') | |
let down = [] | |
let up = [] | |
for(let j=0; j<keys.length; ++j){ | |
let key = keys[j] | |
let action = key[0] | |
let code = key.substring(1) | |
if(action == '+') down.push(code) | |
else if(action == '-') up.push(code) | |
else console.warn('unknown action: ' + action) | |
} | |
result.push({down, up, dt}) | |
} | |
return result | |
} | |
function writeLog(log){ | |
let s = '' | |
for(let i=0; i<log.length; ++i){ | |
let {up, down, dt} = log[i] | |
if(s.length) s += ' ' | |
if(up.length) | |
s += up.map(x=>'-' + x).join(',') + | |
(dt>0? ":" + dt : "") | |
if(down.length) | |
s += down.map(x=>'+' + x).join(',') + | |
(dt>0? ":" + dt : "") | |
} | |
return s | |
} | |
function replayLog(log, keyhandler){ | |
let t = 0 | |
let callbacks = [] | |
let replayTimer = { | |
now: ()=>t, | |
timeout: (f, dt)=>{ | |
callbacks.push([()=>f(), t+dt]) | |
} | |
} | |
let {keydown, keyup} = keyhandler(replayTimer) | |
for(let i=0; i<log.length; ++i){ | |
let {up, down, dt} = log[i] | |
t += dt | |
nextCallback() | |
up.forEach(code=>keyup({keyCode: code})) | |
down.forEach(code=>keydown({keyCode: code})) | |
} | |
t += 60*1000 | |
nextCallback() | |
function nextCallback(){ | |
callbacks.forEach(([f, s], i)=>{ | |
if(s < t){ | |
f() | |
callbacks[i] = null | |
} | |
}) | |
callbacks = callbacks.filter(x=>x!==null) | |
} | |
} | |
// simultaneous events get reordered so that they are | |
// "sandwhiched" +a +b -b -a to enable chord recognition | |
function sandwhichOrderLog(log){ | |
// lookup table for specific keycodes | |
for(let q=0; q<2; ++q){ | |
let lookup = {} | |
let searchname = ['down', 'up'][q] | |
for(let i=0; i<log.length; ++i){ | |
let search = log[i][searchname] | |
search.forEach((code, j)=>{ | |
if(!(code in lookup)) | |
lookup[code] = [] | |
lookup[code].push(i + j/search.length) | |
}) | |
} | |
for(let i=0; i<log.length; ++i){ | |
let {up, down} = log[i] | |
let list = [up, down][q] | |
let type = ['up', 'down'][q] | |
let startOrEnd = [-1, log.length+1][q] | |
let beforeOrAfter = [ | |
l=>(a,b)=>(b>l? a: Math.max(a,b)), | |
l=>(a,b)=>(b<l? a: Math.min(a,b))][q] | |
let cmp = (a, b) => b[1] - a[1] | |
if(list.length > 1){ | |
// matched is the position of | |
// corresponding key events | |
// to reorder the sandwhich | |
let matched = [] | |
list.forEach((code, j)=>{ | |
let table = lookup[code] ?? [] | |
matched.push(table.reduce(beforeOrAfter(i), startOrEnd)) | |
}) | |
let zipped = list.map((x,i)=>[x, matched[i]]) | |
zipped.sort(cmp) | |
let picked = zipped.map(x=>x[0]) | |
log[i][type] = picked | |
} | |
} | |
} | |
return log | |
} | |
//curried object | |
function keyActionWrapper(keyAction){ | |
return function(timer){ | |
//let activeKeys = {} | |
let lasttime = 0 | |
return {keydown, keyup} | |
function keydown(e){ | |
let code = getCode(e) | |
if(e.repeat || code == 0) return | |
let now = timer.now() | |
let dt = now - lasttime | |
lasttime = now | |
keyAction('down', code, dt, timer) | |
} | |
function keyup(e){ | |
let code = getCode(e) | |
//if(code == 0)// return | |
console.log(`code, key, keyCode, e: ${e.code}, ${e.key}, ${e.keyCode}, ${json(e)}`) | |
let s = "" | |
for(let k in e) | |
if(typeof e[k] != "function") | |
s += ` ${k}: ${e[k]};` | |
console.log(s) | |
let now = timer.now() | |
let dt = now - lasttime | |
lasttime = now | |
keyAction('up', code, dt, timer) | |
} | |
} | |
} | |
function rawKeyAction(buff){ | |
return action | |
function action(name, code, dt){ | |
let action = {'up': '-', 'down': '+'}[name] | |
if(!action) return | |
dt = Math.round(Math.min(env.maxdt, dt)) | |
buff.write(action + code + ":" + dt) | |
} | |
} | |
function chordKeyAction(buff){ | |
let pending = [] | |
return action | |
function action(name, code, dt, timer){ | |
let action = {'up': '-', 'down': '+'}[name] | |
if(!action) return | |
dt = Math.round(Math.min(env.maxdt, dt)) | |
if(!pending.length) timer.timeout(processChords, env.mindt) | |
pending.push({e: action + code, dt}) | |
} | |
function processChords(){ | |
let sup = "" | |
let sdown = "" | |
let list = pending | |
pending = [] | |
let dt_total = 0 | |
let up = [] | |
let down = [] | |
for(let i=0; i<list.length; ++i){ | |
let {e, dt} = list[i] | |
if(e[0] == '-') sup += (sup.length? ',' : '') + e | |
if(e[0] == '+') sdown += (sdown.length? ',' : '') + e | |
dt_total += parseInt(dt) | |
} | |
// don't combine up and down events to be simultaneous, | |
// as that has a special meaning for touch events that slide. | |
if(sup.length && sdown.length){ | |
sup += ":" + dt_total | |
sdown | |
s = sup + ' ' + sdown | |
} else { | |
if(sup.length) s = sup | |
if(sdown.length) s = sdown | |
s += ":" + dt_total | |
} | |
buff.write(s) | |
} | |
} | |
/* END */ | |
//} | |
</script> | |
</body> | |
<html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment