# astal-menu

A touch-friendly GTK4 popup control centre for the hyprlua desktop, replacing
`nwg-dock` and `nwg-drawer`. Triggered from the EWW top bar (or `Super+D`).

Layout: a **2×2 quad grid** of feature modules over a full-width **application
drawer**. Any quad can expand to overlay the other three; the drawer can expand to
the bottom of the screen. A single margined root box letterboxes the menu identically
in every state.

## Stack

- **Frontend:** Python + PyGObject + **GTK4**, as a `wlr-layer-shell` surface
  (`gtk4-layer-shell`). Chosen over Lua because lgi/Astal-Lua only support GTK3.
- **Location map:** a static OSM image stitched from tiles (`backend/staticmap.py`,
  Pillow). libshumate does not paint tiles in this environment (the official
  `shumate-demo` shows the same blank map), though tile downloads work — hence the
  static fallback.
- **Services:** Astal GObject libraries via introspection — `AstalNetwork`,
  `AstalBluetooth`, `AstalApps` — plus our own IP-geolocation singleton.
- **Backends:** Python/Bash scripts in `backend/` (JSON on stdout), run async via
  `Gio.Subprocess` so nothing blocks the UI.
- **Theme:** `style/_colors.css` (`@define-color`, generated from
  `~/Dotfiles/colors.conf` by `apply-theme.sh`) + `style/style.css`.

## Running

`main.py` is single-instance. The autostart launches a hidden resident daemon;
verbs are forwarded over D-Bus:

    scripts/astal-menu-start.sh          # resident daemon (hidden); sets LD_PRELOAD
    scripts/menu-toggle.sh               # --toggle
    scripts/menu-toggle.sh appdrawer     # open with the app drawer expanded

`astal-menu-start.sh` must `LD_PRELOAD` libgtk4-layer-shell (it loads after
libwayland under PyGObject otherwise).

## Adding a module

1. Create `modules/<name>.py` exposing a top-level `SPEC = ModuleSpec(...)` whose
   `build(ctx)` returns a `ModuleInstance(compact=…, expanded=…, …)`.
   - `compact` shows in the 2×2 cell; a distinct `expanded` widget enables the
     expand button (they must be separate instances — GTK widgets have one parent).
   - Declare per-feature toggles via `features=[Feature("id", "Label", default)]`;
     read them with `ctx.feature("id")`. A disabled quad never calls `build()`.
   - Shared state (network, bluetooth, location) is on `ctx.services`.
2. Append its `SPEC` to `ALL_SPECS` in `registry.py`. Nothing else changes.

## Files

    main.py          app + single-instance IPC        window.py    layer-shell + letterbox
    registry.py      module list (extension seam)      settings.py  JSON toggles/order
    module_base.py   ModuleSpec / ModuleInstance / ModuleContext
    appservices.py   shared Astal services + location
    ui/              quadcard, quadgrid, appdrawer
    modules/         location, weather, bluetooth, network
    services/        location (geolocation singleton)
    lib/             ansi (SGR→TextTag), proc (async subprocess)
    backend/         geolocate.py, weather.sh, network.sh
    style/           _colors.css, style.css
