awesomeWM / awesome

awesome window manager
https://awesomewm.org/
GNU General Public License v2.0
6.33k stars 597 forks source link

send_string_to_client #3273

Open comod opened 3 years ago

comod commented 3 years ago

I try to send some keystrokes to an app. https://awesomewm.org/doc/api/libraries/root.html doesnt work for me. this codesnippets tries to focus the client, then send the strings and then focus back the last known client. is there a maybe a better solution to send keystrokes to a given client instance "directly", meanwhile? c.fake_input(...) Or any other workaround?

ShayAgros commented 3 years ago

Hi, in short, it's complicated \=

In more details: The root.fake_input function uses xcb_test library underneath to generate the fake keyboard events. The events seem to be sent correctly and should be captured by the client window that has focus.

So what is not working here: Using xcb_test_fake_input command, which dispatches the fake input to the client, doesn't seem be working. I used another test program that uses this library. On both (Ubuntu 18.04's) Gnome and Awesome the fake key presses don't seem to reach anywhere. They do manage to freak the WM/DE completely to a state that they require a restart.

I suspect there is some race here (which seems ubiquitous with such use cases).

What can you do in the meantime?

I don't have a good solution for you. There is a tool called xdotool (sudo apt-get install xdotool on Ubuntu) which simulates events (keystrokes, typing, mouse events etc.), for example

xdotool type 'hello world!'

would make hello world! string be typed in the window which has focus (presumably the terminal you used to run the command in). You can pass it various parameters to make the events be sent to specif clients (identified by window id).

However, it doesn't seem to integrate very well with awesome keybinding system. There seems to be a race between awesome and xdotool in terms of key grabbing (based on this issue it's something that happens with other frameworks as well). To reduce the race's effect you can use sleep parameter which tells xdotool to wait a little before it does anything:

    awful.key({ modkey,   "Shift" }, "t", function ()
        local function send_string_to_client(string)
            if #string == 0 then
                return
            end
            awful.spawn.with_shell("xdotool sleep 1 type " .. string)
        end

        send_string_to_client("Hello world!")
    end,
    {description = "Print hello world", group = "nahh"}),

few caveats: The Hello world! string is printed w/o the space. Don't know why. The command takes a second to run, so by the time the command is executed no keys should be pressed by you. This is far from ideal solution ... Didn't manage to find anything better for now.

@psychon maybe someone else can check on his/her setup. It doesn't seem to be related to awesome but rather to xcb test, so maybe changing distribution would help (since Ubuntu is not the most up-to-date distribution to say the least). I will try to find what xdotool uses and see if it can be used in awesome instead (in case the other approach actually helps).

psychon commented 3 years ago

used another test program that uses this library.

That's quite a number of compiler warnings... :-)

Anyway, that program is broken. Adding a call to xcb_aux_sync(c); before xcb_disconnect(c); makes the X11 server actually handle the requests that it generates. I am still not sure how to use it. Running this with ./a.out "sleep 1" "str test" makes xev say that lots of weird keys were pressed:

KeyPress event, serial 35, synthetic NO, window 0x3400001,
    root 0x761, subw 0x0, time 7508782, (120,68), root:(991,518),
    state 0x0, keycode 160 (keysym 0x1008ff2d, XF86ScreenSaver), same_screen YES,
    XLookupString gives 0 bytes: 
    XmbLookupString gives 0 bytes: 
    XFilterEvent returns: False

KeyRelease event, serial 35, synthetic NO, window 0x3400001,
    root 0x761, subw 0x0, time 7508782, (120,68), root:(991,518),
    state 0x0, keycode 160 (keysym 0x1008ff2d, XF86ScreenSaver), same_screen YES,
    XLookupString gives 0 bytes: 
    XFilterEvent returns: False

KeyPress event, serial 35, synthetic NO, window 0x3400001,
    root 0x761, subw 0x0, time 7508782, (120,68), root:(991,518),
    state 0x0, keycode 176 (keysym 0x1008ff3e, XF86AudioRewind), same_screen YES,
    XLookupString gives 0 bytes: 
    XmbLookupString gives 0 bytes: 
    XFilterEvent returns: False

KeyRelease event, serial 35, synthetic NO, window 0x3400001,
    root 0x761, subw 0x0, time 7508783, (120,68), root:(991,518),
    state 0x0, keycode 176 (keysym 0x1008ff3e, XF86AudioRewind), same_screen YES,
    XLookupString gives 0 bytes: 
    XFilterEvent returns: False

