Repository: qtile/qtile

Issue:
Wlan widget crashes if ESSID contains markup
### Issue description

The `Wlan` widget crashes if the ESSID contains characters considered as markup by `pango`, e.g. "A&B"

It's easy to reproduce by running
```
>>> from libqtile.pangocffi import parse_markup
>>> parse_markup("A&B")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "[...]/lib/python3.12/site-packages/libqtile/pangocffi.py", line 182, in parse_markup
    raise Exception("parse_markup() failed for %s" % value)
```

I'm happy to provide a pull request if you can confirm this as an actual bug. My suggested approach would be to use `libqtile.pangocffi.markup_escape_text` in `Wlan.poll()` immediately before calling `self.format.format`.

https://github.com/qtile/qtile/blob/ff90e5cfae7418ad62d0accc3cb38757fca2cdb9/libqtile/widget/wlan.py#L102


### Version

0.26.0

### Backend

X11 (default)

### Config

_No response_

### Logs

```text
2024-08-01 09:19:11,384 ERROR libqtile bar.py:_configure_widget():L388 Wlan widget crashed during _configure with error:
Traceback (most recent call last):
  File "/opt/qtile/venv/lib/python3.12/site-packages/libqtile/bar.py", line 379, in _configure_widget
    widget._configure(self.qtile, self)
  File "/opt/qtile/venv/lib/python3.12/site-packages/libqtile/widget/base.py", line 800, in _configure
    _TextBox._configure(self, qtile, bar)
  File "/opt/qtile/venv/lib/python3.12/site-packages/libqtile/widget/base.py", line 573, in _configure
    self.layout = self.drawer.textlayout(
                  ^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/qtile/venv/lib/python3.12/site-packages/libqtile/backend/base/drawer.py", line 322, in textlayout
    textlayout = TextLayout(
                 ^^^^^^^^^^^
  File "/opt/qtile/venv/lib/python3.12/site-packages/libqtile/backend/base/drawer.py", line 393, in __init__
    self.text = text
    ^^^^^^^^^
  File "/opt/qtile/venv/lib/python3.12/site-packages/libqtile/backend/base/drawer.py", line 412, in text
    attrlist, value, accel_char = pangocffi.parse_markup(value)
                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/qtile/venv/lib/python3.12/site-packages/libqtile/pangocffi.py", line 182, in parse_markup
    raise Exception("parse_markup() failed for %s" % value)
Exception: parse_markup() failed for b'A&V'
```


### Required

- [X] I have searched past issues to see if this bug has already been reported, and it hasn't been.
- [X] I understand that people give their precious time for free, and thus I've done my very best to make this problem as easy as possible to investigate.


Discussion:
I think I would call `libqtile.pangocffi.markup_escape_text` on the result of `self.format.format`, but maybe that's what you meant.
Actually, I thought to only escape the ESSID. Wouldn't escaping the result of `self.format.format` prevent any markup in the format string? To my understanding, this would render the `markup` flag in the widget config useless.
Yes. We should escape text before it's formatted.
I see, I thought the `markup` flag was mainly for the `fmt` parameter. I looked at the [cmus widget](https://github.com/qtile/qtile/blob/master/libqtile/widget/cmus.py#L215) where everything is escaped.
If we escape text that includes pango markup then the markup won't work.
@andreashug will you submit a PR for this?
Yes, sorry about the delay.

I'm not so happy with monkeypatching `MockIwlib` in the test, but making it configurable would have required to refactor the other tests. Do you have a better idea?



Patch:
diff --git a/libqtile/widget/wlan.py b/libqtile/widget/wlan.py
index ab7c54349e..e0f0099a30 100644
--- a/libqtile/widget/wlan.py
+++ b/libqtile/widget/wlan.py
@@ -29,6 +29,7 @@
 import iwlib
 
 from libqtile.log_utils import logger
+from libqtile.pangocffi import markup_escape_text
 from libqtile.widget import base
 
 
@@ -99,7 +100,9 @@ def poll(self):
                         return self.disconnected_message
                 else:
                     return self.disconnected_message
-            return self.format.format(essid=essid, quality=quality, percent=(quality / 70))
+            return self.format.format(
+                essid=markup_escape_text(essid), quality=quality, percent=(quality / 70)
+            )
         except EnvironmentError:
             logger.error(
                 "Probably your wlan device is switched off or "


Test patch:
diff --git a/test/widgets/test_wlan.py b/test/widgets/test_wlan.py
index 334a9c8d92..0fea47c3c9 100644
--- a/test/widgets/test_wlan.py
+++ b/test/widgets/test_wlan.py
@@ -103,6 +103,15 @@ def test_wlan_display(minimal_conf_noscreen, manager_nospawn, patched_wlan, kwar
     assert text == expected
 
 
+def test_wlan_display_escape_essid(
+    minimal_conf_noscreen, manager_nospawn, patched_wlan, monkeypatch
+):
+    """Test escaping of pango markup in ESSID"""
+    monkeypatch.setitem(MockIwlib.DATA["wlan0"], "ESSID", b"A&B")
+    widget = patched_wlan.Wlan(format="{essid}")
+    assert widget.poll() == "A&amp;B"
+
+
 @pytest.mark.parametrize(
     "kwargs,state,expected",
     [