diff --git a/CHANGELOG.md b/CHANGELOG.md index ca9c48118..acfc16185 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Added - Docs: link to Lua migration guide in `README.md` and `MIGRATION-LUA.md` - Waybar: add VSCodium and Chromium icon rules to window module +- Theme import: the "More Themes" fzf picker (`hydectl theme import`) marks themes already present in `~/.config/hyde/themes` with a "✓ installed" suffix, so browsing the gallery no longer requires cross-checking what's already on disk ### Fixed - Waybar: honor an optional `$WAYBAR_LAYOUT` in `hypr.theme` during color/theme updates, selecting the layout and matching stylesheet temporarily. Restore the previous layout and independently selected CSS when leaving themes with a preset, preserving them across repeated updates and preset-to-preset switches. Fix theme setting lookup for names containing spaces such as `Mac OS`. @@ -29,6 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Waybar: `gpuinfo` no longer crashes with a division-by-zero error, leaks a plain-text banner into its JSON output on the first poll after a reboot or a `--reset`, or emits an invalid `"percentage":` with no value when no temperature sensor is available; all three used to break the module's parsing - Python environment: `uv sync` now targets the HyDE-managed venv at `~/.local/state/hyde/python_env` instead of creating a project-local `.venv`; also forces `--link-mode copy` to avoid silent reflink failures on ext4 that left packages uninstalled - Hypridle: use Lua DPMS dispatchers so idle screen-off and resume work with the Hyprland Lua config parser +- Theme import: `theme.import.py`'s fzf preview no longer crashes with an `AttributeError` when previewing a theme name that isn't in the cached gallery data ## v26.08.21 diff --git a/Configs/.local/lib/hyde/theme.import.py b/Configs/.local/lib/hyde/theme.import.py index 63b21d5cb..24441f61c 100755 --- a/Configs/.local/lib/hyde/theme.import.py +++ b/Configs/.local/lib/hyde/theme.import.py @@ -16,6 +16,43 @@ "hyde/gallery-database", ) JSON_DATA = None +# A leading, colored checkmark -- not a repeated "installed" text label -- so +# the theme name stays the most legible thing in the row (HIG lists-and-tables: +# selection/status state is an accessory image, not label text). Green is +# reserved for this; fzf's own multi-select marker uses a different glyph +# (see --marker in fzf_menu) so the two states can't be confused. +# +# fzf's --ansi (needed to render the color at all) parses and *strips* SGR +# escape codes out of every value it hands back via `{}` -- both the live +# preview substitution and the final selected-line output on stdout. Only +# the glyph survives that round trip, never the color codes around it. So +# INSTALLED_GLYPH (bare, no escapes) is what every strip/compare has to +# match; INSTALLED_PREFIX (with color) exists only to build what fzf displays. +INSTALLED_GLYPH = "✓ " +INSTALLED_PREFIX = f"\033[32m{INSTALLED_GLYPH}\033[0m" +NOT_INSTALLED_PREFIX = " " # same 2-column width, keeps names aligned + + +def get_installed_themes(): + config_home = os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) + themes_dir = os.path.join(config_home, "hyde/themes") + if not os.path.exists(themes_dir): + return set() + return {d for d in os.listdir(themes_dir) if os.path.isdir(os.path.join(themes_dir, d))} + + +def strip_installed_marker(theme): + # Check the colored form first: it starts with the same glyph the + # ansi-stripped form does, so checking the glyph alone first would chop + # only the glyph off a still-colored string and leave the escape codes + # attached to the name. + if theme.startswith(INSTALLED_PREFIX): + return theme[len(INSTALLED_PREFIX) :] + if theme.startswith(INSTALLED_GLYPH): + return theme[len(INSTALLED_GLYPH) :] + if theme.startswith(NOT_INSTALLED_PREFIX): + return theme[len(NOT_INSTALLED_PREFIX) :] + return theme def fetch_theme_preview_path(theme): @@ -89,6 +126,7 @@ def clone_repo(): def get_theme_preview(theme): + theme = strip_installed_marker(theme) fetch_data() color1 = "#39b1d6" color2 = "#c79bf0" @@ -121,6 +159,7 @@ def get_opposite_color(hex_color): " [TAB] to mark a theme\n" " [Enter] or choose [CONFIRM] to confirm selected themes\n" " [Esc] to exit/cancel\n\n" + f" {INSTALLED_PREFIX}already installed\n\n" "Some helpful shortcuts:\n" " CTRL A : mark all\n" " CTRL D : un-mark all\n" @@ -132,8 +171,11 @@ def get_opposite_color(hex_color): preview_text += bar_bottom + "\n" image = CLONE_DIR + "/preview.png" else: - theme_data = next((t for t in JSON_DATA if t["THEME"] == theme), None) - if theme_data and theme_data.get("PREVIEW"): + theme_data = next((t for t in (JSON_DATA or []) if t["THEME"] == theme), None) + if theme_data is None: + logger.debug(f"Theme not found in gallery data: {theme}") + return f"Image preview not found for {theme}" + if theme_data.get("PREVIEW"): image = random.choice(theme_data["PREVIEW"]) else: image = None @@ -237,10 +279,15 @@ def fzf_menu(): try: fetch_data() if JSON_DATA: - themes = [theme["THEME"] for theme in JSON_DATA] - themes.sort(reverse=True) - themes = ["[CONFIRM]"] + themes + installed = get_installed_themes() + theme_names = sorted((theme["THEME"] for theme in JSON_DATA), reverse=True) + themes = [ + (INSTALLED_PREFIX if name in installed else NOT_INSTALLED_PREFIX) + name + for name in theme_names + ] + themes = [NOT_INSTALLED_PREFIX + "[CONFIRM]"] + themes fzf_options = [ + "--ansi", "--input-label-pos=center", "--cycle", "-m", @@ -253,6 +300,7 @@ def fzf_menu(): "--preview-window=right::70%", ] SELECTED_THEMES = fzf.prompt(themes, fzf_options) + SELECTED_THEMES = [strip_installed_marker(t) for t in SELECTED_THEMES] logger.debug(f"Selected themes: {SELECTED_THEMES}") else: logger.debug("No JSON data available to display themes.") @@ -353,7 +401,7 @@ def main(): logger.debug("LoadedPreview text: " + args.preview) preview_text = args.preview_text print(preview_text) - get_theme_preview(args.preview) + print(get_theme_preview(args.preview)) if args.fetch: fetch_theme(args.fetch) except KeyboardInterrupt: