diff --git a/astro-menu/config.py b/astro-menu/config.py index 96f59b3..ad94e3f 100644 --- a/astro-menu/config.py +++ b/astro-menu/config.py @@ -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/ feature toggles and favorites (module-scoped, read via ctx.feature()), while 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 @@ -12,7 +27,7 @@ import json from paths import CONFIG_FILE, ensure_dirs -_DEFAULTS = {"hologram": True} +_DEFAULTS = {"hologram": True, "layouts": {"enabled": None, "overrides": {}}} def _load() -> dict: @@ -29,3 +44,23 @@ def _load() -> dict: def hologram_enabled() -> bool: 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 diff --git a/astro-menu/ui/taskbar.py b/astro-menu/ui/taskbar.py index f6abbaa..bb0e3e2 100644 --- a/astro-menu/ui/taskbar.py +++ b/astro-menu/ui/taskbar.py @@ -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 / dwindle / master / monocle, enumerated from ~/.cache/astro-menu/layouts.json, 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]. """ @@ -19,8 +21,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, Gtk, GLib # noqa: E402 +import config from lib.proc import run_json, run_text from paths import CACHE_DIR @@ -51,6 +54,7 @@ class Taskbar(Gtk.Box): self._clients: list = [] self._dir_dds = {} self._fit_sws = {} + self._poll_id: int | None = None # 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 @@ -121,24 +125,44 @@ class Taskbar(Gtk.Box): @staticmethod def _load_layouts() -> list: try: - return json.loads(_LAYOUTS_MANIFEST.read_text()) + layouts = json.loads(_LAYOUTS_MANIFEST.read_text()) except (FileNotFoundError, json.JSONDecodeError): # fallback if hypr/layouts hasn't written the manifest yet - return [{"name": "scrolling", "label": "Scrolling", "directional": True, - "dirs": ["down", "up", "right", "left"], "default_dir": "down", - "fit_method": True}, - {"name": "columns", "label": "Columns", "directional": True, - "dirs": ["right", "down"], "dir_labels": ["Left / Right", "Up / Down"], - "default_dir": "right", "fit_method": True}, - {"name": "dwindle", "label": "Dwindle", "directional": False, "dirs": []}, - {"name": "master", "label": "Master", "directional": False, "dirs": []}, - {"name": "monocle", "label": "Monocle", "directional": False, "dirs": []}] + layouts = [{"name": "scrolling", "label": "Scrolling", "directional": True, + "dirs": ["down", "up", "right", "left"], "default_dir": "down", + "fit_method": True}, + {"name": "columns", "label": "Columns", "directional": True, + "dirs": ["right", "down"], "dir_labels": ["Left / Right", "Up / Down"], + "default_dir": "right", "fit_method": True}, + {"name": "dwindle", "label": "Dwindle", "directional": False, "dirs": []}, + {"name": "master", "label": "Master", "directional": False, "dirs": []}, + {"name": "monocle", "label": "Monocle", "directional": False, "dirs": []}] + return config.apply_layout_config(layouts) # -- populate ---------------------------------------------------------- def refresh(self) -> None: run_json(["hyprctl", "clients", "-j"], self._on_clients) 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: if ok and isinstance(data, dict): self._active_ws = data.get("id") diff --git a/astro-menu/window.py b/astro-menu/window.py index 4f4ca3b..77f306c 100644 --- a/astro-menu/window.py +++ b/astro-menu/window.py @@ -192,6 +192,7 @@ class MenuWindow(Gtk.ApplicationWindow): def show_menu(self, focus_appdrawer: bool = False) -> None: self.appdrawer.set_expanded(False) self.taskbar.refresh() + self.taskbar.start_polling() self.grid.on_show() self.appdrawer.on_show() self.set_visible(True) @@ -204,6 +205,7 @@ class MenuWindow(Gtk.ApplicationWindow): self._hologram.start_intro() def hide_menu(self) -> None: + self.taskbar.stop_polling() self.grid.on_hide() self.taskbar.collapse_panel() self.grid.hide_takeover()