Skip to content

Instantly share code, notes, and snippets.

@derekmc
Last active April 16, 2025 11:21
Show Gist options
  • Save derekmc/c9318322a5d0dd9451260296868f8af3 to your computer and use it in GitHub Desktop.
Save derekmc/c9318322a5d0dd9451260296868f8af3 to your computer and use it in GitHub Desktop.
<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