Commit Graph

139 Commits (dce26b2dcfc5f27a3dbb87677569ec60d28045f3)

Author SHA1 Message Date
Amir Alexander Abdelbaki dce26b2dcf feat(terminals): deploy CyberQueer palette across kitty/alacritty everywhere
hyprland's kitty theme was still the stock template (only 5 colors set,
ANSI colors commented out); brought it in line with hyprdrive/hyprlua's
full neon 16-color palette and dropped the now-redundant inline cursor
overrides in kitty.conf. Alacritty had no color scheme at all in any DE,
so added matching [colors] blocks to hyprdrive/hyprlua/hyprland so it
renders identically to kitty/ghostty. Registered alacritty.toml and
ghostty/config in apply-theme.sh so future colors.conf edits propagate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 10:01:35 +02:00
Amir Alexander Abdelbaki 2d7336c593 feat(hyprlua/kitty): finish the CyberQueer kitty theme
Same fix as hyprdrive's kitty (6c07c10): the 16 ANSI colors were still
stock template defaults clashing with the violet/crimson palette used
everywhere else in the suite. Fill them in from colors.conf plus a
few new neon tints in the same hue family, set background to the
real #1A1A1A instead of pure black, add matching border/bell/mark
colors, and drop the stale cursor-color pair in kitty.conf that the
theme include was silently overriding anyway.
2026-07-20 08:56:52 +02:00
Amir Alexander Abdelbaki 9d59565469 revert(hyprlua): remove Cosmonaut Shell components, restore pre-Friday state
orbit-menu and horizon-dock (and their eww-bar launcher buttons, autostart
hooks, and O-bind wrapping) were built inside hyprlua before hyprdrive
existed as a separate DE. hyprdrive is now the home for the full Cosmonaut
Shell suite, so hyprlua drops back to eww bars + vicinae launcher + the
astal-menu pop-open menu, unchanged, matching last Thursday's state.
hyprdrive itself is untouched.
2026-07-20 08:37:12 +02:00
Amir Alexander Abdelbaki 90375a5733 feat(hyprdrive): add hyprdrive DE with full Cosmonaut Shell suite
New leaner Hyprland DE (setup/modules/Desktop-Environments/hyprdrive.sh),
a sibling of hyprlua that drops the older EWW/nwg-shell/wofi/walker/
ulauncher stack in favor of a purpose-built, GTK4 layer-shell component
suite sharing one CyberQueer hologram visual language:

- orbit-menu    radial power/utility menu (focal-point zoom nav, holographic
                overlay, cosmetic satellites)
- horizon-dock  hover-scrollable orbital app dock (open windows/favorites/
                all apps orbits, SNI tray satellite)
- astro-menu    astal-menu re-themed as a hologram info display (Location/
                Weather/Bluetooth/Network quads, taskbar, app drawer)
- station-bar   the EWW top bar's replacement ("Voidstation Status Bar"):
                a thin curved layer-shell bar tracking Hyprland workspaces/
                active window natively over .socket2.sock, no polling

Every component now shares one consistent hologram treatment: translucent
violet panel/node fills (replacing solid gray), a config-toggleable
scanline+sweep+noise overlay (lib/hologram.py, ported to horizon-dock and
station-bar to match orbit-menu/astro-menu), and glow-on-hover throughout.

Also adds horizon-dock to hyprlua (built there first, before hyprdrive
existed) and wires dependency checks, config-updater entries, and
apply-theme.sh color sync for every new component across both DEs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nu8JkQ95hPAT8vADeGDAmL
2026-07-17 15:28:07 +02:00
Amir Alexander Abdelbaki 4fbffee5a0 feat(hyprlua/orbit-menu): sci-fi zoom navigation + holographic overlay + opening intro
Replaces the plain Gtk.Stack crossfade with a focal-point zoom (Gtk.Fixed +
Gsk.Transform): the clicked node's screen position becomes a pivot the parent
page zooms into while the child page slides in from that exact spot and grows
to become the new center — like a moon becoming the planet you now orbit.
Going back reverses the same motion around the same remembered point
(_stack_origins tracks it per depth).

Adds a holographic overlay (config.json "hologram" toggle, default on):
violet scanlines + a looping sweep band + magenta/red noise specs with real
per-particle lifetimes (fade in/out, not single-frame flicker), all composited
through a radial mask so it reads as one soft-edged glow over the menu rather
than tinting the whole square canvas.

Adds an opening-only intro (never plays between nodes): each button starts
invisible and fades in on a staggered cascade — center first, then ring nodes
in order — with a bright noise burst flashing at each node's position the
instant it starts appearing, like it's condensing out of the static.

Also fixes a transparency regression: the new hologram DrawingArea sits above
everything via Gtk.Overlay, and the cyberqueer theme's aggressive `*` selector
was painting it an opaque background, hiding the whole menu — forced
transparent via CSS, same fix pattern as the earlier orbit-node label bug.
2026-07-17 11:29:51 +02:00
Amir Alexander Abdelbaki 779e365cb8 feat(hyprlua/orbit-menu): cosmetic mouse-reactive satellites around ring nodes
Adds a decorative layer to the radial menu: small nerd-font glyphs orbit the
ring nodes (and occasionally the center) at a fixed radial "height" per
satellite, only their angle changes — easing toward the cursor plus a slight
idle wobble. Faint dashed/dotted/solid orbit-path circles trace each one.

Per page build (not per frame, so it never flickers):
- total count is 2 + random(1, (3 % node-amount) + 2), clamped to node
  capacity, spread across at least 3 distinct nodes when there are that many
- a node can get up to 2 satellites; since each has its own fixed height they
  never overlap, and the gap between a doubled pair is widened further
- a doubled pair splits its motion — one tracks only the cursor's X, the
  other only Y, each with its own random phase offset so multiple doubled
  nodes on one page don't move in lockstep
- every satellite gets independent horizontal/vertical response multipliers,
  so it can read as more reactive to one direction than the other
- some satellites get tangential bracket flourishes (-<, -[, -(, -{, -|)
  rotated to the orbit's local tangent

Toggleable via ~/.local/state/orbit-menu/config.json ({"satellites": false})
since config-updater wipes ~/.config/orbit-menu on every sync.
2026-07-17 10:45:24 +02:00
Amir Alexander Abdelbaki 2fa3414e31 feat(hyprlua): add orbit-menu radial power/utility menu
New GTK4 layer-shell radial menu (desktopenvs/hyprlua/orbit-menu/), following
astal-menu's single-instance D-Bus daemon conventions. Super+Shift+O opens
straight into a Power ring (sleep/reboot/poweroff/soft-reboot via hyprshutdown);
Super+Ctrl+O opens the full 5-category root (Power, Tools, Scripts sourced live
from ~/Documents/Scripts, Management, and a live open-Windows list with
go-to/bring-here/close/force-kill/get-pid/send-to-workspace actions). Adds a
round launcher button to all 3 eww bar variants with matching glow/hover CSS.

Also retunes hypridle (lock 30s, sleep 1min) and adds a HibernateDelaySec
systemd-sleep.conf.d fragment (deployed by sysupdate.sh) so the machine
hibernates ~80 minutes after going idle instead of drifting to sleep indefinitely.
2026-07-17 09:51:02 +02:00
Amir Alexander Abdelbaki 2fd7b510ef feat(hyprlua): size a fresh workspace's initial column count to monitor width
A workspace with no seeded/published column count used to always start
at a hardcoded 2, regardless of monitor size. Now it picks
floor(usable_width / 700), so ~700px per column — 1080p still lands on
2, wider monitors start with more instead of one cramped column.
2026-07-16 09:08:23 +02:00
Amir Alexander Abdelbaki c313f0694c fix(hyprlua): columns layout breaks on fresh workspaces, wrap focus u/d
hypr-nav fell back to the raw "lua:columns" backend string on any
workspace that had never been explicitly switched via the menu (i.e.
every fresh workspace, since columns is the global default), which
didn't match the "columns" layout name it checks against — so focus/
move/resize silently took the generic dwindle-style path instead of
being forwarded to the custom layout. Strip the "lua:" prefix in the
fallback.

Also make focus/scroll u/d within a column wrap around instead of
stopping at the ends.
2026-07-16 09:01:21 +02:00
Amir Alexander Abdelbaki bdecba0cf3 feat(hyprlua): float xdg portal dialogs above their parent
Portal-backed file/app choosers (xdg-desktop-portal-gtk) — e.g. Steam's
"select library folder" picker, plus Chromium/Electron/Flatpak dialogs —
run in a separate process and open as parentless top-levels with no
xdg-dialog or X11-dialog hint. Hyprland can't tell they're dialogs and
tiles them, unlike in-process xdg dialogs which it auto-floats. Match the
portal backend's class and float + centre it so it lands above its parent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 04:28:42 +02:00
Amir Alexander Abdelbaki 05693a71b7 chore(presence-detect): remove now-unused v4l2loopback provisioning
presence-detect.sh no longer mirrors the camera through a v4l2loopback device,
so the module setup is dead weight. Remove it:

- delete etc-v4l2loopback/ (modprobe.d + modules-load.d configs)
- drop the deploy blocks and linux-headers/v4l2loopback-dkms/v4l2loopback-utils
  package installs from the niri and hyprlua DE installers
- remove _ensure_v4l2loopback() and its call from sysupdate.sh
- reword enroll-biometrics.sh's loopback filter comment (kept as a harmless
  defensive exclusion) so it no longer points at the deleted config

Live system cleanup (module unload + /etc configs) is separate and needs root.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 04:11:16 +02:00
Amir Alexander Abdelbaki c2fa8d3e75 feat(presence-detect): back off while another app holds the camera
Before each tick, check whether another process already has the real camera
open (fuser, which sees same-user holders without root — every camera app runs
as us). If so, skip the read entirely for that tick: attempting would be
pointless (we'd get EBUSY) and a simultaneous open could make the other app
fail. A camera in use is proof the user is present, so mark presence and wait
it out, touching the camera zero times for the whole duration another app holds
it — instead of poking it every tick and racing the app for access.

V4L2 has no way to be notified that another app wants the device, so this
can't preempt an in-progress read; it just guarantees we never contend while
another app is already streaming. Falls through to a normal read (and the
Python helper's exit-3 busy path) if fuser isn't installed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 04:11:16 +02:00
Amir Alexander Abdelbaki 83687764d8 feat(presence-detect): skip the camera entirely during a manual caffeine session
When idle is inhibited manually (a caffeine toggle, not this daemon), the lock
is held regardless of presence, so polling the camera is pointless. Guard the
loop: if the shared idle lock is alive but we don't own it (same test as
caffeine-manual-status.sh), skip the read and leave the camera free for other
apps for the whole manual session. PRESENCE_FLAG is cleared while skipping,
since we're no longer watching — so the widget's presence input reads false
during a manual session instead of showing a stale "camera sees you".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 04:01:16 +02:00
Amir Alexander Abdelbaki bc544898c1 fix(presence-detect): read camera directly instead of v4l2loopback mirror
The loopback feeder (defa69f) held /dev/video0 open 24/7, so real apps —
video calls, howdy — could never open the camera, and the single loopback
device only ever served one reader at a time (a second consumer got
VIDIOC_REQBUFS -EBUSY). Net effect: the camera was permanently blocked and
nothing could access it.

Drop the ffmpeg mirror and read the physical camera directly, only for the
~0.5s per tick it takes to grab frames, then release it — so the camera is
free for other apps the rest of the time. When another app is already holding
the camera, the Python detector now distinguishes "busy/in use" (device node
present but unreadable → exit 3) from "unavailable" (node gone → exit 2), and
the daemon treats busy as presence: a camera in use is itself proof the user
is here, so a video call keeps the session awake.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 03:51:44 +02:00
Amir Alexander Abdelbaki 1fa5ebee70 feat(presence-detect): 4-state caffeine widget + steadier, faster detection
Rework the Eww caffeine widget into two independent inputs — manual inhibit
and presence detected — yielding four states across all three bars:
  neither            → 󰒲 sleep,  purple
  manual only        →  coffee, purple
  presence only      → 󰈈 eye,    pink
  manual + presence  →  coffee, pink
Colour tracks presence; the coffee cup shows only while manually inhibiting.

To make the two inputs independent, the daemon now writes a standalone
/tmp/presence-detected flag each tick (decoupled from lock ownership, so it's
true even during a manual session). presence-status.sh reports that flag
(presence input); new caffeine-manual-status.sh reports lock-held-and-not-
presence-owned (manual input).

Make detection less finnicky: space the detector's frame grabs (0.06s) so slow
movement registers and loopback reads never diff a stale duplicate frame, and
lower the thresholds (delta 25→18, area 2%→0.8%). Add a 90s grace window so
brief stillness never flickers presence off; any motion resets the clock.

Add a third poll tier — INTERVAL_FAST=2s when CPU-or-RAM usage is below 30%
(LOAD_LIGHT) — alongside the existing 20s normal and 120s busy tiers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 03:07:22 +02:00
Amir Alexander Abdelbaki dc9cf6d830 feat(presence-detect): tint caffeine eww widget hot pink while presence holds the lock
Add presence-status.sh reporting true only when the daemon (not a manual
caffeine toggle) owns the shared idle lock, via the existing OWNED_FLAG.
Wire it into all three eww bars: the caffeine button gains a
.caffeine-presence class → the #E40046 Active accent used elsewhere, so
camera-driven auto-awake is visually distinct from a manual session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 02:09:44 +02:00
Amir Alexander Abdelbaki defa69faab feat(presence-detect): mirror camera via v4l2loopback so the daemon never blocks it
presence-detect.sh now feeds the real webcam into a v4l2loopback virtual
device (/dev/video42) via a self-healing ffmpeg background process, and reads
motion off that mirror instead of opening the physical camera directly. This
means the daemon's 20s-interval checks never contend with other apps (video
calls, howdy) for exclusive camera access. Falls back to direct camera access
if the loopback isn't set up yet, so the feature degrades gracefully rather
than breaking.

New etc-v4l2loopback/ ships the modules-load.d/modprobe.d config (fixed
video_nr=42, exclusive_caps=1) following the repo's existing etc-greetd/
etc-lightdm/ convention. Deliberately no runtime sudo/modprobe calls inside
the daemon itself — the module loads declaratively at boot instead, matching
this repo's existing caution around unattended sudo.

hyprlua.sh and niri.sh installers now pull in linux-headers,
v4l2loopback-dkms, and v4l2loopback-utils and deploy the module config
immediately. sysupdate.sh gained _ensure_v4l2loopback(), wired into
sync_configs(), so existing machines pick up the new packages and /etc
config on their next --config or --both run without needing a fresh install.

enroll-biometrics.sh's camera picker now excludes the loopback device itself
(by card_label) so configuring the real camera can't accidentally target the
mirror it feeds.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 16:39:11 +02:00
Amir Alexander Abdelbaki f90dbdae8b feat(presence-detect): switch to motion detection, adaptive interval, shared caffeine lock
Replace the Haar-cascade face detector with lightweight frame-differencing
motion detection so the daemon can poll every 20s (backing off to 120s under
heavy CPU/RAM load) without adding noticeable overhead.

Presence detection now shares caffeine's systemd-inhibit lock instead of
holding its own, so the caffeine status widget reflects either source. A new
ownership flag ensures the daemon only ever releases a lock it acquired
itself, so a manually-started caffeine session is never interrupted by the
user stepping away. Also re-enables the daemon in hyprlua's autostart (was
previously only wired up for niri).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 16:23:49 +02:00
Amir Alexander Abdelbaki 9d4cdc35bd fix(hyprlua): spatial (transform-aware) monitor-jump fallback
Root cause of "can jump one way across monitors but not back" on a
desk with a rotated monitor: both columns.lua's own edge-crossing and
the fallback this adds go through Hyprland's focusmonitor/movewindow
mon: -1/+1, which cycle through Hyprland's internal monitor *list
order* — not actual spatial position. A monitor's transform swaps its
logical width/height (same math as monitor-manager's logical_width/
logical_height) but doesn't reorder that list, so list order and
physical left-to-right order can silently diverge once one monitor is
rotated — confirmed with the reported layout (main 3840x2160 normal +
rotated 1920x1080 to its right): jumping right resolved correctly,
jumping back left did not.

scripts/monitor-adjacent replaces the index cycling with real spatial
adjacency: it sorts monitors by logical bounding-box center each time
and dispatches focusmonitor/movewindow to the actual next/prev monitor
in the requested screen direction. Verified against the exact reported
monitor layout (simulated, transform=1 on the right monitor) — both
directions now resolve to the correct monitor.

Wired in two places (Super+,/. is untouched, still index-cycling, by
request):
  - columns.lua's edge-crossing (off the left/right edge of the column
    strip already flowed onto the next monitor; now via spatial lookup)
  - hypr-nav's scrolling-layout perpendicular "no neighbour" case, as a
    new fallback: regular directional movement (Super+hjkl) that
    previously just stopped at the edge of a monitor now spills onto
    whichever monitor is spatially in that direction, same as columns.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TLz6HWsXCwzQ97LrLt2em6