KeyPress event, serial 35, synthetic NO, window 0x3400001,
    root 0x761, subw 0x0, time 7508783, (120,68), root:(991,518),
    state 0x0, keycode 144 (keysym 0xff68, Find), same_screen YES,
    XLookupString gives 0 bytes: 
    XmbLookupString gives 0 bytes: 
    XFilterEvent returns: False

KeyRelease event, serial 35, synthetic NO, window 0x3400001,
    root 0x761, subw 0x0, time 7508783, (120,68), root:(991,518),
    state 0x0, keycode 144 (keysym 0xff68, Find), same_screen YES,
    XLookupString gives 0 bytes: 
    XFilterEvent returns: False

KeyPress event, serial 35, synthetic NO, window 0x3400001,
    root 0x761, subw 0x0, time 7508783, (120,68), root:(991,518),
    state 0x0, keycode 112 (keysym 0xff55, Prior), same_screen YES,
    XLookupString gives 0 bytes: 
    XmbLookupString gives 0 bytes: 
    XFilterEvent returns: False

There seems to be a race between awesome and xdotool in terms of key grabbing

Well, since you bound this to a key binding with mod+shift+t, I guess that all three keys are still pressed by the time that you generate more key presses. That will likely cause problems.

This is also why I am not a fan of any of these mechanisms to generate key presses, but that ship has sailed...

is there a maybe a better solution to send keystrokes to a given client instance "directly"

X11 does not work that way, sorry. You can fake input events and then all the normal X11 event handling applies. If some modifier is currently active (e.g. shift), these new key inputs have that modifier applied. If something grabbed the resulting key combination, that grab is activated. If anything is left, the result is sent to the currently focused client.

There is no way around that and the code snipped at https://awesomewm.org/doc/api/libraries/root.html#fake_input is as good as it gets, I think.

Or any other workaround?

For testing, I would suggest not to activate your code via a key binding. Just write a global function do_my_test and start it from a terminal with sleep 5 ; awesome-client "do_my_test()". The five seconds should be enough so that you can release all the keys that might currently be pressed. You are also not doing anything with key grabs here. This should surely work.

Integrating this (hopefully working) result with key bindings in AwesomeWM is left as an exercise for the reader. If you figure out a way to make everything work, please tell me. I haven't found one.

The Hello world! string is printed w/o the space. Don't know why.

Random guess: Mod is still pressed and you have a key binding for mod+space. Another random guess: xdotool cannot fake space presses the way you are calling it. Or, actually... the space makes it interpret things as another argument. Does this work?

awful.spawn({"xdotool", "sleep", "1", "type", string})
ShayAgros commented 3 years ago

That's quite a number of compiler warnings... :-)

sorry, it is rather irresponsible of me to ignore them. That program is buggy. Didn't spend time debugging it

Well, since you bound this to a key binding with mod+shift+t, I guess that all three keys are still pressed by the time that you generate more key presses. That will likely cause problems.

Yup ... While it was clear to me that this is an issue with xdotool for some reason I missed it completely with awesome. The problem with running root.fake_input with a keybinding is that the pressed keys (to trigger the key binding) interfered with the fake input I'm sending.

Does this work? awful.spawn({"xdotool", "sleep", "1", "type", string})

Yup, works like a charm. xdotool knows how to handle spaces so the problem seemed to be the interpretation of space as an argument separator.

@comod To sum up: based on @psychon reply you cannot send the input to specific client, it will be sent to the client that has focus at the moment the keybindings are sent. You can use xdotool to send input to specific window (not sure what is the distinction between it and client). I recommend going over xdotool documentation, it has many features that might suit your needs. In Awesome you could use something like this:

    awful.key({ modkey,   "Shift" }, "t", function ()
        local function send_string_to_client(string)
            if #string == 0 then
                return
            end
                        awful.spawn({"xdotool", "sleep", "1", "type","--clearmodifiers", string})
        end

        send_string_to_client("Hello world!")
    end,
    {description = "Print hello world", group = "nahh"}),

You probably should not have the key-bindings pressed the moment this function runs though.

Also, if you want to use awesome's root.fake_input command you can use something like:

function send_string(string)
    for i=1, #string do
        local char = string:sub(i,i)
        if char == ' ' then
        -- space key code
            char = 65
    end
        root.fake_input('key_press'  , char)
        root.fake_input('key_release', char)
    end
end

this can be either called from shell by running awesome-client 'send_string("hello world")' or mapped to a keybinding (again with a delay so that the keybinding's chars won't interfere with the fake input)

globalkeys = gears.table.join(
    awful.key({ modkey,   "Shift" }, "t", function ()
        gears.timer {
            timeout = 2,
            call_now    = false,
            autostart   = true,
            single_shot = true,
            callback    = function()
                send_string("hello world")
            end
        }
    end,
    {description = "Print hello world", group = "nahh"}),
)

the problem with using awesome to send fake input is that it uses xcb's XStringToKeysym function to transform a character to X key symbol. Based on this StackOverflow issue, this function doesn't work for anything other than letters and digits, so characters like spaces are left for you to find their keycode instead (see the if condition in the send_string function definition). Finding the key code of a character can be done using this program I took from the StackOverflow's issue:

/* This program was taken from a StackOverflow issue:
 * https://stackoverflow.com/questions/12343987/convert-ascii-character-to-x11-keycode/25771958#25771958
 * 
 * which retains all copyrights for it (if any)
 *
 * The program can be compiled using:
 * gcc key_code_print.c -lxcb -lxcb-xtest -lX11 -lX11-xcb -o key_code_print
 */
#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/Xlib-xcb.h>
#include <xcb/xcb.h>
#include <xcb/xcb_event.h>
#include <xcb/xtest.h>

int main(int argc, char **argv)
{
    char *pc;
    xcb_connection_t *xconn;
    KeyCode code_a;
    Display *dpy = XOpenDisplay(NULL);

    xconn = XGetXCBConnection(dpy);

    for (pc = argv[1]; *pc != '\0'; ++pc) {
        if (*pc >= (char)0x20) {
            code_a = XKeysymToKeycode(dpy, (KeySym)*pc);
            printf("keycode is %d\n", (int)code_a);
            xcb_test_fake_input(xconn, XCB_KEY_PRESS, code_a, XCB_CURRENT_TIME, XCB_NONE, 0, 0, 0);
            xcb_test_fake_input(xconn, XCB_KEY_RELEASE, code_a, XCB_CURRENT_TIME, XCB_NONE, 0, 0, 0);
            xcb_flush(xconn);
        } else {
            fprintf(stderr, "Eeek - out-of-range character 0x%x\n", (unsigned int)*pc);
        }
    }
    XCloseDisplay(dpy);

    return 0;
}

after compiling it you can find the keycode of a character using ./key_code_print char, e.g.

$ ./key_code_print ' '
keycode is 65

@comod does this helps with/fixes the issue you have?

@psychon we can add the same handling of non-alphanumeric characters in _string_to_key_code function as well. Do you see any pitfalls in doing it ?

psychon commented 3 years ago

Finding the key code of a character can be done using this program I took from the StackOverflow's issue:

You can also use /usr/bin/xev which is available pretty much everywhere. This is what it says when I press space:

KeyPress event, serial 35, synthetic NO, window 0x3a00001,
    root 0x761, subw 0x0, time 23996528, (-249,321), root:(2222,743),
    state 0x0, keycode 65 (keysym 0x20, space), same_screen YES,
    XLookupString gives 1 bytes: (20) " "
    XmbLookupString gives 1 bytes: (20) " "
    XFilterEvent returns: False

KeyRelease event, serial 35, synthetic NO, window 0x3a00001,
    root 0x761, subw 0x0, time 23996584, (-249,321), root:(2222,743),
    state 0x0, keycode 65 (keysym 0x20, space), same_screen YES,
    XLookupString gives 1 bytes: (20) " "
    XFilterEvent returns: False

The important bit is keycode 65.

we can add the same handling of non-alphanumeric characters in _string_to_key_code function as well.

Sorry, I don't understand the question. Do you have a patch to show?

For your example program: Who says that a key symbol is (numerically) equivalent to a char code. I bet that does not work for ä and ß. I think you are supposed to use the definitions from e.g. /usr/include/X11/keysymdef.h with this function. Or perhaps all of /usr/include/X11/*keysym*.

For ä, this seems to be (found by asking Wikipedia for the unicode code point and grep for the right entry):

#define XK_adiaeresis                    0x00e4  /* U+00E4 LATIN SMALL LETTER A WITH DIAERESIS */

