diff --git a/py3status/constants.py b/py3status/constants.py
index 1b61ce98..78bddd93 100644
--- a/py3status/constants.py
+++ b/py3status/constants.py
@@ -61,6 +61,8 @@ ERROR_CONFIG = """
 }
 """
 
+COLOR_NAMES_EXCLUDED = ["good", "bad", "degraded", "separator", "threshold", "None"]
+
 COLOR_NAMES = {
     "aliceblue": "#f0f8ff",
     "antiquewhite": "#faebd7",
diff --git a/py3status/formatter.py b/py3status/formatter.py
index 62992315..bbbd3eea 100644
--- a/py3status/formatter.py
+++ b/py3status/formatter.py
@@ -6,6 +6,7 @@ from math import ceil
 from numbers import Number
 
 from py3status.composite import Composite
+from py3status.constants import COLOR_NAMES, COLOR_NAMES_EXCLUDED
 
 try:
     from urllib.parse import parse_qsl
@@ -52,6 +53,25 @@ class Formatter:
             self.format_string_cache[format_string] = tokens
         return self.format_string_cache[format_string]
 
+    def get_color_names(self, format_string):
+        """
+        Parses the format_string and returns a set of color names.
+        """
+        names = set()
+        # Tokenize the format string and process them
+        for token in self.tokens(format_string):
+            if token.group("command"):
+                name = dict(parse_qsl(token.group("command"))).get("color")
+                if (
+                    not name
+                    or name in COLOR_NAMES_EXCLUDED
+                    or name in COLOR_NAMES
+                    or name[0] == "#"
+                ):
+                    continue
+                names.add(name)
+        return names
+
     def get_placeholders(self, format_string):
         """
         Parses the format_string and returns a set of placeholders.
diff --git a/py3status/modules/keyboard_layout.py b/py3status/modules/keyboard_layout.py
index bf545189..1d2c83e7 100644
--- a/py3status/modules/keyboard_layout.py
+++ b/py3status/modules/keyboard_layout.py
@@ -18,7 +18,7 @@ Color options:
 Requires:
     xkblayout-state:
         or
-    setxkbmap: and `xset` (works for the first two predefined layouts.)
+    setxkbmap: and `xset` (works for the first two predefined layouts. overrides `XkbLayout` when switching layout.)
 
 Examples:
 ```
@@ -68,11 +68,14 @@ class Py3status:
 
     def post_config_hook(self):
         self.colors = getattr(self, "colors", None)  # old config
-        try:
-            self._xkblayout()
-            self._command = self._xkblayout
-        except self.py3.CommandError:
-            self._command = self._setxkbmap
+
+        avail_command = {
+            "xkblayout-state": (self._set_xkblayout, self._get_xkblayout),
+            "setxkbmap": (self._set_setxkbmap, self._get_setxkbmap),
+        }
+        self._set_command, self._get_command = avail_command[
+            self.py3.check_commands(avail_command.keys())
+        ]
 
         if not self.layouts:
             self.layouts = []
@@ -91,7 +94,7 @@ class Py3status:
         }
 
     def keyboard_layout(self):
-        layout, variant = self._command()
+        layout, variant = self._get_command()
         # If the current layout is not in our layouts list we need to add it
         if layout not in self._layouts:
             self._layouts = [layout] + self.layouts
@@ -131,7 +134,7 @@ class Py3status:
 
         return response
 
-    def _xkblayout(self):
+    def _get_xkblayout(self):
         layout, variant = [
             x.strip()
             for x in self.py3.command_output(
@@ -140,7 +143,7 @@ class Py3status:
         ]
         return layout, variant
 
-    def _setxkbmap(self):
+    def _get_setxkbmap(self):
         # this method works only for the first two predefined layouts.
         out = self.py3.command_output(["setxkbmap", "-query"])
         layouts = re.match(LAYOUTS_RE, out).group(1).split(",")
@@ -156,11 +159,24 @@ class Py3status:
         led_mask = re.match(LEDMASK_RE, xset_output).groups(0)[0]
         return layouts[int(led_mask)], ""
 
+    def _set_setxkbmap(self):
+        layout = self._layouts[self._active]
+        # Note: This will override user-defined layout, keyboard shortcut won't work
+        self.py3.command_run("setxkbmap -layout {}".format(layout))
+
+    def _set_xkblayout(self):
+        layout = self._layouts[self._active]
+        layout_pos = (
+            self.py3.command_output(["xkblayout-state", "print", "%S"])
+            .split()
+            .index(layout)
+        )
+        self.py3.command_run("xkblayout-state set {}".format(layout_pos))
+
     def _set_active(self, delta):
         self._active += delta
         self._active = self._active % len(self._layouts)
-        layout = self._layouts[self._active]
-        self.py3.command_run("setxkbmap -layout {}".format(layout))
+        self._set_command()
 
     def on_click(self, event):
         button = event["button"]
diff --git a/py3status/py3.py b/py3status/py3.py
index 0441fc17..1c30fa1a 100644
--- a/py3status/py3.py
+++ b/py3status/py3.py
@@ -730,15 +730,7 @@ class Py3:
             format_strings = [format_strings]
         names = set()
         for string in format_strings:
-            for color in string.replace("&", " ").split("color=")[1::1]:
-                color = color.split()[0]
-                if "#" in color:
-                    continue
-                if color in ["good", "bad", "degraded", "None", "threshold"]:
-                    continue
-                if color in COLOR_NAMES:
-                    continue
-                names.add(color)
+            names.update(self._formatter.get_color_names(string))
         return list(names)
 
     def get_placeholders_list(self, format_string, match=None):

keyboard_layout: keyboard shortcut stop working
Problem
--------------------

Switching keyboard layout causes layout switching shortcut to stop working (This might related to #1742)


Analysis
---------------------

[`_set_active`](https://github.com/ultrabug/py3status/blob/65a609261095f7502fc2814e9a6feb478845a69c/py3status/modules/keyboard_layout.py#L159-L163) switch layout by replacing all layout in `XkbLayout` with the target layout, This causes switching layout shortcut to not work since there's only a single layout left.

Currently, It's not possible to choose layout without replacing `XkbLayout` through `setxkbmap`,
Fortunately, `xkblayout-state` provides a proper method to switch layout.

Will submit PR later.
diff --git a/tests/test_formatter.py b/tests/test_formatter.py
index beadf23a..8882ddbe 100644
--- a/tests/test_formatter.py
+++ b/tests/test_formatter.py
@@ -165,6 +165,20 @@ def update_placeholders(test_dict):
         pytest.fail("Results not as expected")
 
 
+def get_color_names(test_dict):
+    __tracebackhide__ = True
+
+    result = f.get_color_names(test_dict["format"])
+    expected = test_dict.get("expected")
+
+    if result != expected:
+        print("Format\n{}\n".format(test_dict["format"]))
+        print("Expected\n{}".format(pformat(expected)))
+        print("Got\n{}".format(pformat(result)))
+    if result != expected:
+        pytest.fail("Results not as expected")
+
+
 def test_1():
     run_formatter({"format": u"hello ☂", "expected": u"hello ☂"})
 
@@ -1359,6 +1373,42 @@ def test_update_placeholders_5():
     )
 
 
+def test_get_color_names_1():
+    get_color_names(
+        {
+            "format": r"\?color=red \?color=#0f0 green \?color=#0000ff blue",
+            "expected": set(),
+        }
+    )
+
+
+def test_get_color_names_2():
+    get_color_names(
+        {
+            "format": r"[\?color=tobias funke|\?color=bluemangroup troupe]",
+            "expected": {"tobias", "bluemangroup"},
+        }
+    )
+
+
+def test_get_color_names_3():
+    get_color_names(
+        {
+            "format": r"[\?color=good bonsai tree][\?color=None Saibot]",
+            "expected": set(),
+        }
+    )
+
+
+def test_get_color_names_4():
+    get_color_names(
+        {
+            "format": r"\?color=rebeccapurple \?color=rebecca \?color=purple",
+            "expected": {"rebecca"},
+        }
+    )
+
+
 if __name__ == "__main__":
     # run tests
     import sys

pip install -e .[all]
parse_log_pytest
requirements.txt
pytest pytest-flake8 black
apt-get update apt-get install -y gcc
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
black==25.1.0
click==8.1.8
exceptiongroup==1.2.2
flake8==7.2.0
gevent==24.11.1
greenlet==3.1.1
iniconfig==2.1.0
mccabe==0.7.0
mypy-extensions==1.0.0
packaging==24.2
pathspec==0.12.1
platformdirs==4.3.7
pluggy==1.5.0
-e git+https://github.com/ultrabug/py3status.git@cefa4f8d2f5fd49bc6522e03233bcb45da3c094c#egg=py3status
pycodestyle==2.13.0
pyflakes==3.3.1
pytest==8.3.5
pytest-flake8==1.3.0
pyudev==0.24.3
tomli==2.2.1
typing_extensions==4.13.0
zope.event==5.0
zope.interface==7.2

name: py3status
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - black==25.1.0
      - click==8.1.8
      - exceptiongroup==1.2.2
      - flake8==7.2.0
      - gevent==24.11.1
      - greenlet==3.1.1
      - iniconfig==2.1.0
      - mccabe==0.7.0
      - mypy-extensions==1.0.0
      - packaging==24.2
      - pathspec==0.12.1
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - pycodestyle==2.13.0
      - pyflakes==3.3.1
      - pytest==8.3.5
      - pytest-flake8==1.3.0
      - pyudev==0.24.3
      - tomli==2.2.1
      - typing-extensions==4.13.0
      - zope-event==5.0
      - zope-interface==7.2
prefix: /opt/conda/envs/py3status

tests/test_formatter.py::test_get_color_names_1 tests/test_formatter.py::test_get_color_names_2 tests/test_formatter.py::test_get_color_names_3 tests/test_formatter.py::test_get_color_names_4
tests/test_formatter.py::test_1 tests/test_formatter.py::test_2 tests/test_formatter.py::test_3 tests/test_formatter.py::test_4 tests/test_formatter.py::test_5 tests/test_formatter.py::test_6 tests/test_formatter.py::test_7 tests/test_formatter.py::test_8 tests/test_formatter.py::test_9 tests/test_formatter.py::test_10 tests/test_formatter.py::test_11 tests/test_formatter.py::test_12 tests/test_formatter.py::test_13 tests/test_formatter.py::test_14 tests/test_formatter.py::test_15 tests/test_formatter.py::test_16 tests/test_formatter.py::test_16a tests/test_formatter.py::test_16b tests/test_formatter.py::test_17 tests/test_formatter.py::test_18 tests/test_formatter.py::test_19 tests/test_formatter.py::test_20 tests/test_formatter.py::test_21 tests/test_formatter.py::test_22 tests/test_formatter.py::test_23 tests/test_formatter.py::test_24 tests/test_formatter.py::test_24a tests/test_formatter.py::test_24b tests/test_formatter.py::test_25 tests/test_formatter.py::test_26 tests/test_formatter.py::test_27 tests/test_formatter.py::test_28 tests/test_formatter.py::test_29 tests/test_formatter.py::test_30 tests/test_formatter.py::test_31 tests/test_formatter.py::test_32 tests/test_formatter.py::test_33 tests/test_formatter.py::test_34 tests/test_formatter.py::test_35 tests/test_formatter.py::test_36 tests/test_formatter.py::test_37 tests/test_formatter.py::test_38 tests/test_formatter.py::test_39 tests/test_formatter.py::test_40 tests/test_formatter.py::test_41 tests/test_formatter.py::test_42 tests/test_formatter.py::test_43 tests/test_formatter.py::test_44 tests/test_formatter.py::test_45 tests/test_formatter.py::test_46 tests/test_formatter.py::test_47 tests/test_formatter.py::test_48 tests/test_formatter.py::test_49 tests/test_formatter.py::test_50 tests/test_formatter.py::test_51 tests/test_formatter.py::test_52 tests/test_formatter.py::test_53 tests/test_formatter.py::test_54 tests/test_formatter.py::test_55 tests/test_formatter.py::test_56 tests/test_formatter.py::test_57 tests/test_formatter.py::test_58 tests/test_formatter.py::test_58a tests/test_formatter.py::test_59 tests/test_formatter.py::test_59a tests/test_formatter.py::test_60 tests/test_formatter.py::test_61 tests/test_formatter.py::test_62 tests/test_formatter.py::test_63 tests/test_formatter.py::test_64 tests/test_formatter.py::test_65 tests/test_formatter.py::test_66 tests/test_formatter.py::test_67 tests/test_formatter.py::test_68 tests/test_formatter.py::test_69 tests/test_formatter.py::test_70 tests/test_formatter.py::test_70a tests/test_formatter.py::test_71 tests/test_formatter.py::test_72 tests/test_formatter.py::test_73 tests/test_formatter.py::test_74 tests/test_formatter.py::test_75 tests/test_formatter.py::test_76 tests/test_formatter.py::test_77 tests/test_formatter.py::test_78 tests/test_formatter.py::test_else_true tests/test_formatter.py::test_else_false tests/test_formatter.py::test_composite_looks_empty tests/test_formatter.py::test_color_name_1 tests/test_formatter.py::test_color_name_2 tests/test_formatter.py::test_color_name_3 tests/test_formatter.py::test_color_name_4 tests/test_formatter.py::test_color_name_4a tests/test_formatter.py::test_color_name_5 tests/test_formatter.py::test_color_name_5a tests/test_formatter.py::test_color_name_6 tests/test_formatter.py::test_color_name_7 tests/test_formatter.py::test_color_name_7a tests/test_formatter.py::test_color_1 tests/test_formatter.py::test_color_1a tests/test_formatter.py::test_color_2 tests/test_formatter.py::test_color_3 tests/test_formatter.py::test_color_4 tests/test_formatter.py::test_color_5 tests/test_formatter.py::test_color_6 tests/test_formatter.py::test_color_7 tests/test_formatter.py::test_color_7a tests/test_formatter.py::test_color_8 tests/test_formatter.py::test_color_8a tests/test_formatter.py::test_color_9 tests/test_formatter.py::test_color_9a tests/test_formatter.py::test_composite_1 tests/test_formatter.py::test_composite_2 tests/test_formatter.py::test_composite_3 tests/test_formatter.py::test_composite_4 tests/test_formatter.py::test_composite_5 tests/test_formatter.py::test_composite_6 tests/test_formatter.py::test_attr_getter tests/test_formatter.py::test_min_length_1 tests/test_formatter.py::test_min_length_2 tests/test_formatter.py::test_min_length_3 tests/test_formatter.py::test_min_length_4 tests/test_formatter.py::test_min_length_5 tests/test_formatter.py::test_min_length_6 tests/test_formatter.py::test_numeric_strings_1 tests/test_formatter.py::test_numeric_strings_2 tests/test_formatter.py::test_numeric_strings_3 tests/test_formatter.py::test_numeric_strings_4 tests/test_formatter.py::test_numeric_strings_5 tests/test_formatter.py::test_numeric_strings_6 tests/test_formatter.py::test_not_zero_1 tests/test_formatter.py::test_not_zero_2 tests/test_formatter.py::test_not_zero_3 tests/test_formatter.py::test_not_zero_4 tests/test_formatter.py::test_not_zero_5 tests/test_formatter.py::test_not_zero_6 tests/test_formatter.py::test_not_zero_7 tests/test_formatter.py::test_not_zero_8 tests/test_formatter.py::test_not_zero_9 tests/test_formatter.py::test_not_zero_10 tests/test_formatter.py::test_not_zero_11 tests/test_formatter.py::test_bad_composite_color tests/test_formatter.py::test_soft_1 tests/test_formatter.py::test_soft_2 tests/test_formatter.py::test_soft_3 tests/test_formatter.py::test_soft_4 tests/test_formatter.py::test_soft_5 tests/test_formatter.py::test_soft_6 tests/test_formatter.py::test_soft_7 tests/test_formatter.py::test_module_true tests/test_formatter.py::test_module_false tests/test_formatter.py::test_module_true_value tests/test_formatter.py::test_module_false_value tests/test_formatter.py::test_zero_format_1 tests/test_formatter.py::test_zero_format_2 tests/test_formatter.py::test_zero_format_3 tests/test_formatter.py::test_zero_format_4 tests/test_formatter.py::test_inherit_not_zero_1 tests/test_formatter.py::test_inherit_not_zero_2 tests/test_formatter.py::test_inherit_not_zero_3 tests/test_formatter.py::test_inherit_show_1 tests/test_formatter.py::test_inherit_color_1 tests/test_formatter.py::test_inherit_color_2 tests/test_formatter.py::test_conditions_1 tests/test_formatter.py::test_conditions_2 tests/test_formatter.py::test_conditions_3 tests/test_formatter.py::test_conditions_4 tests/test_formatter.py::test_conditions_5 tests/test_formatter.py::test_conditions_6 tests/test_formatter.py::test_conditions_7 tests/test_formatter.py::test_conditions_8 tests/test_formatter.py::test_conditions_9 tests/test_formatter.py::test_conditions_10 tests/test_formatter.py::test_conditions_11 tests/test_formatter.py::test_conditions_12 tests/test_formatter.py::test_conditions_13 tests/test_formatter.py::test_conditions_14 tests/test_formatter.py::test_conditions_15 tests/test_formatter.py::test_conditions_16 tests/test_formatter.py::test_conditions_17 tests/test_formatter.py::test_conditions_18 tests/test_formatter.py::test_conditions_19 tests/test_formatter.py::test_conditions_20 tests/test_formatter.py::test_conditions_21 tests/test_formatter.py::test_conditions_22 tests/test_formatter.py::test_conditions_23 tests/test_formatter.py::test_trailing_zeroes_1 tests/test_formatter.py::test_trailing_zeroes_2 tests/test_formatter.py::test_ceiling_numbers_1 tests/test_formatter.py::test_ceiling_numbers_2 tests/test_formatter.py::test_placeholders_1 tests/test_formatter.py::test_placeholders_2 tests/test_formatter.py::test_placeholders_3 tests/test_formatter.py::test_placeholders_4 tests/test_formatter.py::test_placeholders_5 tests/test_formatter.py::test_update_placeholders_1 tests/test_formatter.py::test_update_placeholders_2 tests/test_formatter.py::test_update_placeholders_3 tests/test_formatter.py::test_update_placeholders_4 tests/test_formatter.py::test_update_placeholders_5
py3status/py3.py py3status/formatter.py py3status/constants.py py3status/modules/keyboard_layout.py
py3status/py3.py py3status/formatter.py py3status/constants.py py3status/modules/keyboard_layout.py