Repository: qtile/qtile

Issue:
External bar creating huge gap
<!--
Please do not ask general questions here! There are [community
contact](https://github.com/qtile/qtile#community) options for that.
-->

## What is happening

As soon as I initialize polybar on my main screen a huge gap appears between the bar and the applications.

![Main Screen](https://user-images.githubusercontent.com/46654987/97813365-f61a1800-1c87-11eb-96cc-b4917b781b0b.jpg)



I use two monitors one of which is rotated. The issue only appeared on the main monitor up to now affecting one bar at a time. However both, top and bottom, have shown this effect.
Disabling the second monitor or just rotating it to a normal position seemed to makes the gap disappear the times I tried it. Also worth noting: the builtin bar works as expected. 


## Qtile-Version
0.16.1

## Stack Trace
qtile.log:
>2020-11-01 21:46:11,538 ERROR libqtile xcore.py:_xpoll():L277 Got an exception in poll loop
>Traceback (most recent call last):
>  File "/usr/lib/python3.8/site-packages/libqtile/backend/x11/xcore.py", line 230, in _xpoll  
>     event = self.conn.conn.poll_for_event()
>  File "/usr/lib/python3.8/site-packages/xcffib/__init__.py", line 571, in wrapper
>    return f(*args)
>  File "/usr/lib/python3.8/site-packages/xcffib/__init__.py", line 612, in poll_for_event
>    return self.hoist_event(e)
>  File "/usr/lib/python3.8/site-packages/xcffib/__init__.py", line 685, in hoist_event
>    return self._process_error(ffi.cast("xcb_generic_error_t *", e))
>  File "/usr/lib/python3.8/site-packages/xcffib/__init__.py", line 648, in _process_error
>    raise error(buf)
>xcffib.xproto.ValueError


.xsession-errors:
>RuntimeError: Cannot call write() after write_eof()
>Task exception was never retrieved
>future: <Task finished name='Task-5' coro=<Server._server_callback() done, defined at /usr/lib/python3.8/site-packages/libqtile/ipc.py:185> exception=RuntimeError('Cannot call write() after write_eof()')
>Traceback (most recent call last):
>  File "/usr/lib/python3.8/site-packages/libqtile/ipc.py", line 212, in _server_callback
>    writer.write(result)
>  File "/usr/lib/python3.8/asyncio/streams.py", line 341, in write
>    self._transport.write(data)
>  File "/usr/lib/python3.8/asyncio/selector_events.py", line 893, in write
>    raise RuntimeError('Cannot call write() after write_eof()')
>RuntimeError: Cannot call write() after write_eof()

<!--
Please attach any stack traces found in:

* `~/.xsession-errors`
* `~/.local/share/qtile/qtile.log`
-->






Discussion:
Seems like something is probably off in our struts math. Can you show the xprop of the root window and your external bar?
Here are the outputs of xprop

for the root window:
> WM_STATE(WM_STATE):
> 		window state: Normal
> 		icon window: 0x0
> WM_PROTOCOLS(ATOM): protocols  WM_DELETE_WINDOW, _NET_WM_PING
> _NET_WM_ICON(CARDINAL) = 
> WM_NORMAL_HINTS(WM_SIZE_HINTS):
> 		program specified size: 800 by 600
> _NET_WM_WINDOW_TYPE(ATOM) = _NET_WM_WINDOW_TYPE_NORMAL
> WM_CLIENT_MACHINE(STRING) = "uname"
> _NET_WM_PID(CARDINAL) = 215151
> WM_CLASS(STRING) = "Alacritty", "Alacritty"
> XdndAware(ATOM) = BITMAP
> _MOTIF_WM_HINTS(_MOTIF_WM_HINTS) = 0x2, 0x0, 0x1, 0x0, 0x0
> _NET_WM_NAME(UTF8_STRING) = "fish /home/uname"
> WM_NAME(STRING) = "fish /home/uname"

for the upper bar (which in this case was the one with gap):

> WM_STATE(WM_STATE):
> 		window state: Normal
> 		icon window: 0x0
> _NET_WM_PID(CARDINAL) = 218263
> _NET_WM_DESKTOP(CARDINAL) = 4294967295
> _NET_WM_STATE(ATOM) = _NET_WM_STATE_STICKY, _NET_WM_STATE_ABOVE
> _NET_WM_WINDOW_TYPE(ATOM) = _NET_WM_WINDOW_TYPE_DOCK
> WM_CLASS(STRING) = "polybar", "Polybar"
> WM_NAME(STRING) = "polybar-qtile_DP-0"
> _NET_WM_STRUT_PARTIAL(CARDINAL) = 0, 0, 495, 0, 0, 0, 0, 0, 1080, 3639, 0, 0
> _NET_WM_STRUT(CARDINAL) = 0, 0, 495, 0

and for the bottom one:

> WM_STATE(WM_STATE):
> 		window state: Normal
> 		icon window: 0x0
> _NET_WM_PID(CARDINAL) = 218341
> _NET_WM_DESKTOP(CARDINAL) = 4294967295
> _NET_WM_STATE(ATOM) = _NET_WM_STATE_STICKY, _NET_WM_STATE_ABOVE
> _NET_WM_WINDOW_TYPE(ATOM) = _NET_WM_WINDOW_TYPE_DOCK
> WM_CLASS(STRING) = "polybar", "Polybar"
> WM_NAME(STRING) = "polybar-bottom_DP-0"
> _NET_WM_STRUT_PARTIAL(CARDINAL) = 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 1080, 3639
> _NET_WM_STRUT(CARDINAL) = 0, 0, 0, 15
> WM_HINTS(WM_HINTS):
> _NET_WM_DESKTOP(CARDINAL) = 3
> 



You appear to be correct about something being off with the strut values. The upper bar shows a top strut value of 495 which correlates perfectly with the size of the gap. Where could such a huge value come from?
Yeah, looks like qtile is doing the right thing and the bar is requesting a huge gap.

On Mon, Nov 16, 2020, at 4:13 PM, SpringerJack wrote:
> 

> Here are the outputs of xprop

> for the root window:


>> WM_STATE(WM_STATE):
>> window state: Normal
>> icon window: 0x0
>> WM_PROTOCOLS(ATOM): protocols WM_DELETE_WINDOW, _NET_WM_PING
>> _NET_WM_ICON(CARDINAL) = �
>> WM_NORMAL_HINTS(WM_SIZE_HINTS):
>> program specified size: 800 by 600
>> _NET_WM_WINDOW_TYPE(ATOM) = _NET_WM_WINDOW_TYPE_NORMAL
>> WM_CLIENT_MACHINE(STRING) = "uname"
>> _NET_WM_PID(CARDINAL) = 215151
>> WM_CLASS(STRING) = "Alacritty", "Alacritty"
>> XdndAware(ATOM) = BITMAP
>> _MOTIF_WM_HINTS(_MOTIF_WM_HINTS) = 0x2, 0x0, 0x1, 0x0, 0x0
>> _NET_WM_NAME(UTF8_STRING) = "fish /home/uname"
>> WM_NAME(STRING) = "fish /home/uname"

> for the upper bar (which in this case was the one with gap):


>> WM_STATE(WM_STATE):
>> window state: Normal
>> icon window: 0x0
>> _NET_WM_PID(CARDINAL) = 218263
>> _NET_WM_DESKTOP(CARDINAL) = 4294967295
>> _NET_WM_STATE(ATOM) = _NET_WM_STATE_STICKY, _NET_WM_STATE_ABOVE
>> _NET_WM_WINDOW_TYPE(ATOM) = _NET_WM_WINDOW_TYPE_DOCK
>> WM_CLASS(STRING) = "polybar", "Polybar"
>> WM_NAME(STRING) = "polybar-qtile_DP-0"
>> _NET_WM_STRUT_PARTIAL(CARDINAL) = 0, 0, 495, 0, 0, 0, 0, 0, 1080, 3639, 0, 0
>> _NET_WM_STRUT(CARDINAL) = 0, 0, 495, 0

> and for the bottom one:


>> WM_STATE(WM_STATE):
>> window state: Normal
>> icon window: 0x0
>> _NET_WM_PID(CARDINAL) = 218341
>> _NET_WM_DESKTOP(CARDINAL) = 4294967295
>> _NET_WM_STATE(ATOM) = _NET_WM_STATE_STICKY, _NET_WM_STATE_ABOVE
>> _NET_WM_WINDOW_TYPE(ATOM) = _NET_WM_WINDOW_TYPE_DOCK
>> WM_CLASS(STRING) = "polybar", "Polybar"
>> WM_NAME(STRING) = "polybar-bottom_DP-0"
>> _NET_WM_STRUT_PARTIAL(CARDINAL) = 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 1080, 3639
>> _NET_WM_STRUT(CARDINAL) = 0, 0, 0, 15
>> WM_HINTS(WM_HINTS):
>> _NET_WM_DESKTOP(CARDINAL) = 3

> You appear to be correct about something being off with the strut values. The upper bar shows a top STRUT value of 495 which correlates perfectly with the size of the gap. Where could such a huge value come from?


> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub <https://github.com/qtile/qtile/issues/1947#issuecomment-728332541>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AAF7VVYQ7YTYBV7PNUPJWEDSQGIWPANCNFSM4TGX5PHA>.

Okay, I had a look at the numbers again and to me it seems like polybar is requesting correct values. My monitor layout has one vertical and one horizontal, creating a blank space above the affected monitor of 480. Adding the height of the bar gives exactly the output 495. Might this issue arise from polybar requesting a value relative to the xscreens borders but qtile handling it as relative to the physical screens borders?


This would not explain why I observed this with the bottom bar but I haven't been able to recreate that one since so this seems likely to have had other reasons anyway and I'll drop it for now. 
So reading https://specifications.freedesktop.org/wm-spec/wm-spec-1.3.html#idm46165187053600 it doesn't say anything about screen offsets vs physical screens IIUC. The math happens in `add_strut()`, so if you can come up with a fix that works for you that doesn't break anyone else, I'm happy to merge it.
Quoting from that page:

> Assume that the set up uses two monitors, one running at 1280x1024 and the other to the right running at 1024x768, with the top edge of the two physical displays aligned. If the panel wants to fill the entire bottom edge of the smaller display with a panel 50 pixels tall, it should set a bottom strut of 306, with bottom_start_x of 1280, and bottom_end_x of 2303. Note that the strut is relative to the screen edge, and not the edge of the xinerama monitor. 

I feel like that is fairly similar to my situation and to me it sounds like exactly what I guessed. I just tested on my laptop which  suffered from the same issue and the numbers work out perfectly for that either so I am getting rather convinced that having empty space on the X screen is the cause of this. I'll be digging into the code and maybe I can come up with a way. Might take a while though. Is the mailing list from the README a good place to ask about the issues on the way?
Ah ha, yes, I didn't read that far. So I think you're exactly right, the struts math is wrong. Yep, mailing list is fine, but more people read IRC, so you might have better luck there.

Resolving patch:
diff --git a/libqtile/backend/x11/xcbq.py b/libqtile/backend/x11/xcbq.py
--- a/libqtile/backend/x11/xcbq.py
+++ b/libqtile/backend/x11/xcbq.py
@@ -201,7 +201,6 @@ def rdict(d):
     '_NET_WM_DESKTOP',
     '_NET_WM_WINDOW_TYPE',
     '_NET_WM_STATE',
-    '_NET_WM_STRUT',
     '_NET_WM_STRUT_PARTIAL',
     '_NET_WM_PID',
 ]
diff --git a/libqtile/core/manager.py b/libqtile/core/manager.py
--- a/libqtile/core/manager.py
+++ b/libqtile/core/manager.py
@@ -527,25 +527,24 @@ def current_group(self):
     def current_window(self):
         return self.current_screen.group.current_window
 
-    def add_strut(self, strut):
+    def reserve_space(self, reserved_space, screen):
         from libqtile.bar import Bar, Gap
 
         for i, pos in enumerate(["left", "right", "top", "bottom"]):
-            if strut[i]:
-                bar = getattr(self.current_screen, pos)
+            if reserved_space[i]:
+                bar = getattr(screen, pos)
                 if isinstance(bar, Bar):
-                    bar.adjust_for_strut(strut[i])
+                    bar.adjust_for_strut(reserved_space[i])
                 elif isinstance(bar, Gap):
-                    bar.size += strut[i]
+                    bar.size += reserved_space[i]
                     if bar.size <= 0:
-                        setattr(self.current_screen, pos, None)
+                        setattr(screen, pos, None)
                 else:
-                    setattr(self.current_screen, pos, Gap(strut[i]))
+                    setattr(screen, pos, Gap(reserved_space[i]))
+        screen.resize()
 
-        self.current_screen.resize()
-
-    def remove_strut(self, strut):
-        self.add_strut([-i for i in strut])
+    def free_reserved_space(self, reserved_space, screen):
+        self.reserve_space([-i for i in reserved_space], screen)
 
     def map_window(self, window: xcbq.Window) -> None:
         c = self.manage(window)
@@ -589,7 +588,7 @@ def manage(self, w):
                 except (xcffib.xproto.WindowError, xcffib.xproto.AccessError):
                     return
 
-                if w.get_wm_type() == "dock" or c.strut:
+                if w.get_wm_type() == "dock" or c.reserved_space:
                     c.cmd_static(self.current_screen.index)
                     return
 
@@ -613,8 +612,8 @@ def unmanage(self, win):
         c = self.windows_map.get(win)
         if c:
             hook.fire("client_killed", c)
-            if c.strut:
-                self.remove_strut(c.strut)
+            if isinstance(c, window.Static):
+                self.free_reserved_space(c.reserved_space, c.screen)
             if getattr(c, "group", None):
                 c.group.remove(c)
             del self.windows_map[win]
diff --git a/libqtile/window.py b/libqtile/window.py
--- a/libqtile/window.py
+++ b/libqtile/window.py
@@ -173,7 +173,7 @@ def __init__(self, window, qtile):
         self.borderwidth = 0
         self.bordercolor = None
         self.name = "<no name>"
-        self.strut = None
+        self.reserved_space = None
         self.state = NormalState
         self._float_state = NOT_FLOATING
         self._demands_attention = False
@@ -727,17 +727,45 @@ def update_strut(self):
             "_NET_WM_STRUT_PARTIAL",
             unpack=int
         )
-        strut = strut or self.window.get_property(
-            "_NET_WM_STRUT",
-            unpack=int
-        )
         if strut:
-            self.qtile.add_strut(strut)
-        self.strut = strut
+            x_screen_dimensions = self.qtile.root.get_geometry()
+            if strut[0]:    # left
+                x = strut[0]
+                y = (strut[4] + strut[5]) / 2 or (strut[6] + strut[7]) / 2
+            elif strut[1]:  # right
+                x = x_screen_dimensions.width - strut[1]
+                y = (strut[4] + strut[5]) / 2 or (strut[6] + strut[7]) / 2
+            elif strut[2]:  # top
+                x = (strut[8] + strut[9]) / 2 or (strut[10] + strut[11]) / 2
+                y = strut[2]
+            else:           # bottom
+                x = (strut[8] + strut[9]) / 2 or (strut[10] + strut[11]) / 2
+                y = x_screen_dimensions.height - strut[3]
+            self.screen = self.qtile.find_screen(x, y)
+
+            if self.screen is None:
+                logger.error("No screen at target")
+                return
+            elif None in [self.screen.x, self.screen.y, self.screen.height, self.screen.width]:
+                logger.error("Missing screen information")
+                return
+
+            empty_space = [
+                self.screen.x,
+                x_screen_dimensions.width - self.screen.x - self.screen.width,
+                self.screen.y,
+                x_screen_dimensions.height - self.screen.y - self.screen.height
+            ]
+            self.reserved_space = [strut[i] - empty if strut[i] else 0 for i, empty in enumerate(empty_space)]
+
+            self.qtile.reserve_space(self.reserved_space, self.screen)
+
+        else:
+            self.reserved_space = None
 
     def handle_PropertyNotify(self, e):  # noqa: N802
         name = self.qtile.conn.atoms.get_name(e.atom)
-        if name in ("_NET_WM_STRUT_PARTIAL", "_NET_WM_STRUT"):
+        if name == "_NET_WM_STRUT_PARTIAL":
             self.update_strut()
 
     def __repr__(self):


Test patch:
diff --git a/test/test_manager.py b/test/test_manager.py
--- a/test/test_manager.py
+++ b/test/test_manager.py
@@ -1469,17 +1469,33 @@ def no_input_hint():
 
 @manager_config
 def test_strut_handling(manager):
-    w = None
+    w = []
     conn = xcbq.Connection(manager.display)
 
     def has_struts():
         nonlocal w
-        w = conn.create_window(0, 0, 10, 10)
-        w.set_property("_NET_WM_STRUT", [0, 0, 0, 10])
-        w.map()
+        w.append(conn.create_window(0, 0, 10, 10))
+        w[-1].set_property("_NET_WM_STRUT_PARTIAL", [0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 800])
+        w[-1].map()
+        conn.conn.flush()
+
+    def with_gaps_left():
+        nonlocal w
+        w.append(conn.create_window(800, 0, 10, 10))
+        w[-1].set_property("_NET_WM_STRUT_PARTIAL", [820, 0, 0, 0, 0, 480, 0, 0, 0, 0, 0, 0])
+        w[-1].map()
+        conn.conn.flush()
+
+    def with_gaps_bottom():
+        nonlocal w
+        w.append(conn.create_window(800, 0, 10, 10))
+        w[-1].set_property("_NET_WM_STRUT_PARTIAL", [0, 0, 0, 130, 0, 0, 0, 0, 0, 0, 800, 1440])
+        w[-1].map()
         conn.conn.flush()
 
     def test_initial_state():
