From 0315a1060202194bb7e955f2650ff8a877f031bc Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Mon, 23 Feb 2026 13:56:21 +0100 Subject: [PATCH] feat(client_utils): theme can be changed from the CLI --- bec_widgets/cli/client_utils.py | 19 +++++++++ .../containers/main_window/main_window.py | 2 +- tests/unit_tests/test_client_utils.py | 41 +++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/bec_widgets/cli/client_utils.py b/bec_widgets/cli/client_utils.py index 77a24e49..37527b70 100644 --- a/bec_widgets/cli/client_utils.py +++ b/bec_widgets/cli/client_utils.py @@ -297,6 +297,25 @@ class BECGuiClient(RPCBase): return self._raise_all() return self._start(wait=wait) + def change_theme(self, theme: Literal["light", "dark"] | None = None) -> None: + """ + Apply a GUI theme or toggle between dark and light. + + Args: + theme(Literal["light", "dark"] | None): Theme to apply. If None, the current + theme is fetched from the GUI and toggled. + """ + if not self._check_if_server_is_alive(): + self._start(wait=True) + + with wait_for_server(self): + if theme is None: + current_theme = self.launcher._run_rpc("fetch_theme") + next_theme = "light" if current_theme == "dark" else "dark" + else: + next_theme = theme + self.launcher._run_rpc("change_theme", theme=next_theme) + def new( self, name: str | None = None, diff --git a/bec_widgets/widgets/containers/main_window/main_window.py b/bec_widgets/widgets/containers/main_window/main_window.py index 55fdf1f1..e33bb8a0 100644 --- a/bec_widgets/widgets/containers/main_window/main_window.py +++ b/bec_widgets/widgets/containers/main_window/main_window.py @@ -254,7 +254,7 @@ class BECMainWindow(BECWidget, QMainWindow): self.ui = loader.loader(ui_file) self.setCentralWidget(self.ui) - def _fetch_theme(self) -> str: + def fetch_theme(self) -> str: return self.app.theme.theme def _get_launcher_from_qapp(self): diff --git a/tests/unit_tests/test_client_utils.py b/tests/unit_tests/test_client_utils.py index db2589e8..fbaa84b5 100644 --- a/tests/unit_tests/test_client_utils.py +++ b/tests/unit_tests/test_client_utils.py @@ -86,3 +86,44 @@ def test_client_utils_passes_client_config_to_server(bec_dispatcher): config=mixin._client._service_config.config, logger=mock.ANY, ) + + +@contextmanager +def _no_wait_for_server(_client): + yield + + +@pytest.mark.parametrize("theme", ["light", "dark"]) +def test_client_utils_apply_theme_explicit(theme): + gui = BECGuiClient() + launcher = mock.MagicMock() + + with mock.patch.object( + BECGuiClient, "launcher", new_callable=mock.PropertyMock + ) as launcher_prop: + launcher_prop.return_value = launcher + with mock.patch("bec_widgets.cli.client_utils.wait_for_server", _no_wait_for_server): + with mock.patch.object(gui, "_check_if_server_is_alive", return_value=True): + gui.change_theme(theme) + + launcher._run_rpc.assert_called_once_with("change_theme", theme=theme) + + +@pytest.mark.parametrize("current_theme, expected_theme", [("light", "dark"), ("dark", "light")]) +def test_client_utils_apply_theme_toggles_when_none(current_theme, expected_theme): + gui = BECGuiClient() + launcher = mock.MagicMock() + launcher._run_rpc.side_effect = [current_theme, None] + + with mock.patch.object( + BECGuiClient, "launcher", new_callable=mock.PropertyMock + ) as launcher_prop: + launcher_prop.return_value = launcher + with mock.patch("bec_widgets.cli.client_utils.wait_for_server", _no_wait_for_server): + with mock.patch.object(gui, "_check_if_server_is_alive", return_value=True): + gui.change_theme(None) + + assert launcher._run_rpc.call_args_list == [ + mock.call("fetch_theme"), + mock.call("change_theme", theme=expected_theme), + ]