2026-07-09 16:49:18 +02:00
Amir Alexander Abdelbaki dc0436136b fix(astal-menu): fix layout-tab sync race across workspace switches
_sync_layout_controls() fires 3 concurrent hyprctl calls, and refresh()
can trigger it twice per open (once from the clients query, once from
the activeworkspace one) — each rebuilding a fresh set of layout
widgets. Two bugs followed:

1. The 3 callbacks each toggled the shared self._syncing flag
   independently (True at start, False at end). Whichever resolved
   first dropped the guard while the others — notably the tab-
   selection one, which programmatically flips the visible stack
   child — were still in flight. That let _on_tab_switch/_on_opt_change
   misfire as if the user had clicked, re-applying stale layout data
   via layouts.set() to whatever workspace was active by the time the
   callback landed.

2. A second _sync_layout_controls() call (from the redundant rebuild)
   could land mid-flight of the first, so a stale callback from sync
   #1 could decrement sync #2's guard or write sync #1's data into
   sync #2's freshly built widgets.

Fixed with a generation token: any sync superseded by a newer one is
invalidated outright, and self._syncing only lifts once the *current*
generation's 3 calls have all resolved. This is what "query the
current workspace's layout when opened, so it doesn't break switching
workspaces" needed — refresh() already re-queried on open, the gap was
in trusting stale in-flight results.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TLz6HWsXCwzQ97LrLt2em6
2026-07-09 16:48:42 +02:00
Amir Alexander Abdelbaki 052611abf5 feat(hyprlua): default to columns, special:magic to scrolling
Session default flips from scrolling to columns; special:magic keeps
a downward scrolling layout of its own. layouts.restore() still wins
if the user has since picked something else for it via the astal-menu.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TLz6HWsXCwzQ97LrLt2em6
2026-07-09 16:48:12 +02:00
Amir Alexander Abdelbaki 7a5c715da3 feat(hyprlua): open opendeck on special:magic workspace
Matches the existing kitty autostart rule so opendeck launches hidden
on the scratchpad workspace instead of the active one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TLz6HWsXCwzQ97LrLt2em6
2026-07-09 15:59:32 +02:00
Amir Alexander Abdelbaki 6194b5c271 fix(hyprlua): drop racy cw+ccw autostart rotation workaround
The double-rotate on every Hyprland start existed to fix monitor scale
getting corrupted by rotation. Verified live (Hyprland 0.55.4): scale
stays exactly pinned across a transform change as long as it's set
explicitly (monitors.lua already does scale=1.0, not "auto"), so the
workaround is no longer needed upstream.

