psychon / x11rb

X11 bindings for the rust programming language, similar to xcb being the X11 C bindings
Apache License 2.0
366 stars 40 forks source link

How to get screen number by given name? #796

Closed andreykaere closed 1 year ago

andreykaere commented 1 year ago

Hi! So, basically, as the title says, I am trying to write a function, that receives a name of the monitor(screen) and returns it's number, but to no avail. I would really appreciate if you could help me here. I guess it's some magical combination of get_screen_resources, get_output_info and something else, however I don't know how to get it all work together:)

psychon commented 1 year ago

First impression:

monitor(screen)

X11 is complicated. Which of the many concepts that are used to you want (core screens, XINERAMA, RANDR screens, RANDR monitors).

But then you mention get_screen_resources and get_output_info, so you are apparently already looking at randr. Searching for "name" in https://github.com/psychon/x11rb/blob/master/xcb-proto-1.15.2/src/randr.xml has five hits. The field in GetOutputInfo and GetMonitorInfo seems fitting.

I don't know how to get it all work together:)

I think the canonical information for this kind of thing is the randr protocol specification: https://gitlab.freedesktop.org/xorg/proto/xorgproto/-/blob/master/randrproto.txt

The most relevant part of this is

Version 1.5 adds monitors

which is why there are both "monitors" and "outputs".

If you want an example implementation, I would suggest to look at https://github.com/linebender/druid/blob/4d29ce81f4d28018042cb33793984c90d142bdf6/druid-shell/src/backend/x11/screen.rs#L60. This does not deal with XINERAMA, but besides that it goes through all possibilities.

Sadly, this does not look at the name of things. For that, you need the "name" field of either MonitorInfo or GetOutputInfoReply. A MonitorInfo should be what e.g. this iterates over: https://github.com/linebender/druid/blob/4d29ce81f4d28018042cb33793984c90d142bdf6/druid-shell/src/backend/x11/screen.rs#L101

Outputs are not something that this goes does yet. It looks at CRTCs in get_monitors_randr_crtcs_timestamp. The GetCrtcInfoReply struct that one gets from the reply of randr_get_crtc_info has a Vec<Output> with name outputs. For each of them, one can use GetOutputInfo to get something containing a name field.

And core protocol simply does not have outputs (see get_monitors_core).

All of this is complicated. If you want it simple, I would suggest to only go with monitors and just not support RandR before version 1.5. According to https://cgit.freedesktop.org/xorg/proto/randrproto/tree/randrproto.txt version 1.5. is from 2015, so perhaps eight years is enough that everyone you care about has support for randr monitors.

andreykaere commented 1 year ago

Here https://docs.rs/x11rb/latest/x11rb/protocol/randr/struct.MonitorInfo.html the field "name" is just u32 value, which is not really something I expect to get. Besides, I am a bit confused by terminology: why any function in randr expects me to supply "window" argument, which is usually &conn.setup().roots[screen_num]? I mean, I don't really understand hierarchy here ... Like, screens are monitors in x11, as I understand ... Then why to get info about one of the monitor, I need to provide something else?

psychon commented 1 year ago

Oh, I missed that part. Sorry.

It's an Atom: https://github.com/psychon/x11rb/blob/15153e514a4e3e4c116279f76ab1c3635abc05e5/xcb-proto-1.15.2/src/randr.xml#L867

You can use GetAtomName to turn that into something human readable: https://docs.rs/x11rb/latest/x11rb/protocol/xproto/fn.get_atom_name.html

psychon commented 1 year ago

Like, screens are monitors in x11, as I understand ... Then why to get info about one of the monitor, I need to provide something else?

TL;DR: Just always use the root window here.

Historic reasons. Core X11 protocol has something called "screens" / zaphod mode. Later things then use a window to identify which core-protocol-screen is meant. Put another way, the protocol does not use screen_num, but conn.setup().roots[screen_num].root to identify this screen.

andreykaere commented 1 year ago

What is root in this sense? The "root window"? And what does it mean technically

andreykaere commented 1 year ago

And I have a question about randr_get_monitors parameter get_active - what does it affect? https://docs.rs/x11rb/latest/x11rb/protocol/randr/trait.ConnectionExt.html#method.randr_get_monitors

