Migrating from Sway/Waybar to Hyprland/Noctalia

I spent a few weeks moving from Sway + Waybar to Hyprland with Noctalia (a Quickshell-based desktop shell). The migration surface is bigger than it looks — here’s what I wish I’d known upfront.

Why Noctalia?

Noctalia replaces your bar, control center, and launcher with proper Qt6/QML widgets. Instead of a dozen shell scripts with return-type: json, you get native CPU/network/bluetooth widgets, a side panel for settings, and a plugin system for custom indicators. The bar is a real QML app, not a hack.

The tradeoff: Noctalia intercepts Hyprland’s config and replaces it with Lua. Every hyprctl dispatch call gets rerouted through hl.dsp.*. Classic dispatcher strings silently fail.

The Lua Dispatch Trap

This is the biggest gotcha. In standard Hyprland you write:

bind = SUPER, G, exec, hyprctl dispatch togglegroup

In Noctalia’s Lua config:

hl.keybind({ mods = "SUPER", key = "G" }, function()
    hl.dsp.group.toggle()
end)

The problem is every script and alias that calls hyprctl dispatch <classic string> breaks. Classic dispatchers like togglespecialworkspace, moveintoworkspace, or togglegroup error out because the Lua parser intercepts the dispatch and doesn’t know what to do with a plain string.

The worst one I hit: group-all. The old dispatcher was movegroupwindow — which only reorders windows already in a group. The real merge dispatcher is:

hl.dsp.window.move({ into_group = "l" })

Not hl.dsp.group.move_window() (reorder-only). Took me two sessions to find the right API.

Rule of thumb: rg 'hyprctl dispatch' across your scripts and configs before you start. Every single one needs the hl.dsp.* treatment.

What Maps Where

Waybar had ~20 modules. Here’s where they landed:

DestinationCountExamples
Native Noctalia widget~10workspaces, clock, CPU, memory, disk
Control center side panel~6network, bluetooth, power profile, DND, volume
CustomButton (polling)~7swap monitor, transcribe toggle, recording indicator

The control center handles things like network and bluetooth better than a bar widget ever did — you get the full connection UI on click, not a dropdown script.

CustomButton Limits

CustomButton is Noctalia’s escape hatch for anything without a native widget. But it has sharp edges:

  • No return-type: json — you can’t return structured data like Waybar’s "text": "...", "class": "...". Just plain text.
  • No signal-based refresh — only polling via textIntervalMs (interval in ms) or a textStream script (continuous stdout).
  • No per-instance name — all CustomButtons are identical in the widget tree, so you can’t style them individually without workarounds.

Want an animated recording pulse? Can’t do it in a CustomButton — you need a proper QML plugin.

Bar as a Systemd Service

By default Noctalia launches from autostart.lua as a one-shot. If it crashes, it stays dead until next login. Move it to a systemd user service:

[Unit]
Description=Noctalia Shell
After=graphical-session.target
 
[Service]
ExecStart=/usr/bin/qs -c noctalia-shell
Restart=always
RestartSec=1
StartLimitIntervalSec=0

Gotcha: if you had an old qs process from autostart, it holds a single-instance lock. The new service fails, restarts, fails again — infinite loop until you kill the orphan. Kill it first, then enable the service.

Plugin Gotchas

Noctalia plugins (QML widgets) have a few non-obvious rules:

  • Plugin dir must be a real directory, not a symlink. Noctalia won’t load it.
  • Enable separately in ~/.config/noctalia/plugins.json — adding it to settings.json isn’t enough.
  • Unloaded plugins in the layout get auto-deleted from settings.json. If your plugin fails to load, the layout entry disappears and you have to re-add it.

Lua API Quirks

A few things that tripped me up:

  • % is broken in hl.size — use fixed px or monitor_w * 0.x instead.
  • Monitor selector only accepts "+1"/"-1" (or name/id), not directional l/r.
  • Window rules apply at creation only — reloading config won’t resize open windows.
  • hyprctl keyword animations:enabled false is rejected — use hyprctl eval 'hl.animation(...)' instead.

Closing Thoughts

Noctalia is a real upgrade from Sway+Waybar. The native widgets, control center, and QML plugin system are genuinely better than the shell-script bar approach. But budget time for the dispatch migration — every hyprctl dispatch in your configs and scripts needs the hl.dsp.* treatment, and the API isn’t always where you expect it.