Skip to content

Instantly share code, notes, and snippets.

@amunchet
Last active November 27, 2025 08:00
Show Gist options
  • Select an option

  • Save amunchet/4cfaf0274f3d238946f9f8f94fa9ee02 to your computer and use it in GitHub Desktop.

Select an option

Save amunchet/4cfaf0274f3d238946f9f8f94fa9ee02 to your computer and use it in GitHub Desktop.
Copy/Paste for noVNC Proxmox
// ==UserScript==
// @name noVNC Paste for Proxmox
// @namespace http://tampermonkey.net/
// @version 0.2a
// @description Pastes text into a noVNC window (for use with Proxmox specifically)
// @author Chester Enright
// @match https://*
// @include /^.*novnc.*/
// @require http://code.jquery.com/jquery-3.3.1.min.js
// @grant none
// ==/UserScript==
const delay = 1
;(function () {
'use strict'
window.sendString = function(text) {
var el = document.getElementById("canvas-id")
text.split("").forEach(x=>{
setTimeout(()=>{
var needs_shift = x.match(/[A-Z!@#$%^&*()_+{}:\"<>?~|]/)
let evt
if (needs_shift) {
evt = new KeyboardEvent("keydown", {keyCode: 16})
el.dispatchEvent(evt)
evt = new KeyboardEvent("keydown", {key: x, shiftKey: true})
el.dispatchEvent(evt)
evt = new KeyboardEvent("keyup", {keyCode: 16})
el.dispatchEvent(evt)
}else{
evt = new KeyboardEvent("keydown", {key: x})
}
el.dispatchEvent(evt)
}, delay)
})
}
$(document).ready(function() {
setTimeout(()=>{
console.log("Starting up noVNC Copy/Paste (for Proxmox)")
$("canvas").attr("id", "canvas-id")
$("canvas").on("mousedown", (e)=>{
if(e.button == 2){ // Right Click
navigator.clipboard.readText().then(text =>{
window.sendString(text)
})
}
})
}, 1000);
})
})()
@4Sitam4
Copy link

4Sitam4 commented Sep 9, 2024

I have forked this script and edited it to work with French AZERTY Keyboards (Windows default French Keyboard)

KB_-AZERTY-FR-Windows-_FR

Hope this help my fellow French users

@UAVXP
Copy link

UAVXP commented Oct 15, 2024

For some reason the "|" symbol wasn't pasting properly for me, so I did this:

// ==UserScript==
// @name         noVNC Paste for Proxmox
// @namespace    http://tampermonkey.net/
// @version      0.2a
// @description  Pastes text into a noVNC window (for use with Proxmox specifically)
// @author       Chester Enright
// @match        https://*
// @include      /^.*novnc.*/
// @require http://code.jquery.com/jquery-3.3.1.min.js
// @grant        none
// ==/UserScript==
const delay = 1
;(function () {
    'use strict'
    window.sendString = function(text) {

        var el = document.getElementById("canvas-id")
        text.split("").forEach(x=>{
            setTimeout(()=>{
                var needs_shift = x.match(/[A-Z!@#$%^&*()_+{}:"<>?~|]/)
                 let evt
                 if (needs_shift) {
                     switch (x) {
                         case '|':
                             evt = new KeyboardEvent("keydown", {which: 16, keyCode: 16, shiftKey: true, key: "Shift"})
                             el.dispatchEvent(evt)
                             evt = new KeyboardEvent("keydown", {which: 220, keyCode: 220, shiftKey: true, key: "|"})
                             el.dispatchEvent(evt)
                             evt = new KeyboardEvent("keyup", {which: 16, keyCode: 16, shiftKey: false, key: "Shift"})
                             el.dispatchEvent(evt)
                             break;
                         default:
                             evt = new KeyboardEvent("keydown", {which: 16, keyCode: 16, shiftKey: true, key: "Shift"})
                             el.dispatchEvent(evt)
                             evt = new KeyboardEvent("keydown", {shiftKey: true, key: x})
                             el.dispatchEvent(evt)
                             evt = new KeyboardEvent("keyup", {which: 16, keyCode: 16, shiftKey: false, key: "Shift"})
                             el.dispatchEvent(evt)
                             break;
                     }
                     /**/

                 }else{
                     evt = new KeyboardEvent("keydown", {key: x})
                     el.dispatchEvent(evt)
                     evt = new KeyboardEvent("keyup", {key: x})
                     el.dispatchEvent(evt)
                }
            }, delay)
        })

    }


    $(document).ready(function() {
        setTimeout(()=>{
            console.log("Starting up noVNC Copy/Paste (for Proxmox)")

            $("canvas").attr("id", "canvas-id")

            $("canvas").on("mousedown", (e)=>{
                if(e.button == 2){ // Right Click
                    navigator.clipboard.readText().then(text =>{
                        window.sendString(text)
                    })
                }
            })
        }, 1000);
    })


})()

I hope this would be helpful for somebody

@zakhar-kogan
Copy link

zakhar-kogan commented Oct 20, 2024

Did some modifications (mostly with LLMs, yet tested massively and haven't found any bugs) to correctly work with new lines, symbols etc:

(function () {
    'use strict';

    let capsLockOn = false;
    const KEY_DELAY = 50;
    const SHIFT_NEEDED = /[A-Z!@#$%^&*()_+{}:"<>?~|]/;

    function simulateKeyEvent(el, eventType, key, options = {}) {
        const evt = new KeyboardEvent(eventType, { key, ...options });
        el.dispatchEvent(evt);
    }

    window.sendString = function(text) {
        const el = document.getElementById("novnc-canvas");
        if (!el) {
            console.error("Canvas element not found");
            return;
        }

        text.split('').forEach((char, index) => {
            setTimeout(() => {
                if (char === '\n') {
                    // Simulate "Enter" key press for line breaks
                    simulateKeyEvent(el, "keydown", "Enter");
                    simulateKeyEvent(el, "keyup", "Enter");
                } else {
                    const needsShift = SHIFT_NEEDED.test(char);
                    const isUpperCase = char >= 'A' && char <= 'Z';

                    if (needsShift) {
                        simulateKeyEvent(el, "keydown", "Shift", { keyCode: 16 });
                    }

                    if (isUpperCase && capsLockOn) {
                        simulateKeyEvent(el, "keydown", char.toLowerCase());
                        simulateKeyEvent(el, "keyup", char.toLowerCase());
                    } else {
                        simulateKeyEvent(el, "keydown", char);
                        simulateKeyEvent(el, "keyup", char);
                    }

                    if (needsShift) {
                        simulateKeyEvent(el, "keyup", "Shift", { keyCode: 16 });
                    }

                    if (char === "CapsLock") {
                        capsLockOn = !capsLockOn;
                        console.log("Caps Lock state changed:", capsLockOn);
                    }
                }
            }, index * KEY_DELAY);
        });
    };

    function waitForCanvas() {
        return new Promise((resolve) => {
            const checkCanvas = () => {
                const canvas = $("canvas");
                if (canvas.length > 0) {
                    canvas.attr("id", "novnc-canvas");
                    resolve(canvas);
                } else {
                    setTimeout(checkCanvas, 500);
                }
            };
            checkCanvas();
        });
    }

    async function setupNoVNCPaste() {
        try {
            console.log("Starting up noVNC Copy/Paste (for Proxmox) - Improved Version with Line Breaks");

            const canvas = await waitForCanvas();

            canvas.on("mousedown", (e) => {
                if (e.button == 2) { // Right Click
                    navigator.clipboard.readText()
                        .then(text => {
                            window.sendString(text);
                        })
                        .catch(err => {
                            console.error("Failed to read clipboard:", err);
                        });
                }
            });

            console.log("noVNC Copy/Paste setup completed");
        } catch (error) {
            console.error("Error setting up noVNC Copy/Paste:", error);
        }
    }

    $(document).ready(setupNoVNCPaste);
})();

@JonasKrausch
Copy link

Forked and updated it for German Mac Keyboard.
Added Async writing to be able to paste walls of text.

Tried many mapping variants, but mapping the whole keyboard worked best.

https://gist.github.com/JonasKrausch/3c64e4ca9f4a9bdde66a91759f24e574

@eerison
Copy link

eerison commented Nov 14, 2024

it worked, thank you <3

@Laz2047
Copy link

Laz2047 commented Nov 21, 2024

Is anyone else having a problem with carriage returns? I have a multi-line copy that is pasted into one line on Proxmox. Sometimes, the leading character of the next line is omitted, too.

@amunchet
Copy link
Author

@Laz2047 I would try the version above by @zakhar-kogan - it might handle carriage returns a bit better (I haven't actually tried it myself). The original version was just meant to paste in long passwords or single line items, so I didn't really consider multi-lines.

@Laz2047
Copy link

Laz2047 commented Nov 21, 2024

Thank you. I'll check it out.

@PhotographybyAlex
Copy link

@amunchet Just came here to say I love you for making this script. It's such a massive quality of life improvement!

@Mohamed-Ali-Nakouri
Copy link

Mohamed-Ali-Nakouri commented Aug 5, 2025

used this in console to paste ssh key using right mouse click.


(function () {
    const delay = 50; // Slow down typing to avoid missed characters


    function loadjQuery(callback) {
        if (typeof window.jQuery !== 'undefined') {
            callback(window.jQuery);
        } else {
            const script = document.createElement('script');
            script.src = "https://code.jquery.com/jquery-3.3.1.min.js";
            script.onload = () => callback(window.jQuery);
            document.head.appendChild(script);
        }
    }

    function sendChar(el, char) {
        if (char === '\n' || char === '\r') {
            // Handle Enter
            el.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", which: 13, keyCode: 13 }));
            el.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", which: 13, keyCode: 13 }));
            return;
        }

        const needsShift = char.match(/[A-Z!@#$%^&*()_+{}:"<>?~|]/);

        if (needsShift) {
            el.dispatchEvent(new KeyboardEvent("keydown", { key: "Shift", code: "ShiftLeft", shiftKey: true }));
        }

        el.dispatchEvent(new KeyboardEvent("keydown", { key: char, code: undefined, shiftKey: !!needsShift }));
        el.dispatchEvent(new KeyboardEvent("keyup", { key: char, code: undefined, shiftKey: !!needsShift }));

        if (needsShift) {
            el.dispatchEvent(new KeyboardEvent("keyup", { key: "Shift", code: "ShiftLeft", shiftKey: false }));
        }
    }

    function sendString(text) {
        const el = document.getElementById("canvas-id");
        if (!el) {
            console.warn("Canvas not found");
            return;
        }

        text.split("").forEach((char, i) => {
            setTimeout(() => sendChar(el, char), delay * i);
        });
    }

    loadjQuery(($) => {
        setTimeout(() => {
            console.log("noVNC Paste active");
            $("canvas").attr("id", "canvas-id");

            $("canvas").on("mousedown", (e) => {
                if (e.button === 2) { // Right-click
                    navigator.clipboard.readText().then(text => {
                        sendString(text);
                    });
                }
            });
        }, 1000);
    });
})();

@bakatz
Copy link

bakatz commented Oct 17, 2025

The canvas finding logic is broken, use this for sendString() if you're using novnc and it'll work:

function sendString(text) {

        var el = document.querySelector("#noVNC_container canvas")
        text.split("").forEach(x=>{
            setTimeout(()=>{
                 var needs_shift = x.match(/[A-Z!@#$%^&*()_+{}:\"<>?~|]/)
                 let evt
                 if (needs_shift) {

                     evt = new KeyboardEvent("keydown", {keyCode: 16})
                     el.dispatchEvent(evt)
                     evt = new KeyboardEvent("keydown", {key: x, shiftKey: true})
                     el.dispatchEvent(evt)
                     evt = new KeyboardEvent("keyup", {keyCode: 16})
                     el.dispatchEvent(evt)

                 }else{
                     evt = new KeyboardEvent("keydown", {key: x})
                }
                el.dispatchEvent(evt)
            }, 100)
        })

    }

@cjpjxjx
Copy link

cjpjxjx commented Nov 27, 2025

感谢作者的代码!

受此启发,我在 Claude 的帮助下基于原代码开发了一个增强版的 PVE 粘贴输入工具。 主要改进包括:

  1. 交互优化:增加了图形化界面(模态框),支持实时字符统计。
  2. 核心机制:重写了键盘事件模拟逻辑,更好地处理 Shift 组合键和特殊符号。
  3. 部署方式:除了油猴脚本,还支持 Nginx 注入方式,适合反向代理场景。

⚠️ 说明:目前项目文档和界面仅支持中文。

项目地址:https://github.com/cjpjxjx/pve-paste-input


Thanks for the code!

Inspired by this, I developed an enhanced PVE paste input tool with the help of Claude. Key improvements include:

  1. Better UI: Added a graphical interface (modal dialog) with real-time character counting.
  2. Core Mechanism: Rewrote the keyboard event simulation logic for better handling of Shift keys and special symbols.
  3. Deployment: Supports both Tampermonkey script and Nginx injection (suitable for reverse proxy setups).

⚠️ Note: The project documentation and interface are currently in Chinese only.

Repo: https://github.com/cjpjxjx/pve-paste-input

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment