feat(home): switch launcher/menu from fuzzel to rofi 2.0

rofi 2.0 landed in nixpkgs 26.05 with native Wayland (mainline upstreamed
the lbonn rofi-wayland fork — `wayland enabled`), so the original reason
to prefer fuzzel (rofi being X11-only / needing a community fork) is gone.
rofi's `.rasi` is far more expressive than fuzzel's flat INI, which lets
the launcher be a proper themed hero surface and revives the legacy
per-theme launcher designs.

- modules/home/rofi.nix replaces fuzzel.nix: per-element theme generated
  from theme-state.json (accent border, highlighted selection, rounded
  inputbar) via lib.formats.rasi.mkLiteral; themes/<slug>/rofi.rasi
  whole-swap; the nomarchy-menu dispatcher re-pointed to `rofi -dmenu`
  (power/theme/clipboard/calc with -mesg/files/web + root picker).
- SUPER+D is now `rofi -show drun`; the menu binds are unchanged.
- nomarchy.fuzzel.enable → nomarchy.rofi.enable; fuzzel dropped from
  home.packages. Pango handles nf-glyph fallback (no explicit font list
  needed as fuzzel's fcft did). show-icons off until an icon theme ships.

Verified: flake check green, HM generation builds, the generated
theme.rasi parses under `rofi -dump-theme`, Hyprland config verifies.
Not verified: rofi's on-screen rendering needs a real session.

Docs: README menu/override/roadmap sections and the module tree updated;
the per-theme launcher roadmap item now points at porting the legacy
rofi.rasi designs (the whole-swap mechanism is in place).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Bernardo Magri
2026-06-13 14:41:46 +01:00
parent 76582c1ee9
commit 961314ea86
9 changed files with 532 additions and 201 deletions

View File

@@ -12,7 +12,7 @@
./waybar.nix
./ghostty.nix
./btop.nix
./fuzzel.nix # launcher theming + the nomarchy-menu dispatcher
./rofi.nix # launcher theming + the nomarchy-menu dispatcher
./swaync.nix # notification daemon, themed from the same JSON
./idle.nix # hyprlock + hypridle, themed from the same JSON
./yazi.nix # flagship TUI file manager, themed + plugins
@@ -31,7 +31,6 @@
home.packages = with pkgs; [
awww # wallpaper daemon with animated transitions (the swww fork)
fuzzel # launcher / dmenu (used by the theme picker bind)
libnotify
];

View File

@@ -1,171 +0,0 @@
# Fuzzel — launcher, dmenu renderer for the menu system, themed from
# theme-state.json like every other surface. Also home of nomarchy-menu,
# the dispatcher script all menu modules grow out of.
#
# Per-theme identity: themes/<slug>/fuzzel.ini is a whole-swap (like
# waybar.css) — it replaces the generated config entirely, probed at
# eval time.
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy;
t = cfg.theme;
c = t.colors;
# Per-theme override probe (same convention as waybar.nix).
iniOverride = cfg.themesDir + "/${t.slug}/fuzzel.ini";
hasIniOverride = builtins.pathExists iniOverride;
# fuzzel.ini colors are rrggbbaa, no leading #.
hexa = color: alpha: "${lib.removePrefix "#" color}${alpha}";
nomarchy-menu = pkgs.writeShellScriptBin "nomarchy-menu" ''
# Nomarchy menu dispatcher thin presentation layer over
# `fuzzel --dmenu`; actions delegate to nomarchy-theme-sync,
# systemctl, hyprctl and friends. Icons are nf-md glyphs (any
# shipped Nerd Font carries them). `nomarchy-menu` with no
# argument shows the module picker.
urlencode() {
local s="$1" out="" ch
for ((i = 0; i < ''${#s}; i++)); do
ch=''${s:i:1}
case "$ch" in
[a-zA-Z0-9.~_-]) out+="$ch" ;;
' ') out+="+" ;;
*) printf -v ch '%%%02X' "'$ch"; out+="$ch" ;;
esac
done
printf '%s' "$out"
}
case "''${1:-}" in
power)
choice=$(printf '%s\n' \
"󰌾 Lock" \
"󰍃 Logout" \
"󰤄 Suspend" \
"󰒲 Hibernate" \
"󰜉 Reboot" \
"󰐥 Shutdown" \
| fuzzel --dmenu --prompt "power " --lines 6 --width 24) || exit 0
case "$choice" in
*Lock) loginctl lock-session ;;
*Logout) hyprctl dispatch exit ;;
*Suspend) systemctl suspend ;;
*Hibernate) systemctl hibernate ;;
*Reboot) systemctl reboot ;;
*Shutdown) systemctl poweroff ;;
esac ;;
theme)
choice=$(nomarchy-theme-sync list \
| fuzzel --dmenu --prompt "theme ") || exit 0
[ -n "$choice" ] && exec nomarchy-theme-sync apply "$choice" ;;
clipboard)
sel=$(cliphist list | fuzzel --dmenu --prompt "clip " --width 60) || exit 0
printf '%s' "$sel" | cliphist decode | wl-copy ;;
calc)
expr=$(fuzzel --prompt-only "= ") || exit 0
[ -n "$expr" ] || exit 0
result=$(qalc -t -- "$expr" 2>&1 | tail -n 1)
choice=$(printf '%s\n' "󰆏 Copy result" "󰃬 New calculation" \
| fuzzel --dmenu --lines 2 --width 40 \
--prompt "= " --mesg "$expr = $result") || exit 0
case "$choice" in
*Copy*) printf '%s' "$result" | wl-copy ;;
*New*) exec "$0" calc ;;
esac ;;
files)
sel=$(fd . "$HOME" --type f --hidden --exclude .git --exclude .cache 2>/dev/null \
| head -n 50000 \
| sed "s|^$HOME/||" \
| fuzzel --dmenu --prompt "file " --width 60) || exit 0
[ -n "$sel" ] && exec xdg-open "$HOME/$sel" ;;
web)
q=$(fuzzel --prompt-only "search ") || exit 0
[ -n "$q" ] || exit 0
exec xdg-open "https://duckduckgo.com/?q=$(urlencode "$q")" ;;
"")
choice=$(printf '%s\n' \
"󰀻 Apps" \
"󰏘 Theme" \
"󰅌 Clipboard" \
"󰃬 Calculator" \
"󰈞 Files" \
"󰖟 Web search" \
"󰐥 Power" \
| fuzzel --dmenu --prompt "menu " --lines 7 --width 24) || exit 0
case "$choice" in
*Apps*) exec fuzzel ;;
*Theme*) exec "$0" theme ;;
*Clipboard*) exec "$0" clipboard ;;
*Calc*) exec "$0" calc ;;
*Files*) exec "$0" files ;;
*Web*) exec "$0" web ;;
*Power*) exec "$0" power ;;
esac ;;
*)
echo "usage: nomarchy-menu [power|theme|clipboard|calc|files|web]" >&2
exit 64 ;;
esac
'';
in
{
config = lib.mkIf cfg.fuzzel.enable {
home.packages = [
nomarchy-menu
pkgs.libqalculate # qalc, the calc module's engine
pkgs.fd # files module
pkgs.xdg-utils # xdg-open for files/web
];
programs.fuzzel = {
enable = true;
# Whole-swap themes bring their own fuzzel.ini; otherwise the
# config is generated from the palette.
settings = lib.mkIf (!hasIniOverride) {
main = {
# UI font first; the mono Nerd Font follows so menu icons
# (nf-md glyphs) resolve — fcft falls back per glyph.
font = "${t.fonts.ui}:size=${toString (t.fonts.size + 1)}, ${t.fonts.mono}:size=${toString (t.fonts.size + 1)}";
terminal = cfg.terminal;
layer = "overlay";
lines = 12;
width = 40;
horizontal-pad = 24;
vertical-pad = 16;
inner-pad = 8;
};
colors = {
background = hexa c.base "f2";
text = hexa c.text "ff";
prompt = hexa c.subtext "ff";
placeholder = hexa c.muted "ff";
input = hexa c.text "ff";
match = hexa c.accent "ff";
selection = hexa c.surface "ff";
selection-text = hexa c.text "ff";
selection-match = hexa c.accent "ff";
border = hexa c.accent "ee";
};
border = {
width = t.ui.borderSize;
radius = t.ui.rounding;
};
};
};
xdg.configFile."fuzzel/fuzzel.ini" = lib.mkIf hasIniOverride {
source = iniOverride;
};
};
}