I am still a bit confused about why I need to specify concrete scree_num to get screen to use randr_get_monitors in order to get information about all monitors. It's weird to me, or i don't understand something? And is that the correct code?

fn get_screen_name<C: Connection>(
    conn: &C,
    screen_num: usize,
) -> Result<String, Box<dyn Error>> {
    let screen = &conn.setup().roots[screen_num];
    let randr_monitors = conn.randr_get_monitors(screen.root, true)?.reply()?;

    for (i, monitor) in randr_monitors.monitors.into_iter().enumerate() {
        let name = conn.get_atom_name(monitor.name)?.reply()?;
        let monitor_name = String::from_utf8(name.name)?;

        if i == screen_num {
            return Ok(monitor_name);
        }
    }

    unimplemented!();
}
andreykaere commented 1 year ago

And what can be specified in rust x11rb::connect() except for None? What's the difference between monitor and display name? What is the second argument returned?

psychon commented 1 year ago

What is root in this sense? The "root window"? And what does it mean technically

Yup.

Each screen has its root window. So each screen has its own window hierarchy. In "classical X11", it is not possible to move windows between screens.

And I have a question about randr_get_monitors parameter get_active - what does it affect?

https://gitlab.freedesktop.org/xorg/proto/xorgproto/-/blob/master/randrproto.txt says

If 'get_active' is set it returns only active monitors (non-0x0 monitors). 'get_active' should always be set by toolkits, and not by configuration clients.

It's weird to me, or i don't understand something?

That's just how old pre-RandR-X11 did multi-screen. Separate window hierarchies and only the mouse cursor can move between them. When RandR came, it started separating one "screen" into multiple "outputs". So, to "old clients" there is just a single, big screen and no "moving between screens" happen. These outputs then just show different parts of this big screen.

And what can be specified in rust x11rb::connect() except for None? What's the difference between monitor and display name? What is the second argument returned?

If you use None, the code uses the value of the $DISPLAY environment variable. The code for parsing that is here and produces a struct ParsedDisplay: https://github.com/psychon/x11rb/blob/15153e514a4e3e4c116279f76ab1c3635abc05e5/x11rb-protocol/src/parse_display/mod.rs#L12-L27 The tests for that code show some examples. For example, the X11 server could be on a different machine and we connect to it via IPv4. The last part, the screen, is just the number that connect returns. So, when you set DISPLAY=:42, connect() would return 42 for the screen number (but only if your X11 server actually offers 43 screens - if there are less, an error is returned instead).

andreykaere commented 1 year ago

But what if I have to monitors connected and I run x11rb:connect(None), what returned number would mean? Number of monitor x11rb connected to? How did it choose? Can't we connect to both of them?

andreykaere commented 1 year ago

And please, could you explain to me this: "I am still a bit confused about why I need to specify concrete scree_num to get screen to use randr_get_monitors in order to get information about all monitors"

psychon commented 1 year ago

But what if I have to monitors connected and I run x11rb:connect(None), what returned number would mean? Number of monitor x11rb connected to? How did it choose?

It's the value of $DISPLAY. If you set that environment variable to :42, it will return screen 42.

Can't we connect to both of them?

In classical X11 from 1984? Nope. That's only possible since XINERAMA which was created in 1998.

And please, could you explain to me this: "I am still a bit confused about why I need to specify concrete scree_num to get screen to use randr_get_monitors in order to get information about all monitors"

Well... ignore screen numbers. Screen numbers come from classical X11. For all practical purposes, you can assume you have only one screen with number 0 and you are trying to get information about that.

But okay, here is the hierarchy:

So, with the screen number, you select the screen from the point of view of the core X11 protocol. One screen can have multiple RandR CRTCs / outputs / monitors. Thus, to select which of the protocol screens you are querying, you send along a window id of some window that is on that screen.

New attempt:

And please, could you explain to me this: "I am still a bit confused about why I need to specify concrete scree_num to get screen to use randr_get_monitors in order to get information about all monitors"

Because you are not getting information about all monitors. You only get information about monitors that belong to that one screen.

andreykaere commented 1 year ago

Okay, this makes more sense now

Because you are not getting information about all monitors. You only get information about monitors that belong to that one screen.

But what is the monitor in X11 terminology and what is the screen? And how is that correlate with our human understanding of "monitor"?

