Skip to content

Instantly share code, notes, and snippets.

@amunchet
Last active September 6, 2026 08:41
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);
})
})()
@UAVXP

UAVXP commented Oct 15, 2024

Copy link
Copy Markdown

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

zakhar-kogan commented Oct 20, 2024 •

Copy link
Copy Markdown

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
Copy Markdown

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

eerison commented Nov 14, 2024

Copy link
Copy Markdown

it worked, thank you <3

@Laz2047

Laz2047 commented Nov 21, 2024

Copy link
Copy Markdown

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
Copy Markdown
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

Laz2047 commented Nov 21, 2024

Copy link
Copy Markdown

Thank you. I'll check it out.

@PhotographybyAlex

Copy link
Copy Markdown

@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

Mohamed-Ali-Nakouri commented Aug 5, 2025 •

Copy link
Copy Markdown

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

bakatz commented Oct 17, 2025 •

Copy link
Copy Markdown

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

cjpjxjx commented Nov 27, 2025 •

Copy link
Copy Markdown

感谢作者的代码!

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

  1. 核心机制:优先直接调用 noVNC 客户端自身的按键发送接口(RFB.sendKey),传入 keysym 与物理按键 code,无需再逆向猜测事件映射;无法获取该接口时才回退到合成键盘事件。对 Shift 组合键和特殊符号的处理更稳定。
  2. 交互优化:图形化模态框,支持实时字符统计、可调节并记忆的字符输入间隔,以及输入过程中的进度浮层(可随时取消)。
  3. 智能适配:实时检测不支持的非 ASCII 字符(中文、Emoji 等)并提示;根据 PVE 界面语言自动切换中英文;虚拟机关机或仅查看模式时自动隐藏按钮。
  4. 部署方式:除油猴脚本外,还支持 Nginx 注入方式,适合反向代理场景。

📖 项目文档与界面均已支持中英双语。

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


Thanks for the code!

Inspired by this, I built an enhanced PVE paste input tool with the help of Claude Code (now updated to 2.0). Key improvements include:

  1. Core Mechanism: Prefers calling noVNC's own key-send API directly (RFB.sendKey) with the keysym and physical key code, instead of reverse-engineering event mappings; falls back to synthetic keyboard events only when that API isn't reachable. More reliable handling of Shift combinations and special symbols.
  2. Better UX: A graphical modal dialog with real-time character counting, an adjustable-and-remembered typing interval, and a progress panel during input (cancelable anytime).
  3. Smart Adaptation: Detects unsupported non-ASCII characters (Chinese, Emoji, etc.) in real time; auto-switches between Chinese and English based on the PVE UI language; auto-hides the button when the VM is off or in view-only mode.
  4. Deployment: Supports both a Tampermonkey userscript and Nginx injection (suitable for reverse-proxy setups).

📖 Both the documentation and the interface are now available in Chinese and English.

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

@SisypheOvO

SisypheOvO commented Apr 13, 2026 •

Copy link
Copy Markdown

@amunchet this script will run on all sites becuase

// @match        https://*
// @include      /^.*novnc.*/

matches all https websites. Changing the two lines above to the single line below will easily fix this I think:

// @match        *://*/*novnc*

@pulichkin

Copy link
Copy Markdown

i updated the original script to vanilla js, without external libs

// ==UserScript==
// @name         noVNC Paste for Proxmox (Fixed)
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  скрипт для вставки текста в noVNC (Proxmox) по правому клику
// @author       Chester Enright & AI
// @match        https://*:8006/*
// @match        https://*/*vnc*
// @include      /^.*novnc.*/
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    // Функция поочередного ввода символов с правильной задержкой
    async function sendString(el, text) {
        const delayMs = 10; // Безопасная задержка между символами (в миллисекундах)

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

            if (needsShift) {
                // Нажимаем Shift
                el.dispatchEvent(new KeyboardEvent("keydown", { key: "Shift", keyCode: 16, bubbles: true }));
                // Нажимаем символ с зажатым Shift
                el.dispatchEvent(new KeyboardEvent("keydown", { key: char, shiftKey: true, bubbles: true }));
                el.dispatchEvent(new KeyboardEvent("keypress", { key: char, shiftKey: true, bubbles: true }));
                el.dispatchEvent(new KeyboardEvent("keyup", { key: char, shiftKey: true, bubbles: true }));
                // Отпускаем Shift
                el.dispatchEvent(new KeyboardEvent("keyup", { key: "Shift", keyCode: 16, bubbles: true }));
            } else {
                // Обычный ввод без Shift
                el.dispatchEvent(new KeyboardEvent("keydown", { key: char, bubbles: true }));
                el.dispatchEvent(new KeyboardEvent("keypress", { key: char, bubbles: true }));
                el.dispatchEvent(new KeyboardEvent("keyup", { key: char, bubbles: true }));
            

            // Ждем перед следующим символом, чтобы VNC успел его обработать
            await new Promise(resolve => setTimeout(resolve, delayMs));
        }
    }

    // Инициализация при клике (работает без jQuery и жестких таймаутов)
    document.addEventListener('mousedown', async (e) => {
        // Проверяем, что это правый клик (button == 2) и кликнули по canvas (экран VNC)
        if (e.button === 2 && e.target.tagName.toLowerCase() === 'canvas') {
            const canvas = e.target;

            try {
                // Запрашиваем текст из буфера обмена
                const text = await navigator.clipboard.readText();
                if (text) {
                    console.log(`[noVNC Paste] Вставляю ${text.length} символов...`);
                    // Отменяем стандартное контекстное меню браузера
                    e.preventDefault(); 
                    await sendString(canvas, text);
                }
            } catch (err) {
                console.error('[noVNC Paste] Не удалось прочитать буфер обмена:', err);
            }
        }
    }, true); // Используем capture-фазу, чтобы перехватить клик раньше встроенных скриптов Proxmox
})();

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