mirror of
https://github.com/qmk/qmk_userspace.git
synced 2026-04-22 11:30:23 -04:00
Add key event logger for testing tap-hold behavior
https://claude.ai/code/session_01Q6jUPkVNbXkBqPgkmLWsTK
This commit is contained in:
parent
1ff06d9530
commit
6882a06e2c
1 changed files with 178 additions and 0 deletions
178
keylogger.html
Normal file
178
keylogger.html
Normal 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>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue