fix(astal-menu): working favourites, 3-day weather, deploy-safe settings
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
feat/astal-menu
parent
16f75edb13
commit
3aafd2cc17
|
|
@ -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// /+}"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue