awesome icon indicating copy to clipboard operation
awesome copied to clipboard

How does screen:swap work?

Open mschloesser opened this issue 4 years ago • 13 comments

Hello,

is there any good documentation available on how screen:swap works? I couldn't find any that (be it official docs, blogs, Redit, stackoverflow, etc) could explain the the actual behavior I am experiencing.

I am on Ubuntu 20.04 running Awesome v4.3.

$ awesome -v
awesome v4.3 (Too long)
 • Compiled against Lua 5.3.3 (running with Lua 5.3)
 • D-Bus support: ✔
 • execinfo support: ✔
 • xcb-randr version: 1.6
 • LGI version: 0.9.2

I configured some keybindings to swap screens but it wouldn't do anything in that regard. My debug output led me to the conclusion that I was doing the right thing (i.e. computing the correct screen indices) but any call to screen[a]:swap(screen[b]) doesn't do anything.

I then managed to swap my screens with a statically defined call (e.g. screen[2]:swap(screen[1])). However there are two caveats here

  1. It only works when I call the code before the call to awful.screen.connect_for_each_screen. When I insert the same code after this code block it won't swap anything.
  2. When I want to swap the screens back one might expect to call something like screen[2]:swap(screen[1]) again or screen[1]:swap(screen[2]). However, I need to call screen[1]:swap(screen[1]) which, in my opinion, doesn't make any sense. If it does to anybody, I am more than willing to learn why

Long story short, how does swapping screens work?

If this is the wrong place for my question (at least there's a question label) I apologize.

Thanks for your hard work to make Awesome ... well ... exactly that!

mschloesser avatar Mar 03 '21 15:03 mschloesser

Looking at the screen:swap function (in screen.c) the screens, which are stored in an array, are switched in their index. Meaning that screen[1]:swap(screen[2])) would cause "screen[1]" to be in index two, and "screen[2]" to be in index 1.

So it shouldn't matter what indices you use, as long as they are the same ones from the call before (e.g. calling screen[1]:swap(screen[2]) twice would swap between the screens twice, thus returning them to the original layout).

If you have screens of different size you can see that they indeed change their index using this code

    awful.key({ modkey,   "Shift" }, "s", function ()
				local s1, s2 = nil, nil

				print("Swapping screens")

				for s in screen do
					if not s1 then
						s1 = s
					elseif not s2 then
						s2 = s
					else
						break
					end
				end

				if not s1 or not s2 then
					print("Didn't find two screens to swap")
				end

				print("Swapping between", s1.name, "with geo", s1.geometry.width .. "x" .. s1.geometry.height)
				print("and", s2.name, "with geo", s2.geometry.width .. "x" .. s2.geometry.height)

				s1:swap(s2)

			  end,
              {description = "Swap between screens", group = "nahh"}),

Why don't the screen's content is swapped as well ? hmm I'm not sure. After the screen indices are swapped the list signal is emitted causing layout.arrange in lib/awful/layout/init.lua to run. I'm still trying to understand what this function does (will update).

There might be something broken here (maybe might be the expected behavior)

ShayAgros avatar Mar 04 '21 07:03 ShayAgros

