Closed andreykaere closed 8 months ago
What exactly do you mean with the "char/string of the key"? For example, let's look at my key q
(I have a German keyboard layout). xev
says that this is keycode 24.
Alt Gr
is pressed (that might be a key specific to German keyboards?), I get: @So, I guess the answer to your question is "you do not, because keyboards do not work that way".
These days (that is, ever since the "X keyboard extension" exists, which was released 1996), the "proper way" to do keyboard layouts is via XKB. XKB is complicated enough that there is a library just do it properly: xkbcommon (well, libX11 also includes lots of code for XKB).
You will thus need something from https://crates.io/search?q=xkbcommon. https://crates.io/crates/toy_xcb seems interesting. Personally, I never used anything like this. I'll take a look...
But even in xev
there is state
, which seems to indicate if any modifier is pressed.
Thank you for your answer, I thought that xkb
library for Rust contains a lot of unsafe code ...
Well, the state
field is also present in KeyPressEvent, it is just not enough to properly implement keyboard layouts with this (AFAIK). @ is usually the key that does not work for programs that use X11 without XKB for keyboard stuff.
I am sorry, I can't find how I can get the name of key, that was pressed, I searched for this in xcb
, xkb
... I only found something in x11
, but it's raw ffi ... Could you help me?
Yup, I am already working on an example using xkbcommon. So far all it does it open a window. :see_no_evil:
Yay. xkbcommon
depends on the xcb
crate in an unavoidable way. Thus, I need to use an X11 connection from xcb just to pass around a raw pointer. And thanks to xcb not allowing to construct such a connection from a raw pointer, I also need it to establish the connection. Only alternative seems to be using the xkbcommon-sys
crate, but that's a bit too raw for my current taste...
Anyway: I basically used https://crates.io/crates/toy_xcb as a template and tried to figure out how to use xkbcommon.
Dependencies:
[dependencies]
xkbcommon = { version = "0.5", features = ["x11"] }
x11rb = { version = "0.11", features = ["allow-unsafe-code", "xkb"] }
xcb = "1.1"
Code:
use x11rb::atom_manager;
use x11rb::connection::{Connection as _, RequestConnection as _};
use x11rb::errors::ReplyOrIdError;
use x11rb::protocol::xkb::{self, ConnectionExt as _};
use x11rb::protocol::xproto::{
self, ConnectionExt as _, CreateWindowAux, EventMask, PropMode, WindowClass,
};
use x11rb::protocol::Event;
use x11rb::wrapper::ConnectionExt as _;
use x11rb::xcb_ffi::XCBConnection;
use xkbcommon::xkb as xkbc;
// A collection of the atoms we will need.
atom_manager! {
pub AtomCollection: AtomCollectionCookie {
WM_PROTOCOLS,
WM_DELETE_WINDOW,
_NET_WM_NAME,
UTF8_STRING,
}
}
/// Handle a single key press or key release event
fn handle_key(event: xproto::KeyPressEvent, press: bool, state: &xkbc::State) {
let kind = if press { "press" } else { "release" };
let sym = state.key_get_one_sym(event.detail.into());
let utf8 = state.key_get_utf8(event.detail.into());
println!("Got key {} event for keysym {sym:#x} and utf8 {utf8}", kind);
// Just as an example on how this works:
if sym == xkbc::keysyms::KEY_BackSpace {
println!("Pressed key was backspace");
}
}
/// Create and return a window
fn create_window<'a>(
conn: &XCBConnection,
screen_num: usize,
atoms: &AtomCollection,
) -> Result<xproto::Window, ReplyOrIdError> {
let screen = &conn.setup().roots[screen_num];
let window = conn.generate_id()?;
conn.create_window(
screen.root_depth,
window,
screen.root,
0,
0,
100,
100,
0,
WindowClass::INPUT_OUTPUT,
screen.root_visual,
&CreateWindowAux::new()
.background_pixel(screen.white_pixel)
.event_mask(EventMask::KEY_PRESS | EventMask::KEY_RELEASE),
)?;
let title = "Keyboard tester";
conn.change_property8(
PropMode::REPLACE,
window,
xproto::AtomEnum::WM_NAME,
xproto::AtomEnum::STRING,
title.as_bytes(),
)?;
conn.change_property8(
PropMode::REPLACE,
window,
atoms._NET_WM_NAME,
atoms.UTF8_STRING,
title.as_bytes(),
)?;
conn.change_property32(
PropMode::REPLACE,
window,
atoms.WM_PROTOCOLS,
xproto::AtomEnum::ATOM,
&[atoms.WM_DELETE_WINDOW],
)?;
conn.change_property8(
PropMode::REPLACE,
window,
xproto::AtomEnum::WM_CLASS,
xproto::AtomEnum::STRING,
b"simple_window\0simple_window\0",
)?;
Ok(window)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// The XCB crate requires ownership of the connection, so we need to use it to connect to the
// X11 server.
let (xcb_conn, screen_num) = xcb::Connection::connect(None)?;
let screen_num = usize::try_from(screen_num).unwrap();
// Now get us an x11rb connection using the same underlying libxcb connection
let conn = {
let raw_conn = xcb_conn.get_raw_conn().cast();
unsafe { XCBConnection::from_raw_xcb_connection(raw_conn, false) }
}?;
conn.prefetch_extension_information(xkb::X11_EXTENSION_NAME)?;
let atoms = AtomCollection::new(&conn)?;
let xkb = conn.xkb_use_extension(1, 0)?;
let atoms = atoms.reply()?;
let xkb = xkb.reply()?;
assert!(xkb.supported);
// TODO: No idea what to pick here. I guess this is asking unnecessarily for too much?
let events = xkb::EventType::NEW_KEYBOARD_NOTIFY
| xkb::EventType::MAP_NOTIFY
| xkb::EventType::STATE_NOTIFY;
// TODO: No idea what to pick here. I guess this is asking unnecessarily for too much?
let map_parts = xkb::MapPart::KEY_TYPES
| xkb::MapPart::KEY_SYMS
| xkb::MapPart::MODIFIER_MAP
| xkb::MapPart::EXPLICIT_COMPONENTS
| xkb::MapPart::KEY_ACTIONS
| xkb::MapPart::KEY_BEHAVIORS
| xkb::MapPart::VIRTUAL_MODS
| xkb::MapPart::VIRTUAL_MOD_MAP;
conn.xkb_select_events(
xkb::ID::USE_CORE_KBD.into(),
0u8.into(),
events,
map_parts,
map_parts,
&xkb::SelectEventsAux::new(),
)?;
let context = xkbc::Context::new(xkbc::CONTEXT_NO_FLAGS);
let device_id = xkbc::x11::get_core_keyboard_device_id(&xcb_conn);
let keymap = xkbc::x11::keymap_new_from_device(
&context,
&xcb_conn,
device_id,
xkbc::KEYMAP_COMPILE_NO_FLAGS,
);
let mut state = xkbc::x11::state_new_from_device(&keymap, &xcb_conn, device_id);
let window = create_window(&conn, screen_num, &atoms)?;
conn.map_window(window)?;
conn.flush()?;
loop {
match conn.wait_for_event()? {
Event::ClientMessage(event) => {
let data = event.data.as_data32();
if event.format == 32 && event.window == window && data[0] == atoms.WM_DELETE_WINDOW
{
println!("Window was asked to close");
break;
}
}
Event::XkbStateNotify(event) => {
if i32::try_from(event.device_id).unwrap() == device_id {
// Inform xkbcommon that the keyboard state changed
state.update_mask(
event.base_mods.into(),
event.latched_mods.into(),
event.locked_mods.into(),
event.base_group.try_into().unwrap(),
event.latched_group.try_into().unwrap(),
event.locked_group.into(),
);
}
}
Event::KeyPress(event) => handle_key(event, true, &state),
Event::KeyRelease(event) => handle_key(event, false, &state),
event => println!("Ignoring event {event:?}"),
}
conn.flush()?;
}
Ok(())
}
Hm.... should the above be added as a new example to this git repo? I am not sure what to think about that. And would love to get rid of the useless xcb
dependency... :-(
I have an old project lying around where I was trying to build a wrapper around xkbcommon
with breadx
. Maybe it's time to pick that up again, and use x11rb
instead...
@psychon, thank you a lot for above example and your interest to this issue! And I think that is due to my lack of Rust understanding, but what does use foo as _;
do? Thanks again!
Sure, no problem. Thanks for making me find missing stuff. Thanks to you, there is now https://github.com/psychon/as-raw-xcb-connection.
what does use foo as _; do?
Yes, thanks a lot of explanation!
However, now I am struggling with integrating it into my project, which uses x11rb. Basically, all I want is to implement the function, which will convert keycodes to the strings. So, I copied a first few lines from your main function and now I am trying to get it working, but for some reason I keep getting the following error:
xkbcommon: ERROR: xkb_x11_keymap_new_from_device: illegal device ID: -1
Segmentation fault (core dumped)
I don't really know how to deal with it ... I would really appreciate your help!
Uhm... at least I can turn the abort into a panic. Replace
let device_id = xkbc::x11::get_core_keyboard_device_id(&xcb_conn);
with
let device_id = xkbc::x11::get_core_keyboard_device_id(&xcb_conn);
assert!(device_id >= 0);
But I do not know which this fails in the first place. Is this some weird X11 server that does not support XKB? Does adding conn.sync()?.reply()?
after get_core_keyboard_device_id
and before new_from_device
produce some different kind of error? According to https://github.com/xkbcommon/libxkbcommon/blob/4a576ab1844f68ca1eb8da922e863eb426e38957/src/x11/util.c#L109-L124 this can only really fail because the xcb connection is broken... I am really unsure what is going on, sorry....
No no, when I run exactly your example, everything is working fine. But when I try to integrate it into my code it seems to be broken ... I'll give it another try and if I won't succeed, I'll leave another comment, thank you.
UPD: Okay, I got it working, it was my bad, I didn't copy the line, where you connect xkb extension ... I have another question, but it's not related to the issue, so I don't know if I should I ask it here or somewhere else ... Anyway, I am gonna ask here: I don't know how to send, for example, key press to the window (which I fully grabbed keyboard of); I came up with this code, which doesn't work ... Can you give me an advice how to fix it? @psychon
if let Event::KeyPress(e) = event {
send_event(conn, false, window, EventMask::KEY_PRESS, e)?;
}
where type of event
is Event
Can you give me an advice how to fix it?
:see_no_evil:
Not really and that is complicated. X11 clients can tell apart the different keyboards via the xinput
extension. This extension has a SendExtensionEvent
request that can be used, but I do not really know the details either.
I guess the short version is: You don't. Sorry.
Are you sure you want to send the key presses yourself? xproto
has the AllowEvents
request that can be used for something like this: https://www.x.org/releases/X11R7.6/doc/xproto/x11protocol.html#requests:AllowEvents
However, I am always confused when I try to understand X11 input stuff. But... somehow it is possible to use AllowEvents
to filter out certain events without filtering out others. But I do not really know the details and if one forgets a required AllowEvents
request, all input freezes up, so....
But how do things like xdotool
work then? I'm pretty sure there's a way to simulate key press ... I don't if it has anything to do with xlib or something else related...
But how do things like xdotool work then?
xdotool
uses the xtest
X11 extension. This has a FakeInput
request. However, this really just fakes input as if you pressed a key on a keyboard. This thus does not have a Window
argument and goes through the normal X11 event processing. In other words. This cannot be used to bypass your own grabs.
xdotool --type
apparently has a --window
flag that does not work too well: https://manpages.ubuntu.com/manpages/trusty/man1/xdotool.1.html#sendevent%20notes
It is important to note that for key and mouse events, we only use XSendEvent when a specific window is targeted. Otherwise, we use XTEST.
XSendEvent
is the send_event()
you already found above which you say does not work. I guess that is the same problem as this xdotool
man page describes.
Okay, and how again I can grab keyboard without, for example, a key t
?
Sorry, what? Why doesn't your keyboard have a key t
? Or do you want to grab everything except for the t
key? Why would you do that? O.o
I want to do the following: fully control keyboard of application. But then I can't simulate any key press. (if I understood you correctly) And to be able to do that I need to allow this key pressed event to be sent to application, therefore not grabbing it in the first place...
Hm, yeah, I fear X11 doesn't really make that possible... or at least not easy. Sorry.
But I know a program written in Python, which does it. I just don't know how to implement it using x11rb ...
Which program? How does it do it?
It uses send_event
... Maybe I am passing the event
wrong?
Well... got a link or something like that?
Sure: https://github.com/gillescastel/inkscape-shortcut-manager
Function: press
in class in main
x11rb port that works for me against urxvt
(detail: 52
is y
on a German keyboard layout):
use x11rb::{
connection::Connection as _,
protocol::xproto::{ConnectionExt as _, KeyPressEvent, KEY_PRESS_EVENT, KEY_RELEASE_EVENT},
wrapper::ConnectionExt as _,
};
// KeyPressEvent and KeyReleaseEvent are the same type, but have different response_type
type KeyEvent = KeyPressEvent;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let (conn, screen_idx) = x11rb::connect(None)?;
let screen = &conn.setup().roots[screen_idx];
let focus = conn.get_input_focus()?.reply()?.focus;
println!("Input focus is {focus:x}");
let send_event = |response_type| {
let event = KeyEvent {
sequence: 0,
response_type,
time: x11rb::CURRENT_TIME,
root: screen.root,
event: focus,
child: x11rb::NONE,
// No modifiers pressed
state: Default::default(),
// They key that is being toggled
detail: 52,
// All of the following are technically wrong?!
same_screen: false,
root_x: 0,
root_y: 0,
event_x: 0,
event_y: 0,
};
conn.send_event(true, focus, Default::default(), event)
};
send_event(KEY_PRESS_EVENT)?;
send_event(KEY_RELEASE_EVENT)?;
conn.sync()?;
Ok(())
}
I'll hijack this issue for a similar issue that I'm experiencing right now as I'm trying to write my own tiling window manager. I've gotten to the point where I want to implement keybinds but I just can't get it to work.
So I've begun the implementation by setting up the keyboard with the help of your comment above using xkbcommon
and xcb
and that all works. The issue presents itself when I want to intercept key press/release events. If I grab the entire keyboard I get these events just fine but I can't type into applications since I grabbed the keyboard.
My new approach, after looking at the dwm
source, is to grab the keybinds specifically instead of the entire keyboard and it does that without reporting any errors.
In dwm
they do it like this in the setup
function:
// they do other stuff before this...
wa.event_mask = SubstructureRedirectMask|SubstructureNotifyMask
|ButtonPressMask|PointerMotionMask|EnterWindowMask
|LeaveWindowMask|StructureNotifyMask|PropertyChangeMask;
XChangeWindowAttributes(dpy, root, CWEventMask|CWCursor, &wa);
XSelectInput(dpy, root, wa.event_mask);
grabkeys();
And their grabkeys
function looks like this:
void
grabkeys(void)
{
updatenumlockmask();
{
unsigned int i, j, k;
unsigned int modifiers[] = { 0, LockMask, numlockmask, numlockmask|LockMask };
int start, end, skip;
KeySym *syms;
XUngrabKey(dpy, AnyKey, AnyModifier, root);
XDisplayKeycodes(dpy, &start, &end);
syms = XGetKeyboardMapping(dpy, start, end - start + 1, &skip);
if (!syms)
return;
for (k = start; k <= end; k++)
for (i = 0; i < LENGTH(keys); i++)
/* skip modifier codes, we do that ourselves */
if (keys[i].keysym == syms[(k - start) * skip])
for (j = 0; j < LENGTH(modifiers); j++)
XGrabKey(dpy, k,
keys[i].mod | modifiers[j],
root, True,
GrabModeAsync, GrabModeAsync);
XFree(syms);
}
}
I do the grabbing like so:
let keybinds = commands::setup_keybinds();
// trying to replicate grabbing like they do it in dwm
let numlockmask = {
let mut nlm: u16 = 0;
let modmap = conn.get_modifier_mapping()?.reply()?;
let max_keypermod = modmap.keycodes_per_modifier();
for i in 0..8 {
for j in 0..max_keypermod {
let idx = (i * max_keypermod + j) as usize;
if modmap.keycodes[idx] == KEY_Num_Lock as u8 {
nlm = 1 << i;
}
}
}
ModMask::from(nlm)
};
let modifiers = [
ModMask::from(0u16),
ModMask::LOCK,
numlockmask,
numlockmask | ModMask::LOCK,
];
conn.ungrab_key(GRAB_ANY, screen.root, ModMask::ANY)?;
for keybind in &keybinds {
let mod_mask = ModMask::from(u16::from(keybind.mods));
for m in &modifiers {
conn.grab_key(
true,
screen.root,
mod_mask | *m,
keybind.keysym as u8,
GrabMode::ASYNC,
GrabMode::ASYNC,
)?;
}
}
My event mask on screen.root
is the following:
let change = ChangeWindowAttributesAux::default().event_mask(
EventMask::SUBSTRUCTURE_REDIRECT
| EventMask::SUBSTRUCTURE_NOTIFY
| EventMask::KEY_PRESS
| EventMask::KEY_RELEASE
| EventMask::STRUCTURE_NOTIFY
| EventMask::PROPERTY_CHANGE,
);
Lastly, here is the focus method which delegates focus to the window in question:
fn focus(&mut self, frame: Window, window: Window) -> Result<(), ReplyError> {
println!("setting focus on frame {frame} containing window {window}");
// simple_window_manager.rs says InputFocus::PARENT but dwm uses InputFocus::POINTER_ROOT
self.conn
.set_input_focus(InputFocus::PARENT, window, CURRENT_TIME)?;
self.conn.configure_window(
frame,
&ConfigureWindowAux::new().stack_mode(StackMode::ABOVE),
)?;
let focus_aux = ChangeWindowAttributesAux::new().border_pixel(theme::WINDOW_BORDER_FOCUSED);
self.conn.change_window_attributes(frame, &focus_aux)?;
self.conn.flush()?;
self.last_focused = Some((frame, window));
Ok(())
}
Okay, so, the issue is that no key press/release events are being reported when I input any of the grabbed keybinds. I can't see any, even though I've registered for them in the event mask, grabbed the relevant keybinds according to the keymap, and set up a relevant event handler but I'm missing something...
I know you said you are not very well versed in input stuff above @psychon but I'll ask anyways just in case you might spot something :)
Well... from a quick look: I can't see anything different between the dwm code you posted and what you are doing.
I'll hijack this issue
Could you open a new issue and provide a runnable example (or at least a link to your GitHub repo)? This issue is titled "keycode to string" and your post is about something different. I'll mark this comment and your comment as "off topic".
I have a question: why do we have to use Default::default()
(or what's the same EventMask::NO_EVENT
) here?
conn.send_event(true, focus, Default::default(), event)
Why with EventMask::KEY_PRESS
it doesn't work? What does this argument even mean?
P.S. Thank you in advance! And Happy New Year :)
From https://www.x.org/releases/X11R7.6/doc/xproto/x11protocol.html#requests:SendEvent . I suggest reading all of that, but to answer your question:
If the event-mask is the empty set, then the event is sent to the client that created the destination window.
And if that mask is not empty:
then the event is sent to every client selecting on destination any of the event types in event-mask.
Got you, thanks a lot! And why would you want to use "propagate" with true
value? I mean, it can result in some other window getting the event ... And I still don't quite understand the description of both:
"If propagate is False, then the event is sent to every client selecting on destination any of the event types in event-mask.
If propagate is True and no clients have selected on destination any of the event types in event-mask, then destination is replaced with the closest ancestor of destination for which some client has selected a type in event-mask and no intervening window has that type in its do-not-propagate-mask. If no such window exists or if the window is an ancestor of the focus window and InputFocus was originally specified as the destination, then the event is not sent to any clients. Otherwise, the event is reported to every client selecting on the final destination any of the types specified in event-mask."
1) If I am sending signals to some window (like image editor, for example) how do I know what kind of events it is selecting? 2) What does that mean "no intervening window has that type in its do-not-propagate-mask"? 3) What does "final" mean here "Otherwise, the event is reported to every client selecting on the final destination any of the types specified in event-mask."? Does it mean that "otherwise" it just falls back to "false" version?
And why would you want to use "propagate" with true value? I
Because that's how normal X11 event propagating works. If you click in a window and no client subscribed to button clicks on that window, the event is reported to the parent (and its parent, and so on).
If I am sending signals to some window (like image editor, for example) how do I know what kind of events it is selecting?
Uhm... I'll go with "you do not" for my answer, but in theory GetWindowAttributes
tells you about event masks. I just cannot think of any good use of that.
What does that mean "no intervening window has that type in its do-not-propagate-mask"?
With a ChangeWindow
request, you can ask for events by setting an event mask. Similarly, you can set a do_not_propagate_mask
to prevent event propagation through this window. This feature is so heavily used, that no one ever noticed the typo here (propOgate instead of propAgate). /s
What does "final" mean here
Well, the text before that described how a destination window is picked. I guess "final" here just wants to emphasise that the window that was picked as destination
according to this rule is used instead of the destination
value from the request.
Okay, thank you very much! Just want to ask about your opinion: when should one use "propagate" as "false" and when as "true"?
:shrug:
My opinion would be to never use SendEvent
(unless one explicitly is required to do so by some specification, e.g. ICCCM, EWMH, XEmbed, ...).
But what's the alternative then? If I want to emulate certain key presses, for example? As in xdotool
or something ...
Oh, wow, xdotool
also uses SendEvent
(but prefers XTest if possible): https://github.com/jordansissel/xdotool/blob/7e02cef5d9216bd0ce69b44f62217b587cc7c31e/xdo.c#L1565-L1587
But what's wrong with using SendEvent
anyway?
It completely ignores the expectations of the target app and just delivers it a raw event.
Does the app use XKB or core X11 for keyboard handling? Does it perhaps use the xinput extension? One cannot know. The approach is basically "we assume that the app uses XYZ and just hope it actually does".
(And I'm not too sure about keyboard state either. What if some weird modifier is currently pressed? Or latched? Or locked? Stuff like Caps-Lock. With core events, this can be dealt with relatively easily since the key events contain the mask state, but XKB should be more complicated (but I don't actually know XKB all that well and thus cannot really say for sure).)
But what's the "right" way to do it? And how xtest is better?
I don't know about the right way to do it, but xtest is better, because it lets the X11 server do all its normal input processing. XTest is basically "dear X11 server, pretend that there is an extra keyboard present that just generated the following key presses". The X11 server then treats this just like any other input event, routing it to the correct window, and doing the normal event processing (thus doing all the XKB / XInput stuff "just right").
Oh, wow,
xdotool
also usesSendEvent
(but prefers XTest if possible): https://github.com/jordansissel/xdotool/blob/7e02cef5d9216bd0ce69b44f62217b587cc7c31e/xdo.c#L1565-L1587
What is x11rb
alternative of XTestFakeKeyEvent
? Is it xtest_fake_input
? If yes, then what should I put as type_
? response_type
from KeyPressEvent
?
Because I tried this:
let event = match event {
Event::KeyPress(e) | Event::KeyRelease(e) => e,
_ => {
return Ok(());
}
};
conn.xtest_fake_input(
event.response_type,
event.detail,
event.time,
event.root,
event.root_x,
event.root_y,
device_id,
);
And it doesn't work ... Nothing happens except that program hangs :)
What is x11rb alternative of XTestFakeKeyEvent? Is it xtest_fake_input?
Yup.
If yes, then what should I put as type_?
Puh, I don't know. https://www.x.org/releases/X11R7.7/doc/xextproto/xtest.html#Server_Requests says
This must be one of KeyPress, KeyRelease, MotionNotify, ButtonPress, or ButtonRelease, or else a Value error occurs.
For some reasonxtest
doesn't work for me, nothing happens ... So, I guess, I'll stick to send_event
...
How can I convert keycode to the char/string of the key, that has given keycode? I haven't found any function for that