My confusion comes from the fact, that in real life people use terms screen and monitor interchangeably ...

psychon commented 1 year ago

My confusion comes from the fact, that in real life people use terms screen and monitor interchangeably

Yup. :-)

Basically: You want a monitor. Ignore screens.

The long version:

At first there were screens. Back in the day, these were a lot different from each other. Some could show 16 colors, some just two, some even 256 colors! X11 supported all of that, but one cannot abstract away these differences. Thus, windows also were specific to such a screen. Put differently: Windows could not be moved between screens.

Then came Xinerama and RandR. They pretend that there is only a single, large screen ("screen" as in "protocol screen"; not the physical device in front of you). Instead, something new is added that is the new representation for "the physical device in front of you". These were outputs. Then, as already mentioned above, this again became a problem and now a new representation called "monitors" were invented.

So, yes, there is only one kind of "physical device in front of you", but due to previous decision, a new representation of this "physical device" needed to be added. More than once. And the previous representation became deprecated (basically - CRTCs and outputs are still relevant for configuring "physical device"-monitors, but not for getting information about which ones exist).

Thus, the newest thing is monitors from RandR and all the previous stuff should be ignored. But still exists.

andreykaere commented 1 year ago

Okay, thank you for clarifying some stuff! But still some questions: when I write

let (conn, screen_num) = x11rb::connect(None)?;
...

What is screen_num? Is it always zero or it's what I want -- some kind of id for monitor x11 is connected to? Or it is always zero?

If it's always zero, than how can I connect to specific monitor? With this kind of code?

let screen = &conn.setup().roots[mon_id];

where mon_id is id of monitor I am interested in?

Thank you in advance!

psychon commented 1 year ago

That is a screen number, not a monitor number. So, this previous answer still applies:

Ignore screens.

But okay, let's try again:

What is screen_num? Is it always zero or it's what I want -- some kind of id for monitor x11 is connected to? Or it is always zero?

It's a screen number. Here is some example code:

    let (conn, screen_num) = x11rb::connect(None).unwrap();
    println!("{}", screen_num);

When I cargo run this, it prints 0. When I use DISPLAY=:1 cargo run, it prints.... uhm... Connection refused? Oh, I was wrong about the syntax.

New attempt: When I use DISPLAY=:0.1 cargo run, this fails with thread 'main' panicked at 'called 'Result::unwrap()' on anErrvalue: InvalidScreen' because my X11 server only has one screen.

Now, let's get an X11 server with more than one screen. I run Xephyr -listen tcp -screen 640x480 -screen 640x480 -screen 640x480 :4 in another terminal. This runs Xephyr on display :4. For what Xephyr is, I'll point to https://en.wikipedia.org/wiki/Xephyr. Anyway, now I can DISPLAY=:4.0 cargo run and get the output 0. With DISPLAY=:4.1, I get 1 and with DISPLAY=:4.2 I get 2. DISPLAY=:4.3 leads again to the InvalidScreen error since there are only three screens.

This Xephyr-based X11 server has three different screens. Each of its screens is shown in a window. Again, you will most likely not find a "proper" X11 server with more than one screen (where I mean "screen" in the sense of "x11 screen" and not "physical device in front of you").

andreykaere commented 1 year ago

I'm sorry, but it seems that you don't quite understand what I want to hear. For me monitor is something listed in xrandr -q. It lists all possible monitors and print the status. My question is: how do I "connect" to one of these monitors to display something there? I am asking if this is the right way:

let screen = &conn.setup().roots[mon_id];

And then something like

conn.create_window(
    x11rb::COPY_FROM_PARENT as u8,
    win,
    screen.root,
    x.try_into()?,
    y.try_into()?,
    width.try_into()?,
    height.try_into()?,
    0,
    WindowClass::COPY_FROM_PARENT,
    screen.root_visual,
    &window_aux,
)?;
psychon commented 1 year ago

Ah, sorry. Yup, I misunderstood.

let screen = &conn.setup().roots[mon_id];

Nah, roots is most likely a Vec of length 1, so you can only use index 0 there.

I am not sure how you can actually pick a monitor yourself. Your best bet is to choose x and y of your window so that they fall into one of the regions of that monitor, but whether this actually works depends on your window manager.

Random test:

