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>
kitty: add a seamlessly-tiled scanlines+noise+ambient-bloom PNG as
background_image, giving kitty a static approximation of the suite's
HologramOverlay treatment since it has no per-frame draw hook or shader
support to do real bloom.
ghostty: port the CyberQueer theme (palette, cursor/selection colors)
to a new Ghostty config, with a real per-pixel custom-shader instead of
a static image — color-preserving bloom, procedural scanlines/grain, an
occasional analog glitch/roll burst (tear + vertical roll + chromatic
aberration), and a one-time hologram boot intro that resolves out of
colored static via a downward sweep. Window decorations and the tab bar
are disabled to match kitty's borderless look under Hyprland.
Not wired up anywhere yet — no keybind or $TERMINAL changes point at
Ghostty. This is just the config, ready to test.
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.
The 16 ANSI colors were still stock template defaults (plain green/
blue/cyan) 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, and add matching border/bell/mark colors. Also
drop a stale cursor-color pair in kitty.conf that the theme include
was silently overriding anyway.
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.
Replaces the ∿-glyph label divider with a Gtk.DrawingArea that strokes a sampled
sine wave (soft-glow + bright-core passes), colour per urgency (magenta / accent),
phase-advanced each tick so it travels like a live signal. Background stays
transparent — a class-specific rule overrides the CyberQueer theme's universal
`background-color`, so no dark pill shows behind it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
beacon is a GTK4 layer-shell notification daemon in the Cosmonaut Shell idiom.
It owns org.freedesktop.Notifications and renders each notification as a
holographic card: scanline/sweep/noise overlay (shared lib/hologram.py) clipped
to a rounded violet holo-glass card, emitted-magenta text, glow-violet frame
(accent + squiggle for critical), materialise-out-of-static intro / dissolve
outro, top-centre stack.
Implements the interactive spec subset: body markup, urgency, replaces_id,
per-urgency timeouts, action pills, icons (name/path/raw), click-to-dismiss, and
the NotificationClosed / ActionInvoked signals.
- scripts/beacon-start.sh: LD_PRELOAD launcher
- autostart.lua: dunst -> beacon
- windowrules.lua: blur layer-rule for the `beacon` namespace (no_anim; it has
its own hologram intro)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Themes dunst in the astro-menu idiom: emitted-magenta text, violet holo-glass
backgrounds (transparent for the compositor blur), glow-violet/accent frames,
Agave Nerd Font Mono, and a "radio squiggle" sine-wave divider between title and
body. (Superseded by the beacon daemon in a following commit, but kept as themed
fallback config.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The materialise intro was driven only by the frame-clock tick, which stops
advancing when the compositor withholds frame callbacks (e.g. the bar restarted
into an idle compositor) — freezing it at t=0 with content stuck at opacity 0,
i.e. permanent static. Arm a GLib.timeout alongside start_intro() that
force-resolves the intro regardless, so content always appears.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Right-click now fetches the item's com.canonical.dbusmenu layout and renders it
in a GTK popover anchored to the icon, instead of calling the SNI ContextMenu
method. Fixes menus opening at screen (0,0) for apps that implement it (Discord)
and gives a menu at all for dbusmenu-only apps (nm-applet, blueman). Supports
sections, submenus, checkmarks, icons and Event-on-activate; themed to match the
suite (.station-tray-menu).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`hyprctl dispatch` evaluates its argument as Lua here (hyprlua), so the old
native `dispatch exec <cmd>` form parsed as a Lua syntax error and was silently
dropped — every Tools/Scripts/Management entry failed to launch. Route through
hl.dsp.exec_cmd(...) instead (matching binds.lua), keeping the [tag ...] rule
prefix support.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hyprdrive has no DE component owning org.kde.StatusNotifierWatcher (waybar/eww
used to), so tray apps had nowhere to register and every SNI host — including the
bar's own tray.py — saw an empty tray.
- sni_watcher.py: a minimal StatusNotifierWatcher — owns the name, tracks
registered items/hosts, drops items on NameOwnerChanged, and emits the
registration signals + PropertiesChanged so hosts stay live.
- main.py: start it in do_startup, before the bars.
- tray.py: watch the watcher name (bus_watch_name) instead of connecting once, so
the host attaches when our watcher appears and recovers if any watcher restarts.
Verified: the watcher owns the bus name, IsStatusNotifierHostRegistered is true,
and running tray apps (udiskie, nm-applet, blueman, Steam) register + render in
the bar the moment it comes up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second daily-driving pass over the Cosmonaut Shell suite.
Animations (all four surfaces):
- Removed every slide-in reveal; each surface now only "materialises out of
static" (compositor no_anim layerrule + in-app hologram intro). Longer, noisier,
gradual fade-in: the content opacity ramps 0->1 (fade_widget) instead of a solid
haze block popping on. Added a quick reverse "dissolve into static" outro on
close. horizon-dock/orbit-menu use a slower 2.4s intro. Fixed a warm-reopen bug
where a stale frame-clock baseline skipped the intro entirely.
horizon-dock:
- Center planet: bigger, sinks ~1/4 below the screen edge, translucent holographic
body with a wide bloom halo, specular sheen, rim glow and a breathing pulse.
- Ornamental satellite glyphs: two rings framing the windows orbit, each with a
random 3-7 satellites whose motion source is randomised (toward-cursor / mouseX
/ mouseY), so the quiet edges feel alive.
- Super+D now opens horizon-dock (astro-menu moved to Super+Shift+D).
orbit-menu:
- Classic dmenu mode: `--dmenu` reads newline items from stdin, shows them as ring
nodes, prints the choice to stdout (exit 1 on cancel). Standalone NON_UNIQUE app
so it never routes to the resident daemon; scripts/orbit-dmenu.sh wraps it with
the gtk4-layer-shell preload.
astro-menu (sci-fi rework):
- Replaced the map module with "Ship Systems": a Gtk.Grid system monitor backed by
backend/sysmon.sh. CPU->Thrusters, GPU->Hyperdrive, RAM->Life Support Systems,
Storage->Cargo Hold (one aligned row per mounted drive with capacity), each a
bracketed -<████░░░░>- usage meter + -( 54°C )- temperature readout.
- Renamed Bluetooth -> System Radio, Network -> Laser Antenna Uplink.
- Weather gains a "Planetary Environment Report" headline.
station-bar:
- The battery readout uses a fuel-tank glyph (the ship runs on fuel).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Daily-driving pass over the Cosmonaut Shell suite: transparency, holographic
effects, and layout/geometry fixes across every component built for the DE.
Global holographic layer (astro-menu/horizon-dock/station-bar lib/hologram.py):
- edge-fade mask (A8 DEST_OUT) so scanlines fade out at panel borders
- magenta particle field; "materialize from static" noise-mask intro animation
- optional clip_func so the mask follows non-rectangular / animated content
- universal `* { background-color: transparent }` at USER+1 to defeat the
CyberQueer theme painting opaque bg on plain containers; blur layer_rules
astro-menu: magenta text (alt red) for readability; thinner, glowier borders;
rounded map-texture corners; fixed transparency stacking + square corners.
orbit-menu: scanline mask now follows the zoom transition into a submenu.
horizon-dock: reworked into an orbit-menu node ~1/3 on-screen (<=2/3 width) with
horizon perspective (arched orbit rows), a non-functional center clock-sphere,
orbit glyph decorations, graceful edge fade-out, and centered/filled orbits;
tray removed; giant-surface clip fixed.
station-bar: redesigned as an extra Horizon Dock orbit — bare glowing color-coded
glyphs (no pills), width-adaptive, one bar per monitor, Super+Z toggles the
focused monitor's bar. Clickable HUD trapezoid around the title opens Astro Menu.
Workspaces are now per-monitor: each bar shows only its own monitor's workspaces
and highlights that monitor's active workspace. Populated workspaces show the
station glyph, empty ones the bare spaceship + number; the focused one is framed
by a `-< >-` reticle with the rocket prepended when there's a station to dock at.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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
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.
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.
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.
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.
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.
The dotfiles clone used to land directly in /mnt/etc/skel, gated solely by
clone_skel_dotfiles. That meant run_tui=YES + clone_skel_dotfiles=NO cloned
nothing anywhere, so tui-install.sh (which only exists inside the repo, not
in the embedded live-ISO scripts) had nothing to run and the TUI step always
failed.
Clone the repo once into the live environment ($HOME/Dotfiles) whenever
either toggle is YES, then serve both the skel copy and — as a fallback
right before the TUI runs — a direct copy into the new user's home from
that single clone, no extra network fetch needed. arch-autoinstall.sh's
skel clone also moved out of the chroot heredoc so it can share the same
live clone.
When clone_skel_dotfiles=NO, the fallback copy in the new user's home is
transient: after tui-install.sh finishes, materialize every symlink that
shell-setup.sh/hyprlua/hyprland/niri point into ~/Dotfiles (shell rc's,
nvim/starship/alot/yazi/spotify-tui configs, config-updater scripts) into
real copies, then remove the checkout — so declining skel persistence
doesn't leave a system full of dangling symlinks.
Decouple the base installer's "run dotfiles TUI" choice from cloning the
dotfiles repo into /etc/skel and from the new user's default shell, so
declining both actually yields a bare system: no dotfiles, plain bash,
no DE, no display manager, and a working agetty login on tty1. Also add
an explicit toggle in the TUI for syncing the configured user's ~/.config
back into /etc/skel, previously unconditional.
Separately, core.sh's "svc" component unconditionally staged a greetd
config that launches Hyprland and enabled greetd.service — even when no
DE was selected, and even for DEs (GNOME, KDE Plasma, COSMIC, XFCE,
LXQt, Sway) that install their own display manager. That left greetd
racing the DE's native DM (or agetty, in the no-DE case) for tty1, and
Sway additionally got a greeter pointed at the wrong compositor. Gate
greetd on the DE selection and have every DE module disown getty/greetd
before enabling its own DM, matching the pattern hyprland.sh/niri.sh
already used; add a Sway-specific tuigreet config.
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>
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>
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>
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>
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>
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>
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>
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>
Flatpak apps don't inherit the host's QT_QPA_PLATFORMTHEME=gtk3, so Qt apps
(Wireshark, PrismLauncher, ...) ignored the cyberqueer theme even though
GTK_THEME was already being overridden per-app.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
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
_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
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
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
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
Found via a fresh-install test in a QEMU VM using the repo's answerfile
system (hyprlua, tablet/touch eww bar variant).
1. hyprlua.sh/hyprland.sh/niri.sh all compile EWW (GTK3) from source but
never installed gtk-layer-shell — only gtk4-layer-shell, which is a
separate package for astal-menu's GTK4 surface. Every fresh install
failed at the gtk-layer-shell-sys build script with a pkg-config
error ("gtk-layer-shell-0 ... was not found").
2. The EWW `git clone` isn't idempotent: a leftover ~/install-tmp/eww
from an earlier interrupted/retried install (e.g. the failure above)
makes the next clone abort with "already exists and is not an empty
directory", killing the whole script under set -e. `rm -rf eww`
before cloning fixes retries.
Both reproduced and fixed directly in the test VM before being applied
here: installing gtk-layer-shell and clearing the stale checkout let
the same run complete successfully.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TLz6HWsXCwzQ97LrLt2em6
For sessions that need to run a string of sudo commands with nothing
around to answer a password prompt (unattended VM builds, agent-driven
scripting). Drops a validated sudoers.d NOPASSWD entry for the current
user on enable, removes it on disable. Off by default, not part of the
regular install/update flow.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TLz6HWsXCwzQ97LrLt2em6
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
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
Without -d, cage never negotiates xdg-decoration, so GTK4/ReGreet falls
back to drawing its own client-side title bar chrome on some machines.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
greetd's PAM stack included system-login, whose own pam_u2f line runs
before the password stack — combined with greetd's own trailing
pam_u2f line, ReGreet prompted FIDO, then password, then FIDO again.
Spell out system-login's auth phase minus that stray line so FIDO is
asked once, after the password, matching hyprlock's order.
Also add setup/tools/set-regreet-background.sh to pin a specific image
as the greeter background, independent of the wallpaper-sync unit.
sysupdate.sh's config sync now closes several gaps where a fresh or drifted
machine would silently be missing pieces that config-updater can't reach
(it only copies files, never installs packages or touches /etc):
- _ensure_plymouth_regreet: detects whether the machine is still on
greetd+tuigreet (or no greeter at all) and offers to run the existing
migrate-to-greetd.sh tool rather than duplicating its logic.
- _ensure_astal_menu_deps / _ensure_hyprshutdown: detect and offer to install
the AUR/pacman packages astal-menu and the new power binds need but that
config-updater never installs on its own.
- _redeploy_usr_configs: hypr/usr/ is intentionally excluded from the
automated sync to protect per-device customisation (monitors.lua,
wallpaper.conf), which left feature/bind updates in the dotfiles template
with no path onto an already-provisioned machine. Adds an explicit,
file-by-file dialog checklist (device-specific files default off) so those
updates can be pulled in deliberately instead of hand-copied.
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.
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>
- 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
Diagnosed the intermittent "login won't take input / frozen cursor" on this
dual-AMD-GPU box: cage (wlroots) could pick the display-less iGPU, and amdgpu
hardware cursors can freeze. regreet-session.sh now:
- pins WLR_DRM_DEVICES to the first DRM card with a connected output (portable,
evaluated live — no hard-coded PCI path),
- sets WLR_NO_HARDWARE_CURSORS=1 (fixes the stuck cursor),
- runs cage with `-m last` so the login card centres on a single monitor instead
of the default "extend", which stretched the canvas across all three monitors
and left the card floating in the middle of the span.
sysupdate.sh now deploys /etc/greetd config (config/regreet/session, not PAM) when
greetd is the active greeter, using the single primed sudo credential.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUN7gg6GGfnToghMijLm5Y
- 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