feat(hyprdrive): configurable/live-polled workspace layout picker in astro-menu
config.json gains a "layouts" key to narrow which hypr/layouts-discovered layouts astro-menu offers (and reorder them), plus per-layout option overrides (trim directions, hide fit_method/stepper), without touching the .lua layout sources. Also: the taskbar's workspace/layout panel now polls every second while the menu is open, since switching workspaces doesn't notify (or get blocked by) the menu and the panel previously only refreshed on open. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>main
parent
008e4d980c
commit
7ebff0d2a4
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,10 +125,10 @@ 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,
|
||||
layouts = [{"name": "scrolling", "label": "Scrolling", "directional": True,
|
||||
"dirs": ["down", "up", "right", "left"], "default_dir": "down",
|
||||
"fit_method": True},
|
||||
{"name": "columns", "label": "Columns", "directional": True,
|
||||
|
|
@ -133,12 +137,32 @@ class Taskbar(Gtk.Box):
|
|||
{"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")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue