From 3aafd2cc17db5ba7bf87dafa54e81759cd5bbec9 Mon Sep 17 00:00:00 2001 From: The_miro Date: Sun, 5 Jul 2026 23:35:23 +0200 Subject: [PATCH] fix(astal-menu): working favourites, 3-day weather, deploy-safe settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Favourites: - Pin/unpin from the drawer never worked: the right-click / long-press gestures were added to a Gtk.Button in the default (bubble) phase, where the button's own primary-click gesture swallowed them. Move them to the CAPTURE phase and claim the sequence (shared add_pin_gestures helper) so they fire reliably and don't also launch the app. - Show a ★ on pinned tiles and re-render tiles when favourites change, so the state is visible and updates live. - Verified end-to-end (right-click pins, right-click again unpins). Weather: the expanded quad showed current conditions only. weather.sh used `${2:-0}`, which rewrote the expanded view's intentionally-empty opts to "0" (current only); use `${2-0}` so the empty value reaches wttr.in as its default 3-day forecast. Verified the expanded view now renders all three days. Settings: move settings.json from CONFIG_DIR to XDG_STATE_HOME. The config-updater `rm -rf`s ~/.config/astal-menu on every deploy, which was wiping pinned favourites and quad toggles; the state dir is untouched by config sync. Migrates the old file. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP --- .../hyprlua/astal-menu/backend/weather.sh | 5 +++- desktopenvs/hyprlua/astal-menu/paths.py | 7 ++++- desktopenvs/hyprlua/astal-menu/settings.py | 10 +++++-- .../hyprlua/astal-menu/style/style.css | 1 + .../hyprlua/astal-menu/ui/appdrawer.py | 29 ++++++++++++------- .../hyprlua/astal-menu/ui/favorites.py | 28 +++++++++++++++--- 6 files changed, 62 insertions(+), 18 deletions(-) diff --git a/desktopenvs/hyprlua/astal-menu/backend/weather.sh b/desktopenvs/hyprlua/astal-menu/backend/weather.sh index 9991f61..9426c89 100755 --- a/desktopenvs/hyprlua/astal-menu/backend/weather.sh +++ b/desktopenvs/hyprlua/astal-menu/backend/weather.sh @@ -5,7 +5,10 @@ set -uo pipefail loc="${1:-}" -opts="${2:-0}" +# Note: ${2-0}, not ${2:-0} — the expanded view passes an *empty* opts to request +# wttr.in's default 3-day forecast, and :- would wrongly rewrite that empty string +# to "0" (current conditions only). Default to "0" only when no opts arg is given. +opts="${2-0}" # URL-encode spaces in a city name. loc="${loc// /+}" diff --git a/desktopenvs/hyprlua/astal-menu/paths.py b/desktopenvs/hyprlua/astal-menu/paths.py index f24cebd..aae86e0 100644 --- a/desktopenvs/hyprlua/astal-menu/paths.py +++ b/desktopenvs/hyprlua/astal-menu/paths.py @@ -11,8 +11,12 @@ STYLE_DIR = BASE_DIR / "style" CONFIG_DIR = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "astal-menu" CACHE_DIR = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) / "astal-menu" +# User settings live under XDG_STATE_HOME, NOT CONFIG_DIR: the config-updater does +# `rm -rf ~/.config/astal-menu` on every deploy, which would wipe pinned favourites +# and quad toggles. The state dir is never touched by config sync. +STATE_DIR = Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local" / "state")) / "astal-menu" -SETTINGS_FILE = CONFIG_DIR / "settings.json" +SETTINGS_FILE = STATE_DIR / "settings.json" APP_ID = "eu.abdelbaki.astalmenu" @@ -20,3 +24,4 @@ APP_ID = "eu.abdelbaki.astalmenu" def ensure_dirs() -> None: CONFIG_DIR.mkdir(parents=True, exist_ok=True) CACHE_DIR.mkdir(parents=True, exist_ok=True) + STATE_DIR.mkdir(parents=True, exist_ok=True) diff --git a/desktopenvs/hyprlua/astal-menu/settings.py b/desktopenvs/hyprlua/astal-menu/settings.py index d4ce39d..ff564c8 100644 --- a/desktopenvs/hyprlua/astal-menu/settings.py +++ b/desktopenvs/hyprlua/astal-menu/settings.py @@ -10,7 +10,7 @@ from __future__ import annotations import json from typing import Callable -from paths import SETTINGS_FILE, ensure_dirs +from paths import CONFIG_DIR, SETTINGS_FILE, ensure_dirs class Settings: @@ -21,8 +21,14 @@ class Settings: # -- persistence ------------------------------------------------------- def load(self) -> None: + path = SETTINGS_FILE + if not path.exists(): + # one-time migration from the old CONFIG_DIR location (pre state-dir) + legacy = CONFIG_DIR / "settings.json" + if legacy.exists(): + path = legacy try: - self._data.update(json.loads(SETTINGS_FILE.read_text())) + self._data.update(json.loads(path.read_text())) except (FileNotFoundError, json.JSONDecodeError): pass diff --git a/desktopenvs/hyprlua/astal-menu/style/style.css b/desktopenvs/hyprlua/astal-menu/style/style.css index 101cb90..119f712 100644 --- a/desktopenvs/hyprlua/astal-menu/style/style.css +++ b/desktopenvs/hyprlua/astal-menu/style/style.css @@ -124,6 +124,7 @@ button:checked.quad-action { background: @accent; color: @bg; border-color: @acc } .app-tile:hover { border-color: @accent; background: alpha(@violet, 0.18); } .app-tile label { color: @text; font-size: 10pt; } +.fav-star { color: @accent; font-size: 13pt; margin: 2px 4px; } /* ---- favourites row (top of drawer) --------------------------------- */ .favorites-row { padding: 4px 0; } diff --git a/desktopenvs/hyprlua/astal-menu/ui/appdrawer.py b/desktopenvs/hyprlua/astal-menu/ui/appdrawer.py index 58a9976..f7efe52 100644 --- a/desktopenvs/hyprlua/astal-menu/ui/appdrawer.py +++ b/desktopenvs/hyprlua/astal-menu/ui/appdrawer.py @@ -11,9 +11,9 @@ import gi gi.require_version("Gtk", "4.0") gi.require_version("AstalApps", "0.1") -from gi.repository import AstalApps, Gtk # noqa: E402 +from gi.repository import AstalApps, GLib, Gtk # noqa: E402 -from ui.favorites import Favorites +from ui.favorites import Favorites, add_pin_gestures _STRIP_HEIGHT = 150 # collapsed grid height (logical px) @@ -55,6 +55,12 @@ class AppDrawer(Gtk.Box): self._apply_mode() self._populate(self._sorted_all()) + # re-render tiles when favourites change so the pinned star stays in sync + settings.subscribe(self._on_settings_changed) + + def _on_settings_changed(self) -> None: + # Deferred so we never rebuild the FlowBox from inside a tile's own gesture. + GLib.idle_add(lambda: (self._on_search(self._search), False)[1]) def _build_header(self) -> Gtk.Widget: header = Gtk.CenterBox() @@ -114,6 +120,7 @@ class AppDrawer(Gtk.Box): btn = Gtk.Button(valign=Gtk.Align.START) btn.add_css_class("app-tile") btn.app = app + overlay = Gtk.Overlay() content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) icon = Gtk.Image.new_from_icon_name(app.get_icon_name() or "application-x-executable") icon.set_pixel_size(48) @@ -121,15 +128,17 @@ class AppDrawer(Gtk.Box): justify=Gtk.Justification.CENTER) content.append(icon) content.append(label) - btn.set_child(content) + overlay.set_child(content) + # a star marks pinned apps (visible only when favourited) + star = Gtk.Label(label="★") + star.add_css_class("fav-star") + star.set_halign(Gtk.Align.END) + star.set_valign(Gtk.Align.START) + star.set_visible(self.settings.is_favorite(app.get_entry())) + overlay.add_overlay(star) + btn.set_child(overlay) btn.connect("clicked", lambda *_: self._launch(app)) - # pin / unpin: right-click or long-press - rclick = Gtk.GestureClick(button=3) - rclick.connect("released", lambda *_a: self._toggle_fav(app)) - btn.add_controller(rclick) - longpress = Gtk.GestureLongPress() - longpress.connect("pressed", lambda *_a: self._toggle_fav(app)) - btn.add_controller(longpress) + add_pin_gestures(btn, lambda: self._toggle_fav(app)) return btn def _toggle_fav(self, app) -> None: diff --git a/desktopenvs/hyprlua/astal-menu/ui/favorites.py b/desktopenvs/hyprlua/astal-menu/ui/favorites.py index 995d5ba..d303adc 100644 --- a/desktopenvs/hyprlua/astal-menu/ui/favorites.py +++ b/desktopenvs/hyprlua/astal-menu/ui/favorites.py @@ -14,6 +14,28 @@ gi.require_version("AstalApps", "0.1") from gi.repository import AstalApps, Gtk # noqa: E402 +def add_pin_gestures(widget: Gtk.Widget, on_toggle) -> None: + """Wire right-click and long-press on `widget` to pin/unpin. + + Uses the CAPTURE phase and claims the sequence so the gesture fires reliably on a + Gtk.Button (whose own primary-click gesture would otherwise swallow it) and does + not also trigger the button's launch action.""" + def fire(gesture, *_a) -> None: + on_toggle() + gesture.set_state(Gtk.EventSequenceState.CLAIMED) + + rclick = Gtk.GestureClick(button=3) + rclick.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) + rclick.connect("pressed", fire) + widget.add_controller(rclick) + + longpress = Gtk.GestureLongPress() + longpress.set_touch_only(False) + longpress.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) + longpress.connect("pressed", fire) + widget.add_controller(longpress) + + class Favorites(Gtk.Box): def __init__(self, settings, on_launch): super().__init__(orientation=Gtk.Orientation.VERTICAL) @@ -72,10 +94,8 @@ class Favorites(Gtk.Box): content.append(Gtk.Label(label=app.get_name(), ellipsize=3, max_width_chars=14)) btn.set_child(content) btn.connect("clicked", lambda *_a: self._launch(app)) - # right-click unpins - gesture = Gtk.GestureClick(button=3) - gesture.connect("released", lambda *_a: self.settings.toggle_favorite(app.get_entry())) - btn.add_controller(gesture) + # right-click / long-press unpins + add_pin_gestures(btn, lambda: self.settings.toggle_favorite(app.get_entry())) return btn def _launch(self, app) -> None: