We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Ampersand in window name return an error
### The issue:

## Qtile version
0.22.1

## Issue
Ampersands in window name return an error with the WindowTabs widget
```
Traceback (most recent call last):
  File "/usr/lib/python3.10/site-packages/libqtile/hook.py", line 404, in fire
    i(*args, **kwargs)
  File "/usr/lib/python3.10/site-packages/libqtile/widget/windowtabs.py", line 82, in update
    self.text = self.separator.join(names)
  File "/usr/lib/python3.10/site-packages/libqtile/widget/base.py", line 483, in text
    self.layout.text = self.formatted_text
  File "/usr/lib/python3.10/site-packages/libqtile/drawer.py", line 72, in text
    attrlist, value, accel_char = pangocffi.parse_markup(value)
  File "/usr/lib/python3.10/site-packages/libqtile/pangocffi.py", line 186, in parse_markup
    raise Exception("parse_markup() failed for %s" % value)
Exception: parse_markup() failed for b'<b>Search \xc2\xb7 & \xe2\x80\x94 Mozilla Firefox</b>'
```

The same goes for the Mpris2 widget
```
2023-01-07 17:07:22,656 ERROR libqtile loop.py:_handle_exception():L63 parse_markup() failed for b'Fireman & Dancer - Royal Republic'
NoneType: None
2023-01-07 17:07:22,656 ERROR libqtile loop.py:_handle_exception():L63 parse_markup() failed for b'Fireman & Dancer - Royal Republic'
NoneType: None
````

Found a similar issue [#1685](https://github.com/qtile/qtile/issues/1685) but for the WindowName widget

### Required:

- [X] I have searched past issues to see if this bug has already been reported.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `libqtile/widget/windowtabs.py`
Content:
```
1 # Copyright (c) 2012-2013 Craig Barnes
2 # Copyright (c) 2012 roger
3 # Copyright (c) 2012, 2014 Tycho Andersen
4 # Copyright (c) 2014 Sean Vig
5 # Copyright (c) 2014 Adi Sieker
6 #
7 # Permission is hereby granted, free of charge, to any person obtaining a copy
8 # of this software and associated documentation files (the "Software"), to deal
9 # in the Software without restriction, including without limitation the rights
10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 # copies of the Software, and to permit persons to whom the Software is
12 # furnished to do so, subject to the following conditions:
13 #
14 # The above copyright notice and this permission notice shall be included in
15 # all copies or substantial portions of the Software.
16 #
17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 # SOFTWARE.
24 
25 from libqtile import bar, hook
26 from libqtile.log_utils import logger
27 from libqtile.widget import base
28 
29 
30 class WindowTabs(base._TextBox):
31     """
32     Displays the name of each window in the current group.
33     Contrary to TaskList this is not an interactive widget.
34     The window that currently has focus is highlighted.
35     """
36 
37     defaults = [
38         ("separator", " | ", "Task separator text."),
39         ("selected", ("<b>", "</b>"), "Selected task indicator"),
40         (
41             "parse_text",
42             None,
43             "Function to parse and modify window names. "
44             "e.g. function in config that removes excess "
45             "strings from window name: "
46             "def my_func(text)"
47             '    for string in [" - Chromium", " - Firefox"]:'
48             '        text = text.replace(string, "")'
49             "   return text"
50             "then set option parse_text=my_func",
51         ),
52     ]
53 
54     def __init__(self, **config):
55         width = config.pop("width", bar.STRETCH)
56         base._TextBox.__init__(self, width=width, **config)
57         self.add_defaults(WindowTabs.defaults)
58         if not isinstance(self.selected, (tuple, list)):
59             self.selected = (self.selected, self.selected)
60 
61     def _configure(self, qtile, bar):
62         base._TextBox._configure(self, qtile, bar)
63         hook.subscribe.client_name_updated(self.update)
64         hook.subscribe.focus_change(self.update)
65         hook.subscribe.float_change(self.update)
66         self.add_callbacks({"Button1": self.bar.screen.group.next_window})
67 
68     def update(self, *args):
69         names = []
70         for w in self.bar.screen.group.windows:
71             state = ""
72             if w.maximized:
73                 state = "[] "
74             elif w.minimized:
75                 state = "_ "
76             elif w.floating:
77                 state = "V "
78             task = "%s%s" % (state, w.name if w and w.name else " ")
79             if w is self.bar.screen.group.current_window:
80                 task = task.join(self.selected)
81             names.append(task)
82         self.text = self.separator.join(names)
83         if callable(self.parse_text):
84             try:
85                 self.text = self.parse_text(self.text)
86             except:  # noqa: E722
87                 logger.exception("parse_text function failed:")
88         self.bar.draw()
89
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/libqtile/widget/windowtabs.py b/libqtile/widget/windowtabs.py
--- a/libqtile/widget/windowtabs.py
+++ b/libqtile/widget/windowtabs.py
@@ -22,7 +22,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
-from libqtile import bar, hook
+from libqtile import bar, hook, pangocffi
 from libqtile.log_utils import logger
 from libqtile.widget import base
 
