- Installer: add python-pillow (map tile stitching) and networkmanager (the Network
quad's nmcli backend) so a fresh install has a working map and network panel.
- Super+D binds explicitly to `menu-toggle.sh toggle top` so it pops in from the top.
The astal-menu config (incl. new backend/nm.py, ui/statsbar.py, etc.) already ships
via the config-updater CONFIGS list and is re-synced by sysupdate.sh; settings live
in XDG_STATE_HOME so updates don't wipe them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
- Add a minimalist system-stats line at the top of the panel: CPU / MEM / GPU / disk
utilisation + network down/up rate, read straight from /proc and /sys on a 2s timer
(no subprocess). ui/statsbar.py.
- Opening side is selectable: `main.py --side top|bottom|left|right` (also via
`menu-toggle.sh <verb> <side>` or just `menu-toggle.sh <side>`). The panel anchors
flush to that edge, and the compositor's layer slide brings it in from there.
Unified all layer anchoring into MenuWindow._apply_anchor.
- App drawer now does a bouncy pop-expand (CSS scale, play-once guard like the quads).
- Tighten the action-pill buttons vertically (padding 6->1px, min-height 32->22) so
they stop wasting vertical space.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
The Location map was a fixed static image with no way to move the viewport, on the
light OSM tiles. Now:
- Drag to pan (re-renders at the new centre; the pin stays anchored to the real
location and clamps to the edge when panned away), scroll to zoom, double-click to
recentre. Panning/zooming coalesce into ~11 renders/sec.
- Tiles default to the dark CartoDB "night" map (fits the theme); a new "Satellite
view" feature toggle switches to Esri World Imagery. staticmap.py gained a style
arg + a separate marker coordinate, and caches tiles per style.
Verified: dark + satellite render, drag re-centres the view (pin goes off-centre),
and the coordinate round-trip is exact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
- Connect flow now shows real status: a spinner + Cancel while connecting, and a red
"failed" tag + Retry if the attempt stops without linking (via notify::connecting
going false) or a 25s timeout backstop fires. Previously Connect gave no feedback
and could look stuck forever.
- Add a Forget button on paired devices (adapter.remove_device) to unpair them.
- Track per-device pending/failed state so the row reflects connecting/failed/
connected/paired accurately.
Verified the paired row renders Forget + Connect and the connected row renders
Forget + Disconnect.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
The device rows already render a "Disconnect" button when a device is connected,
but the list only rebuilt on adapter-level notify::devices / notify::is-powered —
not on per-device state changes. So a device connected (via the menu or elsewhere)
while the menu was open kept its "Connect" button and offered no way to disconnect.
Hook each device's notify::connected/connecting/paired (once each) to refresh the
list, so a connected device flips to "Disconnect".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
Switches sat with the default FILL vertical alignment, so in taller rows (e.g. the
Bluetooth Power switch in the expanded card's header) they stretched to the row
height and rendered as a squared/vertical blob. Give every Gtk.Switch
valign=CENTER so it keeps its fixed pill height regardless of the row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
The .quad-pop CSS animation stayed applied for the whole time a quad was expanded,
so every re-layout of the expanded card (module content streaming in, the compact
grid updating beneath the overlay) restarted the transform animation — making the
pop repeat continuously. Remove the class ~340ms after expanding (just past the
300ms animation) so it plays exactly once and no later re-layout can retrigger it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
- Main open: enable Hyprland's `layers` animation with the bouncy myBezier curve and
a slide style, so the layer-shell control centre slides in from the top edge with a
little overshoot instead of just appearing. (Also gives notifications/bar the same
springy slide, which suits the DE.) A per-namespace layerrule was tried first but
this hyprlua build doesn't honour its animation timing.
- Quad expand: a CSS @keyframes scale-bounce (scale 0.82 -> 1.05 -> 1.0) added to the
expanded card on expand and cleared on collapse so it replays every time. Verified
GTK4 CSS transforms/keyframes render on this build, and that the card scales during
the animation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
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
- Switches: give them a fixed pill with a bordered round knob (the reset theme
left them shapeless).
- Close (x) button: inset with a margin so it no longer overlaps the drawn border.
- Height: the panel overflowed the monitor. Trim the map compact height, quad
min-height, card padding, grid/panel spacing, top margin, and drawer strip so it
fits (≈1600 -> ≈1310 logical on a 1440 display).
- App drawer expand button: use the same nf-fa-expand/compress glyphs as the module
expand buttons instead of chevrons, so all expand controls look uniform.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
Network expanded view is rewritten into four tabs over a new nmcli backend
(backend/nm.py, JSON in/out):
- Adapters: each device foldout with editable IPv4 and IPv6 — Automatic/DHCP
toggle, address, a linked subnet-mask <-> CIDR pair (edit either, the other
follows), gateway; applied via `nmcli connection modify/up`. Manual fields dim
when DHCP is on.
- Routes: the kernel routing table (read-only; editing kernel routes needs root).
- VLAN: pick a parent device + ID to create/bring up a VLAN, list + activate/delete.
- DNS: effective servers per device + a per-connection override (servers +
ignore-automatic).
Verified reads and the write path end-to-end (VLAN add/delete via nmcli; each tab
rendered in a harness).
Also: flatten the settings-cog MenuButton's inner `button` node so it shows one
ring instead of two, and drop the border on the close (x) button.
Weather keeps its wttr.in 3-day forecast (unchanged, per request).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
- Borders: with the app stylesheet now above the theme, the CSS `.quad-card`
border rendered a second ring concentric with the Cairo bordered() ring. Drop the
CSS border/background/radius from the card boxes so only the Cairo card shows.
- Bluetooth: the power/scan/connect controls were correctly wired to AstalBluetooth,
but the adapter was rfkill soft-blocked, making set_powered a silent no-op. The
power toggle now runs `rfkill unblock bluetooth` before powering on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
The panel showed a solid #1a1a1a slab between modules. Root cause: the CyberQueer
GTK theme is installed as ~/.config/gtk-4.0/gtk.css (a symlink), which GTK4 loads
at PRIORITY_USER (800) — above our PRIORITY_APPLICATION (600) — and its reset rule
`* { background-color: #1a1a1a }` painted every node, including the structural
containers between modules. Our transparency overrides silently lost the cascade.
Fixes:
- theme.py: load the app stylesheets at USER+1 so they sit just above the
user-level theme symlink and actually win.
- style.css: blank the structural container nodes (window/.panel/overlay/revealer/
grid/scrolledwindow/viewport/drawingarea) so the desktop shows between modules.
- window.py/quadgrid.py: draw each module's card background with the Cairo
bordered(fill_bg=True) so cards stay opaque (incl. rounded corners) while the
gaps go transparent — this GTK build doesn't paint CSS backgrounds on containers,
hence the Cairo fill.
Popovers/buttons keep their backgrounds (the theme's rule still covers those nodes;
only structural containers are overridden). Verified visually end-to-end.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
The Open-windows taskbar dispatched `hyprctl dispatch focuswindow address:…`,
but this is a hyprlua setup where `hyprctl dispatch` evaluates its argument as
Lua — so the native dispatcher syntax raised a Lua parse error and nothing
happened. Switch to `hl.dsp.focus({ window = "address:…" })`, the hyprlua focus
dispatcher, which focuses the window and switches to its workspace. Verified it
moves across workspaces end-to-end.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
astal-menu-start.sh LD_PRELOADs libgtk4-layer-shell so it loads before
libwayland-client. That preload is only needed at the daemon's own exec — the
library is resident afterwards and the variable is never re-read — but every app
started via AstalApps.launch() inherited it, and Firefox aborts at startup with
libgtk4-layer-shell preloaded (gdk_display_manager_get() before gtk_init()).
main.py now drops LD_PRELOAD from its environment right after startup, so
launched GUI apps (and backend subprocesses) run with a clean env. Verified the
layer-shell library stays mapped and the menu surface still maps afterwards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
Widget settings popover toggles now take effect immediately. QuadCard only
rebuilt a card when its Enabled state flipped, so per-module feature toggles
(Wi-Fi, discovery, routes, CLI art, …) were persisted but never applied. The
card now rebuilds whenever any of its own feature values change, and the
incomplete partial per-module refresh callbacks (which also leaked a dead
subscription per rebuild) are dropped in favour of that single path.
Make the backmost surface transparent so each module floats on its own drawn
border/background: force `window`/`.background`/`.panel` transparent, since a
plain `.menu-window` rule did not override the GTK theme's solid window node.
Locate via IP now works and uses traceroute: geolocate.py traceroutes to
1.1.1.1, takes the first globally-routable hop (the ISP egress) and resolves it
through a public geolocation API, falling back to self-IP when traceroute is
missing or finds no public hop. The ip_locate toggle gates the lookup entirely
(placeholder when off). Adds `traceroute` to the hyprlua package list.
Autostart: `sleep 1 && hyprctl reload` after all spawns so layer-shell clients
settle on the final config.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
A touch-friendly GTK4 popup control centre (Python + PyGObject, wlr-layer-shell)
launched from the EWW bar or Super+D. Replaces nwg-dock and nwg-drawer.
Layout: a floating, drawn-bordered panel (anchored top-centre so it never blocks
the rest of the screen) with a 2x2 quad grid over a full-width app drawer, plus a
top taskbar. Any quad expands over the others; the drawer expands to the bottom.
Modules:
- Location — static OSM map (backend/staticmap.py) on IP geolocation
(libshumate won't paint tiles in this env; shumate-demo is blank too)
- Weather — wttr.in ANSI art rendered via an SGR parser into a TextView
- Bluetooth — AstalBluetooth: discovery/connect/disconnect + local history
- Network — AstalNetwork wifi + nmcli/ip/ss for IP, DHCP/manual, routes, ports,
public IP; per-feature toggles
- Taskbar — open windows from hyprctl, grouped by app with pop-out per instance
- Favorites — pinned apps (right-click/long-press to pin) atop the app drawer
Notes:
- Frontend is Python/GTK4, not Lua: lgi/Astal-Lua are GTK3-only.
- Module borders are drawn with a Cairo overlay (lib/border.py); this GTK build's
renderer skips CSS border/background on plain container widgets.
- Consumes Astal GObject libs (io/apps/network/bluetooth) via introspection.
Wiring: autostart + Super+D/Super+Shift+A binds, EWW launcher button in all three
bar variants, apply-theme.sh (_colors.css), installer packages, config-updater.
drawer.sh moved to scripts/deprecated/.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dialog was landing off-cursor and at ~half-screen. Two Hyprland
gotchas were fighting each other: `move` is evaluated against the
window's natural size and `size` is applied afterwards from the top-left,
so a cursor_x-(window_w/2) move centred the pre-shrink window. zenity's
file chooser also ignores --width/--height and has a ~738x363 GTK
minimum.
Fix the size at 760x460 and hard-code the move offsets to half that
(380x230) so the shrunk window lands exactly on the pointer. Drop the
dead zenity --width/--height sizing from the script.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a size rule alongside the move so the zenity dialog no longer opens
at its default ~50% size. zenity clamps to its GTK minimum (~738x363),
which is well under half the screen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename the at-cursor rule to filepicker and move the dialog to a fixed
position 10% into the display (from the top-left) instead of following
the cursor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a dedicated at-cursor window rule (float + move to cursor) and point
the Super+F / Super+Shift+F filepicker binds at it, so the zenity dialog
opens centered on the mouse instead of mid-screen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Prefix the Super+F / Super+Shift+F filepicker binds with [tag +centered]
so the zenity dialog spawns as a centered floating window (60% x 60%),
matching the centered rule in windowrules.lua. Same tagged-spawn
mechanism as Super+U.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add filepicker-clipboard.sh: a zenity file dialog that copies the
selected file's path (Super+F) or contents (Super+Shift+F) to the
clipboard via wl-copy. Path mode collapses $HOME to ~.
Repoints Super+F / Super+Shift+F from wofi-file-search/foldersearch
(kept as comments) in usr/binds.lua.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diagnosed live in a VM: the bar rendered twice (two stacked rows). `hyprctl
layers` showed two `gtk-layer-shell` bar surfaces owned by two different eww
pids — an `eww daemon` and an `eww open bar`. The monitor count was correctly 1,
so the per-monitor loop was not at fault.
Root cause: ewwstart.sh started `eww daemon` and then immediately ran `eww open`
in a loop. The first `eww open` frequently ran before the freshly-started daemon
was ready, so it spawned its OWN second daemon and drew a second bar.
Fix (all three DE variants — hyprlua, hyprland, niri):
- Drop the separate `eww daemon`; rely on `eww open` to auto-start the daemon and
block until the window is mapped, so the first call establishes a single daemon
that the rest reuse (no race, no second daemon).
- killall eww + short settle before opening, so a reload starts clean.
- hyprlua/hyprland: count monitors via `hyprctl monitors -j | jq length` instead
of the fragile `hyprctl monitors | grep ID | wc -l`.
- export GTK_THEME (the bare assignment never took effect).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R5kHioUMK3mtf2eiLEozCM
Same Vicinae theming bug as hyprlua, now fixed across the remaining DE modules:
- Vicinae loads named custom themes only from ~/.local/share/vicinae/themes/*.toml,
not ~/.config/vicinae/. Both modules now deploy cyberqueer.toml there so
settings.json's theme.dark.name="cyberqueer" resolves instead of falling back
to the stock dark theme.
- hyprland: the source had settings.json double-nested at vicinae/vicinae/, so it
deployed to ~/.config/vicinae/vicinae/settings.json and was never read. Flattened
the source to vicinae/{settings.json,cyberqueer.toml} (matching hyprlua), so it
now lands at ~/.config/vicinae/settings.json.
- niri/vicinae is a symlink to hyprlua/vicinae and niri.sh uses `cp -rL`, so its
settings.json was already correct; only the theme-deploy line was needed.
Verified by simulating both deploys into a temp HOME: final layout matches the
known-good hyprlua result.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R5kHioUMK3mtf2eiLEozCM
Target ~/.config/hypr/usr/monitors.lua (the config Hyprland actually
loads) so `hyprctl reload` applies layouts immediately; the Dotfiles
repo copy stays the deploy source and is no longer written.
Add overlap geometry helpers and integrate them into the apply flow:
- block moves that would drive a monitor into a neighbor (TUI coords)
- snap positions to the MOVE_STEP_FINE grid to avoid frozen digits
- auto-resolve snap-induced collisions by re-reading the live layout
and nudging the moved monitor clear, up to MAX_RESOLVE_ITERS
- warn on residual overlap after apply and after save/reload
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the machine-specific DP-1/DP-3/HDMI-A-1 layout with a wildcard
catch-all rule (preferred mode, auto position, auto scale) that brings up
any connected output. Per-machine layout stays the job of monitor-manager,
which overwrites this file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Installs ydotool via pacman and OpenDeck via Flatpak, wires ydotoold
and OpenDeck into the Hyprland autostart. Registers the module in the
TUI installer, answerfile generator, and docs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Steps taken:
- Added overlay/airootfs/etc/os-release with NAME="M-Archy", ID=m-archy,
ID_LIKE=arch so fastfetch and other tools show the correct distro name while
keeping pacman/AUR helpers happy via ID_LIKE.
- Added overlay/airootfs/etc/fastfetch/config.jsonc — system-wide fastfetch
config equivalent to:
--logo-type file --logo /etc/fastfetch/m-archy-SPC.txt
--logo-color-1 red --logo-color-2 red --color red
(keys + title colored red, custom file logo, no other defaults changed).
- Added overlay/airootfs/etc/fastfetch/m-archy-SPC.txt — copy of the pin.txt
ASCII logo; lands on the live ISO via the existing cp -r overlay/airootfs/
step in build.sh, no build.sh or profiledef.sh changes needed.
- Renamed resources/pin.txt → resources/m-archy-SPC.txt and updated all
references in .bashrc, .zshrc, and both kitty/bash-remoteconf files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
logical_width/height now divide physical pixels by scale so the canvas
correctly represents the compositor's coordinate space (e.g. 3840px @1.5x
is 2560 logical px wide, not 3840).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The merge agent reverted the switch from usr.monitors to root-level monitors
(managed by hyprmoncfg). Restore main's version and update the surrounding
comment to explain the new arrangement.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces fixed 0.25 increments with mathematically valid scales p/q
(lowest terms, q≤6) where both width/s and height/s are integers.
For 1920x1200 this gives 25 steps including 2.4, matching what
Hyprland actually applies — no more mismatch between TUI display
and live value.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
hyprctl returns mirrorOf:"none" (string) for non-mirrored monitors.
Python treated it as truthy, causing apply_monitor to always emit a
mirror command, resetting resolution and scale on every keypress.
Also restores monitors.lua with correct mode/scale/transform fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Python curses TUI for managing Hyprland monitors interactively:
- Canvas shows monitors as boxes at their real relative positions
- Tab/Shift+Tab to cycle selection; hjkl/HJKL to move (50/10 px)
- u/i to rotate CCW/CW; n/N to cycle display modes live
- m to mirror (pick target with Tab, confirm with Enter)
- s saves to hypr/usr/monitors.lua atomically
- Scale cached and only recomputed on resize or viewport overflow
- Bound to Super+Shift+M as a centered-L floating kitty popup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- caffeine: inhibit only `idle`, not `sleep`, so lid close still locks
- binds: lid-close unconditionally calls hyprlock; lid-open does dpms on
- hypridle: reduce lock timeout from 3 min to 2.5 min (150 s)
- eww (all 3 variants): add caffeine toggle button (☕/) with tooltip
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Brio (and most UVC webcams) expose a second /dev/video* node that is
metadata-capture only. The chooser listed it, the existence-only guard
accepted it, and howdy aborted with a bare exit 1 because that node can
never deliver a frame.
- list_cameras: skip nodes with no capture pixel formats (is_capture_device)
- howdy_camera_ok: require an actual capture node, not just an existing path,
so a stale metadata-node device_path triggers reconfiguration
- howdy_add: tee howdy's output and surface the real reason on failure
instead of only the exit code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enrollment failed because howdy's own config.ini kept device_path=none,
so VideoCapture aborted before ever opening the lens (generic exit 1).
The presence-detect camera setting is separate and never reached howdy.
- add a "Howdy face auth — configure camera" menu entry that writes
device_path into /usr/lib/security/howdy/config.ini
- guard howdy_add: detect an unset/stale device_path and offer to fix it
before enrolling, instead of surfacing howdy's opaque exit 1
- shared camera chooser shows a live still from the selected node via
kitty's icat (gated on KITTY_WINDOW_ID), used for presence + howdy
- support non-IR colour webcams: detect IR vs colour from the preview's
saturation and tune recording_plugin (ffmpeg) + dark_threshold per type
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>