It was also a source of a real race: unified-rotate.sh reads current
transform via `hyprctl monitors -j`, and the fixed 0.3s gap between the
two invocations isn't guaranteed long enough for the first rotate to
commit before the second reads state — especially with a dozen other
autostart commands firing concurrently in the same block. A lost race
leaves the monitor (and touchscreen, which unified-rotate.sh also
retargets) transform mismatched after boot. Root-caused this while
investigating why a QEMU test VM's monitor stayed stuck at transform:1
after every fresh boot.

unified-rotate.sh itself is untouched — still used by the manual
rotate buttons on the touch bar.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TLz6HWsXCwzQ97LrLt2em6
2026-07-09 15:48:23 +02:00
Amir Alexander Abdelbaki 716902f8e1 perf(astal-menu): use gdbus Activate fast path in menu-toggle.sh
menu-toggle.sh always cold-started a second python3+PyGObject+GTK4
process to forward one verb to the already-running daemon, per
GApplication's command-line-forwarding mechanism. That's ~0.5s
baseline and spikes past 5s under load, easily read as an
unresponsive/broken menu button on repeat taps.

main.py's own do_command_line comment already describes the intended
fix: call the registered org.gtk.Actions directly over D-Bus for an
already-running instance. Wire that up (gdbus call, ~20-100ms) and
keep the python3 spawn only as the cold-bootstrap path when no
instance is registered yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TLz6HWsXCwzQ97LrLt2em6
2026-07-09 09:18:34 +02:00
Amir Alexander Abdelbaki 0541177974 fix(eww-touch): use nobattery bar's astal-menu launcher button
The touch bar's menu button used a bare icon-btn with no explicit
toggle side, unlike the nobattery bar's accent-coloured launcher.
Mirror the nobattery implementation (class + explicit "toggle top"
args + .menu-launcher styling) for visual and behavioral consistency
between bar variants.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TLz6HWsXCwzQ97LrLt2em6
2026-07-09 08:34:41 +02:00
Amir Alexander Abdelbaki 84420733bb adjustments for astal+ewwbar 2026-07-08 21:56:51 +02:00
Amir Alexander Abdelbaki ed44b84afa feat(hyprlua/columns): center-focused toggle, mirroring scrolling's fit method
Adds a "center-focused" switch to the Columns layout's menu tab: when a column
overflows and scrolls, it can now center the focused window in the column
instead of only nudging the pan the minimum amount to keep it in view. Off by
default so existing behavior is unchanged unless explicitly enabled.

The astal-menu switch widget already existed generically (built for scrolling's
native focus_fit_method) but was hard-wired to that one Hyprland option; it's
now layout-aware and reads/writes columns.lua's own persisted state instead.
2026-07-08 13:35:47 +02:00
Amir Alexander Abdelbaki 964ce103a0 feat(hyprlua/columns): resizable+scrolling columns, window parking, reload-safe
Overhaul the custom "columns" Lua layout and its navigation wiring:

- Flexible sizing: per-column width weights + per-window height weights
  (Super+Alt resize), scale-to-fill when a column is underfull, scroll/pan
  when overfull. Drops the old fixed viewport/rows window heights.
- Window parking: move u/d within a column parks two windows side by side in
  one slot (persistent, split across the column axis with a divider parallel
  to the scroll direction; the column closes up so there is no gap); moving a
  parked window again pops it out past its partner.
- New windows stack into the working column (with a balance guard) instead of
  scattering, so two windows share a column/height by default.
- Cross-monitor focus/move at the column edges (relative +1/-1 selectors).
- hypr-nav: adds resize + scroll kinds; Super+wheel travels along the focused
  strip (column / tape) and falls back to workspace switch elsewhere.
- Super+Alt arrows/hjkl route resize through the layout; Super+rightclick
  auto-floats then resizes ONLY in columns (no pointer-resize hook for custom
  layouts) and is a plain resize everywhere else.
- Reload-safety: hyprctl reload dropped per-ws layout rules + Lua state,
  silently reverting columns workspaces to scrolling. init.lua now replays
  layouts-state.json (M.restore) and columns.lua re-seeds cols/rows/orient.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 19:49:28 +02:00
Amir Alexander Abdelbaki 4c747221f2 style(astal-menu): unfullscreen glyphs, animated takeover, CyberQueer TUIs
- Expand/collapse controls now use consistent fullscreen ()/unfullscreen ()
  glyphs: the quad "Back" and the taskbar-panel "Back" became  buttons (matching
  the appdrawer's - toggle).
- The taskbar panel takeover now plays the same bouncy quad-pop scale as the quads
  when it expands, so every expansion in the menu is animated.
- monitor-manager (curses) and timer-pick (bash TUI) now use the CyberQueer accent
  palette (red #E40046 + violet #5018dd, true-colour with ANSI fallback) instead of
  cyan/green, matching the rest of the config.
- Added a .stepper-value style for the Columns count stepper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUN7gg6GGfnToghMijLm5Y
2026-07-07 13:43:11 +02:00
Amir Alexander Abdelbaki de666acac7 feat(astal-menu): Columns count stepper + expanded taskbar is one module
- Columns layout publishes per-workspace column/row counts to columns-state.json
  and handles a direct "cols +1/-1/N" message. The menu's Columns page now shows a
  "Columns  (−)[N](+)" stepper that reads the live count and drives it.
- Expanding the taskbar now fully collapses the strip (header included) via its
  revealer, so the panel that covers the quads reads as a single module instead of a
  thin strip plus a detached card below it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUN7gg6GGfnToghMijLm5Y
2026-07-07 13:31:45 +02:00
Amir Alexander Abdelbaki 9d1ec69a11 feat: hyprshutdown power binds, 76% scrolling windows, single-sudo sysupdate
- hyprshutdown wired into the graceful power flow (Super+Shift+O logout,
  Super+Ctrl+O power off, Super+Ctrl+Shift+O reboot) so apps close cleanly with a
  UI before Hyprland exits, instead of a hard systemctl call. Added to the hyprlua
  package list.
- Scrolling layout: default window is now 76% of the monitor along the scroll axis
  (column_width 0.5 -> 0.76).
- sysupdate.sh: primes sudo once at startup and keeps the credential alive for the
  whole run, so no later step re-prompts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUN7gg6GGfnToghMijLm5Y
2026-07-07 13:26:48 +02:00
Amir Alexander Abdelbaki 48505a5726 feat(hyprlua): real independent-pan "columns" layout (custom Lua layout)
Replaces the scrolling-preset "columns" with a genuine custom tiling layout
(hl.layout.register "lua:columns"): a row of columns where each column is an
INDEPENDENTLY panning vertical strip. Moving focus up/down inside a column pans
only that column to keep the focus in view; the other columns stay frozen — the
combination-lock behaviour that the built-in scrolling engine (single shared tape
offset) structurally can't do.

- columns.lua: recalculate places every window box; per-workspace state holds each
  window's column assignment and each column's pan offset. layout_msg handles the
  focus/move/count/orient commands. New windows fill the least-populated column;
  changing the column count reflows evenly.
- init.lua: for Lua layouts (lua = true), orientation is sent via layout_msg
  ("orient h|v") instead of the global scrolling:direction option.
- hypr-nav: forwards focus/move/count for the columns layout to hl.dsp.layout(...);
  the built-in scrolling path is unchanged.
- binds.lua: Super+Ctrl+Alt+H/J/K/L manage the column / windows-per-view count
  (axes follow the workspace's horizontal/vertical orientation).

Menu dropdown for "Columns" now selects orientation (Horizontal / Vertical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUN7gg6GGfnToghMijLm5Y
2026-07-07 10:08:34 +02:00
Amir Alexander Abdelbaki 6631405570 feat(hyprlua): taskbar quad-takeover, columns layout, screen-absolute nav
astal-menu: the taskbar ⤢ toggle now collapses the compact strip and mounts
its workspace/window panel as a takeover overlay covering the 2x2 quads (with
a Back button), instead of a popover/inline reveal. QuadGrid gains a generic
takeover overlay; the menu window coordinates show/hide and resets on close.

hypr: new "Columns" layout (scrolling backend presented as independent columns)
with a menu dropdown to pick orientation — "Left / Right" or "Up / Down" — via a
new dir_labels manifest field. The layout menu now tracks direction dropdowns and
fit switches per-layout, so a second directional layout no longer clobbers the
first.

binds/scripts: focus & window-move keys (hjkl + arrows) route through a new
hypr-nav helper so they stay screen-absolute in the scrolling/columns layouts
(the layout otherwise resolves a perpendicular direction onto "scroll the tape",
rotating the keys with scrolling:direction). Adds explicit cross-monitor binds:
Super+,/. focus prev/next monitor, Super+Shift+,/. move the window there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUN7gg6GGfnToghMijLm5Y
2026-07-07 09:28:41 +02:00
Amir Alexander Abdelbaki 4196b84ad3 feat(astal-menu): tabbed workspace-layout selector
Replace the layout dropdown with a tab bar (one tab per layout — Dwindle · Master ·
Monocle · Scrolling, from the manifest) over a Gtk.Stack of per-layout options that
switches with the tab. Selecting a tab applies that layout to the currently focused
workspace; the scrolling page carries its options — a "direction" dropdown and a
"center-focused" switch (focus_fit_method). The active tab reflects the workspace's
real (per-ws) layout via the layouts state file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
2026-07-06 11:59:47 +02:00
Amir Alexander Abdelbaki fa097da436 feat(hyprlua): per-file layout registry (default scrolling↓); menu layout+window controls
- hypr/layouts/: drop-in layout registry. init.lua auto-discovers layouts/<name>.lua
  (each self-describing) and exposes the global `layouts` table: set_default (picked
  in hyprland.lua — now scrolling, direction down), set(ws,name,dir) for live
  per-workspace switching (verified it re-tiles the active ws), set_fit(0/1). Writes
  a manifest + per-ws state for the menu. Ships scrolling/dwindle/master/monocle.
- Scrolling default: column_width 0.5 (active column < viewport so prev/next stay
  clickable), focus_fit_method 1 (centered follow), fullscreen_on_one_column.
- astal-menu taskbar pop-open: workspace layout selector + direction + a "Centered"
  toggle (focus_fit_method), and a per-window list — [icon+title → jump][⇤ here →
  pull to current workspace], keeping jump-to-window intact.
- eww bar menu buttons open the menu explicitly from the top (menu-toggle.sh toggle top).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XUWCXM4KhjRkwheaA3X7bP
2026-07-06 11:52:46 +02:00
Amir Alexander Abdelbaki 4fe6181a93 made the hw-stats bar taller, so the close button doesn't touch any borders 2026-07-06 10:12:13 +02:00
Amir Alexander Abdelbaki b3b46a125b build(hyprlua): package astal-menu's new deps; Super+D opens from top
- 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
2026-07-06 09:29:58 +02:00
Amir Alexander Abdelbaki 57dede403e feat(astal-menu): stats line, side-slide flag, drawer pop, tighter buttons
- 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
2026-07-06 09:26:37 +02:00
Amir Alexander Abdelbaki e63c99f299 feat(astal-menu): pannable map with night/satellite tiles
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
2026-07-06 08:59:05 +02:00
Amir Alexander Abdelbaki abea1bec43 feat(astal-menu): Bluetooth connect status + Forget button
- 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
2026-07-06 08:43:13 +02:00
Amir Alexander Abdelbaki e79d427c2a fix(astal-menu): refresh Bluetooth list on device connection changes
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
2026-07-06 00:17:35 +02:00
Amir Alexander Abdelbaki 5cd880c388 screw the presence detect 2026-07-06 00:16:06 +02:00
Amir Alexander Abdelbaki 221d0dde55 fix(astal-menu): keep toggle switches pill-shaped (valign center)
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
2026-07-06 00:12:16 +02:00
Amir Alexander Abdelbaki 0e8cdd5335 fix(astal-menu): play the quad-expand pop once, not on a loop
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
2026-07-06 00:06:18 +02:00
Amir Alexander Abdelbaki 6a5a5bce97 feat(astal-menu): bouncy slide-in on open, springy pop on quad expand
- 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
2026-07-06 00:00:14 +02:00
Amir Alexander Abdelbaki 3aafd2cc17 fix(astal-menu): working favourites, 3-day weather, deploy-safe settings
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
2026-07-05 23:35:23 +02:00
Amir Alexander Abdelbaki 16f75edb13 style(astal-menu): fixed-shape switches, inset close button, shorter panel, uniform expand icons
- 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
2026-07-05 20:48:52 +02:00
Amir Alexander Abdelbaki d468d66e5f feat(astal-menu): NetworkManager control panel; single-ring settings/close buttons
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
2026-07-05 20:32:39 +02:00
Amir Alexander Abdelbaki 4e5ee4f9ba fix(astal-menu): single module border; unblock rfkill so Bluetooth powers on
- 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
2026-07-05 20:11:36 +02:00
Amir Alexander Abdelbaki 503b7f3336 fix(astal-menu): float modules on transparent gaps (beat theme's global bg)
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
2026-07-05 19:51:54 +02:00