From 485b7bc7bf7627cc528245b6a2a0856e0c1a05f8 Mon Sep 17 00:00:00 2001 From: Phil Rennie Date: Wed, 4 Nov 2026 17:05:35 +0000 Subject: [PATCH] fix(mainsail): theme installer updates for current Mainsail docs branch (#833) --- .../mainsail_theme_installer_extension.py | 25 ++-- .../mainsail_theme_installer/metadata.json | 2 +- .../tests/test_mainsail_theme_installer.py | 113 ++++++++++++++++++ 3 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 kiauh/extensions/tests/test_mainsail_theme_installer.py diff --git a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py index 3a3cce82..4b05e7fb 100644 --- a/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py +++ b/kiauh/extensions/mainsail_theme_installer/mainsail_theme_installer_extension.py @@ -76,7 +76,7 @@ class MainsailThemeInstallerExtension(BaseExtension): # noinspection PyMethodMayBeStatic class MainsailThemeInstallMenu(BaseMenu): THEMES_URL: str = ( - "https://raw.githubusercontent.com/mainsail-crew/gb-docs/main/_data/themes.csv" + "https://raw.githubusercontent.com/mainsail-crew/docs/zensical/_data/themes.csv" ) def __init__(self, instances: List[Klipper]): @@ -94,8 +94,9 @@ class MainsailThemeInstallMenu(BaseMenu): ) def set_options(self) -> None: + # Menu numbers start at 1 to match the theme IDs on the Mainsail docs page self.options = { - f"{index}": Option(self.install_theme, opt_index=f"{index}") + f"{index + 1}": Option(self.install_theme, opt_index=f"{index}") for index in range(len(self.themes)) } @@ -107,11 +108,12 @@ class MainsailThemeInstallMenu(BaseMenu): f""" ╟───────────────────────────────────────────────────────╢ ║ {line1:<62} ║ - ║ https://docs.mainsail.xyz/theming/themes ║ + ║ https://docs.mainsail.xyz/features/custom- ║ + ║ themes/community-themes/ ║ ╟───────────────────────────────────────────────────────╢ """ )[1:] - for i, theme in enumerate(self.themes): + for i, theme in enumerate(self.themes, start=1): j: str = f" {i}" if i < 10 else f"{i}" row: str = f"{j}) [{theme.name}]" menu += f"║ {row:<53} ║\n" @@ -123,12 +125,19 @@ class MainsailThemeInstallMenu(BaseMenu): themes: List[ThemeData] = [] content: str = response.read().decode() csv_data: List[str] = content.splitlines() - fieldnames = ["name", "short_note", "author", "repo"] - csv_reader = csv.DictReader(csv_data, fieldnames=fieldnames, delimiter=",") - next(csv_reader) # skip the header of the csv file + # read columns by the csv header, so extra columns in the list + # (e.g. "branch" and "preview", used by the docs page) are ignored + csv_reader = csv.DictReader(csv_data, delimiter=",") for row in csv_reader: row: Dict[str, str] # type: ignore - theme: ThemeData = ThemeData(**row) + if not row.get("author") or not row.get("repo"): + continue + theme: ThemeData = ThemeData( + name=row.get("name") or row["repo"], + short_note=row.get("short_note") or "", + author=row["author"], + repo=row["repo"], + ) themes.append(theme) return themes diff --git a/kiauh/extensions/mainsail_theme_installer/metadata.json b/kiauh/extensions/mainsail_theme_installer/metadata.json index fc01274d..020f0f4b 100644 --- a/kiauh/extensions/mainsail_theme_installer/metadata.json +++ b/kiauh/extensions/mainsail_theme_installer/metadata.json @@ -5,7 +5,7 @@ "maintained_by": "dw-0", "display_name": "Mainsail Theme Installer", "description": ["Install Mainsail Themes maintained by the Mainsail community."], - "website": "https://docs.mainsail.xyz/theming/themes", + "website": "https://docs.mainsail.xyz/features/custom-themes/community-themes/", "updates": false } } diff --git a/kiauh/extensions/tests/test_mainsail_theme_installer.py b/kiauh/extensions/tests/test_mainsail_theme_installer.py new file mode 100644 index 00000000..b5d550bf --- /dev/null +++ b/kiauh/extensions/tests/test_mainsail_theme_installer.py @@ -0,0 +1,113 @@ +# ======================================================================= # +# Copyright (C) 2020 - 2026 Dominik Willner # +# # +# This file is part of KIAUH - Klipper Installation And Update Helper # +# https://github.com/dw-0/kiauh # +# # +# This file may be distributed under the terms of the GNU GPLv3 license # +# ======================================================================= # + +from __future__ import annotations + +import pytest +from extensions.mainsail_theme_installer.mainsail_theme_installer_extension import ( + MainsailThemeInstallMenu, + ThemeData, +) + +MODULE = "extensions.mainsail_theme_installer.mainsail_theme_installer_extension" + +# current format of the list in mainsail-crew/docs +DOCS_CSV = ( + "name,short_note,author,repo,branch,preview\n" + "Dracula,,steadyjaw,dracula-mainsail-theme,master,screenshot.jpg\n" + "Printables,(unofficial),8bitmcu,mainsail-printables,main,screenshot.jpg\n" +) + +# legacy format of the list in mainsail-crew/gb-docs +LEGACY_CSV = ( + "name,short_note,author,repo\n" + "Dracula,,steadyjaw,dracula-mainsail-theme\n" + "Printables,(unofficial),8bitmcu,mainsail-printables\n" +) + +EXPECTED_THEMES = [ + ThemeData("Dracula", "", "steadyjaw", "dracula-mainsail-theme"), + ThemeData("Printables", "(unofficial)", "8bitmcu", "mainsail-printables"), +] + + +class _FakeResponse: + def __init__(self, body: str) -> None: + self._body = body.encode() + + def __enter__(self): + return self + + def __exit__(self, *args): + return None + + def read(self) -> bytes: + return self._body + + +def _patch_themes_csv(monkeypatch: pytest.MonkeyPatch, body: str) -> None: + class FakeUrlLib: + @staticmethod + def urlopen(url: str): + return _FakeResponse(body) + + monkeypatch.setattr(f"{MODULE}.urllib.request", FakeUrlLib()) + + +@pytest.fixture +def menu(monkeypatch: pytest.MonkeyPatch) -> MainsailThemeInstallMenu: + _patch_themes_csv(monkeypatch, DOCS_CSV) + return MainsailThemeInstallMenu([]) + + +class TestLoadThemes: + def test_parses_docs_format_and_ignores_extra_columns( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + _patch_themes_csv(monkeypatch, DOCS_CSV) + assert MainsailThemeInstallMenu([]).themes == EXPECTED_THEMES + + def test_parses_legacy_format(self, monkeypatch: pytest.MonkeyPatch) -> None: + _patch_themes_csv(monkeypatch, LEGACY_CSV) + assert MainsailThemeInstallMenu([]).themes == EXPECTED_THEMES + + def test_skips_blank_and_incomplete_rows( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + _patch_themes_csv(monkeypatch, DOCS_CSV + "\n" + "Broken,,,,,\n") + assert MainsailThemeInstallMenu([]).themes == EXPECTED_THEMES + + +class TestThemeMenuNumbering: + def test_options_start_at_one_to_match_docs_ids( + self, menu: MainsailThemeInstallMenu + ) -> None: + menu.set_options() + assert list(menu.options) == ["1", "2"] + assert menu.options["1"].opt_index == "0" + assert menu.options["2"].opt_index == "1" + + def test_menu_lists_themes_from_one( + self, menu: MainsailThemeInstallMenu, capsys + ) -> None: + menu.print_menu() + out = capsys.readouterr().out + assert " 1) [Dracula]" in out + assert " 2) [Printables]" in out + assert " 0) [" not in out + + def test_menu_links_to_current_docs( + self, menu: MainsailThemeInstallMenu, capsys + ) -> None: + menu.print_menu() + out = capsys.readouterr().out + # the url is too long for the menu box, so it is wrapped over two lines + assert "║ https://docs.mainsail.xyz/features/custom- ║" in out + assert "║ themes/community-themes/ ║" in out + assert "theming/themes" not in out