Skip to content

Instantly share code, notes, and snippets.

@amunchet
Last active November 14, 2024 19:40
Show Gist options
  • Save amunchet/4cfaf0274f3d238946f9f8f94fa9ee02 to your computer and use it in GitHub Desktop.
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
Author

I think you can install Tampermonkey on iOS - the question is how well Proxmox's web console works on mobile. I think you would just two finger press on the canvas area of the VM and it would paste.

You could also modify the script to create a button and then press it to type the text in (as someone did in one of the above comments). That might work better if the iPad is fussy about "right click" (or two finger press).

@yuriw
Copy link

yuriw commented Feb 19, 2023

@amunchet thx a million for this script!

I need to remove and install it on different system.
I can follow the steps from above to install it.

what’s the proper way to remove it?

@amunchet
Copy link
Author

You can either uninstall Tampermonkey as a browser extension or you can just click on the Tampermonkey extension (you might have to allow it to be shown on the browser extension bar), then click Delete from there.

@yuriw
Copy link

yuriw commented Feb 19, 2023

🙌

@felix822
Copy link

I'm still seeing this code when viewing Raw form:

if(e.button == 2){ // Right Click
navigator.clipboard.readText().then(text =>{
window.sendString(text);
})
}

@amunchet
Copy link
Author

@felix822 Not 100% sure what you're trying to do. Do you want to do the popup instead of using the right click?

If so, use this reply and modify the script as needed: https://gist.github.com/amunchet/4cfaf0274f3d238946f9f8f94fa9ee02?permalink_comment_id=4464804#gistcomment-4464804

@felix822
Copy link

Thanks for the quick reply. No, I would like to use the right click function. I followed the steps to install it, found here - https://gist.github.com/amunchet/4cfaf0274f3d238946f9f8f94fa9ee02?permalink_comment_id=4475025#gistcomment-4475025

But when I right click in the guest console, I get an error message:

"noVNC encountered an error:
TypeError: navigator.clipboard.readText is not a function
eval:52:41"

@felix822
Copy link

Sorry, I should have mentioned that this was in Firefox. I just tried the same thing in Chrome and it works. Any work-around for FF?

Thanks!

@shalak
Copy link

shalak commented Feb 23, 2023

@felix822 I've posted a workaround in one of my previous comments. Firefox doesn't allow to read clipboard, you must go with popup.

So first install the script, as described by @amunchet, then apply the fix I posted.

@dhoenig
Copy link

dhoenig commented Apr 18, 2023

I've got Germany keyboard layout and I wonder if the key mapping might need a lot more code to be accomplished.
I did update the needs_shift var, but there is lot more to be done as there are additional keys for the Umlauts (äüö) as well as swapping the y with the z. Is this easily possible, has anybody done this yet?

@amunchet
Copy link
Author

I don't have a German keyboard, so I'm not 100% sure, but I would imagine that the umlaut key is a modifier like shift.

You would just need another section of code like this:

                 var needs_shift = x.match(/[A-Z!@#$%^&*()_+{}:\"<>?~|]/)
                
                 // New code
                 var needs_umlaut = x.match(/äüö/)
             
                 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 if(needs_umlaut){
                     
                    var umlaut_keycode = 999999999999999999999 // Replace with the keyCode for the umlaut modifier is.
                      
                     evt = new KeyboardEvent("keydown", {keyCode: umlaut_keycode})
                     el.dispatchEvent(evt)

                     evt = new KeyboardEvent("keydown", {key: x})  // You might need to modify this, I'm not sure.  You can try it without, but it might need some reading of documentation on KeyboardEvents for German keyboards.  Would be something like the above code with the shiftKey

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

I hope that gets you pointed in at least the right direction!

@dhoenig
Copy link

dhoenig commented Apr 19, 2023

Unfortunately, Umlauts are not like the shift key, they are additional keys that exist only in specific layouts like German.
Here's the German layout to demonstrate the differences to the English one:
image

  • I have marked the Umlauts extra keys in red. They are not like shift, but shift works on these (normal letters) to capitalize them
  • z and y are swapped, marked in green
  • to make things worse, there is yet another shift-like modifier key called Alt Gr (marked blue, to the right of the Space key). This modifier works on the other keys in that, if pressed, you get the dark blue result displayed on them

That must look pretty weird to English speakers (I guess it is ;)

@amunchet
Copy link
Author

amunchet commented Apr 19, 2023

(I actually took German in Jr. High, so it's not that odd to me :)....not as weird as like the Japanese layout).

Couple of thoughts:

  • The Alt Gr needs to have the shift style applied (like in the above example)
  • You'll probably need to just swap Z and Y in the javascript. This can be done like this (probably can be done better):
  x = x.split('').map(function(c) {
    if (c === 'z') {
      return 'y';
    } else if (c === 'y') {
      return 'z';
    } else {
      return c;
    }
  }).join('');
  • What keys do the Umlauts (U, O, and A) appear as? You will probably need to do some kind of swap for them as well.

This script's basic operating principle is to take a string and then pretend to type it into Proxmox via Javascript from the browser. I don't have a deep understanding into how the layouts for each browser work - there may be a way to adjust the internal key mappings that I'm
not aware of. KeyboardEvent is the crux of the idea - maybe there is some documentation on how that works for international layouts.

There may also be a way to temporarily switch your keyboard layout to US-en for the duration of the paste, then switch it immediately back. I think the internationalization issues all stem from trying to type letters into an international layout and emulating those keycodes.

@amosquet
Copy link

amosquet commented Jun 8, 2023

I have a QWERTY keyboard and for some reason, the script is replacing lowercase with uppercase and uppercase with lowercase. There also seems to be a character limit?

Is this just me?

@amunchet
Copy link
Author

amunchet commented Jun 8, 2023

So what's happening in this script is the keyboard itself is being emulated. If you have uppercase and lowercase switching, then it's likely something strange is happening on your keyboard (dumb question, but are you certain caps lock isn't on?)

If that's just the way your keyboard is, then change this line:
from

var needs_shift = x.match(/[A-Z!@#$%^&*()_+{}:\"<>?~|]/)

to

var needs_shift = x.match(/[!@#$%^&*()_+{}:\"<>?~|]/)

That removes the script pushing shift to get to those particular letters.

I don't think there should be a character limit in the script, but depending on your browser and operating system, there may be a clipboard character limit that's getting hit. I do know that some ad-block software might cause weird issues like that as well.

@d0zingcat
Copy link

Awesome! It saves my life!

@jorgepsmatos
Copy link

I have a QWERTY keyboard and for some reason, the script is replacing lowercase with uppercase and uppercase with lowercase. There also seems to be a character limit?

Is this just me?

I have the character limit issue too. Looking at the code, I have no clue what could be causing it

@amunchet
Copy link
Author

@jorgepsmatos First thing I can think of is your browser or OS might be limiting the number of characters in your clipboard. Try to see if you can copy and paste that number of characters into your specific browser in other situations (like into the address bar or something). The other possibility may be a timeout with Tampermonkey - there might be a setting somewhere that kills the script after a certain run period (if you're trying to paste a large amount of text). Hope that helps!

@oreganmike
Copy link

Absolute legend. Thanks.

@flightless22
Copy link

flightless22 commented Jan 2, 2024

For anyone interested, I removed the clipboard dependency by changing:

                if(e.button == 2){ // Right Click
                    navigator.clipboard.readText().then(text =>{
                        window.sendString(text);
                    })
                }

Into:

                if(e.button == 2){ // Right Click
                    let text = prompt("Enter text to paste:");
                    if (text != null) window.sendString(text);
                }

That way, you can paste the text into the native JS prompt.

I got errors with the original script. it needs to check if clipboard access is present and than use prompt or other method as work around. otherwise now it's perfect. almost. I also encountered the same issue with text getting cut off at the end with really long text. I'm using mint / firefox / violentmonkey

@dan3805
Copy link

dan3805 commented Mar 23, 2024

Wow! Thank you so much! This script is really nice!

@CrazyWolf13
Copy link

(I actually took German in Jr. High, so it's not that odd to me :)....not as weird as like the Japanese layout).

Couple of thoughts:

  • The Alt Gr needs to have the shift style applied (like in the above example)
  • You'll probably need to just swap Z and Y in the javascript. This can be done like this (probably can be done better):
  x = x.split('').map(function(c) {
    if (c === 'z') {
      return 'y';
    } else if (c === 'y') {
      return 'z';
    } else {
      return c;
    }
  }).join('');
  • What keys do the Umlauts (U, O, and A) appear as? You will probably need to do some kind of swap for them as well.

This script's basic operating principle is to take a string and then pretend to type it into Proxmox via Javascript from the browser. I don't have a deep understanding into how the layouts for each browser work - there may be a way to adjust the internal key mappings that I'm not aware of. KeyboardEvent is the crux of the idea - maybe there is some documentation on how that works for international layouts.

There may also be a way to temporarily switch your keyboard layout to US-en for the duration of the paste, then switch it immediately back. I think the internationalization issues all stem from trying to type letters into an international layout and emulating those keycodes.

Hi
Any updates on this?
I'd really love such a feature, as with the current code I can barely even use it.

Edit:
Swiss Keyboard Layout is even better:
image

@pesposito
Copy link

pesposito commented Jun 8, 2024

Hello all,
Thank you for this excellent script !
I forked your script to adapt french mac keyboard like this one :
macfr

I have to work more to be able to add all the keys but it's a good start.

Working key in french mac keyboard :
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890*=-_+&"(;/

Not working for now : :)[]{}!\' .....

    const keyMap = {

        'a': { key: 'q', keyCode: 65 }, 'b': { key: 'b', keyCode: 66 }, 'c': { key: 'c', keyCode: 67 },
        'd': { key: 'd', keyCode: 68 }, 'e': { key: 'e', keyCode: 69 }, 'f': { key: 'f', keyCode: 70 },
        'g': { key: 'g', keyCode: 71 }, 'h': { key: 'h', keyCode: 72 }, 'i': { key: 'i', keyCode: 73 },
        'j': { key: 'j', keyCode: 74 }, 'k': { key: 'k', keyCode: 75 }, 'l': { key: 'l', keyCode: 76 },
        'm': { key: ':', keyCode: 191 }, 'n': { key: 'n', keyCode: 78 }, 'o': { key: 'o', keyCode: 79 },
        'p': { key: 'p', keyCode: 80 }, 'q': { key: 'a', keyCode: 81 }, 'r': { key: 'r', keyCode: 82 },
        's': { key: 's', keyCode: 83 }, 't': { key: 't', keyCode: 84 }, 'u': { key: 'u', keyCode: 85 },
        'v': { key: 'v', keyCode: 86 }, 'w': { key: 'z', keyCode: 87 }, 'x': { key: 'x', keyCode: 88 },
        'y': { key: 'y', keyCode: 89 }, 'z': { key: 'w', keyCode: 90 },
        'A': { key: 'Q', keyCode: 65, shiftKey: true }, 'B': { key: 'B', keyCode: 66, shiftKey: true },
        'C': { key: 'C', keyCode: 67, shiftKey: true }, 'D': { key: 'D', keyCode: 68, shiftKey: true },
        'E': { key: 'E', keyCode: 69, shiftKey: true }, 'F': { key: 'F', keyCode: 70, shiftKey: true },
        'G': { key: 'G', keyCode: 71, shiftKey: true }, 'H': { key: 'H', keyCode: 72, shiftKey: true },
        'I': { key: 'I', keyCode: 73, shiftKey: true }, 'J': { key: 'J', keyCode: 74, shiftKey: true },
        'K': { key: 'K', keyCode: 75, shiftKey: true }, 'L': { key: 'L', keyCode: 76, shiftKey: true },
        'M': { key: ':', keyCode: 220, shiftKey: true }, 'N': { key: 'N', keyCode: 78, shiftKey: true },
        'O': { key: 'O', keyCode: 79, shiftKey: true }, 'P': { key: 'P', keyCode: 80, shiftKey: true },
        'Q': { key: 'A', keyCode: 81, shiftKey: true }, 'R': { key: 'R', keyCode: 82, shiftKey: true },
        'S': { key: 'S', keyCode: 83, shiftKey: true }, 'T': { key: 'T', keyCode: 84, shiftKey: true },
        'U': { key: 'U', keyCode: 85, shiftKey: true }, 'V': { key: 'V', keyCode: 86, shiftKey: true },
        'W': { key: 'Z', keyCode: 87, shiftKey: true }, 'X': { key: 'X', keyCode: 88, shiftKey: true },
        'Y': { key: 'Y', keyCode: 89, shiftKey: true }, 'Z': { key: 'W', keyCode: 90, shiftKey: true },
        '1': { key: '&', keyCode: 49, shiftKey: true }, '2': { key: 'é', keyCode: 50, shiftKey: true },
        '3': { key: '"', keyCode: 51, shiftKey: true }, '4': { key: '\'', keyCode: 52, shiftKey: true },
        '5': { key: '(', keyCode: 53, shiftKey: true }, '6': { key: '§', keyCode: 54, shiftKey: true },
        '7': { key: 'è', keyCode: 55, shiftKey: true }, '8': { key: '!', keyCode: 56, shiftKey: true },
        '9': { key: 'ç', keyCode: 57, shiftKey: true }, '0': { key: 'à', keyCode: 48, shiftKey: true },
        '=': { key: '=', keyCode: 187 }, // OK
        '(': { key: '(', keyCode: 53, }, // OK
        '&': { key: '&', keyCode: 49,}, // OK
        ';': { key: ',', keyCode: 188 }, // OK
        '"': { key: '"', keyCode: 51, }, // OK
        '+': { key: '+', keyCode: 187, shiftKey: true }, // OK
        '_': { key: '_', keyCode: 56 }, // OK
        '*': { key: '\\', keyCode: 220}, // OK
        '-': { key: '^', keyCode: 219 }, // OK
        '/': { key: '.', keyCode: 188, shiftKey: true }, // OK
/*        '/': { key: '/', keyCode: 191 },
        ':': { key: '/', keyCode: 186 },
        ')': { key: '°', keyCode: 48 },
        '[': { key: '^', keyCode: 219 },
        ']': { key: '$', keyCode: 221 },
        '{': { key: '^', keyCode: 219 },
        '}': { key: '4', keyCode: 221 },
        '!': { key: '', keyCode: 56 },
        '\'': { key: '4', keyCode: 52 }, */

    };

For anyone interested, I removed the clipboard dependency by changing:

Thank you too @flightless22, I used your code too.

@teneburu
Copy link

merci beaucoup <3 you're the best @pesposito

@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

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