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);
})
})()
@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