@@ -76,6 +76,7 @@
             elif w.floating:
                 state = "V "
             task = "%s%s" % (state, w.name if w and w.name else " ")
+            task = pangocffi.markup_escape_text(task)
             if w is self.bar.screen.group.current_window:
                 task = task.join(self.selected)
             names.append(task)

{"golden_diff": "diff --git a/libqtile/widget/windowtabs.py b/libqtile/widget/windowtabs.py\n--- a/libqtile/widget/windowtabs.py\n+++ b/libqtile/widget/windowtabs.py\n@@ -22,7 +22,7 @@\n # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n # SOFTWARE.\n \n-from libqtile import bar, hook\n+from libqtile import bar, hook, pangocffi\n from libqtile.log_utils import logger\n from libqtile.widget import base\n \n@@ -76,6 +76,7 @@\n             elif w.floating:\n                 state = \"V \"\n             task = \"%s%s\" % (state, w.name if w and w.name else \" \")\n+            task = pangocffi.markup_escape_text(task)\n             if w is self.bar.screen.group.current_window:\n                 task = task.join(self.selected)\n             names.append(task)\n", "issue": "Ampersand in window name return an error\n### The issue:\n\n## Qtile version\r\n0.22.1\r\n\r\n## Issue\r\nAmpersands in window name return an error with the WindowTabs widget\r\n```\r\nTraceback (most recent call last):\r\n  File \"/usr/lib/python3.10/site-packages/libqtile/hook.py\", line 404, in fire\r\n    i(*args, **kwargs)\r\n  File \"/usr/lib/python3.10/site-packages/libqtile/widget/windowtabs.py\", line 82, in update\r\n    self.text = self.separator.join(names)\r\n  File \"/usr/lib/python3.10/site-packages/libqtile/widget/base.py\", line 483, in text\r\n    self.layout.text = self.formatted_text\r\n  File \"/usr/lib/python3.10/site-packages/libqtile/drawer.py\", line 72, in text\r\n    attrlist, value, accel_char = pangocffi.parse_markup(value)\r\n  File \"/usr/lib/python3.10/site-packages/libqtile/pangocffi.py\", line 186, in parse_markup\r\n    raise Exception(\"parse_markup() failed for %s\" % value)\r\nException: parse_markup() failed for b'<b>Search \\xc2\\xb7 & \\xe2\\x80\\x94 Mozilla Firefox</b>'\r\n```\r\n\r\nThe same goes for the Mpris2 widget\r\n```\r\n2023-01-07 17:07:22,656 ERROR libqtile loop.py:_handle_exception():L63 parse_markup() failed for b'Fireman & Dancer - Royal Republic'\r\nNoneType: None\r\n2023-01-07 17:07:22,656 ERROR libqtile loop.py:_handle_exception():L63 parse_markup() failed for b'Fireman & Dancer - Royal Republic'\r\nNoneType: None\r\n````\r\n\r\nFound a similar issue [#1685](https://github.com/qtile/qtile/issues/1685) but for the WindowName widget\n\n### Required:\n\n- [X] I have searched past issues to see if this bug has already been reported.\n", "before_files": [{"content": "# Copyright (c) 2012-2013 Craig Barnes\n# Copyright (c) 2012 roger\n# Copyright (c) 2012, 2014 Tycho Andersen\n# Copyright (c) 2014 Sean Vig\n# Copyright (c) 2014 Adi Sieker\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom libqtile import bar, hook\nfrom libqtile.log_utils import logger\nfrom libqtile.widget import base\n\n\nclass WindowTabs(base._TextBox):\n    \"\"\"\n    Displays the name of each window in the current group.\n    Contrary to TaskList this is not an interactive widget.\n    The window that currently has focus is highlighted.\n    \"\"\"\n\n    defaults = [\n        (\"separator\", \" | \", \"Task separator text.\"),\n        (\"selected\", (\"<b>\", \"</b>\"), \"Selected task indicator\"),\n        (\n            \"parse_text\",\n            None,\n            \"Function to parse and modify window names. \"\n            \"e.g. function in config that removes excess \"\n            \"strings from window name: \"\n            \"def my_func(text)\"\n            '    for string in [\" - Chromium\", \" - Firefox\"]:'\n            '        text = text.replace(string, \"\")'\n            \"   return text\"\n            \"then set option parse_text=my_func\",\n        ),\n    ]\n\n    def __init__(self, **config):\n        width = config.pop(\"width\", bar.STRETCH)\n        base._TextBox.__init__(self, width=width, **config)\n        self.add_defaults(WindowTabs.defaults)\n        if not isinstance(self.selected, (tuple, list)):\n            self.selected = (self.selected, self.selected)\n\n    def _configure(self, qtile, bar):\n        base._TextBox._configure(self, qtile, bar)\n        hook.subscribe.client_name_updated(self.update)\n        hook.subscribe.focus_change(self.update)\n        hook.subscribe.float_change(self.update)\n        self.add_callbacks({\"Button1\": self.bar.screen.group.next_window})\n\n    def update(self, *args):\n        names = []\n        for w in self.bar.screen.group.windows:\n            state = \"\"\n            if w.maximized:\n                state = \"[] \"\n            elif w.minimized:\n                state = \"_ \"\n            elif w.floating:\n                state = \"V \"\n            task = \"%s%s\" % (state, w.name if w and w.name else \" \")\n            if w is self.bar.screen.group.current_window:\n                task = task.join(self.selected)\n            names.append(task)\n        self.text = self.separator.join(names)\n        if callable(self.parse_text):\n            try:\n                self.text = self.parse_text(self.text)\n            except:  # noqa: E722\n                logger.exception(\"parse_text function failed:\")\n        self.bar.draw()\n", "path": "libqtile/widget/windowtabs.py"}], "after_files": [{"content": "# Copyright (c) 2012-2013 Craig Barnes\n# Copyright (c) 2012 roger\n# Copyright (c) 2012, 2014 Tycho Andersen\n# Copyright (c) 2014 Sean Vig\n# Copyright (c) 2014 Adi Sieker\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nfrom libqtile import bar, hook, pangocffi\nfrom libqtile.log_utils import logger\nfrom libqtile.widget import base\n\n\nclass WindowTabs(base._TextBox):\n    \"\"\"\n    Displays the name of each window in the current group.\n    Contrary to TaskList this is not an interactive widget.\n    The window that currently has focus is highlighted.\n    \"\"\"\n\n    defaults = [\n        (\"separator\", \" | \", \"Task separator text.\"),\n        (\"selected\", (\"<b>\", \"</b>\"), \"Selected task indicator\"),\n        (\n            \"parse_text\",\n            None,\n            \"Function to parse and modify window names. \"\n            \"e.g. function in config that removes excess \"\n            \"strings from window name: \"\n            \"def my_func(text)\"\n            '    for string in [\" - Chromium\", \" - Firefox\"]:'\n            '        text = text.replace(string, \"\")'\n            \"   return text\"\n            \"then set option parse_text=my_func\",\n        ),\n    ]\n\n    def __init__(self, **config):\n        width = config.pop(\"width\", bar.STRETCH)\n        base._TextBox.__init__(self, width=width, **config)\n        self.add_defaults(WindowTabs.defaults)\n        if not isinstance(self.selected, (tuple, list)):\n            self.selected = (self.selected, self.selected)\n\n    def _configure(self, qtile, bar):\n        base._TextBox._configure(self, qtile, bar)\n        hook.subscribe.client_name_updated(self.update)\n        hook.subscribe.focus_change(self.update)\n        hook.subscribe.float_change(self.update)\n        self.add_callbacks({\"Button1\": self.bar.screen.group.next_window})\n\n    def update(self, *args):\n        names = []\n        for w in self.bar.screen.group.windows:\n            state = \"\"\n            if w.maximized:\n                state = \"[] \"\n            elif w.minimized:\n                state = \"_ \"\n            elif w.floating:\n                state = \"V \"\n            task = \"%s%s\" % (state, w.name if w and w.name else \" \")\n            task = pangocffi.markup_escape_text(task)\n            if w is self.bar.screen.group.current_window:\n                task = task.join(self.selected)\n            names.append(task)\n        self.text = self.separator.join(names)\n        if callable(self.parse_text):\n            try:\n                self.text = self.parse_text(self.text)\n            except:  # noqa: E722\n                logger.exception(\"parse_text function failed:\")\n        self.bar.draw()\n", "path": "libqtile/widget/windowtabs.py"}]}