Repository: qtile/qtile

Issue:
Cmus widget doesn't update when song title has an ampersand ("&")
# Issue description
Whenever a song comes up that has an ampersand in the title, the cmus widget stops updating, even after the song in particular ends. I don't know if it happens with any other symbols, or if it happens when the ampersand is in the artist name. Restarting Qtile fixes the issue as long as the current song does not have an ampersand in its title.

# Qtile version
0.18.1.dev0+g8e7ecc0a.d20210719

# Stack traces
Looking into `~/.local/share/qtile/qtile.log` shows the following

```
2021-07-23 22:20:47,453 ERROR libqtile base.py:on_done():L563 Failed to reschedule.
Traceback (most recent call last):
  File "/usr/lib/python3.9/site-packages/libqtile/widget/base.py", line 555, in on_done
    self.update(result)
  File "/usr/lib/python3.9/site-packages/libqtile/widget/base.py", line 467, in update
    self.text = text
  File "/usr/lib/python3.9/site-packages/libqtile/widget/base.py", line 359, in text
    self.layout.text = self.formatted_text
  File "/usr/lib/python3.9/site-packages/libqtile/drawer.py", line 71, in text
    attrlist, value, accel_char = pangocffi.parse_markup(value)
  File "/usr/lib/python3.9/site-packages/libqtile/pangocffi.py", line 183, in parse_markup
    raise Exception("parse_markup() failed for %s" % value)
Exception: parse_markup() failed for b'\xe2\x99\xab Daisuke Ishiwatari - SHOTGUN&HEAD'
2021-07-23 22:22:22,656 WARNING libqtile lifecycle.py:_atexit():L34 Restarting Qtile with os.execv(...)
2021-07-23 22:23:32,624 ERROR libqtile base.py:on_done():L563 Failed to reschedule.
Traceback (most recent call last):
  File "/usr/lib/python3.9/site-packages/libqtile/widget/base.py", line 555, in on_done
    self.update(result)
  File "/usr/lib/python3.9/site-packages/libqtile/widget/base.py", line 467, in update
    self.text = text
  File "/usr/lib/python3.9/site-packages/libqtile/widget/base.py", line 359, in text
    self.layout.text = self.formatted_text
  File "/usr/lib/python3.9/site-packages/libqtile/drawer.py", line 71, in text
    attrlist, value, accel_char = pangocffi.parse_markup(value)
  File "/usr/lib/python3.9/site-packages/libqtile/pangocffi.py", line 183, in parse_markup
    raise Exception("parse_markup() failed for %s" % value)
Exception: parse_markup() failed for b'\xe2\x99\xab Homestuck - Walk-Stab-Walk (R&E)'
2021-07-23 22:23:32,628 ERROR libqtile base.py:on_done():L563 Failed to reschedule.
Traceback (most recent call last):
  File "/usr/lib/python3.9/site-packages/libqtile/widget/base.py", line 555, in on_done
    self.update(result)
  File "/usr/lib/python3.9/site-packages/libqtile/widget/base.py", line 467, in update
    self.text = text
  File "/usr/lib/python3.9/site-packages/libqtile/widget/base.py", line 359, in text
    self.layout.text = self.formatted_text
  File "/usr/lib/python3.9/site-packages/libqtile/drawer.py", line 71, in text
    attrlist, value, accel_char = pangocffi.parse_markup(value)
  File "/usr/lib/python3.9/site-packages/libqtile/pangocffi.py", line 183, in parse_markup
    raise Exception("parse_markup() failed for %s" % value)
Exception: parse_markup() failed for b'\xe2\x99\xab Homestuck - Walk-Stab-Walk (R&E)'
```



Discussion:
Fixed the formatting for you.

Thanks for the bug report. I'll take a look at this and see if I can fix it.

Resolving patch:
diff --git a/libqtile/widget/cmus.py b/libqtile/widget/cmus.py
--- a/libqtile/widget/cmus.py
+++ b/libqtile/widget/cmus.py
@@ -17,6 +17,7 @@
 import subprocess
 from functools import partial
 
+from libqtile import pangocffi
 from libqtile.widget import base
 
 
@@ -101,7 +102,7 @@ def now_playing(self):
                     now_playing = title
             if now_playing:
                 now_playing = "♫ {0}".format(now_playing)
-        return now_playing
+        return pangocffi.markup_escape_text(now_playing)
 
     def play(self):
         """Play music if stopped, else toggle pause."""


Test patch:
diff --git a/test/widgets/test_cmus.py b/test/widgets/test_cmus.py
--- a/test/widgets/test_cmus.py
+++ b/test/widgets/test_cmus.py
@@ -80,6 +80,15 @@ def reset(cls):
                 "tag title It's Not Unusual",
                 "stream tomjones"
             ],
+            [
+                "status playing",
+                "file /playing/file/always.mp3",
+                "duration 222",
+                "position 14",
+                "tag artist Above & Beyond",
+                "tag album Anjunabeats 14",
+                "tag title Always - Tinlicker Extended Mix"
+            ],
         ]
         cls.index = 0
         cls.is_error = False
@@ -221,3 +230,20 @@ def test_cmus_error_handling(fake_qtile, patched_cmus, fake_window):
     # Widget does nothing with error message so text is blank
     # TODO: update widget to show error?
     assert text == ""
+
+
+def test_escape_text(fake_qtile, patched_cmus, fake_window):
+    widget = patched_cmus.Cmus()
+
+    # Set track to a stopped item
+    MockCmusRemoteProcess.index = 3
+    fakebar = Bar([widget], 24)
+    fakebar.window = fake_window
+    fakebar.width = 10
+    fakebar.height = 10
+    fakebar.draw = no_op
+    widget._configure(fake_qtile, fakebar)
+    text = widget.poll()
+
+    # It's stopped so colour should reflect this
+    assert text == "♫ Above &amp; Beyond - Always - Tinlicker Extended Mix"