You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sims-hq/packages/scanning/src/wedge.ts

49 lines
1.6 KiB
TypeScript

/**
* Keyboard-wedge scanners type like superhuman typists: a burst of characters
* with tiny inter-key gaps, terminated by Enter. This detector distinguishes a
* scan from human typing purely from timing + length, with injected timestamps
* so it is testable without a DOM or a clock.
*/
export interface WedgeOptions {
/** Max gap between scanner keystrokes; humans are slower. */
maxInterKeyMs: number
/** Bursts shorter than this are treated as typed input. */
minLength: number
}
export const DEFAULT_WEDGE: WedgeOptions = { maxInterKeyMs: 35, minLength: 6 }
export type WedgeResult = { type: 'scan'; code: string } | { type: 'typed'; text: string }
export class WedgeDetector {
private chars: string[] = []
private lastAtMs: number | undefined
private maxGapMs = 0
constructor(private readonly opts: WedgeOptions = DEFAULT_WEDGE) {}
feed(char: string, atMs: number): void {
if (char.length !== 1) throw new Error('feed() takes single characters')
if (this.lastAtMs !== undefined) {
this.maxGapMs = Math.max(this.maxGapMs, atMs - this.lastAtMs)
}
this.lastAtMs = atMs
this.chars.push(char)
}
/** Call on Enter. Decides scan vs typed and resets for the next burst. */
terminate(): WedgeResult {
const text = this.chars.join('')
const isScan =
this.chars.length >= this.opts.minLength && this.maxGapMs <= this.opts.maxInterKeyMs
this.reset()
return isScan ? { type: 'scan', code: text } : { type: 'typed', text }
}
reset(): void {
this.chars = []
this.lastAtMs = undefined
this.maxGapMs = 0
}
}