burn / Random Password Generator

// ==UserScript==
// @name         Random Password Generator
// @namespace    https://openuserjs.org/users/burn
// @version      20261003
// @description  Generate secure random password
// @copyright    2026, burn (https://openuserjs.org//users/burn)
// @license      MIT
// @match        *://*/*
// @noframes
// @include      *
// @grant        GM_registerMenuCommand
// @grant        GM.registerMenuCommand
// @grant        GM_setClipboard
// @grant        GM.setClipboard
// ==/UserScript==

// ==OpenUserJS==
// @author burn
// ==/OpenUserJS==

/*jshint esversion: 8 */
(function() {
    'use strict';
    const DBG = false;
    const SCRIPT = GM_info.script.name;
    let log = (str) => {
        DBG && console.log(SCRIPT + " | " + str);
    };

    const TOAST_DURATION = 5000; // ms
    const TOAST_HOST_ID = "rpg-toast-host";

    function secureRandomIndex(max) {
        const range = 0xFFFFFFFF;
        const limit = range - (range % max);
        const buf = new Uint32Array(1);

        do {
            crypto.getRandomValues(buf);
        } while (buf[0] >= limit);

        return buf[0] % max;
    }

    function pick(str) {
        return str[secureRandomIndex(str.length)];
    }

    function shuffle(arr) {
        for (let i = arr.length - 1; i > 0; i--) {
            const j = secureRandomIndex(i + 1);
            [arr[i], arr[j]] = [arr[j], arr[i]];
        }
        return arr;
    }

    function showToast(message) {
        const old = document.getElementById(TOAST_HOST_ID);
        if (old) old.remove();

        const host = document.createElement("div");
        host.id = TOAST_HOST_ID;
        host.style.cssText = "all: initial; position: fixed; top: 20px; right: 20px; z-index: 2147483647;";

        const shadow = host.attachShadow({ mode: "open" });
        shadow.innerHTML = `
            <style>
                .toast {
                    position: relative;
                    box-sizing: border-box;
                    width: 320px;
                    max-width: calc(100vw - 40px);
                    padding: 16px 40px 20px 16px;
                    background: #1f2937;
                    color: #f9fafb;
                    font: 14px/1.4 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
                    border-radius: 8px;
                    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
                    overflow: hidden;
                }
                .close {
                    position: absolute;
                    top: 6px;
                    right: 6px;
                    width: 28px;
                    height: 28px;
                    padding: 0;
                    border: 0;
                    border-radius: 4px;
                    background: transparent;
                    color: #d1d5db;
                    font: 18px/28px system-ui, sans-serif;
                    text-align: center;
                    cursor: pointer;
                }
                .close:hover { background: rgba(255, 255, 255, 0.12); color: #fff; }
                .close:focus-visible { outline: 2px solid #60a5fa; }
                .bar {
                    position: absolute;
                    left: 0;
                    bottom: 0;
                    height: 5px;
                    width: 100%;
                    background: #22c55e;
                    animation: shrink ${TOAST_DURATION}ms linear forwards;
                }
                @keyframes shrink {
                    from { width: 100%; }
                    to   { width: 0%; }
                }
            </style>
            <div class="toast" role="status" aria-live="polite">
                <span class="msg"></span>
                <button class="close" type="button" aria-label="Chiudi">&times;</button>
                <div class="bar"></div>
            </div>
        `;
        shadow.querySelector(".msg").textContent = message;

        const timer = setTimeout(close, TOAST_DURATION);
        function close() {
            clearTimeout(timer);
            host.remove();
        }
        shadow.querySelector(".close").addEventListener("click", close);

        (document.body || document.documentElement).appendChild(host);
    }

    function generatePassword() {
        const LEN = 19;
        const LOWER = "abcdefghijkmnopqrstuvwxyz"; // without lowercase l
        const UPPER = "ABCDEFGHJKLMNPQRSTUVWXYZ"; // without uppercase O and uppercase I
        const NUM = "23456789"; // without 0 and 1
        const SYM = "!@#$%^&*()-_=+[]{};:,.<>?";

        const ALL = LOWER + UPPER + NUM + SYM;

        const result = new Array(
            pick(LOWER),
            pick(UPPER),
            pick(NUM),
            pick(SYM)
        );

        for (let i = result.length; i < LEN; i++) {
            result.push(pick(ALL));
            shuffle(result);
        }

        const password = result.join("");
        let copied = false;

        if (typeof GM_setClipboard !== "undefined") {
            GM_setClipboard(password, "text");
            copied = true;
        } else if (typeof GM !== "undefined" && GM.setClipboard) {
            GM.setClipboard(password, "text");
            copied = true;
        }

        if (copied) {
            showToast("Secure password copied into Clipboard!");
        }
    }

    if (typeof GM_registerMenuCommand !== "undefined") {
        GM_registerMenuCommand("Generate Secure Password", generatePassword);
    } else if (typeof GM !== "undefined" && GM.registerMenuCommand) {
        GM.registerMenuCommand("Generate Secure Password", generatePassword);
    }
})();