Google Docs WPM Tracker

tracks wpm and displays and is draggable

// ==UserScript==
// @name         Google Docs WPM Tracker
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  tracks wpm and displays and is draggable
// @author       chatgpt
// @match        https://docs.google.com/document/*
// @grant        none
// @license CC0
// ==/UserScript==
(function() {
    'use strict';


    const wpmDisplay = document.createElement('div');
    wpmDisplay.style.position = 'fixed';
    wpmDisplay.style.top = '50%';
    wpmDisplay.style.left = '20px';
    wpmDisplay.style.transform = 'translateY(-50%)';
    wpmDisplay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
    wpmDisplay.style.color = 'white';
    wpmDisplay.style.padding = '10px 20px';
    wpmDisplay.style.borderRadius = '12px';
    wpmDisplay.style.fontSize = '18px';
    wpmDisplay.style.zIndex = '9999';
    wpmDisplay.style.fontFamily = 'Verdana';
    wpmDisplay.textContent = 'Live WPM: 0';
    document.body.appendChild(wpmDisplay);

    let startTime = null;
    let typedCharacters = 0;
    let keyTimestamps = [];
    let lastTypedTime = null;
    let liveWPM = 0;
    let attached = false;

    function cleanOldTimestamps() {
        const now = Date.now();
        keyTimestamps = keyTimestamps.filter(ts => now - ts <= 10000);
    }

    function calculateLiveWPM() {
        cleanOldTimestamps();
        const charactersInWindow = keyTimestamps.length;
        const elapsedMinutes = 10 / 60;
        const words = charactersInWindow / 5;
        return Math.round(words / elapsedMinutes);
    }

    function updateDisplayImmediately() {
        if (!lastTypedTime) {
            wpmDisplay.textContent = `Live WPM: 0`;
            return;
        }
        const now = Date.now();
        if (now - lastTypedTime > 10000) {
            wpmDisplay.textContent = `Live WPM: ${liveWPM}`;
        } else {
            liveWPM = calculateLiveWPM();
            wpmDisplay.textContent = `Live WPM: ${liveWPM}`;
        }
    }

    function isTypingKey(e) {
        return e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey;
    }

    function setupTypingTrackerInIframe() {
        if (attached) return;
        const iframe = document.querySelector('iframe.docs-texteventtarget-iframe');
        if (!iframe) return;

        try {
            const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;

            iframeDoc.addEventListener('keydown', (e) => {
                if (isTypingKey(e)) {
                    if (!startTime) {
                        startTime = Date.now();
                    }
                    typedCharacters++;
                    keyTimestamps.push(Date.now());
                    cleanOldTimestamps();
                    lastTypedTime = Date.now();
                    updateDisplayImmediately();
                } else if (e.key === 'Backspace') {
                    typedCharacters = Math.max(typedCharacters - 1, 0);
                    lastTypedTime = Date.now();
                    updateDisplayImmediately();
                }
            }, true);

            attached = true;
        } catch (error) {
            console.error('Failed to access iframe:', error);
        }
    }

    function setupObserver() {
        const observer = new MutationObserver(() => {
            setupTypingTrackerInIframe();
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    setupObserver();
    setupTypingTrackerInIframe();


    let isDragging = false;
    let offsetX, offsetY;

    wpmDisplay.addEventListener('mousedown', (e) => {
        isDragging = true;
        offsetX = e.clientX - wpmDisplay.getBoundingClientRect().left;
        offsetY = e.clientY - wpmDisplay.getBoundingClientRect().top;
        wpmDisplay.style.cursor = 'move';
    });

    document.addEventListener('mousemove', (e) => {
        if (isDragging) {
            wpmDisplay.style.left = `${e.clientX - offsetX}px`;
            wpmDisplay.style.top = `${e.clientY - offsetY}px`;
        }
    });

    document.addEventListener('mouseup', () => {
        isDragging = false;
        wpmDisplay.style.cursor = 'default';
    });
})();