Hi, thanks for your quick reply. I had a look at screen.c too and came to the same conclusion regarding the index swap (of course I couldn't fully understand what, e.g. all the luaA_* calls do in detail).

I implemented several versions of my swap binding, the current one looks like this:

function (c) 
    local focused_screen = awful.screen.focused()
    local s = focused_screen.get_next_in_direction(focused_screen, "right")
    if s then
        naughty.notify({ 
            preset = naughty.config.presets.critical,
            title = "Debug",
            text = "Target screen: " .. tostring(s.index) .. "\n" .. "Focused screen: " .. focused_screen.index,
            screen=focused_screen}
        )
        screen[focused_screen.index]:swap(screen[s.index])
    end
end

It looks a bit dirty because I was playing around and testing so much that I wasn't in the mood...

However, when I observe the notification the indices indeed are swapped. Also, I tested the swapped signal which is also emitted.

It's a bit of a puzzle :) As I described earlier, I can swap the screens as long as I define it statically before the call to awful.screen.connect_for_each_screen. Once that is called, swapping doesn't work anymore.

After the screen indices are swapped the list signal is emitted causing layout.arrange in lib/awful/layout/init.lua to run.

Okay thanks for the hint, maybe I can do a workaround by emitting the list signal or calling layout.arrange by hand.

Maybe it's a bug in the version provided by Ubuntu 20.04.

mschloesser avatar Mar 04 '21 08:03 mschloesser

The screen:swap function emits another signal called swapped to which you can connect and do the swapping yourself. See #1382 code snippet at the end, it switches all clients from one screen to another.

You could implement the same in for swapped signal as well. Something like this

screen.connect_signal("request::desktop_decoration", function(s)

	s:connect_signal("swapped", function(self, other_s, was_op_on_self)

		-- swapped is called twice for each screen as 'self'. If
		-- 'was_op_on_self' is true that 'self' was the screen on the left
		-- side of the expression s1:swap(s2)
		if not was_op_on_self then
			return
		end

		for _, t in ipairs(self.tags) do
			local fallback_tag = awful.tag.find_by_name(other_s, t.name)
			local self_clients = t:clients()
			local other_clients

			-- If we don't have a tag by the same name, we'll just "throw" the
			-- client to the first tag on the other screen
			if not fallback_tag then
				fallback_tag = other_s.tags[1]
				other_clients = {}
			else
				other_clients = fallback_tag:clients()
			end


			for _, c in ipairs(self_clients) do
				c:move_to_tag(fallback_tag)
			end

			for _, c in ipairs(other_clients) do
				c:move_to_tag(t)
			end
		end
	end)
end)

This is a very dirty solution and is not very robust imo in case the screens are of different dimensions (seem to work if the client is maximized when the swapping happens).

The fact that it works when doing it statically before screen arrange tells that awesome doesn't "re-arrange" the clients in their new screen after the swapping. Again, I'm not sure what is the correct behavior. I'll update as soon as I find out what was expected to happen.

This is unlikely imo that it is a distribution related bug. I did the testing myself on Ubuntu 18.04 and it has the same issue. I'll test the behavior on Arch as well later (though I doubt it would change anything)

ShayAgros avatar Mar 04 '21 08:03 ShayAgros

After the screen indices are swapped the list signal is emitted causing layout.arrange in lib/awful/layout/init.lua to run. I'm still trying to understand what this function does (will update).

Layout.arrange is basically the function that places clients on the screen depending on the selected client layout (tile, spirale,...).

I started to write documentation about this a while back... It's not finished but you can maybe find some information there https://github.com/awesomeWM/awesome/pull/2944

Sorry for not helping more here. I sadly never used Awesome with a multiscreen setup so I'm not aware of the intended behavior...

@ShayAgros Please be careful in your digging, tho... The screen API was refactored by elv so the current base code can be different from what there is on 4.3 stable...

Aire-One avatar Mar 04 '21 08:03 Aire-One

@Aire-One yup sorry about that, forgot that I'm using development branch on my setup. tested the code on stable version 4.3 and it works with the exception that the snippet should be put inside awful.screen.connect_for_each_screen(function(s) rather than inside screen.connect_signal("request::desktop_decoration", function(s)

@mschloesser did more digging into this function. Seems like list doesn't move the clients into a new screen, and just updates their geometry in the screen (which looks redundant since their screen doesn't change). After going to the commit message that added this function I found that it leads to issue #1122. To sum it up, many functions in awesome "assume" that the screen in index 1 is left/below the screen in index 2 which is left/below the screen in index 3 (this seems more intuitive this way), however the indices are chosen based on their discovery order by Xrandr (and the indices can change when any randr event happens like screen (dis)connection or layout change). To allow the user to make the screen indices the way he/she perceives right, this screen:swap function was introduced.

After using swap to set the screen indices the order you perceive is right, some keybinding like (from default awesome config)

    awful.key({ modkey, "Control" }, "j", function () awful.screen.focus_relative( 1) end,
              {description = "focus the next screen", group = "screen"}),
    awful.key({ modkey, "Control" }, "k", function () awful.screen.focus_relative(-1) end,
              {description = "focus the previous screen", group = "screen"}),

would change focus in a logical manner.

This is all only relevant for setups with 3+ screens though.

