pulled in update

main
Amir Alexander Abdelbaki 2026-07-24 15:36:03 +02:00
parent 8ba33e5d36
commit 9dbe718ba3
3 changed files with 74 additions and 13 deletions

View File

@ -4,6 +4,21 @@ Same pattern as orbit-menu/config.py and horizon-dock/config.py. Deliberately
separate from settings.py's Settings class: that one persists quad enable/ separate from settings.py's Settings class: that one persists quad enable/
feature toggles and favorites (module-scoped, read via ctx.feature()), while feature toggles and favorites (module-scoped, read via ctx.feature()), while
this is a single whole-window flag read once at startup. this is a single whole-window flag read once at startup.
"layouts" narrows/customises the workspace-layout picker in the taskbar panel
(ui/taskbar.py) on top of what hypr/layouts auto-discovers and writes to
~/.cache/astro-menu/layouts.json:
"enabled": list of layout names to offer, in that order (tab order follows
it) omit/null to show everything hypr/layouts discovered.
"overrides": {layout_name: {partial layout spec}} shallow-merged onto the
discovered spec for that layout e.g. trim "dirs" to fewer
directions, or set "fit_method"/"stepper" to false to hide
those controls for that layout, without touching its .lua file.
Example:
"layouts": {
"enabled": ["scrolling", "master", "monocle"],
"overrides": {"scrolling": {"dirs": ["down", "right"]}}
}
""" """
from __future__ import annotations from __future__ import annotations
@ -12,7 +27,7 @@ import json
from paths import CONFIG_FILE, ensure_dirs from paths import CONFIG_FILE, ensure_dirs
_DEFAULTS = {"hologram": True} _DEFAULTS = {"hologram": True, "layouts": {"enabled": None, "overrides": {}}}
def _load() -> dict: def _load() -> dict:
@ -29,3 +44,23 @@ def _load() -> dict:
def hologram_enabled() -> bool: def hologram_enabled() -> bool:
return bool(_load().get("hologram", True)) return bool(_load().get("hologram", True))
def apply_layout_config(layouts: list[dict]) -> list[dict]:
"""Filter/reorder/override the layouts hypr/layouts discovered, per the
"layouts" config key. `layouts` is the parsed layouts.json (or its
hardcoded fallback) see Taskbar._load_layouts."""
cfg = _load().get("layouts") or {}
enabled = cfg.get("enabled")
overrides = cfg.get("overrides") or {}
by_name = {ly["name"]: ly for ly in layouts}
names = enabled if enabled else [ly["name"] for ly in layouts]
out = []
for name in names:
ly = by_name.get(name)
if ly is None:
continue # config names a layout hypr/layouts never discovered
out.append({**ly, **overrides.get(name, {})})
return out

View File

@ -7,7 +7,9 @@ so it *covers* the 2x2 quads rather than pushing anything down. The panel holds:
* workspace/layout controls pick the current workspace's layout (scrolling / * workspace/layout controls pick the current workspace's layout (scrolling /
dwindle / master / monocle, enumerated from ~/.cache/astro-menu/layouts.json, dwindle / master / monocle, enumerated from ~/.cache/astro-menu/layouts.json,
written by hypr/layouts) and, for directional layouts, its direction. Applied written by hypr/layouts) and, for directional layouts, its direction. Applied
live via `hyprctl eval 'layouts.set(ws, name, dir)'`. live via `hyprctl eval 'layouts.set(ws, name, dir)'`. The set of layouts
offered (and their per-layout options) can be narrowed/customised via
config.py's "layouts" key — see config.apply_layout_config.
* a per-window row list: [icon + title focus/jump] [ pull to this workspace]. * a per-window row list: [icon + title focus/jump] [ pull to this workspace].
""" """
@ -19,8 +21,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, Gtk, GLib # noqa: E402
import config
from lib.proc import run_json, run_text from lib.proc import run_json, run_text
from paths import CACHE_DIR from paths import CACHE_DIR
@ -51,6 +54,7 @@ class Taskbar(Gtk.Box):
self._clients: list = [] self._clients: list = []
self._dir_dds = {} self._dir_dds = {}
self._fit_sws = {} self._fit_sws = {}
self._poll_id: int | None = None
# The workspace/window panel. It is NOT appended here: the menu window mounts # The workspace/window panel. It is NOT appended here: the menu window mounts
# `panel_widget` into the quad region so that expanding COLLAPSES this strip's # `panel_widget` into the quad region so that expanding COLLAPSES this strip's
@ -121,10 +125,10 @@ class Taskbar(Gtk.Box):
@staticmethod @staticmethod
def _load_layouts() -> list: def _load_layouts() -> list:
try: try:
return json.loads(_LAYOUTS_MANIFEST.read_text()) layouts = json.loads(_LAYOUTS_MANIFEST.read_text())
except (FileNotFoundError, json.JSONDecodeError): except (FileNotFoundError, json.JSONDecodeError):
# fallback if hypr/layouts hasn't written the manifest yet # fallback if hypr/layouts hasn't written the manifest yet
return [{"name": "scrolling", "label": "Scrolling", "directional": True, layouts = [{"name": "scrolling", "label": "Scrolling", "directional": True,
"dirs": ["down", "up", "right", "left"], "default_dir": "down", "dirs": ["down", "up", "right", "left"], "default_dir": "down",
"fit_method": True}, "fit_method": True},
{"name": "columns", "label": "Columns", "directional": True, {"name": "columns", "label": "Columns", "directional": True,
@ -133,12 +137,32 @@ class Taskbar(Gtk.Box):
{"name": "dwindle", "label": "Dwindle", "directional": False, "dirs": []}, {"name": "dwindle", "label": "Dwindle", "directional": False, "dirs": []},
{"name": "master", "label": "Master", "directional": False, "dirs": []}, {"name": "master", "label": "Master", "directional": False, "dirs": []},
{"name": "monocle", "label": "Monocle", "directional": False, "dirs": []}] {"name": "monocle", "label": "Monocle", "directional": False, "dirs": []}]
return config.apply_layout_config(layouts)
# -- populate ---------------------------------------------------------- # -- populate ----------------------------------------------------------
def refresh(self) -> None: def refresh(self) -> None:
run_json(["hyprctl", "clients", "-j"], self._on_clients) run_json(["hyprctl", "clients", "-j"], self._on_clients)
run_json(["hyprctl", "activeworkspace", "-j"], self._on_ws) run_json(["hyprctl", "activeworkspace", "-j"], self._on_ws)
# -- keep the panel live while the menu is open -------------------------
# Hyprland doesn't push workspace-switch events to us, and switching
# workspaces isn't blocked by the menu being open — so without this the
# "Workspace N layout" panel goes stale the moment you change workspace
# while astro-menu is still up. Poll instead; the window starts/stops
# this alongside show_menu/hide_menu.
def start_polling(self) -> None:
if self._poll_id is None:
self._poll_id = GLib.timeout_add(1000, self._on_poll)
def stop_polling(self) -> None:
if self._poll_id is not None:
GLib.source_remove(self._poll_id)
self._poll_id = None
def _on_poll(self) -> bool:
self.refresh()
return True # keep polling every second while the menu is open
def _on_ws(self, ok: bool, data) -> None: def _on_ws(self, ok: bool, data) -> None:
if ok and isinstance(data, dict): if ok and isinstance(data, dict):
self._active_ws = data.get("id") self._active_ws = data.get("id")

View File

@ -192,6 +192,7 @@ class MenuWindow(Gtk.ApplicationWindow):
def show_menu(self, focus_appdrawer: bool = False) -> None: def show_menu(self, focus_appdrawer: bool = False) -> None:
self.appdrawer.set_expanded(False) self.appdrawer.set_expanded(False)
self.taskbar.refresh() self.taskbar.refresh()
self.taskbar.start_polling()
self.grid.on_show() self.grid.on_show()
self.appdrawer.on_show() self.appdrawer.on_show()
self.set_visible(True) self.set_visible(True)
@ -204,6 +205,7 @@ class MenuWindow(Gtk.ApplicationWindow):
self._hologram.start_intro() self._hologram.start_intro()
def hide_menu(self) -> None: def hide_menu(self) -> None:
self.taskbar.stop_polling()
self.grid.on_hide() self.grid.on_hide()
self.taskbar.collapse_panel() self.taskbar.collapse_panel()
self.grid.hide_takeover() self.grid.hide_takeover()