Add key event logger for testing tap-hold behavior

https://claude.ai/code/session_01Q6jUPkVNbXkBqPgkmLWsTK
This commit is contained in:
Claude 2026-04-14 22:57:39 +00:00
commit 6882a06e2c
No known key found for this signature in database

178
keylogger.html Normal file
View file

@ -0,0 +1,178 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Key Event Logger</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: monospace; background: #1a1a2e; color: #e0e0e0; padding: 20px; }
h1 { font-size: 18px; margin-bottom: 10px; color: #7fdbca; }
.controls { margin-bottom: 12px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
button { font-family: monospace; padding: 6px 14px; border: 1px solid #444; background: #2a2a4a; color: #e0e0e0; cursor: pointer; border-radius: 4px; }
button:hover { background: #3a3a5a; }
#status { color: #7fdbca; font-size: 14px; }
#capture-area {
width: 100%; min-height: 80px; padding: 12px; margin-bottom: 12px;
background: #0d0d1a; border: 2px solid #7fdbca; border-radius: 6px;
font-size: 16px; color: #fff; outline: none; white-space: pre-wrap;
cursor: text;
}
#capture-area:focus { border-color: #ff79c6; }
#capture-area.recording { border-color: #ff5555; box-shadow: 0 0 8px rgba(255,85,85,0.3); }
.hint { font-size: 12px; color: #888; margin-bottom: 12px; }
#log {
width: 100%; height: 400px; overflow-y: auto; padding: 8px;
background: #0d0d1a; border: 1px solid #333; border-radius: 4px;
font-size: 11px; line-height: 1.4;
}
.ev-down { color: #82aaff; }
.ev-up { color: #666; }
.ev-mod { color: #c792ea; }
.ev-combo { color: #ffcb6b; }
#dump {
width: 100%; min-height: 200px; padding: 8px; margin-top: 12px;
background: #0d0d1a; border: 1px solid #333; border-radius: 4px;
font-size: 11px; font-family: monospace; color: #e0e0e0; display: none;
}
</style>
</head>
<body>
<h1>Keyboard Event Logger</h1>
<div class="controls">
<button id="btn-start">Start Recording</button>
<button id="btn-stop" disabled>Stop</button>
<button id="btn-clear">Clear</button>
<button id="btn-dump">Copy Dump</button>
<span id="status">Ready</span>
</div>
<div id="capture-area" tabindex="0">Click here and start typing...</div>
<div class="hint">All key events are captured and swallowed. Nothing will actually type. Modifier combos (Cmd+A, etc.) are intercepted too.</div>
<div id="log"></div>
<textarea id="dump" readonly></textarea>
<script>
const captureArea = document.getElementById('capture-area');
const log = document.getElementById('log');
const dump = document.getElementById('dump');
const status = document.getElementById('status');
const btnStart = document.getElementById('btn-start');
const btnStop = document.getElementById('btn-stop');
const btnClear = document.getElementById('btn-clear');
const btnDump = document.getElementById('btn-dump');
let recording = false;
let events = [];
let startTime = 0;
let eventCount = 0;
function fmt(ms) {
return (ms / 1000).toFixed(3);
}
function modStr(e) {
const m = [];
if (e.metaKey) m.push('Meta');
if (e.ctrlKey) m.push('Ctrl');
if (e.altKey) m.push('Alt');
if (e.shiftKey) m.push('Shift');
return m.length ? m.join('+') + '+' : '';
}
function logEvent(e) {
if (!recording) return;
const t = performance.now() - startTime;
const type = e.type.replace('key', '');
const mods = modStr(e);
const display = mods + e.key;
const entry = {
t: Math.round(t * 1000) / 1000,
type: type,
key: e.key,
code: e.code,
mods: mods.replace(/\+$/, ''),
repeat: e.repeat
};
events.push(entry);
eventCount++;
const cls = e.type === 'keyup' ? 'ev-up'
: (mods ? 'ev-mod' : 'ev-down');
const repeatTag = e.repeat ? ' [repeat]' : '';
const line = document.createElement('div');
line.className = cls;
line.textContent = `${fmt(t)}s ${type.padEnd(5)} ${display.padEnd(20)} code=${e.code}${repeatTag}`;
log.appendChild(line);
log.scrollTop = log.scrollHeight;
status.textContent = `Recording... ${eventCount} events`;
}
function handler(e) {
if (!recording) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
logEvent(e);
return false;
}
captureArea.addEventListener('keydown', handler, true);
captureArea.addEventListener('keyup', handler, true);
captureArea.addEventListener('keypress', handler, true);
// Also block beforeinput/input to prevent any text insertion
captureArea.addEventListener('beforeinput', e => { if (recording) { e.preventDefault(); } }, true);
captureArea.addEventListener('input', e => { if (recording) { e.preventDefault(); } }, true);
btnStart.addEventListener('click', () => {
recording = true;
startTime = performance.now();
events = [];
eventCount = 0;
log.innerHTML = '';
dump.style.display = 'none';
captureArea.classList.add('recording');
captureArea.textContent = '';
captureArea.focus();
btnStart.disabled = true;
btnStop.disabled = false;
status.textContent = 'Recording... click the box and type';
});
btnStop.addEventListener('click', () => {
recording = false;
captureArea.classList.remove('recording');
btnStart.disabled = false;
btnStop.disabled = true;
status.textContent = `Stopped. ${eventCount} events captured.`;
});
btnClear.addEventListener('click', () => {
events = [];
eventCount = 0;
log.innerHTML = '';
dump.style.display = 'none';
status.textContent = 'Cleared.';
});
btnDump.addEventListener('click', () => {
const output = JSON.stringify(events, null, 2);
dump.value = output;
dump.style.display = 'block';
dump.select();
navigator.clipboard.writeText(output).then(() => {
status.textContent = `Copied ${events.length} events to clipboard.`;
}).catch(() => {
status.textContent = `Dump ready below (${events.length} events). Select all and copy.`;
});
});
</script>
</body>
</html>