Fedora 43 → 44 broke my Hyprland, and I had to rebuild hy3 in shell
A few weeks after the Sway-to-Hyprland post, Fedora 44 dropped. I upgraded the laptop. Half the desktop fell over. This is the log of putting it back together — including realising I had to write my own mini-hy3 in five small shell scripts.
A little background first. I have moved from I3 to Sway to Hyprland, mostly for different technical reasons. Sway had better Hidpi support than I3, Hyprland had better a better portal implementation than Sway. Thats about it. My general usage is not advanced and I like the I3 model and want to keep it.
The upgrade itself
dnf system-upgrade download --releasever=44 refused to resolve.
Conflict on libdisplay-info.so.2 vs .so.3, with the
solopasha/hyprland COPR demanding the old soname:
1package hyprland-0.51.1-3.fc44.x86_64 from copr...
2 requires aquamarine(x86-64) >= 0.9.2
3package aquamarine-0.9.5-2.fc44.x86_64 from copr...
4 requires libdisplay-info.so.2()(64bit), but none of the providers
5 can be installed
6cannot install both libdisplay-info-0.3.0-1.fc44 and
7 libdisplay-info-0.2.0-4.fc43
The COPR I'd used to install Hyprland on F43 hadn't been rebuilt
against F44's libdisplay-info 0.3. --allowerasing was thinking
about it for ten minutes. Easier to remove the COPR's packages first,
do the upgrade clean, then sort Hyprland out on F44:
1sudo dnf copr disable solopasha/hyprland
2sudo dnf remove aquamarine\* hypr\* xdg-desktop-portal-hyprland
3sudo dnf system-upgrade download --releasever=44 -y
4sudo dnf system-upgrade reboot
Reboot to TTY (no compositor), reinstall Hyprland — except now I had
to pick a COPR. The old one (solopasha) was still stuck on Hyprland
0.51 for F44, which is months behind. After some trial and error I
landed on ashbuk/Hyprland-Fedora, which has 0.55.1 from a few days
prior, and uses vendored libraries in
/usr/libexec/hyprland/vendor/ with RPATH isolation. No ABI conflicts
with system packages, no libdisplay-info dance.
1sudo dnf copr enable ashbuk/Hyprland-Fedora
2sudo dnf install hyprland xdg-desktop-portal-hyprland
The COPR's username is lowercase ashbuk even though the GitHub repo
is AshBuk/Hyprland-Fedora. The wrong case fails silently — the COPR
just doesn't get added. Took me a minute to spot.
The catch with vendored builds: no plugins
Hyprland 0.55 comes up clean. But hyprpm update fails:
1-- Checking for module 'aquamarine>=0.9.3'
2-- Package 'aquamarine' not found
3CMake Error: The following required packages were not found:
4 - aquamarine>=0.9.3
The vendored COPR ships only the runtime .so files. No headers, no
.pc files. So plugins can't compile against it. Including hy3, which
is the whole reason I cared about Hyprland in the first place.
The options were:
-
Switch back to
solopasha+ manually installlibdisplay-info 0.2as a compatibility shim. Works, but downgrades Hyprland to 0.51 and relies on a hand-pasted.soin/usr/lib64/. -
Build Hyprland from source so the headers are there. Then I'm a distro maintainer.
-
Ride out the new Hyprland version without hy3. Replace the bits I actually used with shell.
Option 3 won. I'm not an advanced sway user. I used four hy3 features in practice. The rest of hy3 (autotiling, swallowing, deep tree manipulation, locked tabs) I never touched.
Hyprland 0.55 broke the config syntax too
While the upgrade was rebuilding, hyprctl configerrors scrolled
across the screen. Two batches of breakage:
windowrulev2 is gone
Hyprland 0.55 removed windowrulev2. The replacement is windowrule
with space-separated rule and matchers, not commas:
1# old (0.54)
2windowrulev2 = float, class:^(pavucontrol)$
3windowrulev2 = workspace 2 silent, class:^(firefox)$, title:^(\[WS-2\] .*)
4
5# new (0.55)
6windowrule = float class:^(pavucontrol)$
7windowrule = workspace 2 silent class:^(firefox)$ title:^(\[WS-2\] .*)
The error message is "invalid field float: missing a value" because
the parser is trying to interpret float as a parameter that needs
arguments before the comma. Confusing if you don't know what's
changed.
Layout dispatchers moved behind layoutmsg
togglesplit used to be a top-level dispatcher. In 0.55 it's a
"layout message", invoked through the layoutmsg wrapper:
1# old
2bind = $mod, B, togglesplit
3# new
4bind = $mod, B, layoutmsg, togglesplit
Same for swapsplit, preselect, movetoroot — anything
layout-specific. The lua refactor in 0.55 cleaned up the dispatcher
namespace; the cost is that any config older than two months won't
load until you find what got renamed.
Tang/Clevis was wishful thinking on a laptop
While I was at it I tried setting up Tang auto-unlock so I wouldn't have to type the LUKS passphrase on boot. Short version: it doesn't work on Wi-Fi-only laptops, because the initramfs can't bring Wi-Fi up. Separate post coming on this one.
What I rebuilt of hy3, in five shell scripts
This is the meat. The hy3 features I used were:
super+W→ tab everything in this workspace (sway'slayout tabbed)super+arrow→ move focus or cycle through tabs if in a groupsuper+shift+arrow→ move window or reorder tabs in a groupsuper+shift+N→ send focused window to workspace N. If the window is in a group, take only that window, leave the others tabbed.- A windowrule that auto-routes Firefox windows by their
[WS-N]title prefix.
Stock Hyprland gets you halfway. Its native group/tab system exists,
but a group acts as a single layout-unit. Move "the window" and the
whole group migrates with it. The other gap: movefocus l tries to
walk out of the group, instead of cycling tabs inside it.
So I wrote a small set of shell scripts that wrap Hyprland's
dispatchers with group-aware logic. They live in
~/roles/build_myprojs/hyprland which gets synced to all my
machines.
tab-workspace: tab everything in the workspace
Stock togglegroup only adds the focused window to a 1-tab group.
Sway's layout tabbed puts everything in the workspace into one
tab-stack. The workaround is to walk every tiled window in the active
workspace and moveintogroup them one by one, then focus the one
you started on.
The trick is doing it in a single hyprctl --batch call. Otherwise
each individual dispatch races with focus changes mid-script, and you
miss windows in deep dwindle trees.
1# scripts/tab-workspace
2ws_id=$(hyprctl activeworkspace -j | jq -r '.id')
3windows=$(hyprctl clients -j |
4 jq -r ".[] | select(.workspace.id == $ws_id) | .address")
5active_addr=$(hyprctl activewindow -j | jq -r '.address')
6
7cmds="dispatch togglegroup"
8for pass in 1 2; do
9 while IFS= read -r addr; do
10 cmds+=" ; dispatch focuswindow address:$addr"
11 cmds+=" ; dispatch moveintogroup l"
12 cmds+=" ; dispatch moveintogroup r"
13 cmds+=" ; dispatch moveintogroup u"
14 cmds+=" ; dispatch moveintogroup d"
15 done <<< "$windows"
16done
17cmds+=" ; dispatch focuswindow address:$active_addr"
18hyprctl --batch "$cmds"
The two passes handle dwindle's binary tree: a deeply-nested window might not be reachable in one call. Cribbed this from hyprtabs, a Go tool that does the same thing. The original is a couple of hundred lines in Go; the bash version is a third the size and easier to tweak.
Bound to super+W. Toggling: when called on an already-tabbed group,
the same script ungroups everything back to tiles. Not shown above
for brevity.
group-movefocus: focus that knows about tabs
1# scripts/group-movefocus
2dir="$1"
3grouped=$(hyprctl activewindow -j | jq -r '.grouped | length')
4if [[ "$grouped" -gt 1 ]]; then
5 case "$dir" in
6 l) hyprctl dispatch changegroupactive b ;;
7 r) hyprctl dispatch changegroupactive f ;;
8 *) hyprctl dispatch movefocus "$dir" ;;
9 esac
10else
11 hyprctl dispatch movefocus "$dir"
12fi
If the focused window is in a group, left~/~right cycle tabs (using
Hyprland's native changegroupactive). Otherwise normal directional
focus. up~/~down always do normal focus, since vertical movement
within a tab bar doesn't make sense.
A symmetric group-swapwindow wraps swapwindow vs Hyprland's
movegroupwindow for super+shift+arrow. (Reorders tabs in the
group when grouped, swaps with neighbor when not.)
move-to-ws: pop out of group before moving
1# scripts/move-to-ws
2target="$1"
3grouped=$(hyprctl activewindow -j | jq -r '.grouped | length')
4if [[ "$grouped" -gt 0 ]]; then
5 hyprctl --batch "dispatch moveoutofgroup ; dispatch movetoworkspace $target"
6else
7 hyprctl dispatch movetoworkspace "$target"
8fi
Bound to super+shift+N. If you're inside a tab group and you press
super+shift+5, the focused tab pops out and goes to workspace 5,
the other tabs stay grouped on the original workspace. This is the
sway behavior I missed most.
ws-title-router: a daemon for window title rules
This one's not a hy3 feature, but it's a thing the windowrule
workspace N silent, class:firefox, title:[WS-N] used to handle.
Except — windowrules only fire at window creation, before the
Window
Titler extension applies the prefix. So the rule never matches.
Solution: listen to Hyprland's IPC event stream and react to title changes:
1# scripts/ws-title-router (excerpt)
2SOCK="${XDG_RUNTIME_DIR}/hypr/${HYPRLAND_INSTANCE_SIGNATURE}/.socket2.sock"
3socat -U - UNIX-CONNECT:"$SOCK" | while IFS='>>' read -r line; do
4 case "$line" in
5 windowtitlev2*)
6 payload="${line#windowtitlev2>>}"
7 addr="${payload%%,*}"
8 title="${payload#*,}"
9 if [[ "$title" =~ ^\[WS-([0-9]+)\] ]]; then
10 target_ws="${BASH_REMATCH[1]}"
11 # ... query class, current ws, group state, then move
12 fi
13 ;;
14 esac
15done
Hyprland exposes a Unix socket at
$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock
that streams events one per line. windowtitlev2 events fire on every
title change with the window's address. Filter for Firefox + [WS-N]
prefix, and movetoworkspacesilent as needed (popping out of the
group first if the window is currently grouped).
Started via exec-once in hyprland.conf. Survives across the
session.
All of it together
The end state: Hyprland 0.55.1 from the AshBuk COPR, no plugins, five
small bash scripts (tab-workspace, group-movefocus,
group-swapwindow, move-to-ws, ws-title-router) covering the hy3
features I actually used. The whole thing is git-managed so the same
config syncs to my other machines.
It's about 200 lines of bash total. hy3 is a 4000-line C++ plugin. For my use case the trade-off is fine: I lost the deep tree manipulation I never used, gained scripts I can actually read in a minute when something breaks.
The takeaway, if there is one: when the ecosystem you're depending on isn't quite stable, sometimes the most resilient option is to depend on less of it. Hyprland's IPC and dispatchers have been stable across the upgrade chaos. The plugin ABI hasn't, and packaging is a moving target. Five bash scripts hitting an HTTP-ish socket are easier to keep alive than a binary plugin tied to a specific compositor commit.
Now let's see if any of this still works after Fedora 45.