(Huh, I did not know that Xlib does the equivalent of xcb_aux_sync() in XCloseDisplay. Thanks to your example, I looked this up and I am surprised that it actually does that. I was going to complain about the missing sync, but that is actually not needed here.)

comod commented 3 years ago

Hey! Thank you guys for working on this. I tried to execute something like this after set focus to a specific client (in this example "chromium"): awful.spawn({"xdotool", "sleep", "0.1", "key", "ctrl+t", "type", "hello", "key", "enter"}) Expectation: Open new Website: (Ctrl+t), "hello", (Enter) In Reality: It opens Chrome, Opens a new Tab, Types: "helloenter"

Furthermore i dont understand why i cant do something like this: awful.spawn({"xdotool sleep 0.1 key ctrl+t"})

How can i later abstract these different steps of key-combinations and text-input? My Code should be easy-readable like this: focus_and_send_strings({'ctrl+t', 'hello', 'enter'}, c)

psychon commented 3 years ago

I don't know xdotool, but ctrl is a modifer and not a key. The corresponding keys are called Control_L and Control_R (according to xev). No idea how to make xdotool generate a key press for those, but I have a feeling that @ShayAgros will figure it out. :-)

comod commented 3 years ago

"key", "ctrl+t", "type", "hello" works but only the part after this not @psychon

ShayAgros commented 3 years ago

@comod sorry for the late response, this week has been a little busy.

awful.spawn({"xdotool", "sleep", "0.1", "key", "ctrl+t", "type", "hello", "key", "enter"})

There are two issues with this command.

Here is what I came up with

function enter_url_v1(url)
    gears.timer {
        timeout = 2,
        call_now    = false,
        autostart   = true,
        single_shot = true,
        callback    = function()
            awful.spawn.easy_async({"xdotool", "key", "ctrl+t+ctrl+l", "type", url}, function()
                -- Send return once the previous command finished to run
                root.fake_input('key_press'  , 36)
                root.fake_input('key_release', 36)
            end)
        end,
        }
end

globalkeys = gears.table.join(
    awful.key({ modkey,   "Shift" }, "t", function ()
        enter_url_v1('www.reddit.com/r/awesomewm/')
    end,
    {description = "Procrastinate", group = "nahh"}),
)

(I could send the return key with awful.spawn.spawn({"xdotool", "key", "Return"}) if you prefer, it works for me as well)

While this works there are some issues with this command as well. There is some weird issue with typing the character / (slash) and for some reason it prints q instead. There is a (7 year old) bug open about it in xdotool repository and a suggestion that the issue might be irrelevant in master branch but I didn't check. A work-around to this issue, as proposed in the issue thread there, can be to issue setxkbmap with no arguments. For some reason this fixes the issue I had with xdotool, and this seems like a harmless fix.

Another approach would be to implement it all using root.fake_input. As seen by the _v1suffix in the snippet I sent, I was meaning to give it a try myself, but it seems quite some work and I'm not even sure it is needed so I gave up. It should be doable though.

@psychon I only meant "catching" characters that are in the basic ascii table for which (as far as I saw in the /usr/include/X11/keysymdef.h) the character symbol is the same as their "int" representation. I think this would fix the problem with sending characters like Return and space that we have today. Anyway I'll prepare PR to demonstrate my suggestion, it'd be clearer this way. Also it'd allow me to see if it really fixes anything

jordansissel commented 1 year ago

You cannot run xdotool sub-command after type sub-command

You're in luck! Xdotool can do this with a flag. There are two type flags that can help you: --terminator and --args. --terminator lets you terminate the type arguments kind of like a heredoc, such as xdotool type --terminator END hello world END key Return. The --args flag lets you specify how many arguments are given to the type command, such as xdotool type --args 1 hello key Return

is there a maybe a better solution to send keystrokes to a given client instance "directly",

+1 what others have already suggested that sending keystrokes directly to a client in X11 only sometimes works. xdotool has support for it with the --window ... flag but many X11 programs ignore this particular way of directly sending keystroke events. In the details, events like XKeyEvent have a field send_event which is true if the event was sent by a client using XSendEvent. Most programs especially using higher level UI toolkits like GTK seem to discard these events by default.


In the long term, if y'all want this kind of feature in awesome, I'm happy to share any experience I had writing it for xdotool and hopefully spare someone else the madness that is X11 keyboarding. ;)