Repository: qtile/qtile

Issue:
setting foreground in extension_defaults result with non working CommandSet
# Issue description

setting `foreground` in `extension_defaults` result that `CommandSet` don't appear at all
probably other extension don't work too
# Qtile version

0.18.1 up to latest git commit

# Stack traces

```
error, cannot allocate color 'ffffff'
```
# Configuration

```
key_exensions = Key([mod], 's',
                    lazy.run_extension(
                        extension.CommandSet(
                            commands={
                                'reboot': 'reboot',
                                'shutdown': 'shutdown',
                                'suspend': 'systemctl suspend',
                                'lock session': '/home/tokariew/.local/bin/lockme',
                                'restart qtile': 'qtile cmd-obj -o cmd -f restart',
                                'lazy restart': 'lazy.restart',
                                'logout': 'qtile cmd-obj -o cmd -f shutdown',
                            },
                            dmenu_lines=6
                        )
                    )),

keys.extend(key_exensions)

extension_defaults = dict(
    font="FiraCode Nerd Font",
    fontsize=14,
    padding=4,
    padding_y=0,
    foreground='ffffff',
)
```



Discussion:
ok i see…
`extension_defaults` use `foreground` values with `#`
as i was previously coping from widget_defaults which use foreground without `#`
Using a # or not for a colour shouldn't change how we handle it so, if it doesn't work without the #, that sounds like a bug. 
I think this is because `dmenu` only accepts colours starting with a `#` so we should probably include a check/fix for that.

Resolving patch:
diff --git a/libqtile/extension/base.py b/libqtile/extension/base.py
--- a/libqtile/extension/base.py
+++ b/libqtile/extension/base.py
@@ -1,4 +1,5 @@
 # Copyright (c) 2017 Dario Giovannetti
+# Copyright (c) 2021 elParaguayo
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
 # of this software and associated documentation files (the "Software"), to deal
@@ -17,12 +18,15 @@
 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
-
+import re
 import shlex
 from subprocess import PIPE, Popen
 from typing import Any, List, Tuple  # noqa: F401
 
 from libqtile import configurable
+from libqtile.log_utils import logger
+
+RGB = re.compile(r"^#?([a-fA-F0-9]{3}|[a-fA-F0-9]{6})$")
 
 
 class _Extension(configurable.Configurable):
@@ -33,16 +37,40 @@ class _Extension(configurable.Configurable):
     defaults = [
         ("font", "sans", "defines the font name to be used"),
         ("fontsize", None, "defines the font size to be used"),
-        ("background", None, "defines the normal background color"),
-        ("foreground", None, "defines the normal foreground color"),
-        ("selected_background", None, "defines the selected background color"),
-        ("selected_foreground", None, "defines the selected foreground color"),
+        ("background", None, "defines the normal background color (#RGB or #RRGGBB)"),
+        ("foreground", None, "defines the normal foreground color (#RGB or #RRGGBB)"),
+        ("selected_background", None, "defines the selected background color (#RGB or #RRGGBB)"),
+        ("selected_foreground", None, "defines the selected foreground color (#RGB or #RRGGBB)"),
     ]
 
     def __init__(self, **config):
         configurable.Configurable.__init__(self, **config)
         self.add_defaults(_Extension.defaults)
         _Extension.installed_extensions.append(self)
+        self._check_colors()
+
+    def _check_colors(self):
+        """
+        dmenu needs colours to be in #rgb or #rrggbb format.
+
+        Checks colour value, removes invalid values and adds # if missing.
+        """
+        for c in ["background", "foreground", "selected_background", "selected_foreground"]:
+            col = getattr(self, c, None)
+            if col is None:
+                continue
+
+            if not isinstance(col, str) or not RGB.match(col):
+                logger.warning(
+                    f"Invalid extension '{c}' color: {col}. "
+                    f"Must be #RGB or #RRGGBB string."
+                )
+                setattr(self, c, None)
+                continue
+
+            if not col.startswith("#"):
+                col = f"#{col}"
+                setattr(self, c, col)
 
     def _configure(self, qtile):
         self.qtile = qtile


Test patch:
diff --git a/test/extension/__init__.py b/test/extension/__init__.py
new file mode 100644
diff --git a/test/extension/test_base.py b/test/extension/test_base.py
new file mode 100644
--- /dev/null
+++ b/test/extension/test_base.py
@@ -0,0 +1,38 @@
+# Copyright (c) 2021 elParaguayo
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+import pytest
+
+from libqtile.extension.base import _Extension
+
+parameters = [
+    ("#000", "#000"),
+    ("#000000", "#000000"),
+    ("000", "#000"),
+    ("000000", "#000000"),
+    ("#0000", None),
+    ("0000", None),
+    (0, None)
+]
+
+
+@pytest.mark.parametrize("value,expected", parameters)
+def test_valid_colours(value, expected):
+    extension = _Extension(foreground=value)
+    assert extension.foreground == expected