How did it actually change the screens when using swap at the beginning of the config file ? Well, idk. Functions like awful.screen.connect_for_each_screen(function(s) or screen.connect_signal("request::desktop_decoration", function(s)

are probably called after awesome finishes scanning all screens, so running functions like swap before these functions seems risky. The screen's coordination (x, y and width, height) are received by randr library (X system if you will) and none of it changes by calling swap, so it'd be strange if clients change their location "automagically" simply because their screens swapped indices in some array.

If you only want the clients to change screens (and don't intend to use more than two screens) you can use the code snippet I wrote (Hope this answers your question in this issue)

side note: I'm currently going over Elv13's fake screens patchset (#2790) which might (I hope at least) allow for easier management of screens (at least a more elegant way to switch a screen's position). This might take time though

ShayAgros avatar Mar 05 '21 15:03 ShayAgros

Hi, thanks for your support. I really appreciate it!

I did some more testing myself and, at least, could boil down the method call where swap seems to stop working: awful.tag (so it's not awful.screen.connect_for_each_screen). I also replaced awful.screen.connect_for_each_screen with for s in screens and removed the for loop all together, just to make sure. Also, disabled all widgets, wibar, etc. So in the end there was just as single tag definition left.

All calls before awful.tag work as I would expect it (screens swap, including clients). After calling awful.tag they won't. I also removed awful.tag completely which turned out not to be the best idea :) Without defining at least a single tag, Awesome cannot display clients at all. In case I am wrong, please let me know so I can dig deeper. My assumption was that without any manually defined tags there would be something like an implicit or a "fallback" tag that manages the clients.

mschloesser avatar Mar 08 '21 07:03 mschloesser

In case anyone comes across this in the future, this is my implementation:

awful.screen.connect_for_each_screen(function(s)
  -- [...]
  s:connect_signal("swapped", function(self, other, is_source)
    if not is_source then return end

    local st = self.selected_tag
    local sc = st:clients() -- NOTE: this is only here for convinience
    local ot = other.selected_tag
    local oc = ot:clients() -- but this HAS to be saved in a variable because we modify the client list in the process of swapping

    for _, c in ipairs(sc) do
      c:move_to_tag(ot)
    end

    for _, c in ipairs(oc) do
      c:move_to_tag(st)
    end
  end)
  -- [...]
end)

This swaps the currently active tag with the visible tag on the other monitor.

The "other monitor" is specified by your keybind:

awful.key({ modkey, "Control" }, "o",	function ()
    local focused_screen = awful.screen.focused()
    local s = focused_screen.get_next_in_direction(focused_screen, "right")

    -- FIXME: this only makes sense for two screens
    if not s then
      s = focused_screen.get_next_in_direction(focused_screen, "left")
    end

    if not s then 
      naughty.notify { preset = naughty.config.presets.critical, title = "could not get other screen" }
      return
    end
    focused_screen:swap(s)

    end,
    {description = "swap screens", group = "layout"})
)

Hope this helps :)

B4rc1 avatar Jan 29 '22 01:01 B4rc1

@B4rc1 If you just wanna swap the current tag, you can use something like:

local next_screen_tag = awful.screen.focused():get_next_in_direction("right").selected_tag
awful.screen.focused().selected_tag:swap(next_screen_tag)
awful.screen.focus_relative(1)

Just FYI, for anyone wanting to rotate the different screens they have got you can use something like this: direction = -1 for right and 1 for left

function rotate_screens(direction)
    local current_screen = awful.screen.focused()
    local initial_scren = current_screen
    while (true) do
        awful.screen.focus_relative(direction)
        local next_screen = awful.screen.focused()
        if next_screen == initial_scren then
            return
        end

        local current_screen_tag_name = current_screen.selected_tag.name
        local next_screen_tag_name = next_screen.selected_tag.name

        for _, t in ipairs(current_screen.tags) do
            local fallback_tag = awful.tag.find_by_name(next_screen, t.name)
            local self_clients = t:clients()
            local other_clients

            if not fallback_tag then
                -- if not available, use first tag
                fallback_tag = next_screen.tags[1]
                other_clients = {}
            else
                other_clients = fallback_tag:clients()
            end

            for _, c in ipairs(self_clients) do
                c:move_to_tag(fallback_tag)
            end

            for _, c in ipairs(other_clients) do
                c:move_to_tag(t)
            end
        end
        awful.tag.find_by_name(next_screen, current_screen_tag_name):view_only()
        awful.tag.find_by_name(current_screen, next_screen_tag_name):view_only()
        current_screen = next_screen
    end
end

meain avatar Feb 01 '22 12:02 meain

@meain Please share how to integrate rotate_screens function into rc.lua. It'd be great if it can be done with a keybinding as well. I don't know lua. I tried it but couldn't figure it out.

sathishmanohar avatar Jun 11 '23 20:06 sathishmanohar

@sathishmanohar I don't use awesome anymore, but here is what I had in my config. https://github.com/meain/dotfiles/blob/fa63ed2931687bcafb50b229f90b878b8c0380d4/awesome/.config/awesome/rc.lua#L901-L916

meain avatar Jun 13 '23 11:06 meain

@meain Works perfectly! Exactly what I wanted. Thanks!

sathishmanohar avatar Jun 14 '23 05:06 sathishmanohar