View File

@@ -127,7 +127,7 @@ in
bind = [
"$mod, Return, exec, $terminal"
"$mod, D, exec, fuzzel"
"$mod, D, exec, rofi -show drun"
"$mod, E, exec, $terminal -e yazi"
"$mod, Q, killactive"
"$mod, F, fullscreen"
@@ -140,7 +140,7 @@ in
# Cycle the current theme's wallpapers (instant, no rebuild).
"$mod SHIFT, T, exec, nomarchy-theme-sync bg next"
# Power menu (fuzzel dmenu via the nomarchy-menu dispatcher).
# Power menu (rofi dmenu via the nomarchy-menu dispatcher).
# Not Escape: Super+Escape gets swallowed before reaching the
# dispatcher on some setups (input methods / modal handling).
"$mod, X, exec, nomarchy-menu power"

View File

@@ -63,7 +63,7 @@
# ── Component toggles ──────────────────────────────────────────
hyprland.enable = lib.mkEnableOption "Nomarchy's Hyprland configuration" // { default = true; };
waybar.enable = lib.mkEnableOption "Nomarchy's Waybar configuration" // { default = true; };
fuzzel.enable = lib.mkEnableOption "Nomarchy's themed fuzzel + the nomarchy-menu dispatcher (power menu)" // { default = true; };
rofi.enable = lib.mkEnableOption "Nomarchy's themed rofi launcher + the nomarchy-menu dispatcher" // { default = true; };
swaync.enable = lib.mkEnableOption "swaync notifications, themed from the state file" // { default = true; };
idle.enable = lib.mkEnableOption "hyprlock + hypridle (idle lock, display off, suspend)" // { default = true; };
yazi.enable = lib.mkEnableOption "the yazi TUI file manager, themed with a curated plugin set" // { default = true; };

222
modules/home/rofi.nix Normal file
View File

@@ -0,0 +1,222 @@
# rofi (2.0, native Wayland on 26.05) — launcher + dmenu renderer for
# the menu system, themed from theme-state.json. rofi's .rasi is far more
# expressive than a flat scheme, so the generated theme styles each
# element (accent border, highlighted selection, rounded inputbar); a
# themes/<slug>/rofi.rasi whole-swap replaces it entirely.
#
# Home of nomarchy-menu, the dispatcher all menu modules hang off.
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy;
t = cfg.theme;
c = t.colors;
inherit (config.lib.formats.rasi) mkLiteral;
px = n: mkLiteral "${toString n}px";
# Per-theme override probe (same convention as waybar.css).
rasiOverride = cfg.themesDir + "/${t.slug}/rofi.rasi";
hasRasiOverride = builtins.pathExists rasiOverride;
nomarchy-menu = pkgs.writeShellScriptBin "nomarchy-menu" ''
# Nomarchy menu dispatcher thin presentation layer over
# `rofi -dmenu`; actions delegate to nomarchy-theme-sync, systemctl,
# hyprctl and friends. Icons are nf-md glyphs (Pango falls back to a
# Nerd Font for them). `nomarchy-menu` with no argument shows the
# module picker.
urlencode() {
local s="$1" out="" ch i
for ((i = 0; i < ''${#s}; i++)); do
ch=''${s:i:1}
case "$ch" in
[a-zA-Z0-9.~_-]) out+="$ch" ;;
' ') out+="+" ;;
*) printf -v ch '%%%02X' "'$ch"; out+="$ch" ;;
esac
done
printf '%s' "$out"
}
case "''${1:-}" in
power)
choice=$(printf '%s\n' \
"󰌾 Lock" \
"󰍃 Logout" \
"󰤄 Suspend" \
"󰒲 Hibernate" \
"󰜉 Reboot" \
"󰐥 Shutdown" \
| rofi -dmenu -p power) || exit 0
case "$choice" in
*Lock) loginctl lock-session ;;
*Logout) hyprctl dispatch exit ;;
*Suspend) systemctl suspend ;;
*Hibernate) systemctl hibernate ;;
*Reboot) systemctl reboot ;;
*Shutdown) systemctl poweroff ;;
esac ;;
theme)
choice=$(nomarchy-theme-sync list | rofi -dmenu -p theme) || exit 0
[ -n "$choice" ] && exec nomarchy-theme-sync apply "$choice" ;;
clipboard)
sel=$(cliphist list | rofi -dmenu -p clip) || exit 0
printf '%s' "$sel" | cliphist decode | wl-copy ;;
calc)
expr=$(rofi -dmenu -p "= " < /dev/null) || exit 0
[ -n "$expr" ] || exit 0
result=$(qalc -t -- "$expr" 2>&1 | tail -n 1)
choice=$(printf '%s\n' "󰆏 Copy result" "󰃬 New calculation" \
| rofi -dmenu -p "= " -mesg "$expr = $result") || exit 0
case "$choice" in
*Copy*) printf '%s' "$result" | wl-copy ;;
*New*) exec "$0" calc ;;
esac ;;
files)
sel=$(fd . "$HOME" --type f --hidden --exclude .git --exclude .cache 2>/dev/null \
| head -n 50000 \
| sed "s|^$HOME/||" \
| rofi -dmenu -p file) || exit 0
[ -n "$sel" ] && exec xdg-open "$HOME/$sel" ;;
web)
q=$(rofi -dmenu -p search < /dev/null) || exit 0
[ -n "$q" ] || exit 0
exec xdg-open "https://duckduckgo.com/?q=$(urlencode "$q")" ;;
"")
choice=$(printf '%s\n' \
"󰀻 Apps" \
"󰏘 Theme" \
"󰅌 Clipboard" \
"󰃬 Calculator" \
"󰈞 Files" \
"󰖟 Web search" \
"󰐥 Power" \
| rofi -dmenu -p menu) || exit 0
case "$choice" in
*Apps*) exec rofi -show drun ;;
*Theme*) exec "$0" theme ;;
*Clipboard*) exec "$0" clipboard ;;
*Calc*) exec "$0" calc ;;
*Files*) exec "$0" files ;;
*Web*) exec "$0" web ;;
*Power*) exec "$0" power ;;
esac ;;
*)
echo "usage: nomarchy-menu [power|theme|clipboard|calc|files|web]" >&2
exit 64 ;;
esac
'';
in
{
config = lib.mkIf cfg.rofi.enable {
home.packages = [
nomarchy-menu
pkgs.libqalculate # qalc, the calc module's engine
pkgs.fd # files module
pkgs.xdg-utils # xdg-open for files/web
];
programs.rofi = {
enable = true;
terminal = cfg.terminal;
font = "${t.fonts.ui} 12";
extraConfig = {
modi = "drun,run";
show-icons = false; # no icon theme shipped yet — clean text list
drun-display-format = "{name}";
display-drun = "apps";
display-run = "run";
};
# Whole-swap themes bring their own rofi.rasi; otherwise the theme
# is generated from the palette.
theme =
if hasRasiOverride then rasiOverride
else {
"*" = {
bg = mkLiteral c.base;
fg = mkLiteral c.text;
dim = mkLiteral c.subtext;
surface = mkLiteral c.surface;
accent = mkLiteral c.accent;
background-color = mkLiteral "transparent";
text-color = mkLiteral "@fg";
margin = 0;
padding = 0;
spacing = 0;
};
"window" = {
background-color = mkLiteral "@bg";
border = px t.ui.borderSize;
border-color = mkLiteral "@accent";
border-radius = px t.ui.rounding;
width = mkLiteral "40%";
padding = px 8;
};
"mainbox" = {
children = map mkLiteral [ "inputbar" "message" "listview" ];
};
"inputbar" = {
background-color = mkLiteral "@surface";
border-radius = px t.ui.rounding;
padding = mkLiteral "8px 12px";
spacing = px 8;
children = map mkLiteral [ "prompt" "entry" ];
};
"prompt" = { text-color = mkLiteral "@accent"; };
"entry" = {
placeholder = "Search";
placeholder-color = mkLiteral "@dim";
};
"listview" = {
lines = 8;
columns = 1;
dynamic = true;
scrollbar = false;
spacing = px 2;
padding = mkLiteral "8px 0px 0px 0px";
};
"element" = {
padding = mkLiteral "8px 12px";
border-radius = px t.ui.rounding;
spacing = px 8;
};
"element selected" = {
background-color = mkLiteral "@accent";
text-color = mkLiteral "@bg";
};
"element-icon" = {
background-color = mkLiteral "transparent";
size = mkLiteral "1em";
};
"element-text" = {
background-color = mkLiteral "transparent";
text-color = mkLiteral "inherit";
};
"message" = { padding = mkLiteral "8px 0px 0px 0px"; };
"textbox" = {
background-color = mkLiteral "@surface";
text-color = mkLiteral "@fg";
border-radius = px t.ui.rounding;
padding = mkLiteral "8px 12px";
};
};
};
};
}

View File

@@ -1,5 +1,5 @@
# yazi — the flagship file manager: a fast, keyboard-driven TUI that
# fits the distro's identity (Ghostty's Kitty-graphics previews, fuzzel
# fits the distro's identity (Ghostty's Kitty-graphics previews, rofi
# menus, everything from one JSON). Themed from theme-state.json and
# shipped with a curated plugin set. The GUI half (Thunar, "open folder"
# handler) is nomarchy.system.fileManager on the system side.