+        while manager.c.screen.info()["index"] != 0:
+            manager.c.next_screen()
         assert manager.c.window.info()['width'] == 798
         assert manager.c.window.info()['height'] == 578
         assert manager.c.window.info()['x'] == 0
@@ -1488,13 +1504,22 @@ def test_initial_state():
         bar = manager.c.window[bar_id].info()
         assert bar["height"] == 20
         assert bar["y"] == 580
+        manager.c.next_screen()
+        assert manager.c.window.info()['width'] == 638
+        assert manager.c.window.info()['height'] == 478
+        assert manager.c.window.info()['x'] == 800
+        assert manager.c.window.info()['y'] == 0
 
+    manager.test_xcalc()
+    manager.c.next_screen()
     manager.test_xcalc()
     test_initial_state()
 
     try:
+        while manager.c.screen.info()["index"] != 0:
+            manager.c.next_screen()
         manager.create_window(has_struts)
-        manager.c.window.static(0, None, None, None, None)
+        manager.c.window.static(None, None, None, None, None)
         assert manager.c.window.info()['width'] == 798
         assert manager.c.window.info()['height'] == 568
         assert manager.c.window.info()['x'] == 0
@@ -1504,8 +1529,18 @@ def test_initial_state():
         assert bar["height"] == 20
         assert bar["y"] == 570
 
+        manager.c.next_screen()
+        manager.create_window(with_gaps_bottom)
+        manager.c.window.static(None, None, None, None, None)
+        manager.create_window(with_gaps_left)
+        manager.c.window.static(None, None, None, None, None)
+        assert manager.c.window.info()['width'] == 618
+        assert manager.c.window.info()['height'] == 468
+        assert manager.c.window.info()['x'] == 820
+        assert manager.c.window.info()['y'] == 0
     finally:
-        w.kill_client()
+        for window in w:
+            window.kill_client()
         conn.finalize()
 
     test_initial_state()