$ xrandr -q | grep \ connected 
HDMI-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 477mm x 268mm
HDMI-2 connected 1920x1080+1920+0 (normal left inverted right x axis y axis) 527mm x 296mm

When I run xterm -geometry 20x20+0+0 I get a terminal on the left monitor. When I run xterm -geometry 20x20+1930+10.... uhm... I also get a terminal on the left monitor.

According to xtrace, xterm is at least trying, but apparently i3wm does not honor this:

$ xtrace --relative-timestamps xterm -geometry 20x20+1930+10 | grep -E 'ConfigureWindow|ConfigureNotify'
No display name to create specified, trying :9
Got connection from unknown(local)
0.053 000:<:0058: 20: Request(12): ConfigureWindow window=0x0300000e values={width=10 height=17}
0.054 000:>:0059: Event ConfigureNotify(22) event=0x0300000e window=0x0300000e above-sibling=0x01600173 x=0 y=0 width=10 height=17 border-width=1 override-redirect=false(0x00)
0.054 000:<:0061: 20: Request(12): ConfigureWindow window=0x0300000e values={width=124 height=264}
0.054 000:>:0062: Event ConfigureNotify(22) event=0x0300000e window=0x0300000e above-sibling=0x01600173 x=0 y=0 width=124 height=264 border-width=1 override-redirect=false(0x00)
0.054 000:<:0063: 20: Request(12): ConfigureWindow window=0x0300000e values={x=1930 y=10}
0.054 000:>:0065: Event ConfigureNotify(22) event=0x0300000e window=0x0300000e above-sibling=0x01600173 x=1930 y=10 width=124 height=264 border-width=1 override-redirect=false(0x00)
0.088 000:>:0191: Event ConfigureNotify(22) event=0x0300000e window=0x0300000e above-sibling=None(0x00000000) x=0 y=0 width=1918 height=1041 border-width=1 override-redirect=false(0x00)
0.089 000:<:0192: 20: Request(12): ConfigureWindow window=0x0300001a values={width=1918 height=1041}
0.091 000:>:0196: Event (generated) ConfigureNotify(22) event=0x0300000e window=0x0300000e above-sibling=None(0x00000000) x=0 y=37 width=1918 height=1041 border-width=1 override-redirect=false(0x00)
andreykaere commented 1 year ago

Hi! I got it working (display window on certain monitor)! Thank you a lot of useful explanation comments and links, they help a lot! However I do have one more question: when I call methods like randr_get_crtc_info or randr_get_output_info, I need to specify the second argument - timestamp. I am not really sure what's that for and what I should put there. I put 0 and it seems to be working, but not knowing what it actually means kind of bugs me a little. I hope you can explain it to me! Thank you again!

psychon commented 1 year ago

You first send a GetScreenResources request to get a list of CRTCs and outputs. Then you use that information to query individual information via GetCrtcInfo and GetOutputInfo. However, in between these two steps, something else could be reconfiguring the screens. How do you ensure that you get a consistent view of the "state of the world".

Timestamps!

The reply to GetScreenResources includes timestamp and config_timestamp. config_timestamp is the timestamp of the last change to the screen configuration. This timestamp can then be used for the config_timestamp argument to GetOutputInfo. When the screen configuration was changed at another time than the provided timestamp, the request will (the reply will set its status value to InvalidConfigTime) and you know that you should restart your queries (dunno if anything actually implements this). 0 is the special value "current time" (also available as x11rb::CURRENT_TIME) which basically means "I don't care".

I suggest to read https://gitlab.freedesktop.org/xorg/proto/xorgproto/-/blob/master/randrproto.txt. This likely explains it better than I can. From RRGetOutputInfo:

If 'config-timestamp' does not match the current configuration timestamp (as returned by RRGetScreenResources), 'status' is set to InvalidConfigTime and the remaining reply data is empty. Otherwise, 'status' is set to Success.

andreykaere commented 1 year ago

Okay, thank you very much! But how much one should care about matching timestamps and see if that's info actually new? Because I think it's not that often when configuration is changing (in terms of monitors), but checking that will add some boilerplate code into the codebase. Is there any function that will provide that functionality? Something like update_screen_resources?

psychon commented 1 year ago

Uhm... dunno. I also think that is quite unlikely to happen in practice. And since you can apparently just use 0 as a timestamp, at least you get some data...