All files / src typist.js

98.3% Statements 58/59
96.55% Branches 28/29
100% Functions 7/7
100% Lines 53/53

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104    3x                     82x 82x 82x 82x 82x 82x 82x                 18x 18x     18x   18x 3x 3x 2x 3x 2x 2x       18x 18x 18x       5x 5x 5x 5x 5x 5x       142x       41x       36x   36x 5x 5x 5x     31x 31x 3x 3x 3x     28x 28x 28x 28x 1x 1x 1x     27x 27x 1x 1x   26x     27x 27x      
import { BBC } from "./keymap.js";
 
const RepeatedKeyGapMs = 30;
 
/**
 * Types a key sequence into the keyboard matrix from the processor's
 * scheduler, one key per tick, so the CPU stays on its fast path while the
 * text arrives. An entry is one of the model's raw keys, or a number of
 * milliseconds to wait. SHIFT toggles and stays held; any other key is pressed
 * on one tick and released on the next.
 */
export class Typist {
    constructor(processor) {
        this.processor = processor;
        this.keyInterface = processor.keyboardInterface;
        this._shiftKey = processor.model.keys.SHIFT;
        this._keys = [];
        this._lastKey = undefined;
        this._clocksPerMs = 0;
        this._task = processor.scheduler.newTask(() => this._deliver());
    }
 
    /**
     * @param {Array} keys raw keys and millisecond delays, consumed as they are sent
     * @param {boolean} checkCapsAndShiftLocks bracket the keys with a lock toggle when
     *   the machine's lock lights say the letters would come out in the wrong case
     */
    type(keys, checkCapsAndShiftLocks) {
        if (this.isTyping) this.cancel();
        this.keyInterface.disableKeyboard();
        // The task lives on the processor's scheduler, which is polled with peripheral
        // cycles, so the delays stay in real time whatever the CPU multiplier is.
        this._clocksPerMs = this.processor.peripheralCyclesPerSecond / 1000;
 
        if (checkCapsAndShiftLocks) {
            let toggleKey = null;
            if (!this.keyInterface.capsLockLight) toggleKey = BBC.CAPSLOCK;
            else if (this.keyInterface.shiftLockLight) toggleKey = BBC.SHIFTLOCK;
            if (toggleKey) {
                keys.unshift(toggleKey);
                keys.push(toggleKey);
            }
        }
 
        this._keys = keys;
        this._lastKey = undefined;
        this._task.schedule(0);
    }
 
    cancel() {
        Iif (!this.isTyping) return;
        this._task.cancel();
        this._releaseLastKey();
        this._lastKey = undefined;
        this._keys = [];
        this.keyInterface.enableKeyboard();
    }
 
    get isTyping() {
        return this._keys.length > 0 || this._task.scheduled();
    }
 
    _releaseLastKey() {
        if (this._lastKey && this._lastKey !== this._shiftKey) this.keyInterface.keyToggleRaw(this._lastKey);
    }
 
    _deliver() {
        this._releaseLastKey();
 
        if (this._keys.length === 0) {
            this._lastKey = undefined;
            this.keyInterface.enableKeyboard();
            return;
        }
 
        const releaseGapMs = this.processor.model.pasteReleaseGapMs;
        if (this._lastKey && this._lastKey !== this._shiftKey && releaseGapMs) {
            this._lastKey = undefined;
            this._task.schedule(releaseGapMs * this._clocksPerMs);
            return;
        }
 
        const key = this._keys[0];
        const repeated = this._lastKey === key;
        this._lastKey = key;
        if (repeated) {
            this._lastKey = undefined;
            this._task.schedule(RepeatedKeyGapMs * this._clocksPerMs);
            return;
        }
 
        let delayMs = this.processor.model.pasteKeyDelayMs;
        if (typeof key === "number") {
            delayMs = key;
            this._lastKey = undefined;
        } else {
            this.keyInterface.keyToggleRaw(key);
        }
 
        this._keys.shift();
        this._task.schedule(delayMs * this._clocksPerMs);
    }
}