# 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//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; # Keybindings cheatsheet (SUPER+? → the `keybinds` menu module). Built # from the SAME ./keybinds.nix that hyprland.nix binds, so it can never # drift from the live shortcuts. "$mod" reads as SUPER; rows are padded # into two columns. extra[] carries the generated/mouse binds. keybinds = import ./keybinds.nix; padRight = w: s: let n = w - builtins.stringLength s; in s + (if n > 0 then lib.concatStrings (builtins.genList (_: " ") n) else " "); prettyKeys = b: let m = lib.replaceStrings [ "$mod" ] [ "SUPER" ] b.mods; prefix = if m == "" then "" else lib.concatStringsSep " + " (lib.splitString " " m) + " + "; key = lib.replaceStrings [ "question" "slash" ] [ "?" "/" ] b.key; in prefix + key; cheatRows = map (b: padRight 22 (prettyKeys b) + b.desc) keybinds.binds ++ map (e: padRight 22 e.keys + e.desc) keybinds.extra; cheatsheetFile = pkgs.writeText "nomarchy-keybinds.txt" (lib.concatStringsSep "\n" cheatRows + "\n"); 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 ;; power-profile) # power-profiles-daemon profile switcher (the system side ships it # by default on laptops — nomarchy.system.power). Switching goes # through polkit, so no root prompt. if ! command -v powerprofilesctl >/dev/null 2>&1; then notify-send "Power profiles" "power-profiles-daemon isn't running on this machine." exit 0 fi cur=$(powerprofilesctl get 2>/dev/null) choice=$(powerprofilesctl list 2>/dev/null \ | sed -nE 's/^[* ] ([a-z-]+):$/\1/p' \ | rofi -dmenu -p "profile (now: $cur)") || exit 0 [ -n "$choice" ] && powerprofilesctl set "$choice" ;; 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://www.google.com/search?q=$(urlencode "$q")" ;; network) # nmtui in a terminal (NetworkManager is the system network stack). exec ${cfg.terminal} -e nmtui ;; bluetooth) # blueman-manager GUI (services.blueman.enable, system-side). exec blueman-manager ;; capture) choice=$(printf '%s\n' \ "󰆟 Region → clipboard" \ "󰆟 Region → file" \ "󰍹 Full screen → clipboard" \ "󰍹 Full screen → file" \ | rofi -dmenu -p capture) || exit 0 dir="$HOME/Pictures/Screenshots" file="$dir/$(date +%Y%m%d-%H%M%S).png" case "$choice" in *"Region → clipboard"*) grim -g "$(slurp)" - | wl-copy ;; *"Region → file"*) mkdir -p "$dir"; grim -g "$(slurp)" "$file" \ && notify-send "Screenshot saved" "$file" ;; *"Full screen → clipboard"*) grim - | wl-copy ;; *"Full screen → file"*) mkdir -p "$dir"; grim "$file" \ && notify-send "Screenshot saved" "$file" ;; esac ;; keybinds) # Read-only cheatsheet, generated from keybinds.nix at build time. rofi -dmenu -i -p keys < ${cheatsheetFile} >/dev/null || exit 0 ;; ask) # Free-text question → claude CLI in a terminal (OAuth, no API key). # Pulled fresh from npm via npx rather than nixpkgs' claude-code, # which lags behind model releases; npx caches after first run. The # REPL stays open for follow-ups. q=$(rofi -dmenu -p claude < /dev/null) || exit 0 [ -n "$q" ] || exit 0 exec ${cfg.terminal} -e npx --yes @anthropic-ai/claude-code@latest "$q" ;; "") entries=( "󰀻 Apps" "󰏘 Theme" "󰅌 Clipboard" "󰃬 Calculator" "󰈞 Files" "󰖟 Web search" "󰖩 Network" "󰂯 Bluetooth" "󰄄 Capture" "󰋖 Keybindings" "󰭹 Ask Claude" ) # Power-profile picker only when this is a laptop (battery present) # running power-profiles-daemon — matches the Waybar indicator's # self-gating; desktops and the TLP backend don't show it. bats=(/sys/class/power_supply/BAT*) if [ -e "''${bats[0]}" ] && command -v powerprofilesctl >/dev/null 2>&1; then entries+=("󰓅 Power profile") fi entries+=("󰐥 Power") choice=$(printf '%s\n' "''${entries[@]}" | 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 ;; *Network*) exec "$0" network ;; *Bluetooth*) exec "$0" bluetooth ;; *Capture*) exec "$0" capture ;; *Keybindings*) exec "$0" keybinds ;; *Ask*) exec "$0" ask ;; *"Power profile"*) exec "$0" power-profile ;; *Power*) exec "$0" power ;; esac ;; *) echo "usage: nomarchy-menu [power|power-profile|theme|clipboard|calc|files|web|network|bluetooth|capture|keybinds|ask]" >&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 pkgs.nodejs # npx, for the Ask Claude module (claude-code from npm) ]; programs.rofi = { enable = true; terminal = cfg.terminal; font = "${t.fonts.ui} 12"; extraConfig = { modi = "drun,run"; show-icons = true; # app icons via the theme's icon set icon-theme = t.iconTheme; # resolved in theme.nix (Papirus-*/per-theme) 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"; }; }; }; }; }