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
|
set -uo pipefail
|
||||||
|
|
||||||
loc="${1:-}"
|
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.
|
# URL-encode spaces in a city name.
|
||||||
loc="${loc// /+}"
|
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"
|
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"
|
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"
|
APP_ID = "eu.abdelbaki.astalmenu"
|
||||||
|
|
||||||
|
|
@ -20,3 +24,4 @@ APP_ID = "eu.abdelbaki.astalmenu"
|
||||||
def ensure_dirs() -> None:
|
def ensure_dirs() -> None:
|
||||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
CACHE_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
|
import json
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from paths import SETTINGS_FILE, ensure_dirs
|
from paths import CONFIG_DIR, SETTINGS_FILE, ensure_dirs
|
||||||
|
|
||||||
|
|
||||||
class Settings:
|
class Settings:
|
||||||
|
|
@ -21,8 +21,14 @@ class Settings:
|
||||||
|
|
||||||
# -- persistence -------------------------------------------------------
|
# -- persistence -------------------------------------------------------
|
||||||
def load(self) -> None:
|
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:
|
try:
|
||||||
self._data.update(json.loads(SETTINGS_FILE.read_text()))
|
self._data.update(json.loads(path.read_text()))
|
||||||
except (FileNotFoundError, json.JSONDecodeError):
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
pass
|
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:hover { border-color: @accent; background: alpha(@violet, 0.18); }
|
||||||
.app-tile label { color: @text; font-size: 10pt; }
|
.app-tile label { color: @text; font-size: 10pt; }
|
||||||
|
.fav-star { color: @accent; font-size: 13pt; margin: 2px 4px; }
|
||||||
|
|
||||||
/* ---- favourites row (top of drawer) --------------------------------- */
|
/* ---- favourites row (top of drawer) --------------------------------- */
|
||||||
.favorites-row { padding: 4px 0; }
|
.favorites-row { padding: 4px 0; }
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@ import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "4.0")
|
gi.require_version("Gtk", "4.0")
|
||||||
gi.require_version("AstalApps", "0.1")
|
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)
|
_STRIP_HEIGHT = 150 # collapsed grid height (logical px)
|
||||||
|
|
||||||
|
|
@ -55,6 +55,12 @@ class AppDrawer(Gtk.Box):
|
||||||
|
|
||||||
self._apply_mode()
|
self._apply_mode()
|
||||||
self._populate(self._sorted_all())
|
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:
|
def _build_header(self) -> Gtk.Widget:
|
||||||
header = Gtk.CenterBox()
|
header = Gtk.CenterBox()
|
||||||
|
|
@ -114,6 +120,7 @@ class AppDrawer(Gtk.Box):
|
||||||
btn = Gtk.Button(valign=Gtk.Align.START)
|
btn = Gtk.Button(valign=Gtk.Align.START)
|
||||||
btn.add_css_class("app-tile")
|
btn.add_css_class("app-tile")
|
||||||
btn.app = app
|
btn.app = app
|
||||||
|
overlay = Gtk.Overlay()
|
||||||
content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
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 = Gtk.Image.new_from_icon_name(app.get_icon_name() or "application-x-executable")
|
||||||
icon.set_pixel_size(48)
|
icon.set_pixel_size(48)
|
||||||
|
|
@ -121,15 +128,17 @@ class AppDrawer(Gtk.Box):
|
||||||
justify=Gtk.Justification.CENTER)
|
justify=Gtk.Justification.CENTER)
|
||||||
content.append(icon)
|
content.append(icon)
|
||||||
content.append(label)
|
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))
|
btn.connect("clicked", lambda *_: self._launch(app))
|
||||||
# pin / unpin: right-click or long-press
|
add_pin_gestures(btn, lambda: self._toggle_fav(app))
|
||||||
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)
|
|
||||||
return btn
|
return btn
|
||||||
|
|
||||||
def _toggle_fav(self, app) -> None:
|
def _toggle_fav(self, app) -> None:
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,28 @@ gi.require_version("AstalApps", "0.1")
|
||||||
from gi.repository import AstalApps, Gtk # noqa: E402
|
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):
|
class Favorites(Gtk.Box):
|
||||||
def __init__(self, settings, on_launch):
|
def __init__(self, settings, on_launch):
|
||||||
super().__init__(orientation=Gtk.Orientation.VERTICAL)
|
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))
|
content.append(Gtk.Label(label=app.get_name(), ellipsize=3, max_width_chars=14))
|
||||||
btn.set_child(content)
|
btn.set_child(content)
|
||||||
btn.connect("clicked", lambda *_a: self._launch(app))
|
btn.connect("clicked", lambda *_a: self._launch(app))
|
||||||
# right-click unpins
|
# right-click / long-press unpins
|
||||||
gesture = Gtk.GestureClick(button=3)
|
add_pin_gestures(btn, lambda: self.settings.toggle_favorite(app.get_entry()))
|
||||||
gesture.connect("released", lambda *_a: self.settings.toggle_favorite(app.get_entry()))
|
|
||||||
btn.add_controller(gesture)
|
|
||||||
return btn
|
return btn
|
||||||
|
|
||||||
def _launch(self, app) -> None:
|
def _launch(self, app) -> None:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue