hluk / CopyQ

Clipboard manager with advanced features
GNU General Public License v3.0
8.23k stars 429 forks source link

Wayland issues on Hyprland with ydotool #2747

Open cafetestrest opened 1 week ago

cafetestrest commented 1 week ago

Hi @hluk I have the issue with CopyQ with Wayland on Hyprland still after importing Wayland script. 1

This only happens if I start it with either some startup script or use hyprland's own exec-once to start copyq server. You can see on this screenshot that I have all environments and ydotool service running, also ydotool when run from terminal, it works as expected.

It does resolve itself once I stop the copyq and start it again myself manually from terminal. That way it does work perfectly fine, are you familiar with said issue and do you happen to know a fix for it?

This screenshot is after I've exited copyq and started it again from terminal using:

env QT_QPA_PLATFORM=wayland copyq --start-server &

2

You can see that on 15:24 it failed, while at 15:40 it works as expected.

cafetestrest commented 1 week ago

Behaviour is the same no matter what variable QT_QPA_PLATFORM is set to.

gowallasnewpony commented 1 week ago

@cafetestrest, I have exactly the same issue, also on hyprland, are you using nixos?

gowallasnewpony commented 1 week ago

@cafetestrest ahh, I just saw your first screenshot and it looks like you are also on nixos, pretty sure it has to do with that we are on nixos ...

cafetestrest commented 1 week ago

@gowallasnewpony @hluk , I got it working by using the following workaround:

  1. I have a script that opens terminal with fish shell (my default choice) in Hyprland exec-once
  2. By utilising function fish_greeting to start copyq in config.fish:
    function fish_greeting
    if not pidof copyq > /dev/null
      env QT_QPA_PLATFORM=wayland copyq --start-server &
    end
    end

Does anyone happen to know why would it work like this? My suspicion is that hyprland with exec-once starts copyq differently than terminal (perhaps by user rather than root/service...). Any feedback is appreciated.

hluk commented 6 days ago

So this does not seem like a bug in CopyQ, but a problem with how ydotool is set up.

Can you update to the latest Wayland Support command? It should print more details if a command fails.

cafetestrest commented 6 days ago

Hi @hluk ,

My ydotool config looks like following (I've tried to remove as much hardening as possible): https://github.com/cafetestrest/nixos/blob/72b1b7ee096a6e5b43bd7173cacda5ca54cdbf29/nixos/ydotool.nix

I think I'm using the latest Wayland Support command, the one that I have right now has the following content (I was unable to locate newer one from this):

[Command]
Command="
    /*
    This adds support for KDE, Gnome, Sway and Hyprland Wayland sessions.

    For Sway and Hyprland, this requires:
    - `ydotool` utility to send copy/paste shortcuts to applications
    - `grim` for taking screenshot
    - `slurp` for selecting screenshot area

    For KDE, this requires Spectacle for taking screenshots.

    Getting current window title is not supported in KDE.

    Global shortcut commands can be triggered with:

        copyq triggerGlobalShortcut {COMMAND_NAME}

    On Gnome, clipboard monitor is executed as X11 app using XWayland.

    Links:
    - https://github.com/ReimuNotMoe/ydotool
    - https://github.com/emersion/grim
    - https://github.com/emersion/slurp
    */

    function isSway() {
        return env('SWAYSOCK').length != 0
    }

    function isHyprland() {
        return env('HYPRLAND_CMD').length != 0
    }

    function isGnome() {
        return str(env('XAUTHORITY')).includes('mutter-Xwayland')
    }

    function run() {
        var p = execute.apply(this, arguments)
        if (!p) {
            throw 'Failed to start ' + arguments[0]
        }
        if (p.exit_code !== 0) {
            throw 'Failed command ' + arguments[0] + ': ' + str(p.stderr)
        }
        return p.stdout
    }

    function swayGetTree() {
        var tree = run('swaymsg', '--raw', '--type', 'get_tree')
        return JSON.parse(str(tree))
    }

    function swayFindFocused(tree) {
        var nodes = tree['nodes'].concat(tree['floating_nodes'])
        for (var i in nodes) {
            var node = nodes[i]
            if (node['focused'])
                return node
            var focusedNode = swayFindFocused(node)
            if (focusedNode)
                return focusedNode
        }
        return undefined
    }

    function hyprlandFindFocused() {
        var window = run('hyprctl', '-j', 'activewindow')
        return JSON.parse(str(window))
    }

    function sendShortcut(...shortcut) {
        sleep(100)
        run('ydotool', 'key', ...shortcut)
    }

    global.currentWindowTitle = function() {
        if (isSway()) {
            var tree = swayGetTree()
            var focusedNode = swayFindFocused(tree)
            return focusedNode ? focusedNode['name'] : ''
        } else if (isHyprland()) {
            var focusedWindow = hyprlandFindFocused()
            return focusedWindow ? focusedWindow['title'] : ''
        }
        return ''
    }

    global.paste = function() {
        sendShortcut('42:1', '110:1', '110:0', '42:0')
    }

    var copy_ = global.copy
    global.copy = function() {
        if (arguments.length == 0) {
            sendShortcut('29:1', '46:1', '46:0', '29:0')
        } else {
            copy_.apply(this, arguments)
        }
    }

    global.focusPrevious = function() {
        hide()
    }

    var monitorClipboard_ = monitorClipboard
    monitorClipboard = function() {
        if (isGnome() && env('QT_QPA_PLATFORM') != 'xcb') {
            serverLog('Starting X11 clipboard monitor')
            setEnv('QT_QPA_PLATFORM', 'xcb')
            execute('copyq', '--clipboard-access', 'monitorClipboard')
            serverLog('Stopping X11 clipboard monitor')
            return
        }
        return monitorClipboard_()
    }

    var onClipboardChanged_ = onClipboardChanged
    onClipboardChanged = function() {
        var title = currentWindowTitle()
        if (title)
            setData(mimeWindowTitle, title)
        onClipboardChanged_()
    }

    screenshot = function(format, screenName) {
        if (isSway() || isHyprland())
            return run('grim', '-t', format || 'png', '-')
        return run(
            'spectacle',
            '--background',
            '--nonotify',
            '--pointer',
            '--output',
            '/dev/stdout',
        )
    }

    screenshotSelect = function(format, screenName) {
        if (isSway() || isHyprland()) {
            var geometry = run('slurp')
            geometry = str(geometry).trim()
            return run('grim', '-c', '-g', geometry, '-t', format || 'png', '-')
        }
        return run(
            'spectacle',
            '--background',
            '--nonotify',
            '--pointer',
            '--region',
            '--output',
            '/dev/stdout',
        )
    }

    global.triggerGlobalShortcut = function(commandName) {
        var cmds = commands()
        for (var i in cmds) {
            var cmd = cmds[i]
            if (cmd.isGlobalShortcut && cmd.enable && cmd.name == commandName)
                return action(cmd.cmd)
        }
        throw 'Failed to find enabled global command with given name'
    }"
Icon=
IsScript=true
Name=Wayland Support
hluk commented 6 days ago

Oh, sorry, that seems up-to-date - specifically this line: throw 'Failed command ' + arguments[0] + ': ' + str(p.stderr)

But I wonder why ydotool does not print any error. Maybe it prints an error message to stdout? Or showing the p.exit_code might help if these are well-defined in ydotool.